diff --git a/docs/assets/nodes/curve/snap_curves.gif b/docs/assets/nodes/curve/snap_curves.gif new file mode 100644 index 0000000000..d2400bb175 Binary files /dev/null and b/docs/assets/nodes/curve/snap_curves.gif differ diff --git a/docs/nodes/curve/snap_curves.rst b/docs/nodes/curve/snap_curves.rst new file mode 100644 index 0000000000..08e09f9263 --- /dev/null +++ b/docs/nodes/curve/snap_curves.rst @@ -0,0 +1,100 @@ +Snap Curves +=========== + +Functionality +------------- + +This node takes two or more NURBS curve objects, and modifies them in such a +way that they become connected: end point of first curve coincides with start +point of the second curve, and so on. It is possible to define, which curve +must become "main" , and which curve should be adjusted in order to match the +"main" curve. + +Optionally, this node can also adjust directions of curves at their start and +end points. + +For this node, direction and order of curves are important. If you have several +curves with arbitrary directions, you may want to use "Sort Curves" node first, +to ensure that the end of each curve is near the beginning of the next curve. + +This node can work with NURBS and NURBS-like curves only. It adjusts curves by +moving their control points, while trying to move all points as less as +possible. This means that curve structure is important for this node. For +example, + +* If the curve has a lot of control points, the node will have to move only one + or two of it's control points near the end, so that most of the curve will be + left unchanged. +* If the curve has only a few of control points, for example 3 or 4, then + movement of 1 or 2 control points can move almost whole curve. +* For curves with higher degree, each control point controls wider span of the + curve. So when adjusting curves with higher degree, wider span of the curve + will be moved. + +Inputs +------ + +This node has the following inputs: + +* **Curve1**. First curve to process. This input is available and mandatory + only if **Input Mode** parameter is set to **Two Curves**. +* **Curve2**. Second curve to process. This input is available and mandatory + only if **Input Mode** parameter is set to **Two Curves**. +* **Curves**. List of curves to be processed. This input is available and mandatory + only if **Input Mode** parameter is set to **List of Curves**. + +Parameters +---------- + +This node has the following parameters: + +* **Input Mode**. The available options are **Two Curves** and **List of + Curves**. The default option is **Two Curves**. +* **Bias**. This defines where two curves should meet. The available options are: + + * **Middle**. The meeting point will be defined as middle between end of + first curve and start of second curve. + * **Curve 1**. First curve will be considered "main", so it's end point will + not be moved; start point of the second curve will be moved to end point of + the first curve. + * **Curve 2**. Second curve will be considered "main", so it's start point + will not be moved; end point fo the first curve will be moved to start + point of the second curve. + + The default option is **Middle**. +* **Tangents**. This defines whether the node should adjust directions of + curves at their ends, and how exactly. The available options are: + + * **No matter**. The node will not bother about curve directions, and try to + move curve ends properly while moving control points as less as possible. + * **Preserve**. The node will try to preserve directions of the curves at their ends. + * **Medium**. The node will adjust the directions of curves at their ends in + such a way, that their tangent vectors will be equal to average between + tangent vector of the first curve at it's end and tangent vector of the + second curve at it's beginning. + * **Curve 1**. The node will preserve the direction of the first curve at + it's end, and adjust the direction of the second curve at it's beginning to + match the first curve. + * **Curve 2**. The node will preserve the direction of the second curve at + it's beginning, and adjust the direction of the first curve at it's end to + match the second curve. + + The default option is **No matter**. + +* **Cyclic**. If checked, the node will also try to connect the end point of + last curve to the start point of the first curve, in order to create a closed + loop. Unchecked by default. + +Outputs +------- + +This node has the following output: + +* **Curves**. The resulting curves. + +Example of Usage +---------------- + +.. image:: ../../../docs/assets/nodes/curve/snap_curves.gif + :target: ../../../docs/assets/nodes/curve/snap_curves.gif + diff --git a/index.yaml b/index.yaml index 2bea04ed9a..c0b039efb1 100644 --- a/index.yaml +++ b/index.yaml @@ -96,6 +96,7 @@ - SvNurbsCurveNodesNode - --- - SvNurbsCurveMovePointNode + - SvSnapCurvesNode - --- - SvCurveInsertKnotNode - SvCurveRemoveKnotNode 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/menus/full_nortikin.yaml b/menus/full_nortikin.yaml index f25338e873..4c7227ab95 100644 --- a/menus/full_nortikin.yaml +++ b/menus/full_nortikin.yaml @@ -182,6 +182,7 @@ - icon_name: OUTLINER_OB_CURVE - extra_menu: AdvancedObjectsPartialMenu - SvNurbsCurveMovePointNode + - SvSnapCurvesNode - SvExApplyFieldToCurveNode - T SURFACES: - icon_name: SURFACE_DATA diff --git a/nodes/curve/snap_curves.py b/nodes/curve/snap_curves.py new file mode 100644 index 0000000000..7d1a620007 --- /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_SNAP_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/ui/icons/sv_snap_curves.png b/ui/icons/sv_snap_curves.png new file mode 100644 index 0000000000..1e9c29e6e2 Binary files /dev/null and b/ui/icons/sv_snap_curves.png differ diff --git a/ui/icons/svg/sv_snap_curves.svg b/ui/icons/svg/sv_snap_curves.svg new file mode 100644 index 0000000000..d3bf380d36 --- /dev/null +++ b/ui/icons/svg/sv_snap_curves.svg @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + 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] +