diff --git a/docs/nodes/curve/curve_index.rst b/docs/nodes/curve/curve_index.rst index 45e96044f6..f3f4177fb9 100644 --- a/docs/nodes/curve/curve_index.rst +++ b/docs/nodes/curve/curve_index.rst @@ -52,6 +52,7 @@ Curves nurbs_curve approximate_nurbs_curve interpolate_nurbs_curve + move_nurbs_curve_point insert_knot remove_knot refine_nurbs_curve diff --git a/docs/nodes/curve/move_nurbs_curve_point.rst b/docs/nodes/curve/move_nurbs_curve_point.rst new file mode 100644 index 0000000000..9b8259b652 --- /dev/null +++ b/docs/nodes/curve/move_nurbs_curve_point.rst @@ -0,0 +1,137 @@ +Move NURBS Curve Point +====================== + +Functionality +------------- + +This node suggests several ways of adjusting a NURBS curve so that it would go +through another point at specified position, while keeping most of the curve +more or less in place. + +Different methods of curve adjustment allow different degrees of freedom in +specifying what do you want to move and where to. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The NURBS Curve object to be adjusted. This input is mandatory. +* **T**. The value of curve parameter, point at which is to be moved. The default value is 0.5. +* **Index**. This input has different meaning for different curve adjustment methods being used: + + * For **Move one control point** method, this is the index of curve control point to be moved. + * For **Adjust one weight** method, this is the index of curve weight to be adjusted. + * For **Adjust two weights** method, this is the index of first of two curve + weights to be adjusted. The second weight adjusted will be the following one. + * For other methods, this input is not available. + + The default value is 1. + +* **Distance**. The distance for which curve point at **T** parameter is to be moved. + + * For **Adjust one weight** method, positive values mean move the point + toward corresponding control point (index of which is defined in **Index** + input). Negative values mean movement in the oppposite direction. + * For **Adjust two weight** method, positive values mean move the span of + curve towards the corresponding curve control polygon leg (the one between + control points **Index** and **Index+1**). Negative values mean movement in + the opposite direction. + * For other methods, this input is not available. + + The default value is 1.0. + +* **Vector**. The vector for which the curve point at **T** parameter is to be + moved. This input is available only when **Method** is set to **Move one + control point**, **Move control points**, or **Insert knot**. The default + value is ``(1.0, 0.0, 0.0)``. + +Parameters +---------- + +This node has the following parameters: + +* **Method**. The method to be used to adjust the curve. The following methods are available: + + * **Move one control point**. The node will move exactly one control point of + the curve, to move curve point at **T** parameter by **Vector**. The index + of control point being moved is specified in the **Index** input. Note that + it is not always possible to move arbitrary curve point by arbitrary vector + by moving specified control point. In intuitive terms, the point to be + moved has to be near control point being moved. + * **Adjust one weight**. The node will adjust one weight of the curve, to + move curve point at **T** parameter directly towards corresponding control + point, or in the opposite direction. The index of the weight being adjusted + (and the index of corresponding control point) is specified in the + **Index** input. Movement distance is specified in the **Distance** input. + Note that it is not always possible to move arbitrary curve point by + adjusting the specified curve weight. Also, if you try to move the point + too far with this method, you will probably get unexpected curve shapes. + * **Adjust two weights**. The node will adjust two weights of the curve, to + move curve point at **T** parameter, together with neighbouring curve span, + towards the corresponding control polygon leg, or in the opposite + direction. The index of the first weight to be adjusted (and corresponding + control point index) is specified in the **Index** input. Note that it is + not always possible to move an arbitrary curve point by adjusting the + specified weights. Also, if you try to move the point too far with this + method, you will probably get unexpected curve shapes. + * **Move control points**. The node will move several control points of the + curve (approximately ``p`` of them, where ``p`` is the degree of the + curve), to move curve point at **T** parameter by the specified vector. The + node will automatically figure out which control points have to be moved. + This algorithms gives most smooth results, but it requires more + computations, so it is probably less performant. + * **Insert knot**. The node will insert additional knot into curve's + knotvector, and then move three control points, in order to move curve + point at **T** parameter by specified vector. The node will automatically + figure out which control points have to be moved. + + The default option is **Move one control point**. + +* **Preserve tangent**. This parameter is available only when **Method** is set + to **Move control points**. If checked, the node will try to preserve the + direction of curve tangent at the point being moved. In many cases, this + gives only a slight difference; but sometimes this will make the result + smoother. Unchecked by default. + +Outputs +------- + +This node has the following output: + +* **Curve**. The adjusted curve. + +Examples of Usage +----------------- + +An illustration of **Move one control point** method. Here, black is the +original curve; dark blue is it's control polygon; light blue point is the +point at T parameter on the original curve. Green is the resulting curve, and +big green point is the resulting point. In this case, only control point number +7 is moved. + +.. image:: https://user-images.githubusercontent.com/284644/186957079-ceee637d-be54-4d26-8474-04dd4543a011.png + +An example of **Adjust one weight** method. Here, the blue point is moved +towards the control point number 8. Curve control points are not moved, only +one curve weight is changed. + +.. image:: https://user-images.githubusercontent.com/284644/186957074-4f520bad-ff48-48d1-a3b4-ebe2fec1d270.png + +An example of **Adjust two weights** method. Here, the blue point is pushed +away from control polygon leg between control points 4 and 5 (note the negative +value of Distance parameter). Again, control points are not moved, only weights +are changed. + +.. image:: https://user-images.githubusercontent.com/284644/186957069-2bb35686-1d3b-4abb-94cb-fb0fc03a338d.png + +An example of **Move control points** method. Here, the blue point is moved by +specified vector by moving of three control points (6, 7 and 8). + +.. image:: https://user-images.githubusercontent.com/284644/186957065-2b465e62-82f7-48ce-a38a-402580dcd7e7.png + +An example of **Insert knot** method. The point is moved by inserting a knot, +thus creating additional control points, and moving three control points. + +.. image:: https://user-images.githubusercontent.com/284644/186957056-66fb3952-664a-4368-92e3-ab48487d51b6.png + diff --git a/index.md b/index.md index 78cdcc7dce..2f6305a928 100644 --- a/index.md +++ b/index.md @@ -70,6 +70,8 @@ SvDeconstructCurveNode SvNurbsCurveNodesNode --- + SvNurbsCurveMovePointNode + --- SvCurveInsertKnotNode SvCurveRemoveKnotNode SvRefineNurbsCurveNode diff --git a/nodes/curve/move_nurbs_curve_point.py b/nodes/curve/move_nurbs_curve_point.py new file mode 100644 index 0000000000..4e911c6d0f --- /dev/null +++ b/nodes/curve/move_nurbs_curve_point.py @@ -0,0 +1,154 @@ +# 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, repeat_last_for_length +from sverchok.utils.curve import SvCurve +from sverchok.utils.curve.nurbs import SvNurbsCurve +from sverchok.utils.curve.nurbs_algorithms import ( + move_curve_point_by_moving_control_point, + move_curve_point_by_adjusting_one_weight, + move_curve_point_by_adjusting_two_weights, + move_curve_point_by_moving_control_points, TANGENT_PRESERVE, + move_curve_point_by_inserting_knot) + +class SvNurbsCurveMovePointNode(bpy.types.Node, SverchCustomTreeNode): + """ + Triggers: Move NURBS curve point + Tooltip: Adjust NURBS curve to move it's point to another location + """ + bl_idname = 'SvNurbsCurveMovePointNode' + bl_label = 'Move NURBS Curve Point' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_MOVE_CURVE_POINT' + + methods = [ + ('ONE_CPT', "Move one control point", "Move single control point", 0), + ('ONE_WEIGHT', "Adjust one weight", "Change single weight", 1), + ('TWO_WEIGHTS', "Adjust two weights", "Change two weights", 2), + ('MOVE_CPTS', "Move control points", "Move several control points", 3), + ('INSERT_KNOT', "Insert knot", "Insert additional knot and move several control points", 4) + ] + + def update_sockets(self, context): + self.inputs['Index'].hide_safe = self.method not in ['ONE_CPT', 'ONE_WEIGHT', 'TWO_WEIGHTS'] + self.inputs['Distance'].hide_safe = self.method not in ['ONE_WEIGHT', 'TWO_WEIGHTS'] + self.inputs['Vector'].hide_safe = self.method not in ['ONE_CPT', 'MOVE_CPTS', 'INSERT_KNOT'] + updateNode(self, context) + + method : EnumProperty( + name = "Method", + description = "How should we modify the curve control points or weights", + items = methods, + default = 'ONE_CPT', + update = update_sockets) + + t_value : FloatProperty( + name = "T", + description = "Curve parameter value", + default = 0.5, + update = updateNode) + + idx : IntProperty( + name = "Index", + description = "Control point or weight index to be adjusted", + default = 1, + min = 0, + update = updateNode) + + distance : FloatProperty( + name = "Distance", + description = "How far to move the point; negative value mean move in the opposite direction", + default = 1.0, + update = updateNode) + + preserve_tangent : BoolProperty( + name = "Preserve tangent", + default = False, + update = updateNode) + + def draw_buttons(self, context, layout): + layout.prop(self, 'method') + if self.method == 'MOVE_CPTS': + layout.prop(self, 'preserve_tangent') + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "T").prop_name = 't_value' + self.inputs.new('SvStringsSocket', "Index").prop_name = 'idx' + self.inputs.new('SvStringsSocket', "Distance").prop_name = 'distance' + p = self.inputs.new('SvVerticesSocket', "Vector") + p.use_prop = True + p.default_property = (1.0, 0.0, 0.0) + self.outputs.new('SvCurveSocket', "Curve") + self.update_sockets(context) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curve_s = self.inputs['Curve'].sv_get() + t_value_s = self.inputs['T'].sv_get() + index_s = self.inputs['Index'].sv_get() + distance_s = self.inputs['Distance'].sv_get() + vector_s = self.inputs['Vector'].sv_get() + + input_level = get_data_nesting_level(curve_s, data_types=(SvCurve,)) + flat_output = input_level < 2 + + curve_s = ensure_nesting_level(curve_s, 2, data_types=(SvCurve,)) + t_value_s = ensure_nesting_level(t_value_s, 2) + index_s = ensure_nesting_level(index_s, 2) + distance_s = ensure_nesting_level(distance_s, 2) + vector_s = ensure_nesting_level(vector_s, 3) + + curves_out = [] + for params in zip_long_repeat(curve_s, t_value_s, index_s, distance_s, vector_s): + new_curves = [] + for curve, t_value, index, distance, vector in zip_long_repeat(*params): + curve = SvNurbsCurve.to_nurbs(curve) + if curve is None: + raise Exception("One of curves is not NURBS") + + vector = np.array(vector) + if self.method == 'ONE_CPT': + curve = move_curve_point_by_moving_control_point(curve, t_value, index, vector) + elif self.method == 'ONE_WEIGHT': + curve = move_curve_point_by_adjusting_one_weight(curve, t_value, index, distance) + elif self.method == 'TWO_WEIGHTS': + curve = move_curve_point_by_adjusting_two_weights(curve, t_value, index, distance=distance) + elif self.method == 'MOVE_CPTS': + if self.preserve_tangent: + tangent = TANGENT_PRESERVE + else: + tangent = None + curve = move_curve_point_by_moving_control_points(curve, t_value, vector, tangent=tangent) + elif self.method == 'INSERT_KNOT': + curve = move_curve_point_by_inserting_knot(curve, t_value, vector) + else: + raise Exception("Unsupported method") + + new_curves.append(curve) + + if flat_output: + curves_out.extend(new_curves) + else: + curves_out.append(new_curves) + + self.outputs['Curve'].sv_set(curves_out) + +def register(): + bpy.utils.register_class(SvNurbsCurveMovePointNode) + +def unregister(): + bpy.utils.unregister_class(SNurbsCurveMovePointNodevCurveInsertKnotNode) + diff --git a/nodes/curve/nurbs_curve_nodes.py b/nodes/curve/nurbs_curve_nodes.py index 0001e26844..91da9c1f97 100644 --- a/nodes/curve/nurbs_curve_nodes.py +++ b/nodes/curve/nurbs_curve_nodes.py @@ -24,7 +24,7 @@ class SvNurbsCurveNodesNode(bpy.types.Node, SverchCustomTreeNode): bl_idname = 'SvNurbsCurveNodesNode' bl_label = 'NURBS Curve Nodes' bl_icon = 'OUTLINER_OB_EMPTY' - sv_icon = 'SV_DECONSTRUCT_CURVE' + sv_icon = 'SV_CURVE_NODES' def sv_init(self, context): self.inputs.new('SvCurveSocket', "Curve") diff --git a/ui/icons/sv_curve_nodes.png b/ui/icons/sv_curve_nodes.png new file mode 100644 index 0000000000..c2595b530a Binary files /dev/null and b/ui/icons/sv_curve_nodes.png differ diff --git a/ui/icons/sv_move_curve_point.png b/ui/icons/sv_move_curve_point.png new file mode 100644 index 0000000000..39c5e906df Binary files /dev/null and b/ui/icons/sv_move_curve_point.png differ diff --git a/ui/icons/svg/sv_curve_nodes.svg b/ui/icons/svg/sv_curve_nodes.svg new file mode 100644 index 0000000000..5c7ef0cd25 --- /dev/null +++ b/ui/icons/svg/sv_curve_nodes.svg @@ -0,0 +1,417 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/ui/icons/svg/sv_move_curve_point.svg b/ui/icons/svg/sv_move_curve_point.svg new file mode 100644 index 0000000000..ec17a901ff --- /dev/null +++ b/ui/icons/svg/sv_move_curve_point.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/utils/curve/nurbs_algorithms.py b/utils/curve/nurbs_algorithms.py index 6b38d63846..a492830c89 100644 --- a/utils/curve/nurbs_algorithms.py +++ b/utils/curve/nurbs_algorithms.py @@ -693,6 +693,8 @@ def move_curve_point_by_moving_control_point(curve, u_bar, k, vector): vector = vector / distance functions = SvNurbsBasisFunctions(curve.get_knotvector()) x = functions.fraction(k,p, weights)(np.array([u_bar]))[0] + if abs(x) < 1e-6: + raise Exception(f"Specified control point #{k} is too far from curve parameter U = {u_bar}") alpha = distance / x cpts[k] = cpts[k] + alpha * vector return curve.copy(control_points = cpts) @@ -798,6 +800,10 @@ def move_curve_point_by_adjusting_two_weights(curve, u_bar, k, distance=None, sc abk = np.linalg.norm(D - pk1) / control_leg_len abk1 = np.linalg.norm(C - pk) / control_leg_len + eps = 1e-6 + if abs(ak) < eps or abs(abk) < eps or abs(ak1) < eps or abs(abk1) < eps: + raise Exception(f"Specified control point #{k} is too far from curve parameter U = {u_bar}") + numerator = 1.0 - ak - ak1 numerator_brave = 1.0 - abk - abk1 @@ -812,8 +818,9 @@ def move_curve_point_by_adjusting_two_weights(curve, u_bar, k, distance=None, sc WEIGHTS_NONE = 'NONE' WEIGHTS_EUCLIDIAN = 'EUCLIDIAN' +TANGENT_PRESERVE = 'PRESERVE' -def move_curve_point_by_moving_control_points(curve, u_bar, vector, weight_mode = WEIGHTS_NONE): +def move_curve_point_by_moving_control_points(curve, u_bar, vector, weights_mode = WEIGHTS_NONE, tangent = None): """ Adjust the given curve so that at parameter u_bar it goues through the point C[u_bar] + vector instead of C[u_bar]. @@ -880,13 +887,29 @@ def move_curve_point_by_moving_control_points(curve, u_bar, vector, weight_mode kv = curve.get_knotvector() basis = SvNurbsBasisFunctions(kv) alphas = [basis.fraction(k,p, curve_weights)(np.array([u_bar]))[0] for k in range(n)] - A = np.zeros((ndim,ndim*n)) + if tangent is None: + A = np.zeros((ndim,ndim*n)) + else: + if tangent == TANGENT_PRESERVE: + tangent = curve.tangent(u_bar) + A = np.zeros((2*ndim,ndim*n)) + ns = np.array([basis.derivative(k, p, 1)(np.array([u_bar]))[0] for k in range(n)]) + numerator = ns * curve_weights#[np.newaxis].T + denominator = curve_weights.sum() + betas = numerator / denominator for i in range(n): for j in range(ndim): - A[j,ndim*i+j] = alphas[i] * move_weights[i] + A[j, ndim*i+j] = alphas[i] * move_weights[i] + if tangent is not None: + A[ndim + j, ndim*i+j] = betas[i] * move_weights[i] A1 = np.linalg.pinv(A) - B = np.zeros((ndim,1)) - B[0:3,0] = vector[np.newaxis] + if tangent is None: + B = np.zeros((ndim,1)) + B[0:3,0] = vector[np.newaxis] + else: + B = np.zeros((2*ndim,1)) + B[0:3,0] = vector[np.newaxis] + #B[3:6,0] = tangent[np.newaxis] X = (A1 @ B).T W = np.diag(move_weights) d_cpts = W @ X.reshape((n,ndim))