Module sverchok.utils.curve.algorithms
Expand source code
# This file is part of project Sverchok. It's copyrighted by the contributors
# recorded in the version control history of the file, available from
# its original location https://github.com/nortikin/sverchok/commit/master
#
# SPDX-License-Identifier: GPL3
# License-Filename: LICENSE
import numpy as np
import itertools
from mathutils import Vector, Matrix
from sverchok.utils.curve.core import (
SvCurve, ZeroCurvatureException,
SvCurveSegment, SvReparametrizedCurve,
SvFlipCurve, SvConcatCurve,
UnsupportedCurveTypeException
)
from sverchok.utils.surface.core import UnsupportedSurfaceTypeException
from sverchok.utils.geom import PlaneEquation, LineEquation, Spline, LinearSpline, CubicSpline
from sverchok.utils.geom import autorotate_householder, autorotate_track, autorotate_diff
from sverchok.utils.math import (
ZERO, FRENET, HOUSEHOLDER, TRACK, DIFF, TRACK_NORMAL,
NORMAL_DIR, NONE
)
from sverchok.utils.sv_logging import sv_logger
def make_euclidean_ts(pts):
tmp = np.linalg.norm(pts[:-1] - pts[1:], axis=1)
tknots = np.insert(tmp, 0, 0).cumsum()
tknots = tknots / tknots[-1]
return tknots
class SvCurveLengthSolver(object):
def __init__(self, curve):
self.curve = curve
self._reverse_spline = None
self._prime_spline = None
def calc_length_segments(self, tknots):
vectors = self.curve.evaluate_array(tknots)
dvs = vectors[1:] - vectors[:-1]
lengths = np.linalg.norm(dvs, axis=1)
return lengths
def get_total_length(self):
if self._reverse_spline is None:
raise Exception("You have to call solver.prepare() first")
return self._length_params[-1]
def _calc_tknots_fixed(self, resolution):
t_min, t_max = self.curve.get_u_bounds()
tknots = np.linspace(t_min, t_max, num=resolution)
return tknots
def _prepare_find(self, resolution, tolerance, tknots=None, lengths=None, length_params=None):
if tknots is None:
tknots = self._calc_tknots_fixed(resolution)
if lengths is None:
lengths = self.calc_length_segments(tknots)
if length_params is None:
length_params = np.cumsum(np.insert(lengths, 0, 0))
resolution2 = resolution * 2 - 1
tknots2 = self._calc_tknots_fixed(resolution2)
lengths2 = self.calc_length_segments(tknots2)
length_params2 = np.cumsum(np.insert(lengths2, 0, 0))
dl = abs(length_params2[::2] - length_params)
if (dl < tolerance).all():
return tknots2, length_params2
else:
return self._prepare_find(resolution2, tolerance, tknots2, lengths2, length_params2)
def prepare(self, mode, resolution=50, tolerance=None):
if tolerance is None:
tknots = self._calc_tknots_fixed(resolution)
lengths = self.calc_length_segments(tknots)
self._length_params = np.cumsum(np.insert(lengths, 0, 0))
else:
tknots, self._length_params = self._prepare_find(resolution, tolerance)
self._reverse_spline = self._make_spline(mode, tknots, self._length_params)
self._prime_spline = self._make_spline(mode, self._length_params, tknots)
def _make_spline(self, mode, tknots, values):
zeros = np.zeros(len(tknots))
control_points = np.vstack((values, tknots, zeros)).T
if mode == 'LIN':
spline = LinearSpline(control_points, tknots = values, is_cyclic = False)
elif mode == 'SPL':
spline = CubicSpline(control_points, tknots = values, is_cyclic = False)
else:
raise Exception("Unsupported mode; supported are LIN and SPL.")
return spline
def calc_length(self, t_min, t_max):
if self._prime_spline is None:
raise Exception("You have to call solver.prepare() first")
lengths = self._prime_spline.eval(np.array([t_min, t_max]))
return lengths[1][1] - lengths[0][1]
def calc_length_params(self, ts):
if self._prime_spline is None:
raise Exception("You have to call solver.prepare() first")
spline_verts = self._prime_spline.eval(ts)
return spline_verts[:,1]
def solve(self, input_lengths):
if self._reverse_spline is None:
raise Exception("You have to call solver.prepare() first")
spline_verts = self._reverse_spline.eval(input_lengths)
return spline_verts[:,1]
class SvNormalTrack(object):
def __init__(self, curve, resolution):
self.curve = curve
self.resolution = resolution
self._pre_calc()
def _make_quats(self, points, tangents, normals, binormals):
matrices = np.dstack((normals, binormals, tangents))
matrices = np.transpose(matrices, axes=(0,2,1))
matrices = np.linalg.inv(matrices)
return [Matrix(m).to_quaternion() for m in matrices]
def _pre_calc(self):
curve = self.curve
t_min, t_max = curve.get_u_bounds()
ts = np.linspace(t_min, t_max, num=self.resolution)
points = curve.evaluate_array(ts)
tangents, normals, binormals = curve.tangent_normal_binormal_array(ts)
tangents /= np.linalg.norm(tangents, axis=1, keepdims=True)
normal = normals[0]
if np.linalg.norm(normal) > 1e-4:
binormal = binormals[0]
binormal /= np.linalg.norm(binormal)
else:
tangent = tangents[0]
normal = Vector(tangent).orthogonal()
normal = np.array(normal)
binormal = np.cross(tangent, normal)
binormal /= np.linalg.norm(binormal)
out_normals = [normal]
out_binormals = [binormal]
for point, tangent in zip(points[1:], tangents[1:]):
plane = PlaneEquation.from_normal_and_point(Vector(tangent), Vector(point))
normal = plane.projection_of_vector(Vector(point), Vector(point + normal))
normal = np.array(normal.normalized())
binormal = np.cross(tangent, normal)
binormal /= np.linalg.norm(binormal)
out_normals.append(normal)
out_binormals.append(binormal)
self.quats = self._make_quats(points, tangents, np.array(out_normals), np.array(out_binormals))
self.tknots = ts
def evaluate_array(self, ts):
"""
Args:
ts: np.array of snape (n,) or list of floats
Returns:
np.array of shape (n, 3, 3)
"""
ts = np.array(ts)
tknots, quats = self.tknots, self.quats
base_indexes = tknots.searchsorted(ts, side='left')-1
t1s, t2s = tknots[base_indexes], tknots[base_indexes+1]
dts = (ts - t1s) / (t2s - t1s)
#dts = np.clip(dts, 0.0, 1.0) # Just in case...
matrix_out = []
# TODO: ideally this should be vectorized with numpy;
# but that would require implementation of quaternion
# interpolation in numpy.
for dt, base_index in zip(dts, base_indexes):
q1, q2 = quats[base_index], quats[base_index+1]
# spherical linear interpolation.
# TODO: implement `squad`.
if dt < 0:
q = q1
elif dt > 1.0:
q = q2
else:
q = q1.slerp(q2, dt)
matrix = np.array(q.to_matrix())
matrix_out.append(matrix)
return np.array(matrix_out)
class MathutilsRotationCalculator(object):
@classmethod
def get_matrix(cls, tangent, scale, axis, algorithm, scale_all=True, up_axis='X'):
"""
Calculate matrix required to rotate object according to `tangent` vector.
Args:
tangent: np.array of shape (3,)
scale: float
axis: int, 0 - X, 1 - Y, 2 - Z
algorithm: one of HOUSEHOLDER, TRACK, DIFF
scale_all: True to scale along all axes, False to scale along tangent only
up_axis: string, "X", "Y" or "Z", for algorithm == TRACK only.
Returns:
np.array of shape (3,3).
"""
x = Vector((1.0, 0.0, 0.0))
y = Vector((0.0, 1.0, 0.0))
z = Vector((0.0, 0.0, 1.0))
if axis == 0:
ax1, ax2, ax3 = x, y, z
elif axis == 1:
ax1, ax2, ax3 = y, x, z
else:
ax1, ax2, ax3 = z, x, y
if scale_all:
scale_matrix = Matrix.Scale(1/scale, 4, ax1) @ Matrix.Scale(scale, 4, ax2) @ Matrix.Scale(scale, 4, ax3)
else:
scale_matrix = Matrix.Scale(1/scale, 4, ax1)
scale_matrix = np.array(scale_matrix.to_3x3())
tangent = Vector(tangent)
if algorithm == HOUSEHOLDER:
rot = autorotate_householder(ax1, tangent).inverted()
elif algorithm == TRACK:
axis = "XYZ"[axis]
rot = autorotate_track(axis, tangent, up_axis)
elif algorithm == DIFF:
rot = autorotate_diff(tangent, ax1)
else:
raise Exception("Unsupported algorithm")
rot = np.array(rot.to_3x3())
return np.matmul(rot, scale_matrix)
class DifferentialRotationCalculator(object):
def __init__(self, curve, algorithm, resolution=50):
self.curve = curve
self.algorithm = algorithm
if algorithm == TRACK_NORMAL:
self.normal_tracker = SvNormalTrack(curve, resolution)
elif algorithm == ZERO:
self.curve.pre_calc_torsion_integral(resolution)
def get_matrices(self, ts):
n = len(ts)
if self.algorithm == FRENET:
frenet, _ , _ = self.curve.frame_array(ts)
return frenet
elif self.algorithm == ZERO:
frenet, _ , _ = self.curve.frame_array(ts)
angles = - self.curve.torsion_integral(ts)
zeros = np.zeros((n,))
ones = np.ones((n,))
row1 = np.stack((np.cos(angles), np.sin(angles), zeros)).T # (n, 3)
row2 = np.stack((-np.sin(angles), np.cos(angles), zeros)).T # (n, 3)
row3 = np.stack((zeros, zeros, ones)).T # (n, 3)
rotation_matrices = np.dstack((row1, row2, row3))
return frenet @ rotation_matrices
elif self.algorithm == TRACK_NORMAL:
matrices = self.normal_tracker.evaluate_array(ts)
return matrices
else:
raise Exception("Unsupported algorithm")
class SvCurveFrameCalculator(object):
def __init__(self, curve, algorithm, z_axis=2, resolution=50, normal=None):
self.algorithm = algorithm
self.z_axis = z_axis
self.curve = curve
self.normal = normal
if algorithm in {FRENET, ZERO, TRACK_NORMAL}:
self.calculator = DifferentialRotationCalculator(curve, algorithm, resolution)
def get_matrix(self, tangent):
return MathutilsRotationCalculator.get_matrix(tangent, scale=1.0,
axis=self.z_axis,
algorithm = self.algorithm,
scale_all=False)
def get_matrices(self, ts):
if self.algorithm == NONE:
identity = np.eye(3)
n = len(ts)
return np.broadcast_to(identity, (n, 3,3))
elif self.algorithm == NORMAL_DIR:
matrices, _, _ = self.curve.frame_by_plane_array(ts, self.normal)
return matrices
elif self.algorithm in {FRENET, ZERO, TRACK_NORMAL}:
return self.calculator.get_matrices(ts)
elif self.algorithm in {HOUSEHOLDER, TRACK, DIFF}:
tangents = self.curve.tangent_array(ts)
matrices = np.vectorize(lambda t : self.get_matrix(t), signature='(3)->(3,3)')(tangents)
return matrices
else:
raise Exception("Unsupported algorithm")
class SvDeformedByFieldCurve(SvCurve):
def __init__(self, curve, field, coefficient=1.0):
self.curve = curve
self.field = field
self.coefficient = coefficient
self.tangent_delta = 0.001
self.__description__ = "{}({})".format(field, curve)
def get_u_bounds(self):
return self.curve.get_u_bounds()
def evaluate(self, t):
v = self.curve.evaluate(t)
vec = self.field.evaluate(*tuple(v))
return v + self.coefficient * vec
def evaluate_array(self, ts):
vs = self.curve.evaluate_array(ts)
xs, ys, zs = vs[:,0], vs[:,1], vs[:,2]
vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs)
vecs = np.stack((vxs, vys, vzs)).T
return vs + self.coefficient * vecs
class SvCastCurveToPlane(SvCurve):
def __init__(self, curve, point, normal, coefficient):
self.curve = curve
self.point = point
self.normal = normal
self.coefficient = coefficient
self.plane = PlaneEquation.from_normal_and_point(normal, point)
self.tangent_delta = 0.001
self.__description__ = "{} casted to Plane".format(curve)
def evaluate(self, t):
point = self.curve.evaluate(t)
target = np.array(self.plane.projection_of_point(point))
k = self.coefficient
return (1 - k) * point + k * target
def evaluate_array(self, ts):
points = self.curve.evaluate_array(ts)
targets = self.plane.projection_of_points(points)
k = self.coefficient
return (1 - k) * points + k * targets
def get_u_bounds(self):
return self.curve.get_u_bounds()
class SvCastCurveToSphere(SvCurve):
def __init__(self, curve, center, radius, coefficient):
self.curve = curve
self.center = center
self.radius = radius
self.coefficient = coefficient
self.tangent_delta = 0.001
self.__description__ = "{} casted to Sphere".format(curve)
def evaluate(self, t):
return self.evaluate_array(np.array([t]))[0]
def evaluate_array(self, ts):
points = self.curve.evaluate_array(ts)
centered_points = points - self.center
norms = np.linalg.norm(centered_points, axis=1)[np.newaxis].T
normalized = centered_points / norms
targets = self.radius * normalized + self.center
k = self.coefficient
return (1 - k) * points + k * targets
def get_u_bounds(self):
return self.curve.get_u_bounds()
class SvCastCurveToCylinder(SvCurve):
def __init__(self, curve, center, direction, radius, coefficient):
self.curve = curve
self.center = center
self.direction = direction
self.radius = radius
self.coefficient = coefficient
self.line = LineEquation.from_direction_and_point(direction, center)
self.tangent_delta = 0.001
self.__description__ = "{} casted to Cylinder".format(curve)
def evaluate(self, t):
point = self.curve.evaluate(t)
projection_to_line = self.line.projection_of_point(point)
projection_to_line = np.array(projection_to_line)
radial = point - projection_to_line
radius = self.radius * radial / np.linalg.norm(radial)
projection = projection_to_line + radius
k = self.coefficient
return (1 - k) * point + k * projection
def evaluate_array(self, ts):
points = self.curve.evaluate_array(ts)
projection_to_line = self.line.projection_of_points(points)
radial = points - projection_to_line
radius = self.radius * radial / np.linalg.norm(radial, axis=1, keepdims=True)
projections = projection_to_line + radius
k = self.coefficient
return (1 - k) * points + k * projections
def get_u_bounds(self):
return self.curve.get_u_bounds()
class SvCurveLerpCurve(SvCurve):
__description__ = "Lerp"
def __init__(self, curve1, curve2, coefficient):
self.curve1 = curve1
self.curve2 = curve2
self.coefficient = coefficient
self.u_bounds = (0.0, 1.0)
self.c1_min, self.c1_max = curve1.get_u_bounds()
self.c2_min, self.c2_max = curve2.get_u_bounds()
self.tangent_delta = 0.001
@staticmethod
def build(curve1, curve2, coefficient):
if hasattr(curve1, 'lerp_to'):
try:
return curve1.lerp_to(curve2, coefficient)
except UnsupportedCurveTypeException:
pass
return SvCurveLerpCurve(curve1, curve2, coefficient)
def get_u_bounds(self):
return self.u_bounds
def evaluate(self, t):
return self.evaluate_array(np.array([t]))[0]
def evaluate_array(self, ts):
us1 = (self.c1_max - self.c1_min) * ts + self.c1_min
us2 = (self.c2_max - self.c2_min) * ts + self.c2_min
c1_points = self.curve1.evaluate_array(us1)
c2_points = self.curve2.evaluate_array(us2)
k = self.coefficient
return (1.0 - k) * c1_points + k * c2_points
class SvOffsetCurve(SvCurve):
BY_PARAMETER = 'T'
BY_LENGTH = 'L'
def __init__(self, curve, offset_vector,
offset_amount=None,
offset_curve = None, offset_curve_type = BY_PARAMETER,
algorithm=FRENET, resolution=50):
self.curve = curve
if algorithm == NORMAL_DIR and (offset_amount is None and offset_curve is None):
raise Exception("offset_amount or offset_curve is mandatory if algorithm is NORMAL_DIR")
self.offset_amount = offset_amount
self.offset_vector = offset_vector
self.offset_curve = offset_curve
self.offset_curve_type = offset_curve_type
self.algorithm = algorithm
if algorithm in {FRENET, ZERO, TRACK_NORMAL}:
self.calculator = DifferentialRotationCalculator(curve, algorithm, resolution)
if offset_curve_type == SvOffsetCurve.BY_LENGTH:
self.len_solver = SvCurveLengthSolver(curve)
self.len_solver.prepare('SPL', resolution)
self.tangent_delta = 0.001
def get_u_bounds(self):
return self.curve.get_u_bounds()
def evaluate(self, t):
return self.evaluate_array(np.array([t]))[0]
def get_matrix(self, tangent):
return MathutilsRotationCalculator.get_matrix(tangent, scale=1.0,
axis=2,
algorithm = self.algorithm,
scale_all=False)
def get_matrices(self, ts):
if self.algorithm in {FRENET, ZERO, TRACK_NORMAL}:
return self.calculator.get_matrices(ts)
elif self.algorithm in {HOUSEHOLDER, TRACK, DIFF}:
tangents = self.curve.tangent_array(ts)
matrices = np.vectorize(lambda t : self.get_matrix(t), signature='(3)->(3,3)')(tangents)
return matrices
else:
raise Exception("Unsupported algorithm")
def get_offset(self, ts):
u_min, u_max = self.curve.get_u_bounds()
if self.offset_curve is None:
if self.offset_amount is not None:
return self.offset_amount
else:
return np.linalg.norm(self.offset_vector)
elif self.offset_curve_type == SvOffsetCurve.BY_PARAMETER:
off_u_min, off_u_max = self.offset_curve.get_u_bounds()
ts = (off_u_max - off_u_min) * (ts - u_min) / (u_max - u_min) + off_u_min
ps = self.offset_curve.evaluate_array(ts)
return ps[:,1][np.newaxis].T
else:
off_u_max = self.len_solver.get_total_length()
ts = off_u_max * (ts - u_min) / (u_max - u_min)
ts = self.len_solver.solve(ts)
ps = self.offset_curve.evaluate_array(ts)
return ps[:,1][np.newaxis].T
def evaluate_array(self, ts):
n = len(ts)
t_min, t_max = self.curve.get_u_bounds()
extrusion_start = self.curve.evaluate(t_min)
extrusion_points = self.curve.evaluate_array(ts)
extrusion_vectors = extrusion_points - extrusion_start
offset_vector = self.offset_vector / np.linalg.norm(self.offset_vector)
if self.algorithm == NORMAL_DIR:
offset_vectors = np.tile(offset_vector[np.newaxis].T, n).T
tangents = self.curve.tangent_array(ts)
offset_vectors = np.cross(tangents, offset_vectors)
offset_norm = np.linalg.norm(offset_vectors, axis=1, keepdims=True)
offset_amounts = self.get_offset(ts)
offset_vectors = offset_amounts * offset_vectors / offset_norm
else:
offset_vectors = np.tile(offset_vector[np.newaxis].T, n)
matrices = self.get_matrices(ts)
offset_amounts = self.get_offset(ts)
offset_vectors = offset_amounts * (matrices @ offset_vectors)[:,:,0]
result = extrusion_vectors + offset_vectors
result = result + extrusion_start
return result
class SvCurveOnSurface(SvCurve):
def __init__(self, curve, surface, axis=0):
self.curve = curve
self.surface = surface
self.axis = axis
self.tangent_delta = 0.001
self.__description__ = "{} on {}".format(curve, surface)
def get_u_bounds(self):
return self.curve.get_u_bounds()
def evaluate(self, t):
return self.evaluate_array(np.array([t]))[0]
def evaluate_array(self, ts):
points = self.curve.evaluate_array(ts)
xs = points[:,0]
ys = points[:,1]
zs = points[:,2]
if self.axis == 0:
us = ys
vs = zs
elif self.axis == 1:
us = xs
vs = zs
elif self.axis == 2:
us = xs
vs = ys
else:
raise Exception("Unsupported orientation axis")
return self.surface.evaluate_array(us, vs)
class SvCurveOffsetOnSurface(SvCurve):
BY_PARAMETER = 'T'
BY_LENGTH = 'L'
def __init__(self, curve, surface, offset = None, offset_curve = None,
offset_curve_type = BY_PARAMETER, len_resolution = 50,
uv_space=False, axis=0):
self.curve = curve
self.surface = surface
self.offset = offset
self.offset_curve = offset_curve
self.offset_curve_type = offset_curve_type
self.uv_space = uv_space
self.z_axis = axis
self.tangent_delta = 0.001
if offset_curve_type == SvCurveOffsetOnSurface.BY_LENGTH:
self.len_solver = SvCurveLengthSolver(curve)
self.len_solver.prepare('SPL', len_resolution)
def get_u_bounds(self):
return self.curve.get_u_bounds()
def evaluate(self, t):
return self.evaluate_array(np.array([t]))[0]
def get_offset(self, ts):
u_min, u_max = self.curve.get_u_bounds()
if self.offset_curve_type == SvCurveOffsetOnSurface.BY_PARAMETER:
off_u_min, off_u_max = self.offset_curve.get_u_bounds()
ts = (off_u_max - off_u_min) * (ts - u_min) / (u_max - u_min) + off_u_min
ps = self.offset_curve.evaluate_array(ts)
return ps[:,1]
else:
off_u_max = self.len_solver.get_total_length()
ts = off_u_max * (ts - u_min) / (u_max - u_min)
ts = self.len_solver.solve(ts)
ps = self.offset_curve.evaluate_array(ts)
return ps[:,1]
def evaluate_array(self, ts):
if self.z_axis == 2:
U, V = 0, 1
elif self.z_axis == 1:
U, V = 0, 2
else:
U, V = 1, 2
uv_points = self.curve.evaluate_array(ts)
us, vs = uv_points[:,U], uv_points[:,V]
# Tangents of the curve in surface's UV space
uv_tangents = self.curve.tangent_array(ts) # (n, 3), with Z == 0 (Z is ignored anyway)
tangents_u, tangents_v = uv_tangents[:,U], uv_tangents[:,V] # (n,), (n,)
derivs = self.surface.derivatives_data_array(us, vs)
su, sv = derivs.du, derivs.dv
# Take surface's normals as N = [su, sv];
# Take curve's tangent in 3D space as T = (tangents_u * su + tangents_v * sv);
# Take a vector in surface's tangent plane, which is perpendicular to curve's
# tangent, as Nc = [N, T] (call it "curve's normal on a surface");
# Calculate Nc's decomposition in su, sv vectors as Ncu = (Nc, su) and Ncv = (Nc, sv);
# Interpret Ncu and Ncv as coordinates of Nc in surface's UV space.
# If you write down all above in formulas, you will have
#
# Nc = (Tu (Su, Sv) + Tv Sv^2) Su - (Tu Su^2 + Tv (Su, Sv)) Sv
# We could've calculate the offset as (Curve on a surface) + (offset*Nc),
# but there is no guarantee that these points will lie on the surface again
# (especially with not-so-small values of offset).
# So instead we calculate Curve + offset*(Ncu; Ncv) in UV space, and then
# map all that into 3D space.
su2 = (su*su).sum(axis=1) # (n,)
sv2 = (sv*sv).sum(axis=1) # (n,)
suv = (su*sv).sum(axis=1) # (n,)
su_norm, sv_norm = derivs.tangent_lens()
su_norm, sv_norm = su_norm.flatten(), sv_norm.flatten()
delta_u = (tangents_u*suv + tangents_v*sv2) # (n,)
delta_v = - (tangents_u*su2 + tangents_v*suv) # (n,)
delta_s = delta_u[np.newaxis].T * su + delta_v[np.newaxis].T * sv
delta_s = np.linalg.norm(delta_s, axis=1)
if self.offset_curve is None:
offset = self.offset
else:
offset = self.get_offset(ts)
res_us = us + delta_u * offset / delta_s
res_vs = vs + delta_v * offset / delta_s
if self.uv_space:
zs = np.zeros_like(us)
if self.z_axis == 2:
result = np.stack((res_us, res_vs, zs)).T
elif self.z_axis == 1:
result = np.stack((res_us, zs, res_vs)).T
else:
result = np.stack((zs, res_us, res_vs)).T
return result
else:
result = self.surface.evaluate_array(res_us, res_vs)
# Just for testing
# on_curve = self.surface.evaluate_array(us, vs)
# dvs = result - on_curve
# print(np.linalg.norm(dvs, axis=1))
return result
class SvIsoUvCurve(SvCurve):
def __init__(self, surface, fixed_axis, value, flip=False):
self.surface = surface
self.fixed_axis = fixed_axis
self.value = value
self.flip = flip
self.tangent_delta = 0.001
self.__description__ = "{} at {} = {}".format(surface, fixed_axis, value)
@staticmethod
def take(surface, fixed_axis, value, flip=False):
if hasattr(surface, 'iso_curve'):
try:
return surface.iso_curve(fixed_axis, value, flip=flip)
except UnsupportedSurfaceTypeException:
pass
return SvIsoUvCurve(surface, fixed_axis, value, flip=flip)
def get_u_bounds(self):
if self.fixed_axis == 'U':
return self.surface.get_v_min(), self.surface.get_v_max()
else:
return self.surface.get_u_min(), self.surface.get_u_max()
def evaluate(self, t):
if self.fixed_axis == 'U':
if self.flip:
t = self.surface.get_v_max() - t + self.surface.get_v_min()
return self.surface.evaluate(self.value, t)
else:
if self.flip:
t = self.surface.get_u_max() - t + self.surface.get_u_min()
return self.surface.evaluate(t, self.value)
def evaluate_array(self, ts):
if self.fixed_axis == 'U':
if self.flip:
ts = self.surface.get_v_max() - ts + self.surface.get_v_min()
return self.surface.evaluate_array(np.repeat(self.value, len(ts)), ts)
else:
if self.flip:
ts = self.surface.get_u_max() - ts + self.surface.get_u_min()
return self.surface.evaluate_array(ts, np.repeat(self.value, len(ts)))
class SvLengthRebuiltCurve(SvCurve):
def __init__(self, curve, resolution, mode='SPL', tolerance=None):
self.curve = curve
self.resolution = resolution
if hasattr(curve, 'tangent_delta'):
self.tangent_delta = curve.tangent_delta
else:
self.tangent_delta = 0.001
self.mode = mode
self.solver = SvCurveLengthSolver(curve)
self.solver.prepare(self.mode, resolution, tolerance=tolerance)
self.u_bounds = (0.0, self.solver.get_total_length())
self.__description__ = "{} rebuilt".format(curve)
def get_u_bounds(self):
return self.u_bounds
def evaluate(self, t):
c_ts = self.solver.solve(np.array([t]))
return self.curve.evaluate(c_ts[0])
def evaluate_array(self, ts):
c_ts = self.solver.solve(ts)
return self.curve.evaluate_array(c_ts)
def curve_frame_on_surface_array(surface, uv_curve, us, w_axis=2, normalize=True, on_zero_curvature=SvCurve.ASIS):
"""
Curve frame which is lying in the surface.
Frame is oriented as follows:
* X is pointing along surface normal
* Z is pointing along curve tangent
* Y is perpendicular to both X and Z.
Args:
surface: source surface
uv_curve: uv_curve in the surface's UV space
us: values of curve's T parameter; type: np.array of shape (n,)
w_axis: defines which axis of the curve is surface's normal (two
other axes are surface's U and V). Default of 2 means X is U and Y is V.
Returns:
tuple:
* matrices: np.array of shape (n, 3, 3)
* points: np.array of shape (n, 3) - points on the surface
* tangents: np.array of shape (n, 3)
* normals: np.array of shape (n, 3)
* binormals: np.array of shape (n, 3)
"""
if w_axis == 2:
U, V = 0, 1
elif w_axis == 1:
U, V = 0, 2
else:
U, V = 1, 2
uv_points = uv_curve.evaluate_array(us)
curve = SvCurveOnSurface(uv_curve, surface, axis=w_axis)
surf_points = curve.evaluate_array(us)
tangents = curve.tangent_array(us)
if normalize:
tangents = tangents / np.linalg.norm(tangents, axis=1, keepdims=True)
us, vs = uv_points[:,U], uv_points[:,V]
normals = surface.normal_array(us, vs)
if normalize:
normals = normals / np.linalg.norm(normals, axis=1, keepdims=True)
if on_zero_curvature != SvCurve.ASIS:
zero_normal = np.linalg.norm(normals, axis=1) < 1e-6
if zero_normal.any():
if on_zero_curvature == SvCurve.FAIL:
raise ZeroCurvatureException(np.unique(ts[zero_normal]), zero_normal)
elif on_zero_curvature == SvCurve.RETURN_NONE:
return None
binormals = - np.cross(normals, tangents)
matrices_np = np.dstack((normals, binormals, tangents))
matrices_np = np.transpose(matrices_np, axes=(0,2,1))
matrices_np = np.linalg.inv(matrices_np)
return matrices_np, surf_points, tangents, normals, binormals
def unify_curves_degree(curves):
"""
Make sure that all curves have the same degree, by
elevating degree where necessary.
Assumes all curves have get_degree() and elevate_degree() methods.
Can raise UnsupportedCurveTypeException if some degrees can not be elevated.
Args:
curves: list of SvCurve
Returns:
list of SvCurve
"""
max_degree = max(curve.get_degree() for curve in curves)
curves = [curve.elevate_degree(target=max_degree) for curve in curves]
return curves
def concatenate_curves(curves, scale_to_unit=False, allow_generic=True):
"""
Concatenate a list of curves. When possible, use `concatenate` method of
curves to make a "native" concatenation - for example, make one Nurbs out of
several Nurbs.
Args:
curves: list of SvCurve
scale_to_unit: if specified, reparametrize each curve to [0; 1] before concatenation.
allow_generic: what to do if it is not possible to concatenate curves natively:
* True - use generic SvConcatCurve
* False - raise an Exception.
Returns:
an instance of SvCurve.
"""
if not curves:
raise Exception("List of curves must be not empty")
if scale_to_unit:
result = [reparametrize_curve(curves[0])]
else:
result = [curves[0]]
some_native = False
exceptions = []
for idx, curve in enumerate(curves[1:]):
new_curve = None
ok = False
if hasattr(result[-1], 'concatenate'):
try:
if scale_to_unit:
# P.1: try to join with rescaled curve
new_curve = result[-1].concatenate(reparametrize_curve(curve))
else:
new_curve = result[-1].concatenate(curve)
some_native = True
ok = True
except UnsupportedCurveTypeException as e:
exceptions.append(e)
# "concatenate" method can't work with this type of curve
sv_logger.info("Can't natively join curve #%s (%s), will use generic method: %s", idx+1, curve, e)
# P.2: if some curves were already joined natively,
# then we have to rescale each of other curves separately
if some_native and scale_to_unit:
curve = reparametrize_curve(curve)
#print(f"C: {curve}, prev: {result[-1]}, ok: {ok}, new: {new_curve}")
if ok:
result[-1] = new_curve
else:
result.append(curve)
if len(result) == 1:
return result[0]
else:
if allow_generic:
# if any of curves were scaled while joining natively (at P.1),
# then all other were scaled at P.2;
# if no successful joins were made, then we can rescale all curves
# at once.
return SvConcatCurve(result, scale_to_unit and not some_native)
else:
err_msg = "\n".join([str(e) for e in exceptions])
raise Exception(f"Could not join some curves natively. Result is: {result}.\nErrors were:\n{err_msg}")
class SvCurvesSortResult(object):
"""
Result of `sort_curves_for_concat` method.
"""
def __init__(self):
self.curves = []
self.indexes = []
self.flips = []
self.sum_error = 0
def sort_curves_for_concat(curves, allow_flip=False):
"""
Sort list of curves so that they could be concatenated into one curve.
Args:
curves: list of SvCurve to be sorted.
allow_flip: if True, then the method will be allowed to flip (reverse)
some of the curves.
Returns:
an instance of `SvCurvesSortResult`.
"""
if not curves:
return curves
def calc_error(c1, c2):
c1end = c1[1]
c2begin = c2[0]
dc = c1end - c2begin
d = (dc * dc).sum()
return d
def select_next(last_pair, pairs, other_idxs):
min_error = None
best_idx = None
best_flip = False
if allow_flip:
combinations = [(flip, idx) for idx in other_idxs for flip in [False, True]]
else:
combinations = [(False, idx) for idx in other_idxs]
for flip, idx in combinations:
start, end = pairs[idx]
if flip:
start, end = end, start
error = calc_error(last_pair, (start, end))
if min_error is None or error < min_error:
min_error = error
best_idx = idx
best_flip = flip
return min_error, best_idx, best_flip
pairs = []
for curve in curves:
u_min, u_max = curve.get_u_bounds()
begin = curve.evaluate(u_min)
end = curve.evaluate(u_max)
pairs.append((begin, end))
all_idxs = list(range(len(curves)))
result_idxs = [0]
result_flips = [False]
last_pair = pairs[0]
rest_idxs = all_idxs[1:]
result = SvCurvesSortResult()
while rest_idxs:
error, next_idx, next_flip = select_next(last_pair, pairs, rest_idxs)
rest_idxs.remove(next_idx)
result_idxs.append(next_idx)
result_flips.append(next_flip)
last_pair = pairs[next_idx]
result.sum_error += error
if next_flip:
last_pair = last_pair[1], last_pair[0]
for idx, flip in zip(result_idxs, result_flips):
result.indexes.append(idx)
result.flips.append(flip)
curve = curves[idx]
if flip:
curve = reverse_curve(curve)
result.curves.append(curve)
return result
def reparametrize_curve(curve, new_t_min=0.0, new_t_max=1.0):
"""
Reparametrize the curve to new domain.
"""
t_min, t_max = curve.get_u_bounds()
if t_min == new_t_min and t_max == new_t_max:
return curve
if hasattr(curve, 'reparametrize'):
return curve.reparametrize(new_t_min, new_t_max)
else:
return SvReparametrizedCurve(curve, new_t_min, new_t_max)
def reverse_curve(curve):
"""
Reverse the curve, i.e. reverse the direction of it's parametrization.
"""
if hasattr(curve, 'reverse'):
return curve.reverse()
else:
return SvFlipCurve(curve)
def split_curve(curve, splits, rescale=False):
"""
Split one curve into several segments.
Args:
curve: SvCurve to be split.
splits: number of segments you want to receive.
rescale: if True, then each of segments will be reparametrized to
[0; 1] domain.
Returns:
a list of SvCurve.
"""
if hasattr(curve, 'split_at'):
result = []
for split in splits:
head, tail = curve.split_at(split)
if rescale:
head = reparametrize_curve(head, 0, 1)
result.append(head)
curve = tail
if rescale:
tail = reparametrize_curve(tail, 0, 1)
result.append(tail)
return result
else:
t_min, t_max = curve.get_u_bounds()
if splits[0] != t_min:
splits.insert(0, t_min)
if splits[-1] != t_max:
splits.append(t_max)
pairs = zip(splits, splits[1:])
result = []
for start, end in pairs:
segment = SvCurveSegment(curve, start, end, rescale)
result.append(segment)
return result
def curve_segment(curve, new_t_min, new_t_max, use_native=True, rescale=False):
"""
Cut a segment out of the curve.
"""
t_min, t_max = curve.get_u_bounds()
if use_native and hasattr(curve, 'cut_segment'):
return curve.cut_segment(new_t_min, new_t_max, rescale=rescale)
elif use_native and hasattr(curve, 'split_at') and (new_t_min > t_min or new_t_max < t_max):
if new_t_min > t_min:
start, curve = curve.split_at(new_t_min)
if new_t_max < t_max:
curve, end = curve.split_at(new_t_max)
if rescale:
curve = reparametrize_curve(curve, 0, 1)
return curve
else:
return SvCurveSegment(curve, new_t_min, new_t_max, rescale)
Functions
def concatenate_curves(curves, scale_to_unit=False, allow_generic=True)
-
Concatenate a list of curves. When possible, use
concatenate
method of curves to make a "native" concatenation - for example, make one Nurbs out of several Nurbs.Args
curves
- list of SvCurve
scale_to_unit
- if specified, reparametrize each curve to [0; 1] before concatenation.
allow_generic
- what to do if it is not possible to concatenate curves natively: * True - use generic SvConcatCurve * False - raise an Exception.
Returns
an instance of SvCurve.
Expand source code
def concatenate_curves(curves, scale_to_unit=False, allow_generic=True): """ Concatenate a list of curves. When possible, use `concatenate` method of curves to make a "native" concatenation - for example, make one Nurbs out of several Nurbs. Args: curves: list of SvCurve scale_to_unit: if specified, reparametrize each curve to [0; 1] before concatenation. allow_generic: what to do if it is not possible to concatenate curves natively: * True - use generic SvConcatCurve * False - raise an Exception. Returns: an instance of SvCurve. """ if not curves: raise Exception("List of curves must be not empty") if scale_to_unit: result = [reparametrize_curve(curves[0])] else: result = [curves[0]] some_native = False exceptions = [] for idx, curve in enumerate(curves[1:]): new_curve = None ok = False if hasattr(result[-1], 'concatenate'): try: if scale_to_unit: # P.1: try to join with rescaled curve new_curve = result[-1].concatenate(reparametrize_curve(curve)) else: new_curve = result[-1].concatenate(curve) some_native = True ok = True except UnsupportedCurveTypeException as e: exceptions.append(e) # "concatenate" method can't work with this type of curve sv_logger.info("Can't natively join curve #%s (%s), will use generic method: %s", idx+1, curve, e) # P.2: if some curves were already joined natively, # then we have to rescale each of other curves separately if some_native and scale_to_unit: curve = reparametrize_curve(curve) #print(f"C: {curve}, prev: {result[-1]}, ok: {ok}, new: {new_curve}") if ok: result[-1] = new_curve else: result.append(curve) if len(result) == 1: return result[0] else: if allow_generic: # if any of curves were scaled while joining natively (at P.1), # then all other were scaled at P.2; # if no successful joins were made, then we can rescale all curves # at once. return SvConcatCurve(result, scale_to_unit and not some_native) else: err_msg = "\n".join([str(e) for e in exceptions]) raise Exception(f"Could not join some curves natively. Result is: {result}.\nErrors were:\n{err_msg}")
def curve_frame_on_surface_array(surface, uv_curve, us, w_axis=2, normalize=True, on_zero_curvature='asis')
-
Curve frame which is lying in the surface.
Frame is oriented as follows: * X is pointing along surface normal * Z is pointing along curve tangent * Y is perpendicular to both X and Z.
Args
surface
- source surface
uv_curve
- uv_curve in the surface's UV space
us
- values of curve's T parameter; type: np.array of shape (n,)
w_axis
- defines which axis of the curve is surface's normal (two other axes are surface's U and V). Default of 2 means X is U and Y is V.
Returns
tuple: * matrices: np.array of shape (n, 3, 3) * points: np.array of shape (n, 3) - points on the surface * tangents: np.array of shape (n, 3) * normals: np.array of shape (n, 3) * binormals: np.array of shape (n, 3)
Expand source code
def curve_frame_on_surface_array(surface, uv_curve, us, w_axis=2, normalize=True, on_zero_curvature=SvCurve.ASIS): """ Curve frame which is lying in the surface. Frame is oriented as follows: * X is pointing along surface normal * Z is pointing along curve tangent * Y is perpendicular to both X and Z. Args: surface: source surface uv_curve: uv_curve in the surface's UV space us: values of curve's T parameter; type: np.array of shape (n,) w_axis: defines which axis of the curve is surface's normal (two other axes are surface's U and V). Default of 2 means X is U and Y is V. Returns: tuple: * matrices: np.array of shape (n, 3, 3) * points: np.array of shape (n, 3) - points on the surface * tangents: np.array of shape (n, 3) * normals: np.array of shape (n, 3) * binormals: np.array of shape (n, 3) """ if w_axis == 2: U, V = 0, 1 elif w_axis == 1: U, V = 0, 2 else: U, V = 1, 2 uv_points = uv_curve.evaluate_array(us) curve = SvCurveOnSurface(uv_curve, surface, axis=w_axis) surf_points = curve.evaluate_array(us) tangents = curve.tangent_array(us) if normalize: tangents = tangents / np.linalg.norm(tangents, axis=1, keepdims=True) us, vs = uv_points[:,U], uv_points[:,V] normals = surface.normal_array(us, vs) if normalize: normals = normals / np.linalg.norm(normals, axis=1, keepdims=True) if on_zero_curvature != SvCurve.ASIS: zero_normal = np.linalg.norm(normals, axis=1) < 1e-6 if zero_normal.any(): if on_zero_curvature == SvCurve.FAIL: raise ZeroCurvatureException(np.unique(ts[zero_normal]), zero_normal) elif on_zero_curvature == SvCurve.RETURN_NONE: return None binormals = - np.cross(normals, tangents) matrices_np = np.dstack((normals, binormals, tangents)) matrices_np = np.transpose(matrices_np, axes=(0,2,1)) matrices_np = np.linalg.inv(matrices_np) return matrices_np, surf_points, tangents, normals, binormals
def curve_segment(curve, new_t_min, new_t_max, use_native=True, rescale=False)
-
Cut a segment out of the curve.
Expand source code
def curve_segment(curve, new_t_min, new_t_max, use_native=True, rescale=False): """ Cut a segment out of the curve. """ t_min, t_max = curve.get_u_bounds() if use_native and hasattr(curve, 'cut_segment'): return curve.cut_segment(new_t_min, new_t_max, rescale=rescale) elif use_native and hasattr(curve, 'split_at') and (new_t_min > t_min or new_t_max < t_max): if new_t_min > t_min: start, curve = curve.split_at(new_t_min) if new_t_max < t_max: curve, end = curve.split_at(new_t_max) if rescale: curve = reparametrize_curve(curve, 0, 1) return curve else: return SvCurveSegment(curve, new_t_min, new_t_max, rescale)
def make_euclidean_ts(pts)
-
Expand source code
def make_euclidean_ts(pts): tmp = np.linalg.norm(pts[:-1] - pts[1:], axis=1) tknots = np.insert(tmp, 0, 0).cumsum() tknots = tknots / tknots[-1] return tknots
def reparametrize_curve(curve, new_t_min=0.0, new_t_max=1.0)
-
Reparametrize the curve to new domain.
Expand source code
def reparametrize_curve(curve, new_t_min=0.0, new_t_max=1.0): """ Reparametrize the curve to new domain. """ t_min, t_max = curve.get_u_bounds() if t_min == new_t_min and t_max == new_t_max: return curve if hasattr(curve, 'reparametrize'): return curve.reparametrize(new_t_min, new_t_max) else: return SvReparametrizedCurve(curve, new_t_min, new_t_max)
def reverse_curve(curve)
-
Reverse the curve, i.e. reverse the direction of it's parametrization.
Expand source code
def reverse_curve(curve): """ Reverse the curve, i.e. reverse the direction of it's parametrization. """ if hasattr(curve, 'reverse'): return curve.reverse() else: return SvFlipCurve(curve)
def sort_curves_for_concat(curves, allow_flip=False)
-
Sort list of curves so that they could be concatenated into one curve.
Args
curves
- list of SvCurve to be sorted.
allow_flip
- if True, then the method will be allowed to flip (reverse) some of the curves.
Returns
an instance of
SvCurvesSortResult
.Expand source code
def sort_curves_for_concat(curves, allow_flip=False): """ Sort list of curves so that they could be concatenated into one curve. Args: curves: list of SvCurve to be sorted. allow_flip: if True, then the method will be allowed to flip (reverse) some of the curves. Returns: an instance of `SvCurvesSortResult`. """ if not curves: return curves def calc_error(c1, c2): c1end = c1[1] c2begin = c2[0] dc = c1end - c2begin d = (dc * dc).sum() return d def select_next(last_pair, pairs, other_idxs): min_error = None best_idx = None best_flip = False if allow_flip: combinations = [(flip, idx) for idx in other_idxs for flip in [False, True]] else: combinations = [(False, idx) for idx in other_idxs] for flip, idx in combinations: start, end = pairs[idx] if flip: start, end = end, start error = calc_error(last_pair, (start, end)) if min_error is None or error < min_error: min_error = error best_idx = idx best_flip = flip return min_error, best_idx, best_flip pairs = [] for curve in curves: u_min, u_max = curve.get_u_bounds() begin = curve.evaluate(u_min) end = curve.evaluate(u_max) pairs.append((begin, end)) all_idxs = list(range(len(curves))) result_idxs = [0] result_flips = [False] last_pair = pairs[0] rest_idxs = all_idxs[1:] result = SvCurvesSortResult() while rest_idxs: error, next_idx, next_flip = select_next(last_pair, pairs, rest_idxs) rest_idxs.remove(next_idx) result_idxs.append(next_idx) result_flips.append(next_flip) last_pair = pairs[next_idx] result.sum_error += error if next_flip: last_pair = last_pair[1], last_pair[0] for idx, flip in zip(result_idxs, result_flips): result.indexes.append(idx) result.flips.append(flip) curve = curves[idx] if flip: curve = reverse_curve(curve) result.curves.append(curve) return result
def split_curve(curve, splits, rescale=False)
-
Split one curve into several segments.
Args
curve
- SvCurve to be split.
splits
- number of segments you want to receive.
rescale
- if True, then each of segments will be reparametrized to [0; 1] domain.
Returns
a list of SvCurve.
Expand source code
def split_curve(curve, splits, rescale=False): """ Split one curve into several segments. Args: curve: SvCurve to be split. splits: number of segments you want to receive. rescale: if True, then each of segments will be reparametrized to [0; 1] domain. Returns: a list of SvCurve. """ if hasattr(curve, 'split_at'): result = [] for split in splits: head, tail = curve.split_at(split) if rescale: head = reparametrize_curve(head, 0, 1) result.append(head) curve = tail if rescale: tail = reparametrize_curve(tail, 0, 1) result.append(tail) return result else: t_min, t_max = curve.get_u_bounds() if splits[0] != t_min: splits.insert(0, t_min) if splits[-1] != t_max: splits.append(t_max) pairs = zip(splits, splits[1:]) result = [] for start, end in pairs: segment = SvCurveSegment(curve, start, end, rescale) result.append(segment) return result
def unify_curves_degree(curves)
-
Make sure that all curves have the same degree, by elevating degree where necessary. Assumes all curves have get_degree() and elevate_degree() methods. Can raise UnsupportedCurveTypeException if some degrees can not be elevated.
Args
curves
- list of SvCurve
Returns
list of SvCurve
Expand source code
def unify_curves_degree(curves): """ Make sure that all curves have the same degree, by elevating degree where necessary. Assumes all curves have get_degree() and elevate_degree() methods. Can raise UnsupportedCurveTypeException if some degrees can not be elevated. Args: curves: list of SvCurve Returns: list of SvCurve """ max_degree = max(curve.get_degree() for curve in curves) curves = [curve.elevate_degree(target=max_degree) for curve in curves] return curves
Classes
class DifferentialRotationCalculator (curve, algorithm, resolution=50)
-
Expand source code
class DifferentialRotationCalculator(object): def __init__(self, curve, algorithm, resolution=50): self.curve = curve self.algorithm = algorithm if algorithm == TRACK_NORMAL: self.normal_tracker = SvNormalTrack(curve, resolution) elif algorithm == ZERO: self.curve.pre_calc_torsion_integral(resolution) def get_matrices(self, ts): n = len(ts) if self.algorithm == FRENET: frenet, _ , _ = self.curve.frame_array(ts) return frenet elif self.algorithm == ZERO: frenet, _ , _ = self.curve.frame_array(ts) angles = - self.curve.torsion_integral(ts) zeros = np.zeros((n,)) ones = np.ones((n,)) row1 = np.stack((np.cos(angles), np.sin(angles), zeros)).T # (n, 3) row2 = np.stack((-np.sin(angles), np.cos(angles), zeros)).T # (n, 3) row3 = np.stack((zeros, zeros, ones)).T # (n, 3) rotation_matrices = np.dstack((row1, row2, row3)) return frenet @ rotation_matrices elif self.algorithm == TRACK_NORMAL: matrices = self.normal_tracker.evaluate_array(ts) return matrices else: raise Exception("Unsupported algorithm")
Methods
def get_matrices(self, ts)
-
Expand source code
def get_matrices(self, ts): n = len(ts) if self.algorithm == FRENET: frenet, _ , _ = self.curve.frame_array(ts) return frenet elif self.algorithm == ZERO: frenet, _ , _ = self.curve.frame_array(ts) angles = - self.curve.torsion_integral(ts) zeros = np.zeros((n,)) ones = np.ones((n,)) row1 = np.stack((np.cos(angles), np.sin(angles), zeros)).T # (n, 3) row2 = np.stack((-np.sin(angles), np.cos(angles), zeros)).T # (n, 3) row3 = np.stack((zeros, zeros, ones)).T # (n, 3) rotation_matrices = np.dstack((row1, row2, row3)) return frenet @ rotation_matrices elif self.algorithm == TRACK_NORMAL: matrices = self.normal_tracker.evaluate_array(ts) return matrices else: raise Exception("Unsupported algorithm")
class MathutilsRotationCalculator
-
Expand source code
class MathutilsRotationCalculator(object): @classmethod def get_matrix(cls, tangent, scale, axis, algorithm, scale_all=True, up_axis='X'): """ Calculate matrix required to rotate object according to `tangent` vector. Args: tangent: np.array of shape (3,) scale: float axis: int, 0 - X, 1 - Y, 2 - Z algorithm: one of HOUSEHOLDER, TRACK, DIFF scale_all: True to scale along all axes, False to scale along tangent only up_axis: string, "X", "Y" or "Z", for algorithm == TRACK only. Returns: np.array of shape (3,3). """ x = Vector((1.0, 0.0, 0.0)) y = Vector((0.0, 1.0, 0.0)) z = Vector((0.0, 0.0, 1.0)) if axis == 0: ax1, ax2, ax3 = x, y, z elif axis == 1: ax1, ax2, ax3 = y, x, z else: ax1, ax2, ax3 = z, x, y if scale_all: scale_matrix = Matrix.Scale(1/scale, 4, ax1) @ Matrix.Scale(scale, 4, ax2) @ Matrix.Scale(scale, 4, ax3) else: scale_matrix = Matrix.Scale(1/scale, 4, ax1) scale_matrix = np.array(scale_matrix.to_3x3()) tangent = Vector(tangent) if algorithm == HOUSEHOLDER: rot = autorotate_householder(ax1, tangent).inverted() elif algorithm == TRACK: axis = "XYZ"[axis] rot = autorotate_track(axis, tangent, up_axis) elif algorithm == DIFF: rot = autorotate_diff(tangent, ax1) else: raise Exception("Unsupported algorithm") rot = np.array(rot.to_3x3()) return np.matmul(rot, scale_matrix)
Static methods
def get_matrix(tangent, scale, axis, algorithm, scale_all=True, up_axis='X')
-
Calculate matrix required to rotate object according to
tangent
vector.Args
tangent
- np.array of shape (3,)
scale
- float
axis
- int, 0 - X, 1 - Y, 2 - Z
algorithm
- one of HOUSEHOLDER, TRACK, DIFF
scale_all
- True to scale along all axes, False to scale along tangent only
up_axis
- string, "X", "Y" or "Z", for algorithm == TRACK only.
Returns
np.array of shape (3,3).
Expand source code
@classmethod def get_matrix(cls, tangent, scale, axis, algorithm, scale_all=True, up_axis='X'): """ Calculate matrix required to rotate object according to `tangent` vector. Args: tangent: np.array of shape (3,) scale: float axis: int, 0 - X, 1 - Y, 2 - Z algorithm: one of HOUSEHOLDER, TRACK, DIFF scale_all: True to scale along all axes, False to scale along tangent only up_axis: string, "X", "Y" or "Z", for algorithm == TRACK only. Returns: np.array of shape (3,3). """ x = Vector((1.0, 0.0, 0.0)) y = Vector((0.0, 1.0, 0.0)) z = Vector((0.0, 0.0, 1.0)) if axis == 0: ax1, ax2, ax3 = x, y, z elif axis == 1: ax1, ax2, ax3 = y, x, z else: ax1, ax2, ax3 = z, x, y if scale_all: scale_matrix = Matrix.Scale(1/scale, 4, ax1) @ Matrix.Scale(scale, 4, ax2) @ Matrix.Scale(scale, 4, ax3) else: scale_matrix = Matrix.Scale(1/scale, 4, ax1) scale_matrix = np.array(scale_matrix.to_3x3()) tangent = Vector(tangent) if algorithm == HOUSEHOLDER: rot = autorotate_householder(ax1, tangent).inverted() elif algorithm == TRACK: axis = "XYZ"[axis] rot = autorotate_track(axis, tangent, up_axis) elif algorithm == DIFF: rot = autorotate_diff(tangent, ax1) else: raise Exception("Unsupported algorithm") rot = np.array(rot.to_3x3()) return np.matmul(rot, scale_matrix)
class SvCastCurveToCylinder (curve, center, direction, radius, coefficient)
-
Expand source code
class SvCastCurveToCylinder(SvCurve): def __init__(self, curve, center, direction, radius, coefficient): self.curve = curve self.center = center self.direction = direction self.radius = radius self.coefficient = coefficient self.line = LineEquation.from_direction_and_point(direction, center) self.tangent_delta = 0.001 self.__description__ = "{} casted to Cylinder".format(curve) def evaluate(self, t): point = self.curve.evaluate(t) projection_to_line = self.line.projection_of_point(point) projection_to_line = np.array(projection_to_line) radial = point - projection_to_line radius = self.radius * radial / np.linalg.norm(radial) projection = projection_to_line + radius k = self.coefficient return (1 - k) * point + k * projection def evaluate_array(self, ts): points = self.curve.evaluate_array(ts) projection_to_line = self.line.projection_of_points(points) radial = points - projection_to_line radius = self.radius * radial / np.linalg.norm(radial, axis=1, keepdims=True) projections = projection_to_line + radius k = self.coefficient return (1 - k) * points + k * projections def get_u_bounds(self): return self.curve.get_u_bounds()
Ancestors
Inherited members
class SvCastCurveToPlane (curve, point, normal, coefficient)
-
Expand source code
class SvCastCurveToPlane(SvCurve): def __init__(self, curve, point, normal, coefficient): self.curve = curve self.point = point self.normal = normal self.coefficient = coefficient self.plane = PlaneEquation.from_normal_and_point(normal, point) self.tangent_delta = 0.001 self.__description__ = "{} casted to Plane".format(curve) def evaluate(self, t): point = self.curve.evaluate(t) target = np.array(self.plane.projection_of_point(point)) k = self.coefficient return (1 - k) * point + k * target def evaluate_array(self, ts): points = self.curve.evaluate_array(ts) targets = self.plane.projection_of_points(points) k = self.coefficient return (1 - k) * points + k * targets def get_u_bounds(self): return self.curve.get_u_bounds()
Ancestors
Inherited members
class SvCastCurveToSphere (curve, center, radius, coefficient)
-
Expand source code
class SvCastCurveToSphere(SvCurve): def __init__(self, curve, center, radius, coefficient): self.curve = curve self.center = center self.radius = radius self.coefficient = coefficient self.tangent_delta = 0.001 self.__description__ = "{} casted to Sphere".format(curve) def evaluate(self, t): return self.evaluate_array(np.array([t]))[0] def evaluate_array(self, ts): points = self.curve.evaluate_array(ts) centered_points = points - self.center norms = np.linalg.norm(centered_points, axis=1)[np.newaxis].T normalized = centered_points / norms targets = self.radius * normalized + self.center k = self.coefficient return (1 - k) * points + k * targets def get_u_bounds(self): return self.curve.get_u_bounds()
Ancestors
Inherited members
class SvCurveFrameCalculator (curve, algorithm, z_axis=2, resolution=50, normal=None)
-
Expand source code
class SvCurveFrameCalculator(object): def __init__(self, curve, algorithm, z_axis=2, resolution=50, normal=None): self.algorithm = algorithm self.z_axis = z_axis self.curve = curve self.normal = normal if algorithm in {FRENET, ZERO, TRACK_NORMAL}: self.calculator = DifferentialRotationCalculator(curve, algorithm, resolution) def get_matrix(self, tangent): return MathutilsRotationCalculator.get_matrix(tangent, scale=1.0, axis=self.z_axis, algorithm = self.algorithm, scale_all=False) def get_matrices(self, ts): if self.algorithm == NONE: identity = np.eye(3) n = len(ts) return np.broadcast_to(identity, (n, 3,3)) elif self.algorithm == NORMAL_DIR: matrices, _, _ = self.curve.frame_by_plane_array(ts, self.normal) return matrices elif self.algorithm in {FRENET, ZERO, TRACK_NORMAL}: return self.calculator.get_matrices(ts) elif self.algorithm in {HOUSEHOLDER, TRACK, DIFF}: tangents = self.curve.tangent_array(ts) matrices = np.vectorize(lambda t : self.get_matrix(t), signature='(3)->(3,3)')(tangents) return matrices else: raise Exception("Unsupported algorithm")
Methods
def get_matrices(self, ts)
-
Expand source code
def get_matrices(self, ts): if self.algorithm == NONE: identity = np.eye(3) n = len(ts) return np.broadcast_to(identity, (n, 3,3)) elif self.algorithm == NORMAL_DIR: matrices, _, _ = self.curve.frame_by_plane_array(ts, self.normal) return matrices elif self.algorithm in {FRENET, ZERO, TRACK_NORMAL}: return self.calculator.get_matrices(ts) elif self.algorithm in {HOUSEHOLDER, TRACK, DIFF}: tangents = self.curve.tangent_array(ts) matrices = np.vectorize(lambda t : self.get_matrix(t), signature='(3)->(3,3)')(tangents) return matrices else: raise Exception("Unsupported algorithm")
def get_matrix(self, tangent)
-
Expand source code
def get_matrix(self, tangent): return MathutilsRotationCalculator.get_matrix(tangent, scale=1.0, axis=self.z_axis, algorithm = self.algorithm, scale_all=False)
class SvCurveLengthSolver (curve)
-
Expand source code
class SvCurveLengthSolver(object): def __init__(self, curve): self.curve = curve self._reverse_spline = None self._prime_spline = None def calc_length_segments(self, tknots): vectors = self.curve.evaluate_array(tknots) dvs = vectors[1:] - vectors[:-1] lengths = np.linalg.norm(dvs, axis=1) return lengths def get_total_length(self): if self._reverse_spline is None: raise Exception("You have to call solver.prepare() first") return self._length_params[-1] def _calc_tknots_fixed(self, resolution): t_min, t_max = self.curve.get_u_bounds() tknots = np.linspace(t_min, t_max, num=resolution) return tknots def _prepare_find(self, resolution, tolerance, tknots=None, lengths=None, length_params=None): if tknots is None: tknots = self._calc_tknots_fixed(resolution) if lengths is None: lengths = self.calc_length_segments(tknots) if length_params is None: length_params = np.cumsum(np.insert(lengths, 0, 0)) resolution2 = resolution * 2 - 1 tknots2 = self._calc_tknots_fixed(resolution2) lengths2 = self.calc_length_segments(tknots2) length_params2 = np.cumsum(np.insert(lengths2, 0, 0)) dl = abs(length_params2[::2] - length_params) if (dl < tolerance).all(): return tknots2, length_params2 else: return self._prepare_find(resolution2, tolerance, tknots2, lengths2, length_params2) def prepare(self, mode, resolution=50, tolerance=None): if tolerance is None: tknots = self._calc_tknots_fixed(resolution) lengths = self.calc_length_segments(tknots) self._length_params = np.cumsum(np.insert(lengths, 0, 0)) else: tknots, self._length_params = self._prepare_find(resolution, tolerance) self._reverse_spline = self._make_spline(mode, tknots, self._length_params) self._prime_spline = self._make_spline(mode, self._length_params, tknots) def _make_spline(self, mode, tknots, values): zeros = np.zeros(len(tknots)) control_points = np.vstack((values, tknots, zeros)).T if mode == 'LIN': spline = LinearSpline(control_points, tknots = values, is_cyclic = False) elif mode == 'SPL': spline = CubicSpline(control_points, tknots = values, is_cyclic = False) else: raise Exception("Unsupported mode; supported are LIN and SPL.") return spline def calc_length(self, t_min, t_max): if self._prime_spline is None: raise Exception("You have to call solver.prepare() first") lengths = self._prime_spline.eval(np.array([t_min, t_max])) return lengths[1][1] - lengths[0][1] def calc_length_params(self, ts): if self._prime_spline is None: raise Exception("You have to call solver.prepare() first") spline_verts = self._prime_spline.eval(ts) return spline_verts[:,1] def solve(self, input_lengths): if self._reverse_spline is None: raise Exception("You have to call solver.prepare() first") spline_verts = self._reverse_spline.eval(input_lengths) return spline_verts[:,1]
Subclasses
Methods
def calc_length(self, t_min, t_max)
-
Expand source code
def calc_length(self, t_min, t_max): if self._prime_spline is None: raise Exception("You have to call solver.prepare() first") lengths = self._prime_spline.eval(np.array([t_min, t_max])) return lengths[1][1] - lengths[0][1]
def calc_length_params(self, ts)
-
Expand source code
def calc_length_params(self, ts): if self._prime_spline is None: raise Exception("You have to call solver.prepare() first") spline_verts = self._prime_spline.eval(ts) return spline_verts[:,1]
def calc_length_segments(self, tknots)
-
Expand source code
def calc_length_segments(self, tknots): vectors = self.curve.evaluate_array(tknots) dvs = vectors[1:] - vectors[:-1] lengths = np.linalg.norm(dvs, axis=1) return lengths
def get_total_length(self)
-
Expand source code
def get_total_length(self): if self._reverse_spline is None: raise Exception("You have to call solver.prepare() first") return self._length_params[-1]
def prepare(self, mode, resolution=50, tolerance=None)
-
Expand source code
def prepare(self, mode, resolution=50, tolerance=None): if tolerance is None: tknots = self._calc_tknots_fixed(resolution) lengths = self.calc_length_segments(tknots) self._length_params = np.cumsum(np.insert(lengths, 0, 0)) else: tknots, self._length_params = self._prepare_find(resolution, tolerance) self._reverse_spline = self._make_spline(mode, tknots, self._length_params) self._prime_spline = self._make_spline(mode, self._length_params, tknots)
def solve(self, input_lengths)
-
Expand source code
def solve(self, input_lengths): if self._reverse_spline is None: raise Exception("You have to call solver.prepare() first") spline_verts = self._reverse_spline.eval(input_lengths) return spline_verts[:,1]
class SvCurveLerpCurve (curve1, curve2, coefficient)
-
Expand source code
class SvCurveLerpCurve(SvCurve): __description__ = "Lerp" def __init__(self, curve1, curve2, coefficient): self.curve1 = curve1 self.curve2 = curve2 self.coefficient = coefficient self.u_bounds = (0.0, 1.0) self.c1_min, self.c1_max = curve1.get_u_bounds() self.c2_min, self.c2_max = curve2.get_u_bounds() self.tangent_delta = 0.001 @staticmethod def build(curve1, curve2, coefficient): if hasattr(curve1, 'lerp_to'): try: return curve1.lerp_to(curve2, coefficient) except UnsupportedCurveTypeException: pass return SvCurveLerpCurve(curve1, curve2, coefficient) def get_u_bounds(self): return self.u_bounds def evaluate(self, t): return self.evaluate_array(np.array([t]))[0] def evaluate_array(self, ts): us1 = (self.c1_max - self.c1_min) * ts + self.c1_min us2 = (self.c2_max - self.c2_min) * ts + self.c2_min c1_points = self.curve1.evaluate_array(us1) c2_points = self.curve2.evaluate_array(us2) k = self.coefficient return (1.0 - k) * c1_points + k * c2_points
Ancestors
Static methods
def build(curve1, curve2, coefficient)
-
Expand source code
@staticmethod def build(curve1, curve2, coefficient): if hasattr(curve1, 'lerp_to'): try: return curve1.lerp_to(curve2, coefficient) except UnsupportedCurveTypeException: pass return SvCurveLerpCurve(curve1, curve2, coefficient)
Inherited members
class SvCurveOffsetOnSurface (curve, surface, offset=None, offset_curve=None, offset_curve_type='T', len_resolution=50, uv_space=False, axis=0)
-
Expand source code
class SvCurveOffsetOnSurface(SvCurve): BY_PARAMETER = 'T' BY_LENGTH = 'L' def __init__(self, curve, surface, offset = None, offset_curve = None, offset_curve_type = BY_PARAMETER, len_resolution = 50, uv_space=False, axis=0): self.curve = curve self.surface = surface self.offset = offset self.offset_curve = offset_curve self.offset_curve_type = offset_curve_type self.uv_space = uv_space self.z_axis = axis self.tangent_delta = 0.001 if offset_curve_type == SvCurveOffsetOnSurface.BY_LENGTH: self.len_solver = SvCurveLengthSolver(curve) self.len_solver.prepare('SPL', len_resolution) def get_u_bounds(self): return self.curve.get_u_bounds() def evaluate(self, t): return self.evaluate_array(np.array([t]))[0] def get_offset(self, ts): u_min, u_max = self.curve.get_u_bounds() if self.offset_curve_type == SvCurveOffsetOnSurface.BY_PARAMETER: off_u_min, off_u_max = self.offset_curve.get_u_bounds() ts = (off_u_max - off_u_min) * (ts - u_min) / (u_max - u_min) + off_u_min ps = self.offset_curve.evaluate_array(ts) return ps[:,1] else: off_u_max = self.len_solver.get_total_length() ts = off_u_max * (ts - u_min) / (u_max - u_min) ts = self.len_solver.solve(ts) ps = self.offset_curve.evaluate_array(ts) return ps[:,1] def evaluate_array(self, ts): if self.z_axis == 2: U, V = 0, 1 elif self.z_axis == 1: U, V = 0, 2 else: U, V = 1, 2 uv_points = self.curve.evaluate_array(ts) us, vs = uv_points[:,U], uv_points[:,V] # Tangents of the curve in surface's UV space uv_tangents = self.curve.tangent_array(ts) # (n, 3), with Z == 0 (Z is ignored anyway) tangents_u, tangents_v = uv_tangents[:,U], uv_tangents[:,V] # (n,), (n,) derivs = self.surface.derivatives_data_array(us, vs) su, sv = derivs.du, derivs.dv # Take surface's normals as N = [su, sv]; # Take curve's tangent in 3D space as T = (tangents_u * su + tangents_v * sv); # Take a vector in surface's tangent plane, which is perpendicular to curve's # tangent, as Nc = [N, T] (call it "curve's normal on a surface"); # Calculate Nc's decomposition in su, sv vectors as Ncu = (Nc, su) and Ncv = (Nc, sv); # Interpret Ncu and Ncv as coordinates of Nc in surface's UV space. # If you write down all above in formulas, you will have # # Nc = (Tu (Su, Sv) + Tv Sv^2) Su - (Tu Su^2 + Tv (Su, Sv)) Sv # We could've calculate the offset as (Curve on a surface) + (offset*Nc), # but there is no guarantee that these points will lie on the surface again # (especially with not-so-small values of offset). # So instead we calculate Curve + offset*(Ncu; Ncv) in UV space, and then # map all that into 3D space. su2 = (su*su).sum(axis=1) # (n,) sv2 = (sv*sv).sum(axis=1) # (n,) suv = (su*sv).sum(axis=1) # (n,) su_norm, sv_norm = derivs.tangent_lens() su_norm, sv_norm = su_norm.flatten(), sv_norm.flatten() delta_u = (tangents_u*suv + tangents_v*sv2) # (n,) delta_v = - (tangents_u*su2 + tangents_v*suv) # (n,) delta_s = delta_u[np.newaxis].T * su + delta_v[np.newaxis].T * sv delta_s = np.linalg.norm(delta_s, axis=1) if self.offset_curve is None: offset = self.offset else: offset = self.get_offset(ts) res_us = us + delta_u * offset / delta_s res_vs = vs + delta_v * offset / delta_s if self.uv_space: zs = np.zeros_like(us) if self.z_axis == 2: result = np.stack((res_us, res_vs, zs)).T elif self.z_axis == 1: result = np.stack((res_us, zs, res_vs)).T else: result = np.stack((zs, res_us, res_vs)).T return result else: result = self.surface.evaluate_array(res_us, res_vs) # Just for testing # on_curve = self.surface.evaluate_array(us, vs) # dvs = result - on_curve # print(np.linalg.norm(dvs, axis=1)) return result
Ancestors
Class variables
var BY_LENGTH
var BY_PARAMETER
Methods
def get_offset(self, ts)
-
Expand source code
def get_offset(self, ts): u_min, u_max = self.curve.get_u_bounds() if self.offset_curve_type == SvCurveOffsetOnSurface.BY_PARAMETER: off_u_min, off_u_max = self.offset_curve.get_u_bounds() ts = (off_u_max - off_u_min) * (ts - u_min) / (u_max - u_min) + off_u_min ps = self.offset_curve.evaluate_array(ts) return ps[:,1] else: off_u_max = self.len_solver.get_total_length() ts = off_u_max * (ts - u_min) / (u_max - u_min) ts = self.len_solver.solve(ts) ps = self.offset_curve.evaluate_array(ts) return ps[:,1]
Inherited members
class SvCurveOnSurface (curve, surface, axis=0)
-
Expand source code
class SvCurveOnSurface(SvCurve): def __init__(self, curve, surface, axis=0): self.curve = curve self.surface = surface self.axis = axis self.tangent_delta = 0.001 self.__description__ = "{} on {}".format(curve, surface) def get_u_bounds(self): return self.curve.get_u_bounds() def evaluate(self, t): return self.evaluate_array(np.array([t]))[0] def evaluate_array(self, ts): points = self.curve.evaluate_array(ts) xs = points[:,0] ys = points[:,1] zs = points[:,2] if self.axis == 0: us = ys vs = zs elif self.axis == 1: us = xs vs = zs elif self.axis == 2: us = xs vs = ys else: raise Exception("Unsupported orientation axis") return self.surface.evaluate_array(us, vs)
Ancestors
Inherited members
class SvCurvesSortResult
-
Result of
sort_curves_for_concat()
method.Expand source code
class SvCurvesSortResult(object): """ Result of `sort_curves_for_concat` method. """ def __init__(self): self.curves = [] self.indexes = [] self.flips = [] self.sum_error = 0
class SvDeformedByFieldCurve (curve, field, coefficient=1.0)
-
Expand source code
class SvDeformedByFieldCurve(SvCurve): def __init__(self, curve, field, coefficient=1.0): self.curve = curve self.field = field self.coefficient = coefficient self.tangent_delta = 0.001 self.__description__ = "{}({})".format(field, curve) def get_u_bounds(self): return self.curve.get_u_bounds() def evaluate(self, t): v = self.curve.evaluate(t) vec = self.field.evaluate(*tuple(v)) return v + self.coefficient * vec def evaluate_array(self, ts): vs = self.curve.evaluate_array(ts) xs, ys, zs = vs[:,0], vs[:,1], vs[:,2] vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) vecs = np.stack((vxs, vys, vzs)).T return vs + self.coefficient * vecs
Ancestors
Inherited members
class SvIsoUvCurve (surface, fixed_axis, value, flip=False)
-
Expand source code
class SvIsoUvCurve(SvCurve): def __init__(self, surface, fixed_axis, value, flip=False): self.surface = surface self.fixed_axis = fixed_axis self.value = value self.flip = flip self.tangent_delta = 0.001 self.__description__ = "{} at {} = {}".format(surface, fixed_axis, value) @staticmethod def take(surface, fixed_axis, value, flip=False): if hasattr(surface, 'iso_curve'): try: return surface.iso_curve(fixed_axis, value, flip=flip) except UnsupportedSurfaceTypeException: pass return SvIsoUvCurve(surface, fixed_axis, value, flip=flip) def get_u_bounds(self): if self.fixed_axis == 'U': return self.surface.get_v_min(), self.surface.get_v_max() else: return self.surface.get_u_min(), self.surface.get_u_max() def evaluate(self, t): if self.fixed_axis == 'U': if self.flip: t = self.surface.get_v_max() - t + self.surface.get_v_min() return self.surface.evaluate(self.value, t) else: if self.flip: t = self.surface.get_u_max() - t + self.surface.get_u_min() return self.surface.evaluate(t, self.value) def evaluate_array(self, ts): if self.fixed_axis == 'U': if self.flip: ts = self.surface.get_v_max() - ts + self.surface.get_v_min() return self.surface.evaluate_array(np.repeat(self.value, len(ts)), ts) else: if self.flip: ts = self.surface.get_u_max() - ts + self.surface.get_u_min() return self.surface.evaluate_array(ts, np.repeat(self.value, len(ts)))
Ancestors
Static methods
def take(surface, fixed_axis, value, flip=False)
-
Expand source code
@staticmethod def take(surface, fixed_axis, value, flip=False): if hasattr(surface, 'iso_curve'): try: return surface.iso_curve(fixed_axis, value, flip=flip) except UnsupportedSurfaceTypeException: pass return SvIsoUvCurve(surface, fixed_axis, value, flip=flip)
Inherited members
class SvLengthRebuiltCurve (curve, resolution, mode='SPL', tolerance=None)
-
Expand source code
class SvLengthRebuiltCurve(SvCurve): def __init__(self, curve, resolution, mode='SPL', tolerance=None): self.curve = curve self.resolution = resolution if hasattr(curve, 'tangent_delta'): self.tangent_delta = curve.tangent_delta else: self.tangent_delta = 0.001 self.mode = mode self.solver = SvCurveLengthSolver(curve) self.solver.prepare(self.mode, resolution, tolerance=tolerance) self.u_bounds = (0.0, self.solver.get_total_length()) self.__description__ = "{} rebuilt".format(curve) def get_u_bounds(self): return self.u_bounds def evaluate(self, t): c_ts = self.solver.solve(np.array([t])) return self.curve.evaluate(c_ts[0]) def evaluate_array(self, ts): c_ts = self.solver.solve(ts) return self.curve.evaluate_array(c_ts)
Ancestors
Inherited members
class SvNormalTrack (curve, resolution)
-
Expand source code
class SvNormalTrack(object): def __init__(self, curve, resolution): self.curve = curve self.resolution = resolution self._pre_calc() def _make_quats(self, points, tangents, normals, binormals): matrices = np.dstack((normals, binormals, tangents)) matrices = np.transpose(matrices, axes=(0,2,1)) matrices = np.linalg.inv(matrices) return [Matrix(m).to_quaternion() for m in matrices] def _pre_calc(self): curve = self.curve t_min, t_max = curve.get_u_bounds() ts = np.linspace(t_min, t_max, num=self.resolution) points = curve.evaluate_array(ts) tangents, normals, binormals = curve.tangent_normal_binormal_array(ts) tangents /= np.linalg.norm(tangents, axis=1, keepdims=True) normal = normals[0] if np.linalg.norm(normal) > 1e-4: binormal = binormals[0] binormal /= np.linalg.norm(binormal) else: tangent = tangents[0] normal = Vector(tangent).orthogonal() normal = np.array(normal) binormal = np.cross(tangent, normal) binormal /= np.linalg.norm(binormal) out_normals = [normal] out_binormals = [binormal] for point, tangent in zip(points[1:], tangents[1:]): plane = PlaneEquation.from_normal_and_point(Vector(tangent), Vector(point)) normal = plane.projection_of_vector(Vector(point), Vector(point + normal)) normal = np.array(normal.normalized()) binormal = np.cross(tangent, normal) binormal /= np.linalg.norm(binormal) out_normals.append(normal) out_binormals.append(binormal) self.quats = self._make_quats(points, tangents, np.array(out_normals), np.array(out_binormals)) self.tknots = ts def evaluate_array(self, ts): """ Args: ts: np.array of snape (n,) or list of floats Returns: np.array of shape (n, 3, 3) """ ts = np.array(ts) tknots, quats = self.tknots, self.quats base_indexes = tknots.searchsorted(ts, side='left')-1 t1s, t2s = tknots[base_indexes], tknots[base_indexes+1] dts = (ts - t1s) / (t2s - t1s) #dts = np.clip(dts, 0.0, 1.0) # Just in case... matrix_out = [] # TODO: ideally this should be vectorized with numpy; # but that would require implementation of quaternion # interpolation in numpy. for dt, base_index in zip(dts, base_indexes): q1, q2 = quats[base_index], quats[base_index+1] # spherical linear interpolation. # TODO: implement `squad`. if dt < 0: q = q1 elif dt > 1.0: q = q2 else: q = q1.slerp(q2, dt) matrix = np.array(q.to_matrix()) matrix_out.append(matrix) return np.array(matrix_out)
Methods
def evaluate_array(self, ts)
-
Args
ts
- np.array of snape (n,) or list of floats
Returns
np.array of shape (n, 3, 3)
Expand source code
def evaluate_array(self, ts): """ Args: ts: np.array of snape (n,) or list of floats Returns: np.array of shape (n, 3, 3) """ ts = np.array(ts) tknots, quats = self.tknots, self.quats base_indexes = tknots.searchsorted(ts, side='left')-1 t1s, t2s = tknots[base_indexes], tknots[base_indexes+1] dts = (ts - t1s) / (t2s - t1s) #dts = np.clip(dts, 0.0, 1.0) # Just in case... matrix_out = [] # TODO: ideally this should be vectorized with numpy; # but that would require implementation of quaternion # interpolation in numpy. for dt, base_index in zip(dts, base_indexes): q1, q2 = quats[base_index], quats[base_index+1] # spherical linear interpolation. # TODO: implement `squad`. if dt < 0: q = q1 elif dt > 1.0: q = q2 else: q = q1.slerp(q2, dt) matrix = np.array(q.to_matrix()) matrix_out.append(matrix) return np.array(matrix_out)
class SvOffsetCurve (curve, offset_vector, offset_amount=None, offset_curve=None, offset_curve_type='T', algorithm='FRENET', resolution=50)
-
Expand source code
class SvOffsetCurve(SvCurve): BY_PARAMETER = 'T' BY_LENGTH = 'L' def __init__(self, curve, offset_vector, offset_amount=None, offset_curve = None, offset_curve_type = BY_PARAMETER, algorithm=FRENET, resolution=50): self.curve = curve if algorithm == NORMAL_DIR and (offset_amount is None and offset_curve is None): raise Exception("offset_amount or offset_curve is mandatory if algorithm is NORMAL_DIR") self.offset_amount = offset_amount self.offset_vector = offset_vector self.offset_curve = offset_curve self.offset_curve_type = offset_curve_type self.algorithm = algorithm if algorithm in {FRENET, ZERO, TRACK_NORMAL}: self.calculator = DifferentialRotationCalculator(curve, algorithm, resolution) if offset_curve_type == SvOffsetCurve.BY_LENGTH: self.len_solver = SvCurveLengthSolver(curve) self.len_solver.prepare('SPL', resolution) self.tangent_delta = 0.001 def get_u_bounds(self): return self.curve.get_u_bounds() def evaluate(self, t): return self.evaluate_array(np.array([t]))[0] def get_matrix(self, tangent): return MathutilsRotationCalculator.get_matrix(tangent, scale=1.0, axis=2, algorithm = self.algorithm, scale_all=False) def get_matrices(self, ts): if self.algorithm in {FRENET, ZERO, TRACK_NORMAL}: return self.calculator.get_matrices(ts) elif self.algorithm in {HOUSEHOLDER, TRACK, DIFF}: tangents = self.curve.tangent_array(ts) matrices = np.vectorize(lambda t : self.get_matrix(t), signature='(3)->(3,3)')(tangents) return matrices else: raise Exception("Unsupported algorithm") def get_offset(self, ts): u_min, u_max = self.curve.get_u_bounds() if self.offset_curve is None: if self.offset_amount is not None: return self.offset_amount else: return np.linalg.norm(self.offset_vector) elif self.offset_curve_type == SvOffsetCurve.BY_PARAMETER: off_u_min, off_u_max = self.offset_curve.get_u_bounds() ts = (off_u_max - off_u_min) * (ts - u_min) / (u_max - u_min) + off_u_min ps = self.offset_curve.evaluate_array(ts) return ps[:,1][np.newaxis].T else: off_u_max = self.len_solver.get_total_length() ts = off_u_max * (ts - u_min) / (u_max - u_min) ts = self.len_solver.solve(ts) ps = self.offset_curve.evaluate_array(ts) return ps[:,1][np.newaxis].T def evaluate_array(self, ts): n = len(ts) t_min, t_max = self.curve.get_u_bounds() extrusion_start = self.curve.evaluate(t_min) extrusion_points = self.curve.evaluate_array(ts) extrusion_vectors = extrusion_points - extrusion_start offset_vector = self.offset_vector / np.linalg.norm(self.offset_vector) if self.algorithm == NORMAL_DIR: offset_vectors = np.tile(offset_vector[np.newaxis].T, n).T tangents = self.curve.tangent_array(ts) offset_vectors = np.cross(tangents, offset_vectors) offset_norm = np.linalg.norm(offset_vectors, axis=1, keepdims=True) offset_amounts = self.get_offset(ts) offset_vectors = offset_amounts * offset_vectors / offset_norm else: offset_vectors = np.tile(offset_vector[np.newaxis].T, n) matrices = self.get_matrices(ts) offset_amounts = self.get_offset(ts) offset_vectors = offset_amounts * (matrices @ offset_vectors)[:,:,0] result = extrusion_vectors + offset_vectors result = result + extrusion_start return result
Ancestors
Class variables
var BY_LENGTH
var BY_PARAMETER
Methods
def get_matrices(self, ts)
-
Expand source code
def get_matrices(self, ts): if self.algorithm in {FRENET, ZERO, TRACK_NORMAL}: return self.calculator.get_matrices(ts) elif self.algorithm in {HOUSEHOLDER, TRACK, DIFF}: tangents = self.curve.tangent_array(ts) matrices = np.vectorize(lambda t : self.get_matrix(t), signature='(3)->(3,3)')(tangents) return matrices else: raise Exception("Unsupported algorithm")
def get_matrix(self, tangent)
-
Expand source code
def get_matrix(self, tangent): return MathutilsRotationCalculator.get_matrix(tangent, scale=1.0, axis=2, algorithm = self.algorithm, scale_all=False)
def get_offset(self, ts)
-
Expand source code
def get_offset(self, ts): u_min, u_max = self.curve.get_u_bounds() if self.offset_curve is None: if self.offset_amount is not None: return self.offset_amount else: return np.linalg.norm(self.offset_vector) elif self.offset_curve_type == SvOffsetCurve.BY_PARAMETER: off_u_min, off_u_max = self.offset_curve.get_u_bounds() ts = (off_u_max - off_u_min) * (ts - u_min) / (u_max - u_min) + off_u_min ps = self.offset_curve.evaluate_array(ts) return ps[:,1][np.newaxis].T else: off_u_max = self.len_solver.get_total_length() ts = off_u_max * (ts - u_min) / (u_max - u_min) ts = self.len_solver.solve(ts) ps = self.offset_curve.evaluate_array(ts) return ps[:,1][np.newaxis].T
Inherited members