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 @@
+
+
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 @@
+
+
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))