Skip to content

Commit

Permalink
"Snap Curves" node: initial implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
portnov committed Oct 28, 2024
1 parent 80d1045 commit 726d3a3
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 0 deletions.
1 change: 1 addition & 0 deletions menus/full_by_data_type.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
- SvProjectCurveSurfaceNode
- ---
- SvNurbsCurveMovePointNode
- SvSnapCurvesNode
- ---
- SvCurveInsertKnotNode
- SvCurveRemoveKnotNode
Expand Down
139 changes: 139 additions & 0 deletions nodes/curve/snap_curves.py
Original file line number Diff line number Diff line change
@@ -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)

83 changes: 83 additions & 0 deletions utils/curve/nurbs_solver_applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit 726d3a3

Please sign in to comment.