From 5e4f593bc978f5bbf7435fc3d4a661d87d94e0b8 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Tue, 5 Nov 2024 00:44:35 +0500 Subject: [PATCH 1/4] Set of new field nodes. --- menus/full_by_data_type.yaml | 3 + nodes/field/taper_field.py | 112 +++++++++++++++++++ nodes/field/twist_field.py | 170 +++++++++++++++++++++++++++++ nodes/field/vector_field_filter.py | 107 ++++++++++++++++++ utils/field/vector.py | 162 ++++++++++++++++++++++++++- utils/geodesic.py | 2 +- utils/geom.py | 18 +++ 7 files changed, 567 insertions(+), 7 deletions(-) create mode 100644 nodes/field/taper_field.py create mode 100644 nodes/field/twist_field.py create mode 100644 nodes/field/vector_field_filter.py diff --git a/menus/full_by_data_type.yaml b/menus/full_by_data_type.yaml index 94d7615bb4..8aceea5cff 100644 --- a/menus/full_by_data_type.yaml +++ b/menus/full_by_data_type.yaml @@ -417,6 +417,8 @@ - SvExDecomposeVectorFieldNode - SvExScalarFieldPointNode - SvAttractorFieldNodeMk2 + - SvTwistFieldNode + - SvTaperFieldNode - SvRotationFieldNode - SvExImageFieldNode - SvMeshSurfaceFieldNode @@ -433,6 +435,7 @@ - icon_name: TOOL_SETTINGS - SvExScalarFieldMathNode - SvExVectorFieldMathNode + - SvVectorFieldFilterNode - SvScalarFieldCurveMapNode - SvExFieldDiffOpsNode - SvScalarFieldCurvatureNode diff --git a/nodes/field/taper_field.py b/nodes/field/taper_field.py new file mode 100644 index 0000000000..8c08ee83af --- /dev/null +++ b/nodes/field/taper_field.py @@ -0,0 +1,112 @@ +# 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 bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat +from sverchok.utils.field.vector import SvTaperVectorField + +class SvTaperFieldNode(SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Taper Field + Tooltip: Generate taper field + """ + bl_idname = 'SvTaperFieldNode' + bl_label = 'Taper Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EX_ATTRACT' + + def update_sockets(self, context): + self.inputs['MinZ'].hide_safe = not self.use_min_z + self.inputs['MaxZ'].hide_safe = not self.use_max_z + updateNode(self, context) + + use_min_z : BoolProperty( + name = "Use Min Z", + default = False, + update = update_sockets) + + use_max_z : BoolProperty( + name = "Use Max Z", + default = False, + update = update_sockets) + + min_z : FloatProperty( + name = "Min Z", + default = 0.0, + update = updateNode) + + max_z : FloatProperty( + name = "Max Z", + default = 1.0, + update = updateNode) + + flat_output : BoolProperty( + name = "Flat Output", + default = True, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.label(text='Restrict Along Axis:') + r = layout.row(align=True) + r.prop(self, 'use_min_z', toggle=True) + r.prop(self, 'use_max_z', toggle=True) + layout.prop(self, 'flat_output') + + def sv_init(self, context): + d = self.inputs.new('SvVerticesSocket', "Point") + d.use_prop = True + d.default_property = (0.0, 0.0, 0.0) + + d = self.inputs.new('SvVerticesSocket', "Vertex") + d.use_prop = True + d.default_property = (0.0, 0.0, 1.0) + + self.inputs.new('SvStringsSocket', 'MinZ').prop_name = 'min_z' + self.inputs.new('SvStringsSocket', 'MaxZ').prop_name = 'max_z' + + self.outputs.new('SvVectorFieldSocket', "Field") + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + point_s = self.inputs['Point'].sv_get() + vertex_s = self.inputs['Vertex'].sv_get() + if self.use_min_z: + min_z_s = self.inputs['MinZ'].sv_get() + else: + min_z_s = [[None]] + if self.use_max_z: + max_z_s = self.inputs['MaxZ'].sv_get() + else: + max_z_s = [[None]] + + fields_out = [] + for params in zip_long_repeat(point_s, vertex_s, min_z_s, max_z_s): + new_fields = [] + for point, vertex, min_z, max_z in zip_long_repeat(*params): + field = SvTaperVectorField.from_base_point_and_vertex(point, vertex, + min_z = min_z, max_z = max_z) + new_fields.append(field) + if self.flat_output: + fields_out.extend(new_fields) + else: + fields_out.append(new_fields) + + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvTaperFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvTaperFieldNode) + diff --git a/nodes/field/twist_field.py b/nodes/field/twist_field.py new file mode 100644 index 0000000000..8929c9f85a --- /dev/null +++ b/nodes/field/twist_field.py @@ -0,0 +1,170 @@ +# 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 +from math import pi + +import bpy +from bpy.props import FloatProperty, EnumProperty, BoolProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat +from sverchok.utils.field.vector import SvTwistVectorField + +class SvTwistFieldNode(SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Twist Whirl Field + Tooltip: Generate twisting / whirling vector field + """ + bl_idname = 'SvTwistFieldNode' + bl_label = 'Twist / Whirl Field' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EX_ATTRACT' + + angle_along_axis : FloatProperty( + name = "Twist Angle", + description = "Angle for twisting around axis", + default = pi/2, + update = updateNode) + + angle_along_radius : FloatProperty( + name = "Whirl Angle", + description = "Angle for whirl force", + default = 0.0, + update = updateNode) + + flat_output : BoolProperty( + name = "Flat Output", + default = True, + update = updateNode) + + def update_sockets(self, context): + self.inputs['MinZ'].hide_safe = not self.use_min_z + self.inputs['MaxZ'].hide_safe = not self.use_max_z + self.inputs['MinR'].hide_safe = not self.use_min_r + self.inputs['MaxR'].hide_safe = not self.use_max_r + updateNode(self, context) + + use_min_z : BoolProperty( + name = "Use Min Z", + default = False, + update = update_sockets) + + use_max_z : BoolProperty( + name = "Use Max Z", + default = False, + update = update_sockets) + + use_min_r : BoolProperty( + name = "Use Min Radius", + default = False, + update = update_sockets) + + use_max_r : BoolProperty( + name = "Use Max Radius", + default = False, + update = update_sockets) + + min_z : FloatProperty( + name = "Min Z", + default = 0.0, + update = updateNode) + + max_z : FloatProperty( + name = "Max Z", + default = 1.0, + update = updateNode) + + min_r : FloatProperty( + name = "Min Radius", + min = 0.0, + default = 0.0, + update = updateNode) + + max_r : FloatProperty( + name = "Max Radius", + min = 0.0, + default = 1.0, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.label(text='Restrict Along Axis:') + r = layout.row(align=True) + r.prop(self, 'use_min_z', toggle=True) + r.prop(self, 'use_max_z', toggle=True) + layout.label(text='Restrict Along Radius:') + r = layout.row(align=True) + r.prop(self, 'use_min_r', toggle=True) + r.prop(self, 'use_max_r', toggle=True) + layout.prop(self, 'flat_output') + + def sv_init(self, context): + d = self.inputs.new('SvVerticesSocket', "Center") + d.use_prop = True + d.default_property = (0.0, 0.0, 0.0) + + d = self.inputs.new('SvVerticesSocket', "Axis") + d.use_prop = True + d.default_property = (0.0, 0.0, 1.0) + + self.inputs.new('SvStringsSocket', 'TwistAngle').prop_name = 'angle_along_axis' + self.inputs.new('SvStringsSocket', 'WhirlAngle').prop_name = 'angle_along_radius' + + self.inputs.new('SvStringsSocket', 'MinZ').prop_name = 'min_z' + self.inputs.new('SvStringsSocket', 'MaxZ').prop_name = 'max_z' + self.inputs.new('SvStringsSocket', 'MinR').prop_name = 'min_r' + self.inputs.new('SvStringsSocket', 'MaxR').prop_name = 'max_r' + + self.outputs.new('SvVectorFieldSocket', "Field") + self.update_sockets(context) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + center_s = self.inputs['Center'].sv_get() + axis_s = self.inputs['Axis'].sv_get() + twist_angle_s = self.inputs['TwistAngle'].sv_get() + whirl_angle_s = self.inputs['WhirlAngle'].sv_get() + if self.use_min_z: + min_z_s = self.inputs['MinZ'].sv_get() + else: + min_z_s = [[None]] + if self.use_max_z: + max_z_s = self.inputs['MaxZ'].sv_get() + else: + max_z_s = [[None]] + if self.use_min_r: + min_r_s = self.inputs['MinR'].sv_get() + else: + min_r_s = [[None]] + if self.use_max_r: + max_r_s = self.inputs['MaxR'].sv_get() + else: + max_r_s = [[None]] + + fields_out = [] + for params in zip_long_repeat(center_s, axis_s, twist_angle_s, whirl_angle_s, min_z_s, max_z_s, min_r_s, max_r_s): + new_fields = [] + for center, axis, twist_angle, whirl_angle, min_z, max_z, min_r, max_r in zip_long_repeat(*params): + field = SvTwistVectorField(center, axis, twist_angle, whirl_angle, + min_z, max_z, min_r, max_r) + new_fields.append(field) + if self.flat_output: + fields_out.extend(new_fields) + else: + fields_out.append(new_fields) + + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvTwistFieldNode) + +def unregister(): + bpy.utils.unregister_class(SvTwistFieldNode) + + diff --git a/nodes/field/vector_field_filter.py b/nodes/field/vector_field_filter.py new file mode 100644 index 0000000000..2cb79f0608 --- /dev/null +++ b/nodes/field/vector_field_filter.py @@ -0,0 +1,107 @@ +# 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 bpy +from bpy.props import EnumProperty, BoolProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, map_recursive +from sverchok.utils.math import coordinate_modes +from sverchok.utils.field.vector import SvVectorFieldCartesianFilter, SvVectorFieldCylindricalFilter, SvVectorFieldSphericalFilter, SvVectorField + +class SvVectorFieldFilterNode(SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Filter Vector Field + Tooltip: Restrict vector field action by cartesian, cylindrical or spherical coordinates + """ + bl_idname = 'SvVectorFieldFilterNode' + bl_label = 'Vector Field Filter' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_EX_ATTRACT' + + coordinates : EnumProperty( + name = "Coordinates", + items = coordinate_modes, + default = 'XYZ', + update = updateNode) + + use_x : BoolProperty( + name = "X", + default = True, + update = updateNode) + + use_y : BoolProperty( + name = "Y", + default = True, + update = updateNode) + + use_z : BoolProperty( + name = "Z", + default = True, + update = updateNode) + + use_rho : BoolProperty( + name = "Rho", + default = True, + update = updateNode) + + use_phi : BoolProperty( + name = "Phi", + default = True, + update = updateNode) + + use_theta : BoolProperty( + name = "Theta", + default = True, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.label(text="Coordinates:") + layout.prop(self, 'coordinates', text='') + layout.label(text="Use:") + r = layout.row(align=True) + if self.coordinates == 'XYZ': + r.prop(self, 'use_x', toggle=True) + r.prop(self, 'use_y', toggle=True) + r.prop(self, 'use_z', toggle=True) + elif self.coordinates == 'CYL': + r.prop(self, 'use_rho', toggle=True) + r.prop(self, 'use_phi', toggle=True) + r.prop(self, 'use_z', toggle=True) + else: + r.prop(self, 'use_rho', toggle=True) + r.prop(self, 'use_phi', toggle=True) + r.prop(self, 'use_theta', toggle=True) + + def sv_init(self, context): + self.inputs.new('SvVectorFieldSocket', 'Field') + self.outputs.new('SvVectorFieldSocket', 'Field') + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + fields_s = self.inputs['Field'].sv_get() + + def do_filter(field): + if self.coordinates == 'XYZ': + return SvVectorFieldCartesianFilter(field, self.use_x, self.use_y, self.use_z) + elif self.coordinates == 'CYL': + return SvVectorFieldCylindricalFilter(field, self.use_rho, self.use_phi, self.use_z) + else: + return SvVectorFieldSphericalFilter(field, self.use_rho, self.use_phi, self.use_theta) + + fields_out = map_recursive(do_filter, fields_s, data_types=(SvVectorField,)) + self.outputs['Field'].sv_set(fields_out) + +def register(): + bpy.utils.register_class(SvVectorFieldFilterNode) + +def unregister(): + bpy.utils.unregister_class(SvVectorFieldFilterNode) + + diff --git a/utils/field/vector.py b/utils/field/vector.py index 261a3286e0..00948d3995 100644 --- a/utils/field/vector.py +++ b/utils/field/vector.py @@ -10,11 +10,14 @@ from mathutils import Vector from mathutils import bvhtree -from mathutils import kdtree from mathutils import noise from sverchok.utils.curve import SvCurveLengthSolver, SvNormalTrack, MathutilsRotationCalculator -from sverchok.utils.geom import LineEquation, CircleEquation3D -from sverchok.utils.math import from_cylindrical, from_spherical, np_dot +from sverchok.utils.geom import LineEquation, CircleEquation3D, rotate_around_vector_matrix +from sverchok.utils.math import ( + from_cylindrical, from_spherical, + from_cylindrical_np, to_cylindrical_np, + from_spherical_np, to_spherical_np, + np_dot, np_multiply_matrices_vectors) from sverchok.utils.kdtree import SvKdTree from sverchok.utils.field.voronoi import SvVoronoiFieldData @@ -32,8 +35,9 @@ def __repr__(self): description = self.__class__.__name__ return "<{} vector field>".format(description) - def evaluate(self, point): - raise Exception("not implemented") + def evaluate(self, x, y, z): + rxs, rys, rzs = self.evaluate_grid([x], [y], [z]) + return rxs[0], rys[0], rzs[0] def evaluate_grid(self, xs, ys, zs): raise Exception("not implemented") @@ -686,6 +690,83 @@ def evaluate_grid(self, xs, ys, zs): R = vectors.T return R[0], R[1], R[2] +class SvTwistVectorField(SvVectorField): + def __init__(self, center, axis, angle_along_axis, angle_along_radius, + min_z = None, max_z = None, min_r = None, max_r = None): + self.center = np.asarray(center) + self.axis = np.asarray(axis) + self.axis = self.axis / np.linalg.norm(self.axis) + self.angle_along_axis = angle_along_axis + self.angle_along_radius = angle_along_radius + self.min_z = min_z + self.max_z = max_z + self.min_r = min_r + self.max_r = max_r + + def evaluate_grid(self, xs, ys, zs): + pts = np.stack((xs, ys, zs)).T + dpts = pts - self.center + ts = np_dot(dpts, self.axis) + rads = dpts - ts[np.newaxis].T * self.axis + rs = np.linalg.norm(rads, axis=1) + if self.min_z is not None or self.max_z is not None: + ts = np.clip(ts, self.min_z, self.max_z) + if self.min_r is not None or self.max_r is not None: + rs = np.clip(rs, self.min_r, self.max_r) + angles = ts * self.angle_along_axis + rs * self.angle_along_radius + matrices = rotate_around_vector_matrix(self.axis, angles) + new_pts = np_multiply_matrices_vectors(matrices, dpts) + new_pts += self.center + vectors = (new_pts - pts).T + return vectors[0], vectors[1], vectors[2] + + def evaluate(self, x, y, z): + rxs, rys, rzs = self.evaluate_grid([x], [y], [z]) + return rxs[0], rys[0], rzs[0] + +class SvTaperVectorField(SvVectorField): + def __init__(self, center, axis, coefficient, min_z=None, max_z=None): + self.center = np.asarray(center) + self.axis = np.asarray(axis) + self.axis = self.axis / np.linalg.norm(self.axis) + self.coefficient = coefficient + self.min_z = None + self.max_z = None + rho = 1.0 / coefficient + if min_z is not None: + self.max_z = rho - min_z + if max_z is not None: + self.min_z = rho - max_z + + @classmethod + def from_base_point_and_vertex(cls, base_point, vertex, **kwargs): + base_point = np.array(base_point) + vertex = np.array(vertex) + rho = np.linalg.norm(base_point - vertex) + return SvTaperVectorField(vertex, base_point - vertex, 1.0/rho, **kwargs) + + @classmethod + def from_base_point_and_vector(cls, base_point, vector, **kwargs): + base_point = np.array(base_point) + vector = np.array(vector) + return SvTaperVectorField.from_base_point_and_vertex(base_point, base_point + vector, **kwargs) + + def evaluate_grid(self, xs, ys, zs): + pts = np.stack((xs, ys, zs)).T + dpts = pts - self.center + ts = np_dot(dpts, self.axis) + projections = ts[np.newaxis].T * self.axis + rads = dpts - projections + if self.min_z is not None or self.max_z is not None: + ts = np.clip(ts, self.min_z, self.max_z) + rads *= self.coefficient * ts[np.newaxis].T + new_pts = self.center + projections + rads + vectors = (new_pts - pts).T + return vectors[0], vectors[1], vectors[2] + + def evaluate(self, x, y, z): + rxs, rys, rzs = self.evaluate_grid([x], [y], [z]) + return rxs[0], rys[0], rzs[0] class SvSelectVectorField(SvVectorField): def __init__(self, fields, mode): @@ -715,9 +796,77 @@ def evaluate_grid(self, xs, ys, zs): selected = np.argmax(norms, axis=1) all_points = list(range(n)) vectors = vectors[all_points, selected, :] - #print(vectors.shape) return vectors.T +class SvVectorFieldCartesianFilter(SvVectorField): + def __init__(self, field, use_x=True, use_y=True, use_z=True): + self.field = field + self.use_x = use_x + self.use_y = use_y + self.use_z = use_z + desc = "" + if use_x: + desc = desc + 'X' + if use_y: + desc = desc + 'Y' + if use_z: + desc = desc + 'Z' + self.__description__ = f"Filter[{desc}]({field})" + + def evaluate_grid(self, xs, ys, zs): + vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) + if not self.use_x: + vxs[:] = 0 + if not self.use_y: + vys[:] = 0 + if not self.use_z: + vzs[:] = 0 + return vxs, vys, vzs + +class SvVectorFieldCylindricalFilter(SvVectorField): + def __init__(self, field, use_rho=True, use_phi=True, use_z=True): + self.field = field + self.use_rho = use_rho + self.use_phi = use_phi + self.use_z = use_z + + def evaluate_grid(self, xs, ys, zs): + pts = np.stack((xs,ys,zs)).T + vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vxs,vys,vzs)).T + s_rho, s_phi, s_z = to_cylindrical_np(pts.T, mode='radians') + v_rho, v_phi, v_z = to_cylindrical_np((pts + vectors).T, mode='radians') + if not self.use_rho: + v_rho = s_rho + if not self.use_phi: + v_phi = s_phi + if not self.use_z: + v_z = s_z + v_x, v_y, v_z = from_cylindrical_np(v_rho, v_phi, v_z, mode='radians') + return (v_x - xs), (v_y - ys), (v_z - zs) + +class SvVectorFieldSphericalFilter(SvVectorField): + def __init__(self, field, use_rho=True, use_phi=True, use_theta=True): + self.field = field + self.use_rho = use_rho + self.use_phi = use_phi + self.use_theta = use_theta + + def evaluate_grid(self, xs, ys, zs): + pts = np.stack((xs,ys,zs)).T + vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vxs,vys,vzs)).T + s_rho, s_phi, s_theta = to_spherical_np(pts.T, mode='radians') + v_rho, v_phi, v_theta = to_spherical_np((pts + vectors).T, mode='radians') + if not self.use_rho: + v_rho = s_rho + if not self.use_phi: + v_phi = s_phi + if not self.use_theta: + v_theta = s_theta + v_x, v_y, v_z = from_spherical_np(v_rho, v_phi, v_theta, mode='radians') + return (v_x - xs), (v_y - ys), (v_z - zs) + class SvVectorFieldTangent(SvVectorField): def __init__(self, field1, field2): @@ -1068,6 +1217,7 @@ def _evaluate(self, vertices): return self.surface.evaluate_array(us, vs) spline_normals, surf_vertices = self.surface.normal_vertices_array(us, vs) + spline_normals /= np.linalg.norm(spline_normals, axis=1, keepdims=True) zs = vertices[:,self.orient_axis].flatten() zs = zs[np.newaxis].T v1 = zs * spline_normals diff --git a/utils/geodesic.py b/utils/geodesic.py index e36d44acd7..3b7ecdc9b6 100644 --- a/utils/geodesic.py +++ b/utils/geodesic.py @@ -421,7 +421,7 @@ def get_uv_field(self, **kwargs): return make_rbf(self.orig_points, self.uv_points, **kwargs) def get_field(self, **kwargs): - bend = SvBendAlongSurfaceField(self.surface, axis=2, autoscale=True) + bend = SvBendAlongSurfaceField(self.surface, axis=2, autoscale=False) bend.u_bounds = self.surface.get_u_bounds() bend.v_bounds = self.surface.get_v_bounds() uv = self.get_uv_field(**kwargs) diff --git a/utils/geom.py b/utils/geom.py index 3ecb4426fe..697878c394 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -2768,6 +2768,24 @@ def rotate_vector_around_vector_np(v, k, theta): s3 = p1 * p2 * k return s1 + s2 + s3 +def rotate_around_vector_matrix(k, theta): + if isinstance(theta, (list,tuple,np.ndarray)): + theta = np.array(theta) + theta = theta[np.newaxis,np.newaxis].T + kx, ky, kz = k + K = np.zeros((3,3)) + K[0,1] = -kz + K[0,2] = ky + K[1,2] = -kx + K[1,0] = -K[0,1] + K[2,0] = -K[0,2] + K[2,1] = -K[1,2] + I = np.eye(3) + st = np.sin(theta) + ct = np.cos(theta) + R = I + st * K + (1 - ct)*(K @ K) + return R + def calc_bounds(vertices, allowance=0): x_min = min(v[0] for v in vertices) y_min = min(v[1] for v in vertices) From e3cc0e91857b2e8ac6a98c0cb433e6d3f8772f01 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Sat, 16 Nov 2024 10:37:48 +0500 Subject: [PATCH 2/4] Refactor: split utils.field.vector into several modules. --- nodes/field/attractor_field_mk2.py | 30 +- nodes/field/compose_vector_field.py | 2 +- nodes/field/coordinate_scalar_field.py | 1 - nodes/field/differential_operations.py | 5 +- nodes/field/frame_along_curve.py | 3 +- nodes/field/merge_scalar_fields.py | 1 - nodes/field/mesh_normal_field.py | 2 +- nodes/field/rotation_field.py | 3 +- nodes/field/scalar_field_point.py | 2 +- nodes/field/vector_field_filter.py | 6 +- nodes/field/vector_field_math.py | 6 +- old_nodes/attractor_field.py | 25 +- utils/field/attractor.py | 673 +++++++++++++++++++++ utils/field/differential.py | 144 +++++ utils/field/scalar.py | 384 +----------- utils/field/vector.py | 789 +------------------------ utils/field/vector_operations.py | 337 +++++++++++ utils/geodesic.py | 3 +- 18 files changed, 1236 insertions(+), 1180 deletions(-) create mode 100644 utils/field/attractor.py create mode 100644 utils/field/differential.py create mode 100644 utils/field/vector_operations.py diff --git a/nodes/field/attractor_field_mk2.py b/nodes/field/attractor_field_mk2.py index 03328a68f6..132af6577e 100644 --- a/nodes/field/attractor_field_mk2.py +++ b/nodes/field/attractor_field_mk2.py @@ -8,21 +8,23 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat -from sverchok.utils.field.scalar import (SvScalarFieldPointDistance, - SvMergedScalarField, SvKdtScalarField, - SvLineAttractorScalarField, SvPlaneAttractorScalarField, - SvCircleAttractorScalarField, - SvEdgeAttractorScalarField, - SvBvhAttractorScalarField, - SvBvhEdgesAttractorScalarField +from sverchok.utils.field.scalar import SvMergedScalarField +from sverchok.utils.field.vector_operations import ( + SvAverageVectorField, + SvSelectVectorField) +from sverchok.utils.field.attractor import (SvScalarFieldPointDistance, + SvKdtScalarField, + SvLineAttractorScalarField, SvPlaneAttractorScalarField, + SvCircleAttractorScalarField, + SvEdgeAttractorScalarField, + SvBvhAttractorScalarField, + SvVectorFieldPointDistance, + SvKdtVectorField, + SvLineAttractorVectorField, SvPlaneAttractorVectorField, + SvCircleAttractorVectorField, + SvEdgeAttractorVectorField, + SvBvhAttractorVectorField ) -from sverchok.utils.field.vector import (SvVectorFieldPointDistance, - SvAverageVectorField, SvKdtVectorField, - SvLineAttractorVectorField, SvPlaneAttractorVectorField, - SvCircleAttractorVectorField, - SvEdgeAttractorVectorField, - SvBvhAttractorVectorField, - SvSelectVectorField) from sverchok.utils.math import all_falloff_types, falloff_array from sverchok.utils.kdtree import SvKdTree diff --git a/nodes/field/compose_vector_field.py b/nodes/field/compose_vector_field.py index 586a972660..f468b65a1e 100644 --- a/nodes/field/compose_vector_field.py +++ b/nodes/field/compose_vector_field.py @@ -6,7 +6,7 @@ from sverchok.data_structure import zip_long_repeat, updateNode from sverchok.utils.math import coordinate_modes -from sverchok.utils.field.vector import SvComposedVectorField +from sverchok.utils.field.vector_operations import SvComposedVectorField class SvComposeVectorFieldNode(SverchCustomTreeNode, bpy.types.Node): """ diff --git a/nodes/field/coordinate_scalar_field.py b/nodes/field/coordinate_scalar_field.py index f78c9ec01a..efa611d510 100644 --- a/nodes/field/coordinate_scalar_field.py +++ b/nodes/field/coordinate_scalar_field.py @@ -7,7 +7,6 @@ from sverchok.utils.field.scalar import SvCoordinateScalarField - class SvCoordScalarFieldNode(SverchCustomTreeNode, bpy.types.Node): """ Triggers: Coordinate Scalar Field diff --git a/nodes/field/differential_operations.py b/nodes/field/differential_operations.py index d316af7b35..27fc9b55d8 100644 --- a/nodes/field/differential_operations.py +++ b/nodes/field/differential_operations.py @@ -5,8 +5,9 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat -from sverchok.utils.field.scalar import (SvVectorFieldDivergence, SvScalarFieldLaplacian) -from sverchok.utils.field.vector import (SvScalarFieldGradient, SvVectorFieldRotor) +from sverchok.utils.field.differential import ( + SvVectorFieldDivergence, SvScalarFieldLaplacian, + SvScalarFieldGradient, SvVectorFieldRotor) from sverchok.utils.modules.sockets import SvDynamicSocketsHandler, SocketInfo sockets_handler = SvDynamicSocketsHandler() diff --git a/nodes/field/frame_along_curve.py b/nodes/field/frame_along_curve.py index 11d08431cf..8b2df423b4 100644 --- a/nodes/field/frame_along_curve.py +++ b/nodes/field/frame_along_curve.py @@ -1,8 +1,7 @@ import numpy as np import bpy -from mathutils import Quaternion -from bpy.props import EnumProperty, IntProperty, FloatProperty +from bpy.props import EnumProperty, IntProperty from mathutils import Matrix from sverchok.node_tree import SverchCustomTreeNode diff --git a/nodes/field/merge_scalar_fields.py b/nodes/field/merge_scalar_fields.py index 8375255e13..525c4617ff 100644 --- a/nodes/field/merge_scalar_fields.py +++ b/nodes/field/merge_scalar_fields.py @@ -6,7 +6,6 @@ from sverchok.utils.field.scalar import SvMergedScalarField, SvScalarField - class SvMergeScalarFieldsNode(SverchCustomTreeNode, bpy.types.Node): """ Triggers: Merge / Join Scalar Fields diff --git a/nodes/field/mesh_normal_field.py b/nodes/field/mesh_normal_field.py index 9d9a43c6ea..1e5d8b51ed 100644 --- a/nodes/field/mesh_normal_field.py +++ b/nodes/field/mesh_normal_field.py @@ -14,7 +14,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat from sverchok.utils.sv_bmesh_utils import bmesh_from_pydata -from sverchok.utils.field.vector import SvBvhAttractorVectorField +from sverchok.utils.field.attractor import SvBvhAttractorVectorField from sverchok.utils.field.rbf import SvBvhRbfNormalVectorField from sverchok.dependencies import scipy from sverchok.utils.math import rbf_functions diff --git a/nodes/field/rotation_field.py b/nodes/field/rotation_field.py index 18caa45787..a5914500a0 100644 --- a/nodes/field/rotation_field.py +++ b/nodes/field/rotation_field.py @@ -6,7 +6,8 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat -from sverchok.utils.field.vector import (SvAverageVectorField, SvRotationVectorField, SvSelectVectorField) +from sverchok.utils.field.vector import (SvRotationVectorField) +from sverchok.utils.field.vector_operations import (SvAverageVectorField, SvSelectVectorField) from sverchok.utils.math import all_falloff_types, falloff_array class SvRotationFieldNode(SverchCustomTreeNode, bpy.types.Node): diff --git a/nodes/field/scalar_field_point.py b/nodes/field/scalar_field_point.py index 3f2ada909a..92a66bac22 100644 --- a/nodes/field/scalar_field_point.py +++ b/nodes/field/scalar_field_point.py @@ -7,7 +7,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat -from sverchok.utils.field.scalar import SvScalarFieldPointDistance +from sverchok.utils.field.attractor import SvScalarFieldPointDistance from sverchok.utils.math import falloff_types, falloff_array class SvScalarFieldPointNode(SverchCustomTreeNode, bpy.types.Node): diff --git a/nodes/field/vector_field_filter.py b/nodes/field/vector_field_filter.py index 2cb79f0608..2788ac45e5 100644 --- a/nodes/field/vector_field_filter.py +++ b/nodes/field/vector_field_filter.py @@ -11,7 +11,11 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, map_recursive from sverchok.utils.math import coordinate_modes -from sverchok.utils.field.vector import SvVectorFieldCartesianFilter, SvVectorFieldCylindricalFilter, SvVectorFieldSphericalFilter, SvVectorField +from sverchok.utils.field.vector import SvVectorField +from sverchok.utils.field.vector_operations import ( + SvVectorFieldCartesianFilter, + SvVectorFieldCylindricalFilter, + SvVectorFieldSphericalFilter) class SvVectorFieldFilterNode(SverchCustomTreeNode, bpy.types.Node): """ diff --git a/nodes/field/vector_field_math.py b/nodes/field/vector_field_math.py index b929a14355..96788a1237 100644 --- a/nodes/field/vector_field_math.py +++ b/nodes/field/vector_field_math.py @@ -9,11 +9,11 @@ SvVectorFieldsScalarProduct, SvVectorFieldNorm, SvVectorScalarFieldComposition) -from sverchok.utils.field.vector import (SvVectorField, +from sverchok.utils.field.vector import (SvAbsoluteVectorField, SvRelativeVectorField) +from sverchok.utils.field.vector_operations import ( SvVectorFieldBinOp, SvVectorFieldMultipliedByScalar, - SvVectorFieldsLerp, SvVectorFieldCrossProduct, + SvVectorFieldsLerp, SvVectorFieldCrossProduct, SvVectorFieldTangent, SvVectorFieldCotangent, - SvAbsoluteVectorField, SvRelativeVectorField, SvVectorFieldComposition) from sverchok.utils.modules.sockets import SvDynamicSocketsHandler, SocketInfo diff --git a/old_nodes/attractor_field.py b/old_nodes/attractor_field.py index afbed4eda2..906a816707 100644 --- a/old_nodes/attractor_field.py +++ b/old_nodes/attractor_field.py @@ -9,14 +9,23 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat -from sverchok.utils.field.scalar import (SvScalarFieldPointDistance, - SvMergedScalarField, SvKdtScalarField, - SvLineAttractorScalarField, SvPlaneAttractorScalarField, - SvBvhAttractorScalarField) -from sverchok.utils.field.vector import (SvVectorFieldPointDistance, - SvAverageVectorField, SvKdtVectorField, - SvLineAttractorVectorField, SvPlaneAttractorVectorField, - SvBvhAttractorVectorField) +from sverchok.utils.field.scalar import SvMergedScalarField +from sverchok.utils.field.vector_operations import ( + SvAverageVectorField, + SvSelectVectorField) +from sverchok.utils.field.attractor import (SvScalarFieldPointDistance, + SvKdtScalarField, + SvLineAttractorScalarField, SvPlaneAttractorScalarField, + SvCircleAttractorScalarField, + SvEdgeAttractorScalarField, + SvBvhAttractorScalarField, + SvVectorFieldPointDistance, + SvKdtVectorField, + SvLineAttractorVectorField, SvPlaneAttractorVectorField, + SvCircleAttractorVectorField, + SvEdgeAttractorVectorField, + SvBvhAttractorVectorField + ) from sverchok.utils.math import all_falloff_types, falloff_array class SvExAttractorFieldNode(SverchCustomTreeNode, bpy.types.Node): diff --git a/utils/field/attractor.py b/utils/field/attractor.py new file mode 100644 index 0000000000..35a9027377 --- /dev/null +++ b/utils/field/attractor.py @@ -0,0 +1,673 @@ +# 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 + +from math import copysign, pi +import numpy as np + +from mathutils import Vector +from mathutils import bvhtree +from sverchok.utils.geom import LineEquation, CircleEquation3D +from sverchok.utils.kdtree import SvKdTree +from sverchok.utils.field.scalar import SvScalarField +from sverchok.utils.field.vector import SvVectorField + +class SvKdtVectorField(SvVectorField): + + def __init__(self, vertices=None, kdt=None, falloff=None, negate=False, power=2): + self.falloff = falloff + self.negate = negate + if kdt is not None: + self.kdt = kdt + elif vertices is not None: + self.kdt = SvKdTree.new(SvKdTree.best_available_implementation(), vertices, power=power) + else: + raise Exception("Either kdt or vertices must be provided") + self.__description__ = "KDT Attractor" + + def evaluate(self, x, y, z): + nearest, i, distance = self.kdt.query(np.array([x, y, z])) + vector = nearest - np.array([x, y, z]) + if self.falloff is not None: + value = self.falloff(np.array([distance]))[0] + if self.negate: + value = - value + norm = np.linalg.norm(vector) + return value * vector / norm + else: + if self.negate: + return - vector + else: + return vector + + def evaluate_grid(self, xs, ys, zs): + points = np.stack((xs, ys, zs)).T + locs, idxs, distances = self.kdt.query_array(points) + vectors = locs - points + if self.negate: + vectors = - vectors + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + lens = self.falloff(norms) + nonzero = (norms > 0)[:,0] + lens = self.falloff(norms) + vectors[nonzero] = vectors[nonzero] / norms[nonzero] + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvVectorFieldPointDistance(SvVectorField): + def __init__(self, center, metric='EUCLIDEAN', falloff=None, power=2): + self.center = center + self.falloff = falloff + self.metric = metric + self.power = power + self.__description__ = "Distance from {}".format(tuple(center)) + + def evaluate_grid(self, xs, ys, zs): + x0, y0, z0 = tuple(self.center) + xs = x0 - xs + ys = y0 - ys + zs = z0 - zs + vectors = np.stack((xs, ys, zs)) + if self.metric == 'EUCLIDEAN': + norms = np.linalg.norm(vectors, axis=0) + elif self.metric == 'CHEBYSHEV': + norms = np.max(np.abs(vectors), axis=0) + elif self.metric == 'MANHATTAN': + norms = np.sum(np.abs(vectors), axis=0) + elif self.metric == 'CUSTOM': + norms = np.linalg.norm(vectors, axis=0, ord=self.power) + else: + raise Exception('Unknown metric') + if self.falloff is not None: + lens = self.falloff(norms) + R = lens * vectors / norms + else: + R = vectors + return R[0], R[1], R[2] + + def evaluate(self, x, y, z): + point = np.array([x, y, z]) - self.center + if self.metric == 'EUCLIDEAN': + norm = np.linalg.norm(point) + elif self.metric == 'CHEBYSHEV': + norm = np.max(point) + elif self.metric == 'MANHATTAN': + norm = np.sum(np.abs(point)) + elif self.metric == 'CUSTOM': + norm = np.linalg.norm(point, ord=self.power) + else: + raise Exception('Unknown metric') + if self.falloff is not None: + value = self.falloff(np.array([norm]))[0] + return value * point / norm + else: + return point + +class SvLineAttractorVectorField(SvVectorField): + + def __init__(self, center, direction, falloff=None): + self.center = center + self.direction = direction + self.falloff = falloff + self.__description__ = "Line Attractor" + + def evaluate(self, x, y, z): + vertex = np.array([x,y,z]) + direction = self.direction + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) + dv = to_center - projection + if self.falloff is not None: + norm = np.linalg.norm(dv) + dv = self.falloff(norm) * dv / norm + return dv + + def evaluate_grid(self, xs, ys, zs): + direction = self.direction + direction2 = np.dot(direction, direction) + + def func(vertex): + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / direction2 + dv = to_center - projection + return dv + + points = np.stack((xs, ys, zs)).T + vectors = np.vectorize(func, signature='(3)->(3)')(points) + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + nonzero = (norms > 0)[:,0] + lens = self.falloff(norms) + vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvPlaneAttractorVectorField(SvVectorField): + + def __init__(self, center, direction, falloff=None): + self.center = center + self.direction = direction + self.falloff = falloff + self.__description__ = "Plane Attractor" + + def evaluate(self, x, y, z): + vertex = np.array([x,y,z]) + direction = self.direction + to_center = self.center - vertex + dv = np.dot(to_center, direction) * direction / np.dot(direction, direction) + if self.falloff is not None: + norm = np.linalg.norm(dv) + dv = self.falloff(norm) * dv / norm + return dv + + def evaluate_grid(self, xs, ys, zs): + direction = self.direction + direction2 = np.dot(direction, direction) + + def func(vertex): + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / direction2 + return projection + + points = np.stack((xs, ys, zs)).T + vectors = np.vectorize(func, signature='(3)->(3)')(points) + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + lens = self.falloff(norms) + nonzero = (norms > 0)[:,0] + vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvCircleAttractorVectorField(SvVectorField): + __description__ = "Circle Attractor" + + def __init__(self, center, radius, normal, falloff=None): + self.circle = CircleEquation3D.from_center_radius_normal(center, radius, normal) + self.falloff = falloff + + def evaluate(self, x, y, z): + v = np.array([x,y,z]) + projection = self.circle.get_projections([v])[0] + vector = projection - v + if self.fallof is not None: + new_len = self.falloff(np.array([distance]))[0] + norm = np.linalg.norm(vector) + return new_len * vector / norm + else: + return vector + + def evaluate_grid(self, xs, ys, zs): + vs = np.stack((xs, ys, zs)).T + projections = self.circle.get_projections(vs) + vectors = projections - vs + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + lens = self.falloff(norms) + nonzero = (norms > 0)[:,0] + vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvEdgeAttractorVectorField(SvVectorField): + __description__ = "Edge attractor" + + def __init__(self, v1, v2, falloff=None): + self.falloff = falloff + self.v1 = Vector(v1) + self.v2 = Vector(v2) + + def evaluate(self, x, y, z): + v = Vector([x,y,z]) + dv1 = (v - self.v1).length + dv2 = (v - self.v2).length + if dv1 > dv2: + distance_to_nearest = dv2 + nearest_vert = self.v2 + another_vert = self.v1 + else: + distance_to_nearest = dv1 + nearest_vert = self.v1 + another_vert = self.v2 + edge = another_vert - nearest_vert + to_nearest = v - nearest_vert + if to_nearest.length == 0: + return 0 + angle = edge.angle(to_nearest) + if angle > pi/2: + distance = distance_to_nearest + vector = - to_nearest + else: + vector = LineEquation.from_two_points(self.v1, self.v2).projection_of_points(v) + distance = vector.length + vector = np.array(vector) + if self.falloff is not None: + return self.falloff(distance) * vector / distance + else: + return vector + + def evaluate_grid(self, xs, ys, zs): + n = len(xs) + vs = np.stack((xs, ys, zs)).T + v1 = np.array(self.v1) + v2 = np.array(self.v2) + dv1s = np.linalg.norm(vs - v1, axis=1) + dv2s = np.linalg.norm(vs - v2, axis=1) + v1_is_nearest = (dv1s < dv2s) + v2_is_nearest = np.logical_not(v1_is_nearest) + nearest_verts = np.empty_like(vs) + other_verts = np.empty_like(vs) + nearest_verts[v1_is_nearest] = v1 + nearest_verts[v2_is_nearest] = v2 + other_verts[v1_is_nearest] = v2 + other_verts[v2_is_nearest] = v1 + + to_nearest = vs - nearest_verts + + edges = other_verts - nearest_verts + dot = (to_nearest * edges).sum(axis=1) + at_edge = (dot > 0) + at_vertex = np.logical_not(at_edge) + at_v1 = np.logical_and(at_vertex, v1_is_nearest) + at_v2 = np.logical_and(at_vertex, v2_is_nearest) + + line = LineEquation.from_two_points(self.v1, self.v2) + + vectors = np.empty((n,3)) + vectors[at_edge] = line.projection_of_points(vs[at_edge]) - vs[at_edge] + vectors[at_v1] = v1 - vs[at_v1] + vectors[at_v2] = v2 - vs[at_v2] + + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + lens = self.falloff(norms) + nonzero = (norms > 0)[:,0] + vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvBvhAttractorVectorField(SvVectorField): + + def __init__(self, bvh=None, verts=None, faces=None, falloff=None, use_normal=False, signed_normal=False): + self.falloff = falloff + self.use_normal = use_normal + self.signed_normal = signed_normal + if bvh is not None: + self.bvh = bvh + elif verts is not None and faces is not None: + self.bvh = bvhtree.BVHTree.FromPolygons(verts, faces) + else: + raise Exception("Either bvh or verts and faces must be provided!") + self.__description__ = "BVH Attractor" + + def evaluate(self, x, y, z): + vertex = Vector((x,y,z)) + nearest, normal, idx, distance = self.bvh.find_nearest(vertex) + if self.use_normal: + if self.signed_normal: + sign = (v - nearest).dot(normal) + sign = copysign(1, sign) + else: + sign = 1 + return sign * np.array(normal) + else: + dv = np.array(nearest - vertex) + if self.falloff is not None: + norm = np.linalg.norm(dv) + len = self.falloff(norm) + dv = len * dv + return dv + else: + return dv + + def evaluate_grid(self, xs, ys, zs): + def find(v): + nearest, normal, idx, distance = self.bvh.find_nearest(v) + if nearest is None: + raise Exception("No nearest point on mesh found for vertex %s" % v) + if self.use_normal: + if self.signed_normal: + sign = (v - nearest).dot(normal) + sign = copysign(1, sign) + else: + sign = 1 + return sign * np.array(normal) + else: + return np.array(nearest) - v + + points = np.stack((xs, ys, zs)).T + vectors = np.vectorize(find, signature='(3)->(3)')(points) + if self.falloff is not None: + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + nonzero = (norms > 0)[:,0] + lens = self.falloff(norms) + vectors[nonzero] = vectors[nonzero] / norms[nonzero] + R = (lens * vectors).T + return R[0], R[1], R[2] + else: + R = vectors.T + return R[0], R[1], R[2] + +class SvScalarFieldPointDistance(SvScalarField): + def __init__(self, center, metric='EUCLIDEAN', falloff=None, power=2): + self.center = center + self.falloff = falloff + self.metric = metric + self.power = power + self.__description__ = "Distance from {}".format(tuple(center)) + + def evaluate_grid(self, xs, ys, zs): + x0, y0, z0 = tuple(self.center) + xs = xs - x0 + ys = ys - y0 + zs = zs - z0 + points = np.stack((xs, ys, zs)) + if self.metric == 'EUCLIDEAN': + norms = np.linalg.norm(points, axis=0) + elif self.metric == 'CHEBYSHEV': + norms = np.max(np.abs(points), axis=0) + elif self.metric == 'MANHATTAN': + norms = np.sum(np.abs(points), axis=0) + elif self.metric == 'CUSTOM': + norms = np.linalg.norm(points, axis=0, ord=self.power) + else: + raise Exception('Unknown metric') + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + + def evaluate(self, x, y, z): + point = np.array([x, y, z]) - self.center + if self.metric == 'EUCLIDEAN': + norm = np.linalg.norm(point) + elif self.metric == 'CHEBYSHEV': + norm = np.max(np.abs(point)) + elif self.metric == 'MANHATTAN': + norm = np.sum(np.abs(point)) + elif self.metric == 'CUSTOM': + norm = np.linalg.norm(point, ord=self.power) + else: + raise Exception('Unknown metric') + if self.falloff is not None: + return self.falloff(np.array([norm]))[0] + else: + return norm + +class SvKdtScalarField(SvScalarField): + __description__ = "KDT" + + def __init__(self, vertices=None, kdt=None, falloff=None, power=2): + self.falloff = falloff + if kdt is not None: + self.kdt = kdt + elif vertices is not None: + self.kdt = SvKdTree.new(SvKdTree.best_available_implementation(), vertices, power=power) + else: + raise Exception("Either kdt or vertices must be provided") + + def evaluate(self, x, y, z): + nearest, i, distance = self.kdt.query(np.array([x,y,z])) + if self.falloff is not None: + value = self.falloff(np.array([distance]))[0] + return value + else: + return distance + + def evaluate_grid(self, xs, ys, zs): + points = np.stack((xs, ys, zs)).T + locs, idxs, distances = self.kdt.query_array(points) + if self.falloff is not None: + result = self.falloff(distances) + return result + else: + return distances + +class SvLineAttractorScalarField(SvScalarField): + __description__ = "Line Attractor" + + def __init__(self, center, direction, falloff=None): + self.center = center + self.direction = direction + self.falloff = falloff + + def evaluate(self, x, y, z): + vertex = np.array([x,y,z]) + direction = self.direction + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) + dv = to_center - projection + return np.linalg.norm(dv) + + def evaluate_grid(self, xs, ys, zs): + direction = self.direction + direction2 = np.dot(direction, direction) + points = np.stack((xs, ys, zs)).T + to_center = self.center - points + dot = (to_center * direction).sum(axis=1) + projections = (dot * direction[np.newaxis].T / direction2).T + vectors = to_center - projections + norms = np.linalg.norm(vectors, axis=1) + + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + +class SvPlaneAttractorScalarField(SvScalarField): + __description__ = "Plane Attractor" + + def __init__(self, center, direction, falloff=None): + self.center = center + self.direction = direction + self.falloff = falloff + + def evaluate(self, x, y, z): + vertex = np.array([x,y,z]) + direction = self.direction + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) + return np.linalg.norm(projection) + + def evaluate_grid(self, xs, ys, zs): + direction = self.direction + direction2 = np.dot(direction, direction) + + def func(vertex): + to_center = self.center - vertex + projection = np.dot(to_center, direction) * direction / direction2 + return np.linalg.norm(projection) + + points = np.stack((xs, ys, zs)).T + norms = np.vectorize(func, signature='(3)->()')(points) + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + +class SvCircleAttractorScalarField(SvScalarField): + __description__ = "Circle Attractor" + + def __init__(self, center, radius, normal, falloff=None): + self.circle = CircleEquation3D.from_center_radius_normal(center, radius, normal) + self.falloff = falloff + + def evaluate(self, x, y, z): + v = np.array([x,y,z]) + projection = self.circle.get_projections([v])[0] + distance = np.linalg.norm(v - projection) + if self.fallof is not None: + return self.falloff(np.array([distance]))[0] + else: + return distance + + def evaluate_grid(self, xs, ys, zs): + vs = np.stack((xs, ys, zs)).T + projections = self.circle.get_projections(vs) + distances = np.linalg.norm(vs - projections, axis=1) + if self.falloff is not None: + return self.falloff(distances) + else: + return distances + +class SvBvhAttractorScalarField(SvScalarField): + __description__ = "BVH Attractor (faces)" + + def __init__(self, bvh=None, verts=None, faces=None, falloff=None, signed=False): + self.falloff = falloff + self.signed = signed + if bvh is not None: + self.bvh = bvh + elif verts is not None and faces is not None: + self.bvh = bvhtree.BVHTree.FromPolygons(verts, faces) + else: + raise Exception("Either bvh or verts and faces must be provided!") + + def evaluate(self, x, y, z): + nearest, normal, idx, distance = self.bvh.find_nearest((x,y,z)) + if self.signed: + sign = (Vector((x,y,z)) - nearest).dot(normal) + sign = copysign(1, sign) + else: + sign = 1 + value = sign * distance + if self.falloff is None: + return value + else: + return self.falloff(np.array([value]))[0] + + def evaluate_grid(self, xs, ys, zs): + def find(v): + nearest, normal, idx, distance = self.bvh.find_nearest(v) + if nearest is None: + raise Exception("No nearest point on mesh found for vertex %s" % v) + if self.signed: + sign = (v - nearest).dot(normal) + sign = copysign(1, sign) + else: + sign = 1 + return sign * distance + + points = np.stack((xs, ys, zs)).T + norms = np.vectorize(find, signature='(3)->()')(points) + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + +class SvBvhEdgesAttractorScalarField(SvScalarField): + __description__ = "BVH Attractor (edges)" + + def __init__(self, verts, edges, falloff=None): + self.verts = verts + self.edges = edges + self.falloff = falloff + self.bvh = self._make_bvh(verts, edges) + + def _make_bvh(self, verts, edges): + faces = [(i1, i2, i1) for i1, i2 in edges] + return bvhtree.BVHTree.FromPolygons(verts, faces) + + def evaluate(self, x, y, z): + nearest, normal, idx, distance = self.bvh.find_nearest((x,y,z)) + if self.falloff is None: + return distance + else: + return self.falloff(np.array([distance]))[0] + + def evaluate_grid(self, xs, ys, zs): + def find(v): + nearest, normal, idx, distance = self.bvh.find_nearest(v) + return distance + + points = np.stack((xs, ys, zs)).T + norms = np.vectorize(find, signature='(3)->()')(points) + if self.falloff is not None: + result = self.falloff(norms) + return result + else: + return norms + +class SvEdgeAttractorScalarField(SvScalarField): + __description__ = "Edge attractor" + + def __init__(self, v1, v2, falloff=None): + self.falloff = falloff + self.v1 = Vector(v1) + self.v2 = Vector(v2) + + def evaluate(self, x, y, z): + v = Vector([x,y,z]) + dv1 = (v - self.v1).length + dv2 = (v - self.v2).length + if dv1 > dv2: + distance_to_nearest = dv2 + nearest_vert = self.v2 + another_vert = self.v1 + else: + distance_to_nearest = dv1 + nearest_vert = self.v1 + another_vert = self.v2 + edge = another_vert - nearest_vert + to_nearest = v - nearest_vert + if to_nearest.length == 0: + return 0 + angle = edge.angle(to_nearest) + if angle > pi/2: + distance = distance_to_nearest + else: + distance = LineEquation.from_two_points(self.v1, self.v2).distance_to_point(v) + if self.falloff is not None: + value = self.falloff(np.array([distance]))[0] + return value + else: + return distance + + def evaluate_grid(self, xs, ys, zs): + n = len(xs) + vs = np.stack((xs, ys, zs)).T + v1 = np.array(self.v1) + v2 = np.array(self.v2) + dv1 = vs - v1 + dv2 = vs - v2 + edge = v2 - v1 + dot1 = (dv1 * edge).sum(axis=1) + dot2 = -(dv2 * edge).sum(axis=1) + v1_is_nearest = (dot1 < 0) + v2_is_nearest = (dot2 < 0) + at_edge = np.logical_not(np.logical_or(v1_is_nearest, v2_is_nearest)) + + distances = np.empty((n,)) + distances[v1_is_nearest] = np.linalg.norm(dv1[v1_is_nearest], axis=1) + distances[v2_is_nearest] = np.linalg.norm(dv2[v2_is_nearest], axis=1) + distances[at_edge] = LineEquation.from_two_points(self.v1, self.v2).distance_to_points(vs[at_edge]) + + if self.falloff is not None: + distances = self.falloff(distances) + return distances + else: + return distances + diff --git a/utils/field/differential.py b/utils/field/differential.py new file mode 100644 index 0000000000..056384018c --- /dev/null +++ b/utils/field/differential.py @@ -0,0 +1,144 @@ +# 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 + +from sverchok.utils.field.scalar import SvScalarField +from sverchok.utils.field.vector import SvVectorField + +class SvScalarFieldGradient(SvVectorField): + def __init__(self, field, step): + self.field = field + self.step = step + self.__description__ = "Grad({})".format(field) + + def evaluate(self, x, y, z): + return self.field.gradient([x, y, z], step=self.step) + + def evaluate_grid(self, xs, ys, zs): + return self.field.gradient_grid(xs, ys, zs, step=self.step) + +class SvVectorFieldRotor(SvVectorField): + def __init__(self, field, step): + self.field = field + self.step = step + self.__description__ = "Rot({})".format(field) + + def evaluate(self, x, y, z): + step = self.step + _, y_dx_plus, z_dx_plus = self.field.evaluate(x+step,y,z) + _, y_dx_minus, z_dx_minus = self.field.evaluate(x-step,y,z) + x_dy_plus, _, z_dy_plus = self.field.evaluate(x, y+step, z) + x_dy_minus, _, z_dy_minus = self.field.evaluate(x, y-step, z) + x_dz_plus, y_dz_plus, _ = self.field.evaluate(x, y, z+step) + x_dz_minus, y_dz_minus, _ = self.field.evaluate(x, y, z-step) + + dy_dx = (y_dx_plus - y_dx_minus) / (2*step) + dz_dx = (z_dx_plus - z_dx_minus) / (2*step) + dx_dy = (x_dy_plus - x_dy_minus) / (2*step) + dz_dy = (z_dy_plus - z_dy_minus) / (2*step) + dx_dz = (x_dz_plus - x_dz_minus) / (2*step) + dy_dz = (y_dz_plus - y_dz_minus) / (2*step) + + rx = dz_dy - dy_dz + ry = - (dz_dx - dx_dz) + rz = dy_dx - dx_dy + + return np.array([rx, ry, rz]) + + def evaluate_grid(self, xs, ys, zs): + step = self.step + _, y_dx_plus, z_dx_plus = self.field.evaluate_grid(xs+step,ys,zs) + _, y_dx_minus, z_dx_minus = self.field.evaluate_grid(xs-step,ys,zs) + x_dy_plus, _, z_dy_plus = self.field.evaluate_grid(xs, ys+step, zs) + x_dy_minus, _, z_dy_minus = self.field.evaluate_grid(xs, ys-step, zs) + x_dz_plus, y_dz_plus, _ = self.field.evaluate_grid(xs, ys, zs+step) + x_dz_minus, y_dz_minus, _ = self.field.evaluate_grid(xs, ys, zs-step) + + dy_dx = (y_dx_plus - y_dx_minus) / (2*step) + dz_dx = (z_dx_plus - z_dx_minus) / (2*step) + dx_dy = (x_dy_plus - x_dy_minus) / (2*step) + dz_dy = (z_dy_plus - z_dy_minus) / (2*step) + dx_dz = (x_dz_plus - x_dz_minus) / (2*step) + dy_dz = (y_dz_plus - y_dz_minus) / (2*step) + + rx = dz_dy - dy_dz + ry = - (dz_dx - dx_dz) + rz = dy_dx - dx_dy + R = np.stack((rx, ry, rz)) + return R[0], R[1], R[2] + +class SvVectorFieldDivergence(SvScalarField): + def __init__(self, field, step): + self.field = field + self.step = step + self.__description__ = "Div({})".format(field) + + def evaluate(self, x, y, z): + step = self.step + xs_dx_plus, _, _ = self.field.evaluate(x+step,y,z) + xs_dx_minus, _, _ = self.field.evaluate(x-step,y,z) + _, ys_dy_plus, _ = self.field.evaluate(x, y+step, z) + _, ys_dy_minus, _ = self.field.evaluate(x, y-step, z) + _, _, zs_dz_plus = self.field.evaluate(x, y, z+step) + _, _, zs_dz_minus = self.field.evaluate(x, y, z-step) + + dx_dx = (xs_dx_plus - xs_dx_minus) / (2*step) + dy_dy = (ys_dy_plus - ys_dy_minus) / (2*step) + dz_dz = (zs_dz_plus - zs_dz_minus) / (2*step) + + return dx_dx + dy_dy + dz_dz + + def evaluate_grid(self, xs, ys, zs): + step = self.step + xs_dx_plus, _, _ = self.field.evaluate_grid(xs+step, ys,zs) + xs_dx_minus, _, _ = self.field.evaluate_grid(xs-step,ys,zs) + _, ys_dy_plus, _ = self.field.evaluate_grid(xs, ys+step, zs) + _, ys_dy_minus, _ = self.field.evaluate_grid(xs, ys-step, zs) + _, _, zs_dz_plus = self.field.evaluate_grid(xs, ys, zs+step) + _, _, zs_dz_minus = self.field.evaluate_grid(xs, ys, zs-step) + + dx_dx = (xs_dx_plus - xs_dx_minus) / (2*step) + dy_dy = (ys_dy_plus - ys_dy_minus) / (2*step) + dz_dz = (zs_dz_plus - zs_dz_minus) / (2*step) + + return dx_dx + dy_dy + dz_dz + +class SvScalarFieldLaplacian(SvScalarField): + def __init__(self, field, step): + self.field = field + self.step = step + self.__description__ = "Laplace({})".format(field) + + def evaluate(self, x, y, z): + step = self.step + v_dx_plus = self.field.evaluate(x+step,y,z) + v_dx_minus = self.field.evaluate(x-step,y,z) + v_dy_plus = self.field.evaluate(x, y+step, z) + v_dy_minus = self.field.evaluate(x, y-step, z) + v_dz_plus = self.field.evaluate(x, y, z+step) + v_dz_minus = self.field.evaluate(x, y, z-step) + v0 = self.field.evaluate(x, y, z) + + sides = v_dx_plus + v_dx_minus + v_dy_plus + v_dy_minus + v_dz_plus + v_dz_minus + result = (sides - 6*v0) / (8 * step * step * step) + return result + + def evaluate_grid(self, xs, ys, zs): + step = self.step + v_dx_plus = self.field.evaluate_grid(xs+step, ys,zs) + v_dx_minus = self.field.evaluate_grid(xs-step,ys,zs) + v_dy_plus = self.field.evaluate_grid(xs, ys+step, zs) + v_dy_minus = self.field.evaluate_grid(xs, ys-step, zs) + v_dz_plus = self.field.evaluate_grid(xs, ys, zs+step) + v_dz_minus = self.field.evaluate_grid(xs, ys, zs-step) + v0 = self.field.evaluate_grid(xs, ys, zs) + + sides = v_dx_plus + v_dx_minus + v_dy_plus + v_dy_minus + v_dz_plus + v_dz_minus + result = (sides - 6*v0) / (8 * step * step * step) + return result + diff --git a/utils/field/scalar.py b/utils/field/scalar.py index d17f99808f..bb28836488 100644 --- a/utils/field/scalar.py +++ b/utils/field/scalar.py @@ -6,15 +6,10 @@ # License-Filename: LICENSE import numpy as np -from math import copysign, sqrt, sin, cos, atan2, acos, pi +from math import copysign, sqrt, sin, cos, atan2, acos -from mathutils import Matrix, Vector -from mathutils import kdtree -from mathutils import bvhtree - -from sverchok.utils.math import from_cylindrical, from_spherical, to_cylindrical, to_spherical, np_dot -from sverchok.utils.geom import LineEquation, CircleEquation3D -from sverchok.utils.kdtree import SvKdTree +from sverchok.utils.math import to_cylindrical, to_spherical +from sverchok.utils.field.voronoi import SvVoronoiFieldData ################## # # @@ -135,52 +130,6 @@ def evaluate(self, x, y, z): V = self.in_field.evaluate(x, y, z) return self.function(x, y, z, V) -class SvScalarFieldPointDistance(SvScalarField): - def __init__(self, center, metric='EUCLIDEAN', falloff=None, power=2): - self.center = center - self.falloff = falloff - self.metric = metric - self.power = power - self.__description__ = "Distance from {}".format(tuple(center)) - - def evaluate_grid(self, xs, ys, zs): - x0, y0, z0 = tuple(self.center) - xs = xs - x0 - ys = ys - y0 - zs = zs - z0 - points = np.stack((xs, ys, zs)) - if self.metric == 'EUCLIDEAN': - norms = np.linalg.norm(points, axis=0) - elif self.metric == 'CHEBYSHEV': - norms = np.max(np.abs(points), axis=0) - elif self.metric == 'MANHATTAN': - norms = np.sum(np.abs(points), axis=0) - elif self.metric == 'CUSTOM': - norms = np.linalg.norm(points, axis=0, ord=self.power) - else: - raise Exception('Unknown metric') - if self.falloff is not None: - result = self.falloff(norms) - return result - else: - return norms - - def evaluate(self, x, y, z): - point = np.array([x, y, z]) - self.center - if self.metric == 'EUCLIDEAN': - norm = np.linalg.norm(point) - elif self.metric == 'CHEBYSHEV': - norm = np.max(np.abs(point)) - elif self.metric == 'MANHATTAN': - norm = np.sum(np.abs(point)) - elif self.metric == 'CUSTOM': - norm = np.linalg.norm(point, ord=self.power) - else: - raise Exception('Unknown metric') - if self.falloff is not None: - return self.falloff(np.array([norm]))[0] - else: - return norm class SvScalarFieldBinOp(SvScalarField): def __init__(self, field1, field2, function): @@ -351,263 +300,6 @@ def evaluate_grid(self, xs, ys, zs): raise Exception("unsupported operation") return value -class SvKdtScalarField(SvScalarField): - __description__ = "KDT" - - def __init__(self, vertices=None, kdt=None, falloff=None, power=2): - self.falloff = falloff - if kdt is not None: - self.kdt = kdt - elif vertices is not None: - self.kdt = SvKdTree.new(SvKdTree.best_available_implementation(), vertices, power=power) - else: - raise Exception("Either kdt or vertices must be provided") - - def evaluate(self, x, y, z): - nearest, i, distance = self.kdt.query(np.array([x,y,z])) - if self.falloff is not None: - value = self.falloff(np.array([distance]))[0] - return value - else: - return distance - - def evaluate_grid(self, xs, ys, zs): - points = np.stack((xs, ys, zs)).T - locs, idxs, distances = self.kdt.query_array(points) - if self.falloff is not None: - result = self.falloff(distances) - return result - else: - return distances - -class SvLineAttractorScalarField(SvScalarField): - __description__ = "Line Attractor" - - def __init__(self, center, direction, falloff=None): - self.center = center - self.direction = direction - self.falloff = falloff - - def evaluate(self, x, y, z): - vertex = np.array([x,y,z]) - direction = self.direction - to_center = self.center - vertex - projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) - dv = to_center - projection - return np.linalg.norm(dv) - - def evaluate_grid(self, xs, ys, zs): - direction = self.direction - direction2 = np.dot(direction, direction) - points = np.stack((xs, ys, zs)).T - to_center = self.center - points - dot = (to_center * direction).sum(axis=1) - projections = (dot * direction[np.newaxis].T / direction2).T - vectors = to_center - projections - norms = np.linalg.norm(vectors, axis=1) - - if self.falloff is not None: - result = self.falloff(norms) - return result - else: - return norms - -class SvPlaneAttractorScalarField(SvScalarField): - __description__ = "Plane Attractor" - - def __init__(self, center, direction, falloff=None): - self.center = center - self.direction = direction - self.falloff = falloff - - def evaluate(self, x, y, z): - vertex = np.array([x,y,z]) - direction = self.direction - to_center = self.center - vertex - projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) - return np.linalg.norm(projection) - - def evaluate_grid(self, xs, ys, zs): - direction = self.direction - direction2 = np.dot(direction, direction) - - def func(vertex): - to_center = self.center - vertex - projection = np.dot(to_center, direction) * direction / direction2 - return np.linalg.norm(projection) - - points = np.stack((xs, ys, zs)).T - norms = np.vectorize(func, signature='(3)->()')(points) - if self.falloff is not None: - result = self.falloff(norms) - return result - else: - return norms - -class SvCircleAttractorScalarField(SvScalarField): - __description__ = "Circle Attractor" - - def __init__(self, center, radius, normal, falloff=None): - self.circle = CircleEquation3D.from_center_radius_normal(center, radius, normal) - self.falloff = falloff - - def evaluate(self, x, y, z): - v = np.array([x,y,z]) - projection = self.circle.get_projections([v])[0] - distance = np.linalg.norm(v - projection) - if self.fallof is not None: - return self.falloff(np.array([distance]))[0] - else: - return distance - - def evaluate_grid(self, xs, ys, zs): - vs = np.stack((xs, ys, zs)).T - projections = self.circle.get_projections(vs) - distances = np.linalg.norm(vs - projections, axis=1) - if self.falloff is not None: - return self.falloff(distances) - else: - return distances - -class SvBvhAttractorScalarField(SvScalarField): - __description__ = "BVH Attractor (faces)" - - def __init__(self, bvh=None, verts=None, faces=None, falloff=None, signed=False): - self.falloff = falloff - self.signed = signed - if bvh is not None: - self.bvh = bvh - elif verts is not None and faces is not None: - self.bvh = bvhtree.BVHTree.FromPolygons(verts, faces) - else: - raise Exception("Either bvh or verts and faces must be provided!") - - def evaluate(self, x, y, z): - nearest, normal, idx, distance = self.bvh.find_nearest((x,y,z)) - if self.signed: - sign = (Vector((x,y,z)) - nearest).dot(normal) - sign = copysign(1, sign) - else: - sign = 1 - value = sign * distance - if self.falloff is None: - return value - else: - return self.falloff(np.array([value]))[0] - - def evaluate_grid(self, xs, ys, zs): - def find(v): - nearest, normal, idx, distance = self.bvh.find_nearest(v) - if nearest is None: - raise Exception("No nearest point on mesh found for vertex %s" % v) - if self.signed: - sign = (v - nearest).dot(normal) - sign = copysign(1, sign) - else: - sign = 1 - return sign * distance - - points = np.stack((xs, ys, zs)).T - norms = np.vectorize(find, signature='(3)->()')(points) - if self.falloff is not None: - result = self.falloff(norms) - return result - else: - return norms - -class SvBvhEdgesAttractorScalarField(SvScalarField): - __description__ = "BVH Attractor (edges)" - - def __init__(self, verts, edges, falloff=None): - self.verts = verts - self.edges = edges - self.falloff = falloff - self.bvh = self._make_bvh(verts, edges) - - def _make_bvh(self, verts, edges): - faces = [(i1, i2, i1) for i1, i2 in edges] - return bvhtree.BVHTree.FromPolygons(verts, faces) - - def evaluate(self, x, y, z): - nearest, normal, idx, distance = self.bvh.find_nearest((x,y,z)) - if self.falloff is None: - return distance - else: - return self.falloff(np.array([distance]))[0] - - def evaluate_grid(self, xs, ys, zs): - def find(v): - nearest, normal, idx, distance = self.bvh.find_nearest(v) - return distance - - points = np.stack((xs, ys, zs)).T - norms = np.vectorize(find, signature='(3)->()')(points) - if self.falloff is not None: - result = self.falloff(norms) - return result - else: - return norms - -class SvEdgeAttractorScalarField(SvScalarField): - __description__ = "Edge attractor" - - def __init__(self, v1, v2, falloff=None): - self.falloff = falloff - self.v1 = Vector(v1) - self.v2 = Vector(v2) - - def evaluate(self, x, y, z): - v = Vector([x,y,z]) - dv1 = (v - self.v1).length - dv2 = (v - self.v2).length - if dv1 > dv2: - distance_to_nearest = dv2 - nearest_vert = self.v2 - another_vert = self.v1 - else: - distance_to_nearest = dv1 - nearest_vert = self.v1 - another_vert = self.v2 - edge = another_vert - nearest_vert - to_nearest = v - nearest_vert - if to_nearest.length == 0: - return 0 - angle = edge.angle(to_nearest) - if angle > pi/2: - distance = distance_to_nearest - else: - distance = LineEquation.from_two_points(self.v1, self.v2).distance_to_point(v) - if self.falloff is not None: - value = self.falloff(np.array([distance]))[0] - return value - else: - return distance - - def evaluate_grid(self, xs, ys, zs): - n = len(xs) - vs = np.stack((xs, ys, zs)).T - v1 = np.array(self.v1) - v2 = np.array(self.v2) - dv1 = vs - v1 - dv2 = vs - v2 - edge = v2 - v1 - dot1 = (dv1 * edge).sum(axis=1) - dot2 = -(dv2 * edge).sum(axis=1) - v1_is_nearest = (dot1 < 0) - v2_is_nearest = (dot2 < 0) - at_edge = np.logical_not(np.logical_or(v1_is_nearest, v2_is_nearest)) - - distances = np.empty((n,)) - distances[v1_is_nearest] = np.linalg.norm(dv1[v1_is_nearest], axis=1) - distances[v2_is_nearest] = np.linalg.norm(dv2[v2_is_nearest], axis=1) - distances[at_edge] = LineEquation.from_two_points(self.v1, self.v2).distance_to_points(vs[at_edge]) - - if self.falloff is not None: - distances = self.falloff(distances) - return distances - else: - return distances - class SvVectorScalarFieldComposition(SvScalarField): __description__ = "Composition" @@ -624,76 +316,6 @@ def evaluate_grid(self, xs, ys, zs): vx1, vy1, vz1 = self.vfield.evaluate_grid(xs, ys, zs) return self.sfield.evaluate_grid(vx1, vy1, vz1) -class SvVectorFieldDivergence(SvScalarField): - def __init__(self, field, step): - self.field = field - self.step = step - self.__description__ = "Div({})".format(field) - - def evaluate(self, x, y, z): - step = self.step - xs_dx_plus, _, _ = self.field.evaluate(x+step,y,z) - xs_dx_minus, _, _ = self.field.evaluate(x-step,y,z) - _, ys_dy_plus, _ = self.field.evaluate(x, y+step, z) - _, ys_dy_minus, _ = self.field.evaluate(x, y-step, z) - _, _, zs_dz_plus = self.field.evaluate(x, y, z+step) - _, _, zs_dz_minus = self.field.evaluate(x, y, z-step) - - dx_dx = (xs_dx_plus - xs_dx_minus) / (2*step) - dy_dy = (ys_dy_plus - ys_dy_minus) / (2*step) - dz_dz = (zs_dz_plus - zs_dz_minus) / (2*step) - - return dx_dx + dy_dy + dz_dz - - def evaluate_grid(self, xs, ys, zs): - step = self.step - xs_dx_plus, _, _ = self.field.evaluate_grid(xs+step, ys,zs) - xs_dx_minus, _, _ = self.field.evaluate_grid(xs-step,ys,zs) - _, ys_dy_plus, _ = self.field.evaluate_grid(xs, ys+step, zs) - _, ys_dy_minus, _ = self.field.evaluate_grid(xs, ys-step, zs) - _, _, zs_dz_plus = self.field.evaluate_grid(xs, ys, zs+step) - _, _, zs_dz_minus = self.field.evaluate_grid(xs, ys, zs-step) - - dx_dx = (xs_dx_plus - xs_dx_minus) / (2*step) - dy_dy = (ys_dy_plus - ys_dy_minus) / (2*step) - dz_dz = (zs_dz_plus - zs_dz_minus) / (2*step) - - return dx_dx + dy_dy + dz_dz - -class SvScalarFieldLaplacian(SvScalarField): - def __init__(self, field, step): - self.field = field - self.step = step - self.__description__ = "Laplace({})".format(field) - - def evaluate(self, x, y, z): - step = self.step - v_dx_plus = self.field.evaluate(x+step,y,z) - v_dx_minus = self.field.evaluate(x-step,y,z) - v_dy_plus = self.field.evaluate(x, y+step, z) - v_dy_minus = self.field.evaluate(x, y-step, z) - v_dz_plus = self.field.evaluate(x, y, z+step) - v_dz_minus = self.field.evaluate(x, y, z-step) - v0 = self.field.evaluate(x, y, z) - - sides = v_dx_plus + v_dx_minus + v_dy_plus + v_dy_minus + v_dz_plus + v_dz_minus - result = (sides - 6*v0) / (8 * step * step * step) - return result - - def evaluate_grid(self, xs, ys, zs): - step = self.step - v_dx_plus = self.field.evaluate_grid(xs+step, ys,zs) - v_dx_minus = self.field.evaluate_grid(xs-step,ys,zs) - v_dy_plus = self.field.evaluate_grid(xs, ys+step, zs) - v_dy_minus = self.field.evaluate_grid(xs, ys-step, zs) - v_dz_plus = self.field.evaluate_grid(xs, ys, zs+step) - v_dz_minus = self.field.evaluate_grid(xs, ys, zs-step) - v0 = self.field.evaluate_grid(xs, ys, zs) - - sides = v_dx_plus + v_dx_minus + v_dy_plus + v_dy_minus + v_dz_plus + v_dz_minus - result = (sides - 6*v0) / (8 * step * step * step) - return result - class ScalarFieldCurvatureCalculator(object): # Ref.: Curvature formulas for implicit curves and surfaces // Ron Goldman // doi:10.1016/j.cagd.2005.06.005 def __init__(self, field, step): diff --git a/utils/field/vector.py b/utils/field/vector.py index 00948d3995..6e15fb2c83 100644 --- a/utils/field/vector.py +++ b/utils/field/vector.py @@ -54,6 +54,32 @@ def to_relative(self): def to_absolute(self): return SvAbsoluteVectorField(self) +class SvAbsoluteVectorField(SvVectorField): + def __init__(self, field): + self.field = field + self.__description__ = "Absolute({})".format(field) + + def evaluate(self, x, y, z): + r = self.field.evaluate(x, y, z) + return r + np.array([x, y, z]) + + def evaluate_grid(self, xs, ys, zs): + rxs, rys, rzs = self.field.evaluate_grid(xs, ys, zs) + return rxs + xs, rys + ys, rzs + zs + +class SvRelativeVectorField(SvVectorField): + def __init__(self, field): + self.field = field + self.__description__ = "Relative({})".format(field) + + def evaluate(self, x, y, z): + r = self.field.evaluate(x, y, z) + return r - np.array([x, y, z]) + + def evaluate_grid(self, xs, ys, zs): + rxs, rys, rzs = self.field.evaluate_grid(xs, ys, zs) + return rxs - xs, rys - ys, rzs - zs + class SvMatrixVectorField(SvVectorField): def __init__(self, matrix): @@ -88,66 +114,6 @@ def evaluate_grid(self, xs, ys, zs): rz = np.full_like(zs, z) return rx, ry, rz -class SvComposedVectorField(SvVectorField): - def __init__(self, coords, sfield1, sfield2, sfield3): - self.coords = coords - self.sfield1 = sfield1 - self.sfield2 = sfield2 - self.sfield3 = sfield3 - self.__description__ = "{}({}, {}, {})".format(coords, sfield1, sfield2, sfield3) - - def evaluate(self, x, y, z): - v1 = self.sfield1.evaluate(x, y, z) - v2 = self.sfield2.evaluate(x, y, z) - v3 = self.sfield3.evaluate(x, y, z) - if self.coords == 'XYZ': - return np.array((v1, v2, v3)) - elif self.coords == 'CYL': - return np.array(from_cylindrical(v1, v2, v3, mode='radians')) - else: # SPH: - return np.array(from_spherical(v1, v2, v3, mode='radians')) - - def evaluate_grid(self, xs, ys, zs): - v1s = self.sfield1.evaluate_grid(xs, ys, zs) - v2s = self.sfield2.evaluate_grid(xs, ys, zs) - v3s = self.sfield3.evaluate_grid(xs, ys, zs) - if self.coords == 'XYZ': - return v1s, v2s, v3s - elif self.coords == 'CYL': - vectors = np.stack((v1s, v2s, v3s)).T - vectors = np.apply_along_axis(lambda v: np.array(from_cylindrical(*tuple(v), mode='radians')), 1, vectors).T - return vectors[0], vectors[1], vectors[2] - else: # SPH: - vectors = np.stack((v1s, v2s, v3s)).T - vectors = np.apply_along_axis(lambda v: np.array(from_spherical(*tuple(v), mode='radians')), 1, vectors).T - return vectors[0], vectors[1], vectors[2] - -class SvAbsoluteVectorField(SvVectorField): - def __init__(self, field): - self.field = field - self.__description__ = "Absolute({})".format(field) - - def evaluate(self, x, y, z): - r = self.field.evaluate(x, y, z) - return r + np.array([x, y, z]) - - def evaluate_grid(self, xs, ys, zs): - rxs, rys, rzs = self.field.evaluate_grid(xs, ys, zs) - return rxs + xs, rys + ys, rzs + zs - -class SvRelativeVectorField(SvVectorField): - def __init__(self, field): - self.field = field - self.__description__ = "Relative({})".format(field) - - def evaluate(self, x, y, z): - r = self.field.evaluate(x, y, z) - return r - np.array([x, y, z]) - - def evaluate_grid(self, xs, ys, zs): - rxs, rys, rzs = self.field.evaluate_grid(xs, ys, zs) - return rxs - xs, rys - ys, rzs - zs - class SvVectorFieldLambda(SvVectorField): __description__ = "Formula" @@ -178,108 +144,6 @@ def evaluate(self, x, y, z): V = self.in_field.evaluate(x, y, z) return np.array(self.function(x, y, z, V)) -class SvVectorFieldBinOp(SvVectorField): - def __init__(self, field1, field2, function): - self.function = function - self.field1 = field1 - self.field2 = field2 - self.__description__ = f"" - - def evaluate(self, x, y, z): - return self.function(self.field1.evaluate(x, y, z), self.field2.evaluate(x, y, z)) - - def evaluate_grid(self, xs, ys, zs): - def func(xs, ys, zs): - vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) - vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) - R = self.function(np.array([vx1, vy1, vz1]), np.array([vx2, vy2, vz2])) - return R[0], R[1], R[2] - return np.vectorize(func, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) - -class SvAverageVectorField(SvVectorField): - - def __init__(self, fields): - self.fields = fields - self.__description__ = "Average" - - def evaluate(self, x, y, z): - vectors = np.array([field.evaluate(x, y, z) for field in self.fields]) - return np.mean(vectors, axis=0) - - def evaluate_grid(self, xs, ys, zs): - def func(xs, ys, zs): - data = [] - for field in self.fields: - vx, vy, vz = field.evaluate_grid(xs, ys, zs) - vectors = np.stack((vx, vy, vz)).T - data.append(vectors) - data = np.array(data) - mean = np.mean(data, axis=0).T - return mean[0], mean[1], mean[2] - return np.vectorize(func, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) - -class SvVectorFieldCrossProduct(SvVectorField): - def __init__(self, field1, field2): - self.field1 = field1 - self.field2 = field2 - self.__description__ = "{} x {}".format(field1, field2) - - def evaluate(self, x, y, z): - v1 = self.field1.evaluate(x, y, z) - v2 = self.field2.evaluate(x, y, z) - return np.cross(v1, v2) - - def evaluate_grid(self, xs, ys, zs): - vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) - vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) - vectors1 = np.stack((vx1, vy1, vz1)).T - vectors2 = np.stack((vx2, vy2, vz2)).T - R = np.cross(vectors1, vectors2).T - return R[0], R[1], R[2] - -class SvVectorFieldMultipliedByScalar(SvVectorField): - def __init__(self, vector_field, scalar_field): - self.vector_field = vector_field - self.scalar_field = scalar_field - self.__description__ = "{} * {}".format(scalar_field, vector_field) - - def evaluate(self, x, y, z): - scalar = self.scalar_field.evaluate(x, y, z) - vector = self.vector_field.evaluate(x, y, z) - return scalar * vector - - def evaluate_grid(self, xs, ys, zs): - def product(xs, ys, zs): - scalars = self.scalar_field.evaluate_grid(xs, ys, zs) - vx, vy, vz = self.vector_field.evaluate_grid(xs, ys, zs) - vectors = np.stack((vx, vy, vz)) - R = (scalars * vectors) - return R[0], R[1], R[2] - return np.vectorize(product, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) - -class SvVectorFieldsLerp(SvVectorField): - - def __init__(self, vfield1, vfield2, scalar_field): - self.vfield1 = vfield1 - self.vfield2 = vfield2 - self.scalar_field = scalar_field - self.__description__ = "Lerp" - - def evaluate(self, x, y, z): - scalar = self.scalar_field.evaluate(x, y, z) - vector1 = self.vfield1.evaluate(x, y, z) - vector2 = self.vfield2.evaluate(x, y, z) - return (1 - scalar) * vector1 + scalar * vector2 - - def evaluate_grid(self, xs, ys, zs): - scalars = self.scalar_field.evaluate_grid(xs, ys, zs) - vx1, vy1, vz1 = self.vfield1.evaluate_grid(xs, ys, zs) - vectors1 = np.stack((vx1, vy1, vz1)) - vx2, vy2, vz2 = self.vfield2.evaluate_grid(xs, ys, zs) - vectors2 = np.stack((vx2, vy2, vz2)) - R = (1 - scalars) * vectors1 + scalars * vectors2 - return R[0], R[1], R[2] - class SvNoiseVectorField(SvVectorField): def __init__(self, noise_type, seed): self.noise_type = noise_type @@ -299,358 +163,6 @@ def mk_noise(v): vectors = np.stack((xs,ys,zs)).T return np.vectorize(mk_noise, signature="(3)->(),(),()")(vectors) -class SvKdtVectorField(SvVectorField): - - def __init__(self, vertices=None, kdt=None, falloff=None, negate=False, power=2): - self.falloff = falloff - self.negate = negate - if kdt is not None: - self.kdt = kdt - elif vertices is not None: - self.kdt = SvKdTree.new(SvKdTree.best_available_implementation(), vertices, power=power) - else: - raise Exception("Either kdt or vertices must be provided") - self.__description__ = "KDT Attractor" - - def evaluate(self, x, y, z): - nearest, i, distance = self.kdt.query(np.array([x, y, z])) - vector = nearest - np.array([x, y, z]) - if self.falloff is not None: - value = self.falloff(np.array([distance]))[0] - if self.negate: - value = - value - norm = np.linalg.norm(vector) - return value * vector / norm - else: - if self.negate: - return - vector - else: - return vector - - def evaluate_grid(self, xs, ys, zs): - points = np.stack((xs, ys, zs)).T - locs, idxs, distances = self.kdt.query_array(points) - vectors = locs - points - if self.negate: - vectors = - vectors - if self.falloff is not None: - norms = np.linalg.norm(vectors, axis=1, keepdims=True) - lens = self.falloff(norms) - nonzero = (norms > 0)[:,0] - lens = self.falloff(norms) - vectors[nonzero] = vectors[nonzero] / norms[nonzero] - R = (lens * vectors).T - return R[0], R[1], R[2] - else: - R = vectors.T - return R[0], R[1], R[2] - -class SvVectorFieldPointDistance(SvVectorField): - def __init__(self, center, metric='EUCLIDEAN', falloff=None, power=2): - self.center = center - self.falloff = falloff - self.metric = metric - self.power = power - self.__description__ = "Distance from {}".format(tuple(center)) - - def evaluate_grid(self, xs, ys, zs): - x0, y0, z0 = tuple(self.center) - xs = x0 - xs - ys = y0 - ys - zs = z0 - zs - vectors = np.stack((xs, ys, zs)) - if self.metric == 'EUCLIDEAN': - norms = np.linalg.norm(vectors, axis=0) - elif self.metric == 'CHEBYSHEV': - norms = np.max(np.abs(vectors), axis=0) - elif self.metric == 'MANHATTAN': - norms = np.sum(np.abs(vectors), axis=0) - elif self.metric == 'CUSTOM': - norms = np.linalg.norm(vectors, axis=0, ord=self.power) - else: - raise Exception('Unknown metric') - if self.falloff is not None: - lens = self.falloff(norms) - R = lens * vectors / norms - else: - R = vectors - return R[0], R[1], R[2] - - def evaluate(self, x, y, z): - point = np.array([x, y, z]) - self.center - if self.metric == 'EUCLIDEAN': - norm = np.linalg.norm(point) - elif self.metric == 'CHEBYSHEV': - norm = np.max(point) - elif self.metric == 'MANHATTAN': - norm = np.sum(np.abs(point)) - elif self.metric == 'CUSTOM': - norm = np.linalg.norm(point, ord=self.power) - else: - raise Exception('Unknown metric') - if self.falloff is not None: - value = self.falloff(np.array([norm]))[0] - return value * point / norm - else: - return point - -class SvLineAttractorVectorField(SvVectorField): - - def __init__(self, center, direction, falloff=None): - self.center = center - self.direction = direction - self.falloff = falloff - self.__description__ = "Line Attractor" - - def evaluate(self, x, y, z): - vertex = np.array([x,y,z]) - direction = self.direction - to_center = self.center - vertex - projection = np.dot(to_center, direction) * direction / np.dot(direction, direction) - dv = to_center - projection - if self.falloff is not None: - norm = np.linalg.norm(dv) - dv = self.falloff(norm) * dv / norm - return dv - - def evaluate_grid(self, xs, ys, zs): - direction = self.direction - direction2 = np.dot(direction, direction) - - def func(vertex): - to_center = self.center - vertex - projection = np.dot(to_center, direction) * direction / direction2 - dv = to_center - projection - return dv - - points = np.stack((xs, ys, zs)).T - vectors = np.vectorize(func, signature='(3)->(3)')(points) - if self.falloff is not None: - norms = np.linalg.norm(vectors, axis=1, keepdims=True) - nonzero = (norms > 0)[:,0] - lens = self.falloff(norms) - vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T - R = (lens * vectors).T - return R[0], R[1], R[2] - else: - R = vectors.T - return R[0], R[1], R[2] - -class SvPlaneAttractorVectorField(SvVectorField): - - def __init__(self, center, direction, falloff=None): - self.center = center - self.direction = direction - self.falloff = falloff - self.__description__ = "Plane Attractor" - - def evaluate(self, x, y, z): - vertex = np.array([x,y,z]) - direction = self.direction - to_center = self.center - vertex - dv = np.dot(to_center, direction) * direction / np.dot(direction, direction) - if self.falloff is not None: - norm = np.linalg.norm(dv) - dv = self.falloff(norm) * dv / norm - return dv - - def evaluate_grid(self, xs, ys, zs): - direction = self.direction - direction2 = np.dot(direction, direction) - - def func(vertex): - to_center = self.center - vertex - projection = np.dot(to_center, direction) * direction / direction2 - return projection - - points = np.stack((xs, ys, zs)).T - vectors = np.vectorize(func, signature='(3)->(3)')(points) - if self.falloff is not None: - norms = np.linalg.norm(vectors, axis=1, keepdims=True) - lens = self.falloff(norms) - nonzero = (norms > 0)[:,0] - vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T - R = (lens * vectors).T - return R[0], R[1], R[2] - else: - R = vectors.T - return R[0], R[1], R[2] - -class SvCircleAttractorVectorField(SvVectorField): - __description__ = "Circle Attractor" - - def __init__(self, center, radius, normal, falloff=None): - self.circle = CircleEquation3D.from_center_radius_normal(center, radius, normal) - self.falloff = falloff - - def evaluate(self, x, y, z): - v = np.array([x,y,z]) - projection = self.circle.get_projections([v])[0] - vector = projection - v - if self.fallof is not None: - new_len = self.falloff(np.array([distance]))[0] - norm = np.linalg.norm(vector) - return new_len * vector / norm - else: - return vector - - def evaluate_grid(self, xs, ys, zs): - vs = np.stack((xs, ys, zs)).T - projections = self.circle.get_projections(vs) - vectors = projections - vs - if self.falloff is not None: - norms = np.linalg.norm(vectors, axis=1, keepdims=True) - lens = self.falloff(norms) - nonzero = (norms > 0)[:,0] - vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T - R = (lens * vectors).T - return R[0], R[1], R[2] - else: - R = vectors.T - return R[0], R[1], R[2] - -class SvEdgeAttractorVectorField(SvVectorField): - __description__ = "Edge attractor" - - def __init__(self, v1, v2, falloff=None): - self.falloff = falloff - self.v1 = Vector(v1) - self.v2 = Vector(v2) - - def evaluate(self, x, y, z): - v = Vector([x,y,z]) - dv1 = (v - self.v1).length - dv2 = (v - self.v2).length - if dv1 > dv2: - distance_to_nearest = dv2 - nearest_vert = self.v2 - another_vert = self.v1 - else: - distance_to_nearest = dv1 - nearest_vert = self.v1 - another_vert = self.v2 - edge = another_vert - nearest_vert - to_nearest = v - nearest_vert - if to_nearest.length == 0: - return 0 - angle = edge.angle(to_nearest) - if angle > pi/2: - distance = distance_to_nearest - vector = - to_nearest - else: - vector = LineEquation.from_two_points(self.v1, self.v2).projection_of_points(v) - distance = vector.length - vector = np.array(vector) - if self.falloff is not None: - return self.falloff(distance) * vector / distance - else: - return vector - - def evaluate_grid(self, xs, ys, zs): - n = len(xs) - vs = np.stack((xs, ys, zs)).T - v1 = np.array(self.v1) - v2 = np.array(self.v2) - dv1s = np.linalg.norm(vs - v1, axis=1) - dv2s = np.linalg.norm(vs - v2, axis=1) - v1_is_nearest = (dv1s < dv2s) - v2_is_nearest = np.logical_not(v1_is_nearest) - nearest_verts = np.empty_like(vs) - other_verts = np.empty_like(vs) - nearest_verts[v1_is_nearest] = v1 - nearest_verts[v2_is_nearest] = v2 - other_verts[v1_is_nearest] = v2 - other_verts[v2_is_nearest] = v1 - - to_nearest = vs - nearest_verts - - edges = other_verts - nearest_verts - dot = (to_nearest * edges).sum(axis=1) - at_edge = (dot > 0) - at_vertex = np.logical_not(at_edge) - at_v1 = np.logical_and(at_vertex, v1_is_nearest) - at_v2 = np.logical_and(at_vertex, v2_is_nearest) - - line = LineEquation.from_two_points(self.v1, self.v2) - - vectors = np.empty((n,3)) - vectors[at_edge] = line.projection_of_points(vs[at_edge]) - vs[at_edge] - vectors[at_v1] = v1 - vs[at_v1] - vectors[at_v2] = v2 - vs[at_v2] - - if self.falloff is not None: - norms = np.linalg.norm(vectors, axis=1, keepdims=True) - lens = self.falloff(norms) - nonzero = (norms > 0)[:,0] - vectors[nonzero] = vectors[nonzero] / norms[nonzero][:,0][np.newaxis].T - R = (lens * vectors).T - return R[0], R[1], R[2] - else: - R = vectors.T - return R[0], R[1], R[2] - -class SvBvhAttractorVectorField(SvVectorField): - - def __init__(self, bvh=None, verts=None, faces=None, falloff=None, use_normal=False, signed_normal=False): - self.falloff = falloff - self.use_normal = use_normal - self.signed_normal = signed_normal - if bvh is not None: - self.bvh = bvh - elif verts is not None and faces is not None: - self.bvh = bvhtree.BVHTree.FromPolygons(verts, faces) - else: - raise Exception("Either bvh or verts and faces must be provided!") - self.__description__ = "BVH Attractor" - - def evaluate(self, x, y, z): - vertex = Vector((x,y,z)) - nearest, normal, idx, distance = self.bvh.find_nearest(vertex) - if self.use_normal: - if self.signed_normal: - sign = (v - nearest).dot(normal) - sign = copysign(1, sign) - else: - sign = 1 - return sign * np.array(normal) - else: - dv = np.array(nearest - vertex) - if self.falloff is not None: - norm = np.linalg.norm(dv) - len = self.falloff(norm) - dv = len * dv - return dv - else: - return dv - - def evaluate_grid(self, xs, ys, zs): - def find(v): - nearest, normal, idx, distance = self.bvh.find_nearest(v) - if nearest is None: - raise Exception("No nearest point on mesh found for vertex %s" % v) - if self.use_normal: - if self.signed_normal: - sign = (v - nearest).dot(normal) - sign = copysign(1, sign) - else: - sign = 1 - return sign * np.array(normal) - else: - return np.array(nearest) - v - - points = np.stack((xs, ys, zs)).T - vectors = np.vectorize(find, signature='(3)->(3)')(points) - if self.falloff is not None: - norms = np.linalg.norm(vectors, axis=1, keepdims=True) - nonzero = (norms > 0)[:,0] - lens = self.falloff(norms) - vectors[nonzero] = vectors[nonzero] / norms[nonzero] - R = (lens * vectors).T - return R[0], R[1], R[2] - else: - R = vectors.T - return R[0], R[1], R[2] - class SvRotationVectorField(SvVectorField): def __init__(self, center, direction, falloff=None): @@ -768,254 +280,6 @@ def evaluate(self, x, y, z): rxs, rys, rzs = self.evaluate_grid([x], [y], [z]) return rxs[0], rys[0], rzs[0] -class SvSelectVectorField(SvVectorField): - def __init__(self, fields, mode): - self.fields = fields - self.mode = mode - self.__description__ = "{}({})".format(mode, fields) - - def evaluate(self, x, y, z): - vectors = [field.evaluate(x, y, z) for field in self.fields] - vectors = np.array(vectors) - norms = np.linalg.norm(vectors, axis=1) - if self.mode == 'MIN': - selected = np.argmin(norms) - else: # MAX - selected = np.argmax(norms) - return vectors[selected] - - def evaluate_grid(self, xs, ys, zs): - n = len(xs) - vectors = [field.evaluate_grid(xs, ys, zs) for field in self.fields] - vectors = np.stack(vectors) - vectors = np.transpose(vectors, axes=(2,0,1)) - norms = np.linalg.norm(vectors, axis=2) - if self.mode == 'MIN': - selected = np.argmin(norms, axis=1) - else: # MAX - selected = np.argmax(norms, axis=1) - all_points = list(range(n)) - vectors = vectors[all_points, selected, :] - return vectors.T - -class SvVectorFieldCartesianFilter(SvVectorField): - def __init__(self, field, use_x=True, use_y=True, use_z=True): - self.field = field - self.use_x = use_x - self.use_y = use_y - self.use_z = use_z - desc = "" - if use_x: - desc = desc + 'X' - if use_y: - desc = desc + 'Y' - if use_z: - desc = desc + 'Z' - self.__description__ = f"Filter[{desc}]({field})" - - def evaluate_grid(self, xs, ys, zs): - vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) - if not self.use_x: - vxs[:] = 0 - if not self.use_y: - vys[:] = 0 - if not self.use_z: - vzs[:] = 0 - return vxs, vys, vzs - -class SvVectorFieldCylindricalFilter(SvVectorField): - def __init__(self, field, use_rho=True, use_phi=True, use_z=True): - self.field = field - self.use_rho = use_rho - self.use_phi = use_phi - self.use_z = use_z - - def evaluate_grid(self, xs, ys, zs): - pts = np.stack((xs,ys,zs)).T - vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) - vectors = np.stack((vxs,vys,vzs)).T - s_rho, s_phi, s_z = to_cylindrical_np(pts.T, mode='radians') - v_rho, v_phi, v_z = to_cylindrical_np((pts + vectors).T, mode='radians') - if not self.use_rho: - v_rho = s_rho - if not self.use_phi: - v_phi = s_phi - if not self.use_z: - v_z = s_z - v_x, v_y, v_z = from_cylindrical_np(v_rho, v_phi, v_z, mode='radians') - return (v_x - xs), (v_y - ys), (v_z - zs) - -class SvVectorFieldSphericalFilter(SvVectorField): - def __init__(self, field, use_rho=True, use_phi=True, use_theta=True): - self.field = field - self.use_rho = use_rho - self.use_phi = use_phi - self.use_theta = use_theta - - def evaluate_grid(self, xs, ys, zs): - pts = np.stack((xs,ys,zs)).T - vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) - vectors = np.stack((vxs,vys,vzs)).T - s_rho, s_phi, s_theta = to_spherical_np(pts.T, mode='radians') - v_rho, v_phi, v_theta = to_spherical_np((pts + vectors).T, mode='radians') - if not self.use_rho: - v_rho = s_rho - if not self.use_phi: - v_phi = s_phi - if not self.use_theta: - v_theta = s_theta - v_x, v_y, v_z = from_spherical_np(v_rho, v_phi, v_theta, mode='radians') - return (v_x - xs), (v_y - ys), (v_z - zs) - -class SvVectorFieldTangent(SvVectorField): - - def __init__(self, field1, field2): - self.field1 = field1 - self.field2 = field2 - self.__description__ = "Tangent" - - def evaluate(self, x, y, z): - v1 = self.field1.evaluate(x,y,z) - v2 = self.field2.evaluate(x,y,z) - projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) - return projection - - def evaluate_grid(self, xs, ys, zs): - vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) - vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) - vectors1 = np.stack((vx1, vy1, vz1)).T - vectors2 = np.stack((vx2, vy2, vz2)).T - - def project(v1, v2): - projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) - vx, vy, vz = projection - return vx, vy, vz - - return np.vectorize(project, signature="(3),(3)->(),(),()")(vectors1, vectors2) - -class SvVectorFieldCotangent(SvVectorField): - - def __init__(self, field1, field2): - self.field1 = field1 - self.field2 = field2 - self.__description__ = "Cotangent" - - def evaluate(self, x, y, z): - v1 = self.field1.evaluate(x,y,z) - v2 = self.field2.evaluate(x,y,z) - projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) - return v1 - projection - - def evaluate_grid(self, xs, ys, zs): - vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) - vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) - vectors1 = np.stack((vx1, vy1, vz1)).T - vectors2 = np.stack((vx2, vy2, vz2)).T - - def project(v1, v2): - projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) - coprojection = v1 - projection - vx, vy, vz = coprojection - return vx, vy, vz - - return np.vectorize(project, signature="(3),(3)->(),(),()")(vectors1, vectors2) - -class SvVectorFieldComposition(SvVectorField): - - def __init__(self, field1, field2): - self.field1 = field1 - self.field2 = field2 - self.__description__ = "Composition" - - def evaluate(self, x, y, z): - x1, y1, z1 = self.field1.evaluate(x,y,z) - v2 = self.field2.evaluate(x1,y1,z1) - return v2 - - def evaluate_grid(self, xs, ys, zs): - r = self.field1.evaluate_grid(xs, ys, zs) - vx1, vy1, vz1 = r - return self.field2.evaluate_grid(vx1, vy1, vz1) - -class SvPreserveCoordinateField(SvVectorField): - def __init__(self, field, axis): - self.field = field - self.axis = axis - - def evaluate(self, x, y, z): - xyz = self.field.evaluate(x, y, z) - xyz = np.array(xyz) - xyz[self.axis] = [x, y, z][self.axis] - return xyz - - def evaluate_grid(self, xs, ys, zs): - r = self.field.evaluate_grid(xs, ys, zs) - r = np.array(r) - r[self.axis] = [xs, ys, zs][self.axis] - return r - -class SvScalarFieldGradient(SvVectorField): - def __init__(self, field, step): - self.field = field - self.step = step - self.__description__ = "Grad({})".format(field) - - def evaluate(self, x, y, z): - return self.field.gradient([x, y, z], step=self.step) - - def evaluate_grid(self, xs, ys, zs): - return self.field.gradient_grid(xs, ys, zs, step=self.step) - -class SvVectorFieldRotor(SvVectorField): - def __init__(self, field, step): - self.field = field - self.step = step - self.__description__ = "Rot({})".format(field) - - def evaluate(self, x, y, z): - step = self.step - _, y_dx_plus, z_dx_plus = self.field.evaluate(x+step,y,z) - _, y_dx_minus, z_dx_minus = self.field.evaluate(x-step,y,z) - x_dy_plus, _, z_dy_plus = self.field.evaluate(x, y+step, z) - x_dy_minus, _, z_dy_minus = self.field.evaluate(x, y-step, z) - x_dz_plus, y_dz_plus, _ = self.field.evaluate(x, y, z+step) - x_dz_minus, y_dz_minus, _ = self.field.evaluate(x, y, z-step) - - dy_dx = (y_dx_plus - y_dx_minus) / (2*step) - dz_dx = (z_dx_plus - z_dx_minus) / (2*step) - dx_dy = (x_dy_plus - x_dy_minus) / (2*step) - dz_dy = (z_dy_plus - z_dy_minus) / (2*step) - dx_dz = (x_dz_plus - x_dz_minus) / (2*step) - dy_dz = (y_dz_plus - y_dz_minus) / (2*step) - - rx = dz_dy - dy_dz - ry = - (dz_dx - dx_dz) - rz = dy_dx - dx_dy - - return np.array([rx, ry, rz]) - - def evaluate_grid(self, xs, ys, zs): - step = self.step - _, y_dx_plus, z_dx_plus = self.field.evaluate_grid(xs+step,ys,zs) - _, y_dx_minus, z_dx_minus = self.field.evaluate_grid(xs-step,ys,zs) - x_dy_plus, _, z_dy_plus = self.field.evaluate_grid(xs, ys+step, zs) - x_dy_minus, _, z_dy_minus = self.field.evaluate_grid(xs, ys-step, zs) - x_dz_plus, y_dz_plus, _ = self.field.evaluate_grid(xs, ys, zs+step) - x_dz_minus, y_dz_minus, _ = self.field.evaluate_grid(xs, ys, zs-step) - - dy_dx = (y_dx_plus - y_dx_minus) / (2*step) - dz_dx = (z_dx_plus - z_dx_minus) / (2*step) - dx_dy = (x_dy_plus - x_dy_minus) / (2*step) - dz_dy = (z_dy_plus - z_dy_minus) / (2*step) - dx_dz = (x_dz_plus - x_dz_minus) / (2*step) - dy_dz = (y_dz_plus - y_dz_minus) / (2*step) - - rx = dz_dy - dy_dz - ry = - (dz_dx - dx_dz) - rz = dy_dx - dx_dy - R = np.stack((rx, ry, rz)) - return R[0], R[1], R[2] - class SvBendAlongCurveField(SvVectorField): ZERO = 'ZERO' @@ -1280,3 +544,4 @@ def evaluate_grid(self, xs, ys, zs): else: # NORMAL vectors = self.curve.main_normal_array(ts) return vectors[:,0], vectors[:,1], vectors[:,2] + diff --git a/utils/field/vector_operations.py b/utils/field/vector_operations.py new file mode 100644 index 0000000000..713816d402 --- /dev/null +++ b/utils/field/vector_operations.py @@ -0,0 +1,337 @@ +# 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 + +from sverchok.utils.math import ( + from_cylindrical, from_spherical, + from_cylindrical_np, to_cylindrical_np, + from_spherical_np, to_spherical_np) +from sverchok.utils.field.vector import SvVectorField + +class SvVectorFieldBinOp(SvVectorField): + def __init__(self, field1, field2, function): + self.function = function + self.field1 = field1 + self.field2 = field2 + self.__description__ = f"" + + def evaluate(self, x, y, z): + return self.function(self.field1.evaluate(x, y, z), self.field2.evaluate(x, y, z)) + + def evaluate_grid(self, xs, ys, zs): + def func(xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + R = self.function(np.array([vx1, vy1, vz1]), np.array([vx2, vy2, vz2])) + return R[0], R[1], R[2] + return np.vectorize(func, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) + +class SvAverageVectorField(SvVectorField): + + def __init__(self, fields): + self.fields = fields + self.__description__ = "Average" + + def evaluate(self, x, y, z): + vectors = np.array([field.evaluate(x, y, z) for field in self.fields]) + return np.mean(vectors, axis=0) + + def evaluate_grid(self, xs, ys, zs): + def func(xs, ys, zs): + data = [] + for field in self.fields: + vx, vy, vz = field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vx, vy, vz)).T + data.append(vectors) + data = np.array(data) + mean = np.mean(data, axis=0).T + return mean[0], mean[1], mean[2] + return np.vectorize(func, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) + +class SvVectorFieldCrossProduct(SvVectorField): + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "{} x {}".format(field1, field2) + + def evaluate(self, x, y, z): + v1 = self.field1.evaluate(x, y, z) + v2 = self.field2.evaluate(x, y, z) + return np.cross(v1, v2) + + def evaluate_grid(self, xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)).T + vectors2 = np.stack((vx2, vy2, vz2)).T + R = np.cross(vectors1, vectors2).T + return R[0], R[1], R[2] + +class SvVectorFieldMultipliedByScalar(SvVectorField): + def __init__(self, vector_field, scalar_field): + self.vector_field = vector_field + self.scalar_field = scalar_field + self.__description__ = "{} * {}".format(scalar_field, vector_field) + + def evaluate(self, x, y, z): + scalar = self.scalar_field.evaluate(x, y, z) + vector = self.vector_field.evaluate(x, y, z) + return scalar * vector + + def evaluate_grid(self, xs, ys, zs): + def product(xs, ys, zs): + scalars = self.scalar_field.evaluate_grid(xs, ys, zs) + vx, vy, vz = self.vector_field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vx, vy, vz)) + R = (scalars * vectors) + return R[0], R[1], R[2] + return np.vectorize(product, signature="(m),(m),(m)->(m),(m),(m)")(xs, ys, zs) + +class SvVectorFieldsLerp(SvVectorField): + + def __init__(self, vfield1, vfield2, scalar_field): + self.vfield1 = vfield1 + self.vfield2 = vfield2 + self.scalar_field = scalar_field + self.__description__ = "Lerp" + + def evaluate(self, x, y, z): + scalar = self.scalar_field.evaluate(x, y, z) + vector1 = self.vfield1.evaluate(x, y, z) + vector2 = self.vfield2.evaluate(x, y, z) + return (1 - scalar) * vector1 + scalar * vector2 + + def evaluate_grid(self, xs, ys, zs): + scalars = self.scalar_field.evaluate_grid(xs, ys, zs) + vx1, vy1, vz1 = self.vfield1.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)) + vx2, vy2, vz2 = self.vfield2.evaluate_grid(xs, ys, zs) + vectors2 = np.stack((vx2, vy2, vz2)) + R = (1 - scalars) * vectors1 + scalars * vectors2 + return R[0], R[1], R[2] + +class SvComposedVectorField(SvVectorField): + def __init__(self, coords, sfield1, sfield2, sfield3): + self.coords = coords + self.sfield1 = sfield1 + self.sfield2 = sfield2 + self.sfield3 = sfield3 + self.__description__ = "{}({}, {}, {})".format(coords, sfield1, sfield2, sfield3) + + def evaluate(self, x, y, z): + v1 = self.sfield1.evaluate(x, y, z) + v2 = self.sfield2.evaluate(x, y, z) + v3 = self.sfield3.evaluate(x, y, z) + if self.coords == 'XYZ': + return np.array((v1, v2, v3)) + elif self.coords == 'CYL': + return np.array(from_cylindrical(v1, v2, v3, mode='radians')) + else: # SPH: + return np.array(from_spherical(v1, v2, v3, mode='radians')) + + def evaluate_grid(self, xs, ys, zs): + v1s = self.sfield1.evaluate_grid(xs, ys, zs) + v2s = self.sfield2.evaluate_grid(xs, ys, zs) + v3s = self.sfield3.evaluate_grid(xs, ys, zs) + if self.coords == 'XYZ': + return v1s, v2s, v3s + elif self.coords == 'CYL': + vectors = np.stack((v1s, v2s, v3s)).T + vectors = np.apply_along_axis(lambda v: np.array(from_cylindrical(*tuple(v), mode='radians')), 1, vectors).T + return vectors[0], vectors[1], vectors[2] + else: # SPH: + vectors = np.stack((v1s, v2s, v3s)).T + vectors = np.apply_along_axis(lambda v: np.array(from_spherical(*tuple(v), mode='radians')), 1, vectors).T + return vectors[0], vectors[1], vectors[2] + +class SvSelectVectorField(SvVectorField): + def __init__(self, fields, mode): + self.fields = fields + self.mode = mode + self.__description__ = "{}({})".format(mode, fields) + + def evaluate(self, x, y, z): + vectors = [field.evaluate(x, y, z) for field in self.fields] + vectors = np.array(vectors) + norms = np.linalg.norm(vectors, axis=1) + if self.mode == 'MIN': + selected = np.argmin(norms) + else: # MAX + selected = np.argmax(norms) + return vectors[selected] + + def evaluate_grid(self, xs, ys, zs): + n = len(xs) + vectors = [field.evaluate_grid(xs, ys, zs) for field in self.fields] + vectors = np.stack(vectors) + vectors = np.transpose(vectors, axes=(2,0,1)) + norms = np.linalg.norm(vectors, axis=2) + if self.mode == 'MIN': + selected = np.argmin(norms, axis=1) + else: # MAX + selected = np.argmax(norms, axis=1) + all_points = list(range(n)) + vectors = vectors[all_points, selected, :] + return vectors.T + +class SvVectorFieldCartesianFilter(SvVectorField): + def __init__(self, field, use_x=True, use_y=True, use_z=True): + self.field = field + self.use_x = use_x + self.use_y = use_y + self.use_z = use_z + desc = "" + if use_x: + desc = desc + 'X' + if use_y: + desc = desc + 'Y' + if use_z: + desc = desc + 'Z' + self.__description__ = f"Filter[{desc}]({field})" + + def evaluate_grid(self, xs, ys, zs): + vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) + if not self.use_x: + vxs[:] = 0 + if not self.use_y: + vys[:] = 0 + if not self.use_z: + vzs[:] = 0 + return vxs, vys, vzs + +class SvVectorFieldCylindricalFilter(SvVectorField): + def __init__(self, field, use_rho=True, use_phi=True, use_z=True): + self.field = field + self.use_rho = use_rho + self.use_phi = use_phi + self.use_z = use_z + + def evaluate_grid(self, xs, ys, zs): + pts = np.stack((xs,ys,zs)).T + vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vxs,vys,vzs)).T + s_rho, s_phi, s_z = to_cylindrical_np(pts.T, mode='radians') + v_rho, v_phi, v_z = to_cylindrical_np((pts + vectors).T, mode='radians') + if not self.use_rho: + v_rho = s_rho + if not self.use_phi: + v_phi = s_phi + if not self.use_z: + v_z = s_z + v_x, v_y, v_z = from_cylindrical_np(v_rho, v_phi, v_z, mode='radians') + return (v_x - xs), (v_y - ys), (v_z - zs) + +class SvVectorFieldSphericalFilter(SvVectorField): + def __init__(self, field, use_rho=True, use_phi=True, use_theta=True): + self.field = field + self.use_rho = use_rho + self.use_phi = use_phi + self.use_theta = use_theta + + def evaluate_grid(self, xs, ys, zs): + pts = np.stack((xs,ys,zs)).T + vxs, vys, vzs = self.field.evaluate_grid(xs, ys, zs) + vectors = np.stack((vxs,vys,vzs)).T + s_rho, s_phi, s_theta = to_spherical_np(pts.T, mode='radians') + v_rho, v_phi, v_theta = to_spherical_np((pts + vectors).T, mode='radians') + if not self.use_rho: + v_rho = s_rho + if not self.use_phi: + v_phi = s_phi + if not self.use_theta: + v_theta = s_theta + v_x, v_y, v_z = from_spherical_np(v_rho, v_phi, v_theta, mode='radians') + return (v_x - xs), (v_y - ys), (v_z - zs) + +class SvVectorFieldTangent(SvVectorField): + + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "Tangent" + + def evaluate(self, x, y, z): + v1 = self.field1.evaluate(x,y,z) + v2 = self.field2.evaluate(x,y,z) + projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) + return projection + + def evaluate_grid(self, xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)).T + vectors2 = np.stack((vx2, vy2, vz2)).T + + def project(v1, v2): + projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) + vx, vy, vz = projection + return vx, vy, vz + + return np.vectorize(project, signature="(3),(3)->(),(),()")(vectors1, vectors2) + +class SvVectorFieldCotangent(SvVectorField): + + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "Cotangent" + + def evaluate(self, x, y, z): + v1 = self.field1.evaluate(x,y,z) + v2 = self.field2.evaluate(x,y,z) + projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) + return v1 - projection + + def evaluate_grid(self, xs, ys, zs): + vx1, vy1, vz1 = self.field1.evaluate_grid(xs, ys, zs) + vx2, vy2, vz2 = self.field2.evaluate_grid(xs, ys, zs) + vectors1 = np.stack((vx1, vy1, vz1)).T + vectors2 = np.stack((vx2, vy2, vz2)).T + + def project(v1, v2): + projection = np.dot(v1, v2) * v2 / np.dot(v2, v2) + coprojection = v1 - projection + vx, vy, vz = coprojection + return vx, vy, vz + + return np.vectorize(project, signature="(3),(3)->(),(),()")(vectors1, vectors2) + +class SvVectorFieldComposition(SvVectorField): + + def __init__(self, field1, field2): + self.field1 = field1 + self.field2 = field2 + self.__description__ = "Composition" + + def evaluate(self, x, y, z): + x1, y1, z1 = self.field1.evaluate(x,y,z) + v2 = self.field2.evaluate(x1,y1,z1) + return v2 + + def evaluate_grid(self, xs, ys, zs): + r = self.field1.evaluate_grid(xs, ys, zs) + vx1, vy1, vz1 = r + return self.field2.evaluate_grid(vx1, vy1, vz1) + +class SvPreserveCoordinateField(SvVectorField): + def __init__(self, field, axis): + self.field = field + self.axis = axis + + def evaluate(self, x, y, z): + xyz = self.field.evaluate(x, y, z) + xyz = np.array(xyz) + xyz[self.axis] = [x, y, z][self.axis] + return xyz + + def evaluate_grid(self, xs, ys, zs): + r = self.field.evaluate_grid(xs, ys, zs) + r = np.array(r) + r[self.axis] = [xs, ys, zs][self.axis] + return r + diff --git a/utils/geodesic.py b/utils/geodesic.py index 3b7ecdc9b6..49c732dba7 100644 --- a/utils/geodesic.py +++ b/utils/geodesic.py @@ -13,7 +13,8 @@ from sverchok.utils.curve.algorithms import SvCurveOnSurface, SvCurveLengthSolver from sverchok.utils.surface.algorithms import rotate_uv_vectors_on_surface from sverchok.utils.field.rbf import SvRbfVectorField -from sverchok.utils.field.vector import SvBendAlongSurfaceField, SvVectorFieldComposition, SvPreserveCoordinateField +from sverchok.utils.field.vector import SvBendAlongSurfaceField +from sverchok.utils.field.vector_operations import SvVectorFieldComposition, SvPreserveCoordinateField from sverchok.utils.math import np_multiply_matrices_vectors, np_dot, np_vectors_angle from sverchok.utils.sv_logging import get_logger from sverchok.dependencies import scipy From 4d8c2bb00ca547796003c1702fac521ba78023c3 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Tue, 19 Nov 2024 23:00:03 +0500 Subject: [PATCH 3/4] Move twist & taper fields to separate module. --- nodes/field/taper_field.py | 2 +- nodes/field/twist_field.py | 2 +- utils/field/vector.py | 78 ----------------- utils/field/vector_primitives.py | 138 +++++++++++++++++++++++++++++++ utils/geom.py | 2 +- 5 files changed, 141 insertions(+), 81 deletions(-) create mode 100644 utils/field/vector_primitives.py diff --git a/nodes/field/taper_field.py b/nodes/field/taper_field.py index 8c08ee83af..90fbf2abf9 100644 --- a/nodes/field/taper_field.py +++ b/nodes/field/taper_field.py @@ -12,7 +12,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat -from sverchok.utils.field.vector import SvTaperVectorField +from sverchok.utils.field.vector_primitives import SvTaperVectorField class SvTaperFieldNode(SverchCustomTreeNode, bpy.types.Node): """ diff --git a/nodes/field/twist_field.py b/nodes/field/twist_field.py index 8929c9f85a..5baa3d04b2 100644 --- a/nodes/field/twist_field.py +++ b/nodes/field/twist_field.py @@ -13,7 +13,7 @@ from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat -from sverchok.utils.field.vector import SvTwistVectorField +from sverchok.utils.field.vector_primitives import SvTwistVectorField class SvTwistFieldNode(SverchCustomTreeNode, bpy.types.Node): """ diff --git a/utils/field/vector.py b/utils/field/vector.py index 6e15fb2c83..6241c66208 100644 --- a/utils/field/vector.py +++ b/utils/field/vector.py @@ -202,84 +202,6 @@ def evaluate_grid(self, xs, ys, zs): R = vectors.T return R[0], R[1], R[2] -class SvTwistVectorField(SvVectorField): - def __init__(self, center, axis, angle_along_axis, angle_along_radius, - min_z = None, max_z = None, min_r = None, max_r = None): - self.center = np.asarray(center) - self.axis = np.asarray(axis) - self.axis = self.axis / np.linalg.norm(self.axis) - self.angle_along_axis = angle_along_axis - self.angle_along_radius = angle_along_radius - self.min_z = min_z - self.max_z = max_z - self.min_r = min_r - self.max_r = max_r - - def evaluate_grid(self, xs, ys, zs): - pts = np.stack((xs, ys, zs)).T - dpts = pts - self.center - ts = np_dot(dpts, self.axis) - rads = dpts - ts[np.newaxis].T * self.axis - rs = np.linalg.norm(rads, axis=1) - if self.min_z is not None or self.max_z is not None: - ts = np.clip(ts, self.min_z, self.max_z) - if self.min_r is not None or self.max_r is not None: - rs = np.clip(rs, self.min_r, self.max_r) - angles = ts * self.angle_along_axis + rs * self.angle_along_radius - matrices = rotate_around_vector_matrix(self.axis, angles) - new_pts = np_multiply_matrices_vectors(matrices, dpts) - new_pts += self.center - vectors = (new_pts - pts).T - return vectors[0], vectors[1], vectors[2] - - def evaluate(self, x, y, z): - rxs, rys, rzs = self.evaluate_grid([x], [y], [z]) - return rxs[0], rys[0], rzs[0] - -class SvTaperVectorField(SvVectorField): - def __init__(self, center, axis, coefficient, min_z=None, max_z=None): - self.center = np.asarray(center) - self.axis = np.asarray(axis) - self.axis = self.axis / np.linalg.norm(self.axis) - self.coefficient = coefficient - self.min_z = None - self.max_z = None - rho = 1.0 / coefficient - if min_z is not None: - self.max_z = rho - min_z - if max_z is not None: - self.min_z = rho - max_z - - @classmethod - def from_base_point_and_vertex(cls, base_point, vertex, **kwargs): - base_point = np.array(base_point) - vertex = np.array(vertex) - rho = np.linalg.norm(base_point - vertex) - return SvTaperVectorField(vertex, base_point - vertex, 1.0/rho, **kwargs) - - @classmethod - def from_base_point_and_vector(cls, base_point, vector, **kwargs): - base_point = np.array(base_point) - vector = np.array(vector) - return SvTaperVectorField.from_base_point_and_vertex(base_point, base_point + vector, **kwargs) - - def evaluate_grid(self, xs, ys, zs): - pts = np.stack((xs, ys, zs)).T - dpts = pts - self.center - ts = np_dot(dpts, self.axis) - projections = ts[np.newaxis].T * self.axis - rads = dpts - projections - if self.min_z is not None or self.max_z is not None: - ts = np.clip(ts, self.min_z, self.max_z) - rads *= self.coefficient * ts[np.newaxis].T - new_pts = self.center + projections + rads - vectors = (new_pts - pts).T - return vectors[0], vectors[1], vectors[2] - - def evaluate(self, x, y, z): - rxs, rys, rzs = self.evaluate_grid([x], [y], [z]) - return rxs[0], rys[0], rzs[0] - class SvBendAlongCurveField(SvVectorField): ZERO = 'ZERO' diff --git a/utils/field/vector_primitives.py b/utils/field/vector_primitives.py new file mode 100644 index 0000000000..cf46e39695 --- /dev/null +++ b/utils/field/vector_primitives.py @@ -0,0 +1,138 @@ +# 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 + +from sverchok.utils.geom import rotate_around_vector_matrix, CubicSpline +from sverchok.utils.math import (np_dot, np_multiply_matrices_vectors) +from sverchok.utils.integrate import TrapezoidIntegral +from sverchok.utils.field.vector import SvVectorField + +class SvCurveIntegral: + def __init__(self, curve, resolution=100, x_axis=0, y_axis=1): + self.curve = curve + self.resolution = resolution + self.x_axis = x_axis + self.y_axis = y_axis + u_min, u_max = curve.get_u_bounds() + ts = np.linspace(u_min, u_max, num=resolution) + pts = curve.evaluate_array(ts) + ys = pts[:,y_axis] + xs = pts[:,x_axis] + self.integral = TrapezoidIntegral(xs, ts, ys) + self.integral.calc() + + def evaluate(self, ts): + return self.integral.evaluate_linear(ts) + +class SvCurveProjection: + def __init__(self, curve, resolution=100, x_axis=0, y_axis=1): + self.curve = curve + self.resolution = resolution + self.x_axis = x_axis + self.y_axis = y_axis + u_min, u_max = curve.get_u_bounds() + ts = np.linspace(u_min, u_max, num=resolution) + pts = curve.evaluate_array(ts) + ys = pts[:,y_axis] + xs = pts[:,x_axis] + #print("Xs", xs) + #print("Ys", ys) + self.spline = CubicSpline.from_2d_points(xs, ys) + + def evaluate(self, ts): + u_min, u_max = self.curve.get_u_bounds() + ts = (ts - u_min) / (u_max - u_min) + return self.spline.eval(ts)[:,1] + +class SvTwistVectorField(SvVectorField): + def __init__(self, center, axis, angle_along_axis, angle_along_radius, + min_z = None, max_z = None, min_r = None, max_r = None): + self.center = np.asarray(center) + self.axis = np.asarray(axis) + self.axis = self.axis / np.linalg.norm(self.axis) + self.angle_along_axis = angle_along_axis + self.angle_along_radius = angle_along_radius + self.min_z = min_z + self.max_z = max_z + self.min_r = min_r + self.max_r = max_r + + def _calc_angles(self, angle, ts): + if isinstance(angle, (int,float,np.float64)): + return angle * ts + else: + r = angle.evaluate(ts) + return r + + def evaluate_grid(self, xs, ys, zs): + pts = np.stack((xs, ys, zs)).T + dpts = pts - self.center + ts = np_dot(dpts, self.axis) + rads = dpts - ts[np.newaxis].T * self.axis + rs = np.linalg.norm(rads, axis=1) + if self.min_z is not None or self.max_z is not None: + ts = np.clip(ts, self.min_z, self.max_z) + if self.min_r is not None or self.max_r is not None: + rs = np.clip(rs, self.min_r, self.max_r) + angles = self._calc_angles(self.angle_along_axis, ts) + angles += self._calc_angles(self.angle_along_radius, rs) + #angles = ts * self.angle_along_axis + rs * self.angle_along_radius + matrices = rotate_around_vector_matrix(self.axis, angles) + new_pts = np_multiply_matrices_vectors(matrices, dpts) + new_pts += self.center + vectors = (new_pts - pts).T + return vectors[0], vectors[1], vectors[2] + + def evaluate(self, x, y, z): + rxs, rys, rzs = self.evaluate_grid([x], [y], [z]) + return rxs[0], rys[0], rzs[0] + +class SvTaperVectorField(SvVectorField): + def __init__(self, center, axis, coefficient, min_z=None, max_z=None): + self.center = np.asarray(center) + self.axis = np.asarray(axis) + self.axis = self.axis / np.linalg.norm(self.axis) + self.coefficient = coefficient + self.min_z = None + self.max_z = None + rho = 1.0 / coefficient + if min_z is not None: + self.max_z = rho - min_z + if max_z is not None: + self.min_z = rho - max_z + + @classmethod + def from_base_point_and_vertex(cls, base_point, vertex, **kwargs): + base_point = np.array(base_point) + vertex = np.array(vertex) + rho = np.linalg.norm(base_point - vertex) + return SvTaperVectorField(vertex, base_point - vertex, 1.0/rho, **kwargs) + + @classmethod + def from_base_point_and_vector(cls, base_point, vector, **kwargs): + base_point = np.array(base_point) + vector = np.array(vector) + return SvTaperVectorField.from_base_point_and_vertex(base_point, base_point + vector, **kwargs) + + def evaluate_grid(self, xs, ys, zs): + pts = np.stack((xs, ys, zs)).T + dpts = pts - self.center + ts = np_dot(dpts, self.axis) + projections = ts[np.newaxis].T * self.axis + rads = dpts - projections + if self.min_z is not None or self.max_z is not None: + ts = np.clip(ts, self.min_z, self.max_z) + rads *= self.coefficient * ts[np.newaxis].T + new_pts = self.center + projections + rads + vectors = (new_pts - pts).T + return vectors[0], vectors[1], vectors[2] + + def evaluate(self, x, y, z): + rxs, rys, rzs = self.evaluate_grid([x], [y], [z]) + return rxs[0], rys[0], rzs[0] + diff --git a/utils/geom.py b/utils/geom.py index 697878c394..13fd7c1cb2 100644 --- a/utils/geom.py +++ b/utils/geom.py @@ -374,7 +374,7 @@ def create(cls, vertices, tknots = None, metric = None, is_cyclic = False): def from_2d_points(cls, xs, ys): vertices = np.zeros((len(xs), 3)) vertices[:,0] = np.array(xs) - vertices[:,1] = np.zrray(ys) + vertices[:,1] = np.array(ys) return CubicSpline(vertices, metric='X', is_cyclic=False) def eval(self, t_in, tknots = None): From 7dd61c6b93054f8a615ba792415f683eaa995ae2 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Sun, 22 Dec 2024 21:30:21 +0500 Subject: [PATCH 4/4] Twist&Whirl node documentation. --- docs/nodes/field/twist_field.rst | 93 ++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/nodes/field/twist_field.rst diff --git a/docs/nodes/field/twist_field.rst b/docs/nodes/field/twist_field.rst new file mode 100644 index 0000000000..e4a3256a72 --- /dev/null +++ b/docs/nodes/field/twist_field.rst @@ -0,0 +1,93 @@ +Twist / Whirld Field +==================== + +Functionality +------------- + +This node generates a Vector Field, which does Twist or Whirl transformation of +some region of space, or combination of Twist and Whirl. + +We call Twist a transformation which you get if you take some object, for +example a cube, then rotate one of it's sides (for example, bottom one) around +some axis, counterclockwise, and rotate the opposite side (top side, for +example) around the same axis clockwise. + +Whirl transformation rotates all parts of object in the same direction, just +with different angle; the farther from whirl axis, the bigger is rotation +angle. + +Both Twist and Whirl transformation require some axis for rotation. This axis +can be defined by a point, through which the axis goes, and the direction +vector. + +Inputs +------ + +This node has the following inputs: + +* **Center**. Rotation center; a point on twist / whirl axis. The default value + is the origin, `(0, 0, 0)`. +* **Axis**. Direction vector of the twist / whirl axis. The default value is Z + axis, `(0, 0, 1)`. +* **TwistAngle**. This is not exactly an angle, but a coefficient, which + defines the force of twist transformation. More specifically, the plane lying + at distance of 1 from rotation center in direction of rotation axis, will be + rotated by angle specified in this input; plane passing through rotation + center will not be rotated by twist transformation; all other planes + perpendicular to rotation axis will be rotated on an angle proportional to + distance from rotation center. The value is specified in radians. The default + value is `pi/2`. +* **WhirlAngle**. Similar to previous input, this is not exactly an angle, but + a coefficient, which defines the force of whirl transformation. More + specifically, points at distance 1 from rotation axis will be rotated by + angle specified in this input. All other points will be rotated by angle + which is proportional to distance from rotation axis. The value is specified + in radians. The default value is 0. +* **MinZ**, **MaxZ**. These inputs are available only when **Use Min Z** / + **Use Max Z** parameters are enabled. Both twist and whirl transformation + will stop at these distances from rotation center along rotation axis. + Positive direction of rotation axis is the one defined by **Axis** input. The + default values are 0.0 and 1.0, correspondingly. +* **MinR**, **MaxR**. These inputs are available only when **Use Min R** / + **Use Max R** parameters are enabled. Both twist and whirl transformations + will be ceased for points which have distance from rotation axis below + **MinR** or above **MaxR** values. The default values are 0.0 and 1.0. + +Parameters +---------- + +This node has the following parameters: + +* **Use Min Z**, **Use Max Z**. If enabled, these parameters will allow to + define a part of space along rotation axis which will be transformed; space + outside this area will not be twisted any further. +* **Use Min R**, **Use Max R**. If enabled, these parameters allow to define a + (cylindrical) part of space in terms of distance from rotation axis, which + should be transformed; outside this area, the space will not be twisted any + further. + +Outputs +------- + +This node has the following output: + +* **Field**. The generated vector field. + +Examples of usage +----------------- + +Example of Twist transformation: + +.. image:: https://github.com/user-attachments/assets/edb22301-0dd3-492b-abea-a20a2f2ee772 + :target: https://github.com/user-attachments/assets/edb22301-0dd3-492b-abea-a20a2f2ee772 + +Example of Whirl transformation: + +.. image:: https://github.com/user-attachments/assets/3467f0e7-884a-496e-9516-133d3c5596f4 + :target: https://github.com/user-attachments/assets/3467f0e7-884a-496e-9516-133d3c5596f4 + +Twist and Whirl combined: + +.. image:: https://github.com/user-attachments/assets/0b351703-97ae-4960-9f86-42f7c2825c0b + :target: https://github.com/user-attachments/assets/0b351703-97ae-4960-9f86-42f7c2825c0b +