From 726d3a316ef2f94f17ddc6bb700669aab5a7c438 Mon Sep 17 00:00:00 2001 From: "Ilya V. Portnov" Date: Mon, 28 Oct 2024 09:10:00 +0500 Subject: [PATCH] "Snap Curves" node: initial implementation. --- menus/full_by_data_type.yaml | 1 + nodes/curve/snap_curves.py | 139 +++++++++++++++++++++++ utils/curve/nurbs_solver_applications.py | 83 ++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 nodes/curve/snap_curves.py diff --git a/menus/full_by_data_type.yaml b/menus/full_by_data_type.yaml index de3e7db1ca..a0fd8fbdc6 100644 --- a/menus/full_by_data_type.yaml +++ b/menus/full_by_data_type.yaml @@ -291,6 +291,7 @@ - SvProjectCurveSurfaceNode - --- - SvNurbsCurveMovePointNode + - SvSnapCurvesNode - --- - SvCurveInsertKnotNode - SvCurveRemoveKnotNode diff --git a/nodes/curve/snap_curves.py b/nodes/curve/snap_curves.py new file mode 100644 index 0000000000..2f019cb9c0 --- /dev/null +++ b/nodes/curve/snap_curves.py @@ -0,0 +1,139 @@ +# 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, IntProperty + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat, ensure_nesting_level, get_data_nesting_level +from sverchok.utils.curve import SvCurve +from sverchok.utils.curve.nurbs import SvNurbsCurve +from sverchok.utils.curve.nurbs_solver_applications import ( + snap_curves, + BIAS_CURVE1, BIAS_CURVE2, BIAS_MID, + TANGENT_ANY, TANGENT_PRESERVE, TANGENT_MATCH, + TANGENT_CURVE1, TANGENT_CURVE2 + ) + +class SvSnapCurvesNode(SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Snap Curves + Tooltip: Snap ends of curves to common point, optionally controlling curve tangents. + """ + bl_idname = 'SvSnapCurvesNode' + bl_label = 'Snap NURBS Curves' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_CONCAT_CURVES' + + bias_modes = [ + (BIAS_MID, "Middle point", "Snap to middle point between end of first curve and start of second curve", 0), + (BIAS_CURVE1, "Curve 1", "Snap start of second curve to the end of the first curve", 1), + (BIAS_CURVE2, "Curve 2", "Snap end of first curve to the start of the second curve", + 2) + ] + + tangent_modes = [ + (TANGENT_ANY, "No matter", "Tangents will probably change", 0), + (TANGENT_PRESERVE, "Preserve", "Preserve tangent vectors of all curves at both ends", 1), + (TANGENT_MATCH, "Medium", "Adjust tangent vectors of curves so that they will be average between end tangent of the first curve and start tangent of the second curve", 2), + (TANGENT_CURVE1, "Curve 1", "Preserve tangent vector of the first curve at it's end, and adjust the tangent vector of the second curve to match", 3), + (TANGENT_CURVE2, "Curve 2", "Preserve tangent vector of the second curve at it'send, and adjust the tangent vector of the first curve to match", 4) + ] + + input_modes = [ + ('TWO', "Two curves", "Process two curves", 0), + ('N', "List of curves", "Process several curves", 1) + ] + + def update_sockets(self, context): + self.inputs['Curve1'].hide_safe = self.input_mode != 'TWO' + self.inputs['Curve2'].hide_safe = self.input_mode != 'TWO' + self.inputs['Curves'].hide_safe = self.input_mode != 'N' + updateNode(self, context) + + input_mode : EnumProperty( + name = "Input mode", + items = input_modes, + default = 'TWO', + update = update_sockets) + + bias : EnumProperty( + name = "Bias", + items = bias_modes, + update = updateNode) + + tangent : EnumProperty( + name = "Tangents", + items = tangent_modes, + update = updateNode) + + cyclic : BoolProperty( + name = "Cyclic", + default = False, + update = updateNode) + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curves") + self.inputs.new('SvCurveSocket', "Curve1") + self.inputs.new('SvCurveSocket', "Curve2") + self.outputs.new('SvCurveSocket', "Curves") + self.update_sockets(context) + + def draw_buttons(self, context, layout): + layout.prop(self, 'input_mode', text='') + layout.prop(self, 'bias') + layout.prop(self, 'tangent') + layout.prop(self, 'cyclic') + + def get_inputs(self): + curves_s = [] + if self.input_mode == 'TWO': + curve1_s = self.inputs['Curve1'].sv_get() + curve2_s = self.inputs['Curve2'].sv_get() + level1 = get_data_nesting_level(curve1_s, data_types=(SvCurve,)) + level2 = get_data_nesting_level(curve2_s, data_types=(SvCurve,)) + nested_input = level1 > 1 or level2 > 1 + curve1_s = ensure_nesting_level(curve1_s, 2, data_types=(SvCurve,)) + curve2_s = ensure_nesting_level(curve2_s, 2, data_types=(SvCurve,)) + for inputs in zip_long_repeat(curve1_s, curve2_s): + curves_s.append( list( *zip_long_repeat(*inputs) ) ) + else: + curves_s = self.inputs['Curves'].sv_get() + level = get_data_nesting_level(curves_s, data_types=(SvCurve,)) + nested_input = level > 1 + curves_s = ensure_nesting_level(curves_s, 2, data_types=(SvCurve,)) + return nested_input, curves_s + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curves_out = [] + nested_input, curves_s = self.get_inputs() + for curves in curves_s: + curves = [SvNurbsCurve.to_nurbs(c) for c in curves] + if any(c is None for c in curves): + raise Exception("Some of curves are not NURBS!") + new_curves = snap_curves(curves, + bias = self.bias, + tangent = self.tangent, + cyclic = self.cyclic) + if nested_input: + curves_out.append(new_curves) + else: + curves_out.extend(new_curves) + + self.outputs['Curves'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvSnapCurvesNode) + +def unregister(): + bpy.utils.unregister_class(SvSnapCurvesNode) + diff --git a/utils/curve/nurbs_solver_applications.py b/utils/curve/nurbs_solver_applications.py index e090029daa..c3462ee7f8 100644 --- a/utils/curve/nurbs_solver_applications.py +++ b/utils/curve/nurbs_solver_applications.py @@ -393,3 +393,86 @@ def curve_on_surface_to_nurbs(degree, uv_curve, surface, samples, metric = 'DIST else: return interpolate_nurbs_curve(degree, points, cyclic=is_cyclic, tknots = tknots, logger=logger) + +BIAS_CURVE1 = 'C1' +BIAS_CURVE2 = 'C2' +BIAS_MID = 'M' + +TANGENT_ANY = 'A' +TANGENT_PRESERVE = 'P' +TANGENT_MATCH = 'M' +TANGENT_CURVE1 = 'C1' +TANGENT_CURVE2 = 'C2' + +def snap_curves(curves, bias=BIAS_CURVE1, tangent=TANGENT_ANY, cyclic=False): + + class Problem: + def __init__(self,curve): + self.curve = curve + self.t1, self.t2 = curve.get_u_bounds() + self.point1 = None + self.point2 = None + self.tangent1 = None + self.tangent2 = None + + def solve(self): + solver = SvNurbsCurveSolver(src_curve = self.curve) + if self.point1 is not None: + orig_pt1 = self.curve.evaluate(self.t1) + solver.add_goal(SvNurbsCurvePoints.single(self.t1, self.point1 - orig_pt1, relative=True)) + if self.point2 is not None: + orig_pt2 = self.curve.evaluate(self.t2) + solver.add_goal(SvNurbsCurvePoints.single(self.t2, self.point2 - orig_pt2, relative=True)) + if self.tangent1 is not None: + orig_tangent1 = self.curve.tangent(self.t1) + solver.add_goal(SvNurbsCurveTangents.single(self.t1, self.tangent1 - orig_tangent1, relative=True)) + if self.tangent2 is not None: + orig_tangent2 = self.curve.tangent(self.t2) + solver.add_goal(SvNurbsCurveTangents.single(self.t2, self.tangent2 - orig_tangent2, relative=True)) + solver.set_curve_params(len(self.curve.get_control_points()), self.curve.get_knotvector()) + problem_type, residue, curve = solver.solve_ex( + problem_types = {SvNurbsCurveSolver.PROBLEM_UNDERDETERMINED, + SvNurbsCurveSolver.PROBLEM_WELLDETERMINED} + ) + return curve + + def setup_problems(p1, p2): + if bias == BIAS_CURVE1: + target_pt = p1.curve.evaluate(p1.t2) + elif bias == BIAS_CURVE2: + target_pt = p2.curve.evaluate(p2.t1) + else: + pt1 = p1.curve.evaluate(p1.t2) + pt2 = p2.curve.evaluate(p2.t1) + target_pt = 0.5 * (pt1 + pt2) + p1.point2 = target_pt + p2.point1 = target_pt + + if tangent == TANGENT_ANY: + target_tangent1 = None + target_tangent2 = None + elif tangent == TANGENT_PRESERVE: + target_tangent1 = p1.curve.tangent(p1.t2) + target_tangent2 = p2.curve.tangent(p2.t1) + elif tangent == TANGENT_CURVE1: + target_tangent1 = p1.curve.tangent(p1.t2) + target_tangent2 = target_tangent1 + elif tangent == TANGENT_CURVE2: + target_tangent2 = p2.curve.tangent(p2.t1) + target_tangent1 = target_tangent2 + else: + tgt1 = p1.curve.tangent(p1.t2) + tgt2 = p2.curve.tangent(p2.t1) + target_tangent1 = 0.5 * (tgt1 + tgt2) + target_tangent2 = target_tangent1 + p1.tangent2 = target_tangent1 + p2.tangent1 = target_tangent2 + + problems = [Problem(c) for c in curves] + for p1, p2 in zip(problems[:-1], problems[1:]): + setup_problems(p1, p2) + if cyclic: + setup_problems(problems[-1], problems[0]) + + return [p.solve() for p in problems] +