From b6a4bc7cedda1ec5af5076d31c8737b448c08578 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 4 Dec 2022 19:41:17 +0500 Subject: [PATCH 01/12] API for fillets calculation. --- nodes/curve/fillet_polyline.py | 128 ++-------------- utils/curve/core.py | 6 + utils/curve/fillet.py | 266 +++++++++++++++++++++++++++++++++ utils/curve/nurbs.py | 61 ++++++++ utils/curve/primitives.py | 9 ++ utils/curve/splines.py | 9 ++ utils/fillet.py | 5 +- 7 files changed, 368 insertions(+), 116 deletions(-) create mode 100644 utils/curve/fillet.py diff --git a/nodes/curve/fillet_polyline.py b/nodes/curve/fillet_polyline.py index fa416af43c..43d94d39b6 100644 --- a/nodes/curve/fillet_polyline.py +++ b/nodes/curve/fillet_polyline.py @@ -1,15 +1,13 @@ import numpy as np -import bpy,math +import bpy from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty from mathutils import Vector + from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, zip_long_repeat, repeat_last_for_length, ensure_nesting_level -from sverchok.utils.logging import info, exception -from sverchok.utils.curve import SvLine -from sverchok.utils.fillet import calc_fillet -from sverchok.utils.curve.algorithms import concatenate_curves +from sverchok.utils.curve.fillet import FILLET_ARC, FILLET_BEZIER, fillet_polyline_from_vertices class SvFilletPolylineNode(SverchCustomTreeNode, bpy.types.Node): """ @@ -59,15 +57,15 @@ class SvFilletPolylineNode(SverchCustomTreeNode, bpy.types.Node): update = updateNode) arc_modes = [ - ('ARC', "Circular arc", "Circular arc", 0), - ('BEZIER2', "Quadratic Bezier arc", "Quadratic Bezier curve segment", 1) + (FILLET_ARC, "Circular arc", "Circular arc", 0), + (FILLET_BEZIER, "Quadratic Bezier arc", "Quadratic Bezier curve segment", 1) ] arc_mode : EnumProperty( name = "Fillet mode", description = "Type of curve to generate for fillets", items = arc_modes, - default = 'ARC', + default = FILLET_ARC, update = updateNode) def draw_buttons(self, context, layout): @@ -91,110 +89,6 @@ def sv_init(self, context): self.outputs.new('SvCurveSocket', "Curve") self.outputs.new('SvMatrixSocket', "Centers") - def make_curve(self, vertices, radiuses): - if self.cyclic: - if radiuses[-1] == 0 : - last_fillet = None - else: - last_fillet = calc_fillet(vertices[-2], vertices[-1], vertices[0], radiuses[-1]) - vertices = [vertices[-1]] + vertices + [vertices[0]] - prev_edge_start = vertices[0] if last_fillet is None else last_fillet.p2 - corners = list(zip(vertices, vertices[1:], vertices[2:], radiuses)) - else: - prev_edge_start = vertices[0] - corners = zip(vertices, vertices[1:], vertices[2:], radiuses) - - curves = [] - centers = [] - for v1, v2, v3, radius in corners: - if radius == 0 : - fillet = None - else: - fillet = calc_fillet(v1, v2, v3, radius) - if fillet is not None : - edge_direction = np.array(fillet.p1) - np.array(prev_edge_start) - edge_len = np.linalg.norm(edge_direction) - if edge_len != 0 : - edge = SvLine(prev_edge_start, edge_direction / edge_len) - edge.u_bounds = (0.0, edge_len) - curves.append(edge) - if self.arc_mode == 'ARC': - arc = fillet.get_circular_arc() - else: - arc = fillet.get_bezier_arc() - prev_edge_start = fillet.p2 - curves.append(arc) - centers.append(fillet.matrix) - else: - edge = SvLine.from_two_points(prev_edge_start, v2) - prev_edge_start = v2 - curves.append(edge) - - if not self.cyclic: - edge_direction = np.array(vertices[-1]) - np.array(prev_edge_start) - edge_len = np.linalg.norm(edge_direction) - if edge_len != 0 : - edge = SvLine(prev_edge_start, edge_direction / edge_len) - edge.u_bounds = (0.0, edge_len) - curves.append(edge) - - if self.make_nurbs: - if self.concat: - curves = [curve.to_nurbs().elevate_degree(target=2) for curve in curves] - else: - curves = [curve.to_nurbs() for curve in curves] - if self.concat: - concat = concatenate_curves(curves, scale_to_unit = self.scale_to_unit) - return concat, centers - else: - return curves, centers - - def limit(self,vertices,radiuses): - factor = 0.999 - if self.cyclic: - vertices = [vertices[-1]] + vertices + [vertices[0]] - vertices = [Vector(v) for v in vertices] - limit_radiuses = [] - for n in range(len(vertices)-2): - v1,v2,v3,r = vertices[n],vertices[n+1],vertices[n+2],radiuses[n] - vector1,vector2 = v1-v2,v3-v2 - d1,d2 = vector1.length,vector2.length - min_length1 = d1 if d1= min_length2 : - if self.cyclic and n==0: - min_length1 = min_length1/2 - vertices[-1] = (v1+v2)/2 - - angle = vector1.angle(vector2,0) - max_r = math.tan(angle/2)*min_length1 - else: - vec2 = vector2.copy() - vec2.normalize() - vec_1 = vector_1.copy() - vec_1.normalize() - f_vector1 = vec2*min_length1 - f_vector2 = vec_1*(d2-min_length2) - - mid_vector = (f_vector1 + -f_vector2)/2 - mid_vertex = v2 + mid_vector - vertices[n+1] = mid_vertex - min_length = mid_vector.length - - if self.cyclic and n==0: - vertices[-1] = v2 + vector1*(min_length/d1) - - angle = vector1.angle(vector2,0) - max_r = math.tan(angle/2)*min_length - r = max_r*factor if r>max_r else r - limit_radiuses.append(r) - return limit_radiuses - def process(self): if not any(socket.is_linked for socket in self.outputs): return @@ -211,9 +105,13 @@ def process(self): if len(vertices) < 3: raise Exception("At least three vertices are required to make a fillet") radiuses = repeat_last_for_length(radiuses, len(vertices)) - if self.clamp: - radiuses = self.limit(vertices, radiuses) - curve, centers = self.make_curve(vertices, radiuses) + curve, centers = fillet_polyline_from_vertices(vertices, radiuses, + cyclic = self.cyclic, + concat = self.concat, + clamp = self.clamp, + arc_mode = self.arc_mode, + scale_to_unit = self.scale_to_unit, + make_nurbs = self.make_nurbs) curves_out.append(curve) centers_out.append(centers) diff --git a/utils/curve/core.py b/utils/curve/core.py index 348a9c42b6..20a2627ad6 100644 --- a/utils/curve/core.py +++ b/utils/curve/core.py @@ -443,6 +443,12 @@ def is_closed(self, tolerance=1e-6): begin, end = self.get_end_points() return np.linalg.norm(begin - end) < tolerance + def is_polyline(self): + return False + + def get_polyline_vertices(self): + raise Exception("Curve is not a polyline") + def get_degree(self): """ Get curve degree, if applicable. diff --git a/utils/curve/fillet.py b/utils/curve/fillet.py new file mode 100644 index 0000000000..d5812d57dc --- /dev/null +++ b/utils/curve/fillet.py @@ -0,0 +1,266 @@ +# 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 math +import numpy as np + +from mathutils import Vector + +from sverchok.data_structure import repeat_last_for_length +from sverchok.utils.fillet import calc_fillet +from sverchok.utils.curve.primitives import SvLine +from sverchok.utils.curve.biarc import SvBiArc +from sverchok.utils.curve.algorithms import concatenate_curves +from sverchok.utils.curve.bezier import SvCubicBezierCurve, SvBezierCurve + +FILLET_ARC = 'ARC' +FILLET_BEZIER = 'BEZIER2' +FILLET_BEVEL = 'BEVEL' + +SMOOTH_POSITION = '0' +SMOOTH_TANGENT = '1' +SMOOTH_BIARC = '1b' +SMOOTH_ARC = '1a' +SMOOTH_NORMAL = '2' +SMOOTH_CURVATURE = '3' + +def calc_single_fillet(smooth, curve1, curve2, bulge_factor = 0.5, biarc_parameter = 1.0, planar_tolerance = 1e-6): + u1_max = curve1.get_u_bounds()[1] + u2_min = curve2.get_u_bounds()[0] + curve1_end = curve1.evaluate(u1_max) + curve2_begin = curve2.evaluate(u2_min) + tangent1_end = curve1.get_end_tangent() + tangent2_begin = curve2.get_start_tangent() + + if smooth == SMOOTH_POSITION: + return SvLine.from_two_points(curve1_end, curve2_begin) + elif smooth == SMOOTH_TANGENT: + #tangent1 = tangent1_end / np.linalg.norm(tangent1_end) + #tangent2 = tangent2_begin / np.linalg.norm(tangent2_begin) + tangent1 = bulge_factor * tangent1_end + tangent2 = bulge_factor * tangent2_begin + return SvCubicBezierCurve( + curve1_end, + curve1_end + tangent1 / 3.0, + curve2_begin - tangent2 / 3.0, + curve2_begin + ) + elif smooth == SMOOTH_BIARC: + return SvBiArc.calc( + curve1_end, curve2_begin, + tangent1_end, tangent2_begin, + biarc_parameter, + planar_tolerance = planar_tolerance) + elif smooth == SMOOTH_NORMAL: + second_1_end = curve1.second_derivative(u1_max) + second_2_begin = curve2.second_derivative(u2_min) + return SvBezierCurve.blend_second_derivatives( + curve1_end, tangent1_end, second_1_end, + curve2_begin, tangent2_begin, second_2_begin) + elif smooth == SMOOTH_CURVATURE: + second_1_end = curve1.second_derivative(u1_max) + second_2_begin = curve2.second_derivative(u2_min) + third_1_end = curve1.third_derivative_array(np.array([u1_max]))[0] + third_2_begin = curve2.third_derivative_array(np.array([u2_min]))[0] + + return SvBezierCurve.blend_third_derivatives( + curve1_end, tangent1_end, second_1_end, third_1_end, + curve2_begin, tangent2_begin, second_2_begin, third_2_begin) + else: + raise Exception(f"Unsupported smooth level: {smooth}") + +def cut_ends(curve, cut_offset, cut_start=True, cut_end=True): + u_min, u_max = curve.get_u_bounds() + p1, p2 = curve.get_end_points() + l = np.linalg.norm(p1 - p2) + dt = u_max - u_min + k = dt / l + if cut_start: + u1 = u_min + cut_offset * k + else: + u1 = u_min + if cut_end: + u2 = u_max - cut_offset * k + else: + u2 = u_max + return curve.cut_segment(u1, u2) + +def limit_filet_radiuses(vertices, radiuses, cyclic=False): + factor = 0.999 + if cyclic: + vertices = [vertices[-1]] + vertices + [vertices[0]] + vertices = [Vector(v) for v in vertices] + limit_radiuses = [] + for n in range(len(vertices)-2): + v1,v2,v3,r = vertices[n],vertices[n+1],vertices[n+2],radiuses[n] + vector1,vector2 = v1-v2,v3-v2 + d1,d2 = vector1.length,vector2.length + min_length1 = d1 if d1= min_length2 : + if cyclic and n==0: + min_length1 = min_length1/2 + vertices[-1] = (v1+v2)/2 + + angle = vector1.angle(vector2,0) + max_r = math.tan(angle/2)*min_length1 + else: + vec2 = vector2.copy() + vec2.normalize() + vec_1 = vector_1.copy() + vec_1.normalize() + f_vector1 = vec2*min_length1 + f_vector2 = vec_1*(d2-min_length2) + + mid_vector = (f_vector1 + -f_vector2)/2 + mid_vertex = v2 + mid_vector + vertices[n+1] = mid_vertex + min_length = mid_vector.length + + if cyclic and n==0: + vertices[-1] = v2 + vector1*(min_length/d1) + + angle = vector1.angle(vector2,0) + max_r = math.tan(angle/2)*min_length + r = max_r*factor if r>max_r else r + limit_radiuses.append(r) + return limit_radiuses + +def fillet_nurbs_curve(curve, smooth, cut_offset, + bulge_factor = 0.5, + biarc_parameter = 1.0, + planar_tolerance = 1e-6, + tangent_tolerance = 1e-6): + + cyclic = curve.is_closed() + segments = curve.split_at_fracture_points(tangent_tolerance = tangent_tolerance) + n = len(segments) + segments = [cut_ends(s, cut_offset, cut_start = (i > 0 or cyclic), cut_end = (i < n-1 or cyclic)) for i, s in enumerate(segments)] + fillets = [calc_single_fillet(smooth, s1, s2, bulge_factor, biarc_parameter, planar_tolerance) for s1, s2 in zip(segments, segments[1:])] + if cyclic: + fillet = calc_single_fillet(smooth, segments[-1], segments[0], bulge_factor, biarc_parameter, planar_tolerance) + fillets.append(fillet) + new_segments = [[segment, fillet] for segment, fillet in zip(segments, fillets)] + if not cyclic: + new_segments.append([segments[-1]]) + new_segments = sum(new_segments, []) + return concatenate_curves(new_segments) + +def fillet_polyline_from_vertices(vertices, radiuses, + cyclic=False, + concat=True, + clamp=True, + arc_mode = FILLET_ARC, + scale_to_unit=False, + make_nurbs=True): + + if len(radiuses) != len(vertices): + raise Exception(f"Number of radiuses provided ({len(radiuses)}) must be equal to number of vertices ({len(vertices)})") + + if clamp: + radiuses = limit_filet_radiuses(vertices, radiuses, cyclic=cyclic) + + if cyclic: + if radiuses[-1] == 0 : + last_fillet = None + else: + last_fillet = calc_fillet(vertices[-2], vertices[-1], vertices[0], radiuses[-1]) + vertices = [vertices[-1]] + vertices + [vertices[0]] + prev_edge_start = vertices[0] if last_fillet is None else last_fillet.p2 + corners = list(zip(vertices, vertices[1:], vertices[2:], radiuses)) + else: + prev_edge_start = vertices[0] + corners = zip(vertices, vertices[1:], vertices[2:], radiuses) + + curves = [] + centers = [] + for v1, v2, v3, radius in corners: + if radius == 0 : + fillet = None + else: + fillet = calc_fillet(v1, v2, v3, radius) + if fillet is not None : + edge_direction = np.array(fillet.p1) - np.array(prev_edge_start) + edge_len = np.linalg.norm(edge_direction) + if edge_len != 0 : + edge = SvLine(prev_edge_start, edge_direction / edge_len) + edge.u_bounds = (0.0, edge_len) + curves.append(edge) + if arc_mode == FILLET_ARC: + arc = fillet.get_circular_arc() + elif arc_mode == FILLET_BEZIER: + arc = fillet.get_bezier_arc() + elif arc_mode == FILLET_BEVEL: + arc = fillet.get_bevel() + else: + raise Exception(f"Unsupported arc mode: {arc_mode}") + prev_edge_start = fillet.p2 + curves.append(arc) + centers.append(fillet.matrix) + else: + edge = SvLine.from_two_points(prev_edge_start, v2) + prev_edge_start = v2 + curves.append(edge) + + if not cyclic: + edge_direction = np.array(vertices[-1]) - np.array(prev_edge_start) + edge_len = np.linalg.norm(edge_direction) + if edge_len != 0 : + edge = SvLine(prev_edge_start, edge_direction / edge_len) + edge.u_bounds = (0.0, edge_len) + curves.append(edge) + + if make_nurbs: + if concat: + curves = [curve.to_nurbs().elevate_degree(target=2) for curve in curves] + else: + curves = [curve.to_nurbs() for curve in curves] + if concat: + concat = concatenate_curves(curves, scale_to_unit = scale_to_unit) + return concat, centers + else: + return curves, centers + +def fillet_polyline_from_curve(curve, radiuses, + smooth = SMOOTH_ARC, + concat = True, + clamp = True, + scale_to_unit = False, + make_nurbs = True): + + if not curve.is_polyline(): + raise Exception("Curve is not a polyline") + vertices = curve.get_polyline_vertices() + cyclic = curve.is_closed() + + if isinstance(radiuses, (int,float)): + radiuses = [radiuses] + n = len(vertices) + radiuses = repeat_last_for_length(radiuses, n) + + if smooth == SMOOTH_POSITION: + arc_mode = FILLET_BEVEL + elif smooth == SMOOTH_TANGENT: + arc_mode = FILLET_BEZIER + elif smooth == SMOOTH_ARC: + arc_mode = FILLET_ARC + else: + raise Exception(f"Unsupported smooth level: {smooth}") + + return fillet_polyline_from_vertices(vertices, radiuses, + cyclic = cyclic, + concat = concat, + clamp = clamp, + arc_mode = arc_mode, + scale_to_unit = scale_to_unit, + make_nurbs = make_nurbs) + diff --git a/utils/curve/nurbs.py b/utils/curve/nurbs.py index 392818b8df..3ec1a5724c 100644 --- a/utils/curve/nurbs.py +++ b/utils/curve/nurbs.py @@ -560,6 +560,15 @@ def cut_segment(self, new_t_min, new_t_max, rescale=False): curve = curve.reparametrize(0, 1) return curve + def split_at_ts(self, ts): + segments = [] + rest = self + for t in ts: + s1, rest = rest.split_at(t) + segments.append(s1) + segments.append(rest) + return segments + def get_end_points(self): if sv_knotvector.is_clamped(self.get_knotvector(), self.get_degree()): cpts = self.get_control_points() @@ -570,6 +579,14 @@ def get_end_points(self): end = self.evaluate(u_max) return begin, end + def get_start_tangent(self): + cpts = self.get_control_points() + return cpts[1] - cpts[0] + + def get_end_tangent(self): + cpts = self.get_control_points() + return cpts[-1] - cpts[-2] + def is_line(self, tolerance=0.001): """ Check that the curve is nearly a straight line segment. @@ -819,6 +836,50 @@ def has_exactly_one_nearest_point(self, src_point): return False return segments[0].bezier_has_one_nearest_point(src_point) + def is_polyline(self, tolerance = 1e-6): + if self.get_degree() == 1: + return True + + segments = self.split_at_fracture_points() + return all(s.is_line(tolerance) for s in segments) + + def get_polyline_vertices(self): + segments = self.split_at_fracture_points() + points = [s.get_end_points()[0] for s in segments] + points.append(segments[-1].get_end_points()[1]) + return np.array(points) + + def split_at_fracture_points(self, smooth=1, tangent_tolerance = 1e-6): + + def is_fracture(segment1, segment2): + tangent1 = segment1.get_end_tangent() + tangent2 = segment2.get_start_tangent() + tangent1 = tangent1 / np.linalg.norm(tangent1) + tangent2 = tangent2 / np.linalg.norm(tangent2) + delta = np.linalg.norm(tangent1 - tangent2) + return delta >= tangent_tolerance + + def concatenate_non_fractured(segments): + prev_segment = segments[0] + new_segments = [] + for segment in segments[1:]: + if is_fracture(prev_segment, segment): + new_segments.append(prev_segment) + prev_segment = segment + else: + prev_segment = prev_segment.concatenate(segment) + + new_segments.append(prev_segment) + return new_segments + + kv = self.get_knotvector() + p = self.get_degree() + ms = sv_knotvector.to_multiplicity(kv) + possible_fracture_ts = [t for t, s in ms if s == p] + segments = self.split_at_ts(possible_fracture_ts) + segments = concatenate_non_fractured(segments) + return segments + class SvGeomdlCurve(SvNurbsCurve): """ geomdl-based implementation of NURBS curves diff --git a/utils/curve/primitives.py b/utils/curve/primitives.py index 4c3a7a4d03..37eda193b7 100644 --- a/utils/curve/primitives.py +++ b/utils/curve/primitives.py @@ -136,6 +136,15 @@ def reparametrize(self, new_t_min, new_t_max): new_point = self.point + self.direction * (t_min - scale * new_t_min) return SvLine(new_point, new_direction, u_bounds = (new_t_min, new_t_max)) + def is_polyline(self): + return True + + def get_polyline_vertices(self): + return np.array(self.get_end_points()) + + def is_closed(self, *args): + return False + def rotate_radius(radius, normal, thetas): """Internal method""" ct = np.cos(thetas)[np.newaxis].T diff --git a/utils/curve/splines.py b/utils/curve/splines.py index 2e1137c70f..efb3b1f86f 100644 --- a/utils/curve/splines.py +++ b/utils/curve/splines.py @@ -104,3 +104,12 @@ def split_at(self, t): def cut_segment(self, new_t_min, new_t_max, rescale=False): return self.to_nurbs().cut_segment(new_t_min, new_t_max, rescale=rescale) + def is_polyline(self): + return self.spline.get_degree() == 1 + + def get_polyline_vertices(self): + if self.spline.get_degree() == 1: + return self.spline.pts + else: + raise Exception("Curve is not a polyline") + diff --git a/utils/fillet.py b/utils/fillet.py index 8c35f868ef..14678628d5 100644 --- a/utils/fillet.py +++ b/utils/fillet.py @@ -4,7 +4,7 @@ from mathutils import Vector, Matrix -from sverchok.utils.curve.primitives import SvCircle +from sverchok.utils.curve.primitives import SvCircle, SvLine from sverchok.utils.curve.bezier import SvBezierCurve class ArcFilletData(object): @@ -34,6 +34,9 @@ def get_bezier_arc(self): cpts = np.array([self.p1, self.vertex, self.p2]) return SvBezierCurve(cpts) + def get_bevel(self): + return SvLine.from_two_points(self.p1, self.p2) + def calc_fillet(v1, v2, v3, radius): if not isinstance(v1, Vector): v1 = Vector(v1) From 0ddaed85e230ca7cf17fba2a3b3a08b8ab586a50 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 4 Dec 2022 21:34:20 +0500 Subject: [PATCH 02/12] Implement "Fillet Curve" node. --- index.yaml | 1 + nodes/curve/fillet_curve.py | 168 ++++++++++++++++++++++++++++++++++++ utils/curve/fillet.py | 11 ++- 3 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 nodes/curve/fillet_curve.py diff --git a/index.yaml b/index.yaml index f941d8dec8..b032b9dd7c 100644 --- a/index.yaml +++ b/index.yaml @@ -86,6 +86,7 @@ - SvExNurbsCurveNode - SvApproxNurbsCurveMk2Node - SvExInterpolateNurbsCurveNode + - SvFilletCurveNode - SvDeconstructCurveNode - SvNurbsCurveNodesNode - --- diff --git a/nodes/curve/fillet_curve.py b/nodes/curve/fillet_curve.py new file mode 100644 index 0000000000..0a01e1e2e2 --- /dev/null +++ b/nodes/curve/fillet_curve.py @@ -0,0 +1,168 @@ +# 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 mathutils import Vector + +from sverchok.node_tree import SverchCustomTreeNode +from sverchok.data_structure import updateNode, zip_long_repeat, repeat_last_for_length, ensure_nesting_level, get_data_nesting_level +from sverchok.utils.curve.core import SvCurve +from sverchok.utils.curve.nurbs import SvNurbsCurve +from sverchok.utils.curve.fillet import ( + SMOOTH_POSITION, SMOOTH_TANGENT, SMOOTH_ARC, SMOOTH_BIARC, SMOOTH_QUAD, SMOOTH_NORMAL, SMOOTH_CURVATURE, + fillet_polyline_from_curve, fillet_nurbs_curve + ) + +class SvFilletCurveNode(SverchCustomTreeNode, bpy.types.Node): + """ + Triggers: Fillet Curve Bevel + Tooltip: Smooth a NURBS curve (or polyline) by adding fillets or bevels in it's fracture points. + """ + bl_idname = 'SvFilletCurveNode' + bl_label = 'Fillet Curve' + bl_icon = 'OUTLINER_OB_EMPTY' + sv_icon = 'SV_FILLET_POLYLINE' + + radius : FloatProperty( + name = "Radius", + min = 0.0, + default = 0.2, + update = updateNode) + + cut_offset : FloatProperty( + name = "Cut Offset", + default = 0.05, + min = 0.0, + update = updateNode) + + bulge_factor : FloatProperty( + name = "Bulge Factor", + default = 0.5, + min = 0.0, + max = 1.0, + update = updateNode) + + concat : BoolProperty( + name = "Concatenate", + default = True, + update = updateNode) + + scale_to_unit : BoolProperty( + name = "Even domains", + description = "Give each segment and each arc equal T parameter domain of [0; 1]", + default = False, + update = updateNode) + + def get_smooth_modes(self, context): + items = [] + items.append((SMOOTH_POSITION, "0 - Position", "Connect segments with straight line segment", 0)) + items.append((SMOOTH_TANGENT, "1 - Tangency", "Connect segments such that their tangents are smoothly joined", 1)) + if not self.is_polyline: + items.append((SMOOTH_BIARC, "1 - Bi Arc", "Connect segments with Bi Arc, such that tangents are smoothly joined", 2)) + items.append((SMOOTH_NORMAL, "2 - Normals", "Connect segments such that their normals (second derivatives) are smoothly joined", 3)) + items.append((SMOOTH_CURVATURE, "3 - Curvature", "Connect segments such that their curvatures (third derivatives) are smoothly joined", 4)) + else: + items.append((SMOOTH_ARC, "1 - Circular Arc", "Connect segments with circular arcs", 5)) + return items + + def update_sockets(self, context): + self.inputs['Radius'].hide_safe = not self.is_polyline + self.inputs['CutOffset'].hide_safe = self.is_polyline + self.inputs['BulgeFactor'].hide_safe = self.is_polyline or self.smooth_mode != SMOOTH_TANGENT + self.outputs['Centers'].hide_safe = not self.is_polyline + updateNode(self, context) + + smooth_mode : EnumProperty( + name = "Continuity", + description = "How smooth should be the curve at points where original curve is replaced with fillet arcs; bigger value give more smooth touching", + items = get_smooth_modes, + update = update_sockets) + + is_polyline : BoolProperty( + name = "Polylines", + description = "If checked, the node will work with polylines only, but `Circular Arc' option will be available", + default = False, + update = update_sockets) + + def draw_buttons(self, context, layout): + layout.prop(self, 'is_polyline') + layout.label(text="Continuity") + layout.prop(self, 'smooth_mode', text='') + layout.prop(self, "concat") + if self.concat: + layout.prop(self, "scale_to_unit") + + def sv_init(self, context): + self.inputs.new('SvCurveSocket', "Curve") + self.inputs.new('SvStringsSocket', "Radius").prop_name = 'radius' + self.inputs.new('SvStringsSocket', "CutOffset").prop_name = 'cut_offset' + self.inputs.new('SvStringsSocket', "BulgeFactor").prop_name = 'bulge_factor' + self.outputs.new('SvCurveSocket', "Curve") + self.outputs.new('SvMatrixSocket', "Centers") + self.update_sockets(context) + + def process(self): + if not any(socket.is_linked for socket in self.outputs): + return + + curves_s = self.inputs['Curve'].sv_get() + radius_s = self.inputs['Radius'].sv_get() + cut_offset_s = self.inputs['CutOffset'].sv_get() + bulge_factor_s = self.inputs['BulgeFactor'].sv_get() + + input_level = get_data_nesting_level(curves_s, data_types=(SvCurve,)) + nested_output = input_level > 1 + + curves_s = ensure_nesting_level(curves_s, 2, data_types=(SvCurve,)) + if self.is_polyline: + radius_s = ensure_nesting_level(radius_s, 3) + else: + radius_s = ensure_nesting_level(radius_s, 2) + cut_offset_s = ensure_nesting_level(cut_offset_s, 2) + bulge_factor_s = ensure_nesting_level(bulge_factor_s, 2) + + curves_out = [] + centers_out = [] + for params in zip_long_repeat(curves_s, radius_s, cut_offset_s, bulge_factor_s): + new_curves = [] + new_centers = [] + for curve, radiuses, cut_offset, bulge_factor in zip_long_repeat(*params): + curve = SvNurbsCurve.to_nurbs(curve) + if curve is None: + raise Exception("One of curves is not a NURBS") + if self.is_polyline: + curve, centers = fillet_polyline_from_curve(curve, radiuses, + smooth = self.smooth_mode, + concat = self.concat, + scale_to_unit = self.scale_to_unit) + new_centers.append(centers) + new_curves.append(curve) + else: + curve = fillet_nurbs_curve(curve, + smooth = self.smooth_mode, + cut_offset = cut_offset, + bulge_factor = bulge_factor) + new_curves.append(curve) + if nested_output: + curves_out.append(new_curves) + centers_out.append(new_centers) + else: + curves_out.extend(new_curves) + centers_out.extend(new_centers) + + self.outputs['Curve'].sv_set(curves_out) + self.outputs['Centers'].sv_set(centers_out) + +def register(): + bpy.utils.register_class(SvFilletCurveNode) + +def unregister(): + bpy.utils.unregister_class(SvFilletCurveNode) + diff --git a/utils/curve/fillet.py b/utils/curve/fillet.py index d5812d57dc..72cff781b4 100644 --- a/utils/curve/fillet.py +++ b/utils/curve/fillet.py @@ -25,6 +25,7 @@ SMOOTH_TANGENT = '1' SMOOTH_BIARC = '1b' SMOOTH_ARC = '1a' +SMOOTH_QUAD = '1q' SMOOTH_NORMAL = '2' SMOOTH_CURVATURE = '3' @@ -39,8 +40,8 @@ def calc_single_fillet(smooth, curve1, curve2, bulge_factor = 0.5, biarc_paramet if smooth == SMOOTH_POSITION: return SvLine.from_two_points(curve1_end, curve2_begin) elif smooth == SMOOTH_TANGENT: - #tangent1 = tangent1_end / np.linalg.norm(tangent1_end) - #tangent2 = tangent2_begin / np.linalg.norm(tangent2_begin) + tangent1 = tangent1_end / np.linalg.norm(tangent1_end) + tangent2 = tangent2_begin / np.linalg.norm(tangent2_begin) tangent1 = bulge_factor * tangent1_end tangent2 = bulge_factor * tangent2_begin return SvCubicBezierCurve( @@ -239,11 +240,13 @@ def fillet_polyline_from_curve(curve, radiuses, if not curve.is_polyline(): raise Exception("Curve is not a polyline") - vertices = curve.get_polyline_vertices() + vertices = curve.get_polyline_vertices().tolist() cyclic = curve.is_closed() if isinstance(radiuses, (int,float)): radiuses = [radiuses] + if cyclic: + vertices = vertices[:-1] n = len(vertices) radiuses = repeat_last_for_length(radiuses, n) @@ -253,6 +256,8 @@ def fillet_polyline_from_curve(curve, radiuses, arc_mode = FILLET_BEZIER elif smooth == SMOOTH_ARC: arc_mode = FILLET_ARC + elif smooth == SMOOTH_QUAD: + arc_mode = FILLET_BEZIER else: raise Exception(f"Unsupported smooth level: {smooth}") From 60e5e65107f76f0924f73568e163f09fa6357fe3 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 4 Dec 2022 22:19:05 +0500 Subject: [PATCH 03/12] Allow for NURBS curves concatenation in case of different weights. --- utils/curve/nurbs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/curve/nurbs.py b/utils/curve/nurbs.py index 3ec1a5724c..f0f3f5a5d7 100644 --- a/utils/curve/nurbs.py +++ b/utils/curve/nurbs.py @@ -128,7 +128,9 @@ def concatenate(self, curve2, tolerance=1e-6, remove_knots=False): w1 = curve1.get_weights()[-1] w2 = curve2.get_weights()[0] if abs(w1 - w2) > tolerance: - raise UnsupportedCurveTypeException(f"Weights at endpoints do not match: {w1} != {w2}") + coef = w1 / w2 + curve2 = curve2.copy(weights = curve2.get_weights() * coef) + #raise UnsupportedCurveTypeException(f"Weights at endpoints do not match: {w1} != {w2}") p1, p2 = curve1.get_degree(), curve2.get_degree() if p1 > p2: From 55fe722e1889c051f7ed76a1ab0916cdf0cbc487 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Mon, 5 Dec 2022 00:48:48 +0500 Subject: [PATCH 04/12] Workaround for closed curves. --- utils/curve/fillet.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/utils/curve/fillet.py b/utils/curve/fillet.py index 72cff781b4..27e7cd4ff0 100644 --- a/utils/curve/fillet.py +++ b/utils/curve/fillet.py @@ -79,7 +79,10 @@ def cut_ends(curve, cut_offset, cut_start=True, cut_end=True): p1, p2 = curve.get_end_points() l = np.linalg.norm(p1 - p2) dt = u_max - u_min - k = dt / l + if l < 1e-6: + k = 1.0 + else: + k = dt / l if cut_start: u1 = u_min + cut_offset * k else: @@ -88,6 +91,7 @@ def cut_ends(curve, cut_offset, cut_start=True, cut_end=True): u2 = u_max - cut_offset * k else: u2 = u_max + #print(f"cut: {u_min} - {u_max} * cut_offset => {u1} - {u2}") return curve.cut_segment(u1, u2) def limit_filet_radiuses(vertices, radiuses, cyclic=False): From f5870ef46d02b5521e6fdd40dd355dc4a435c5ee Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Thu, 8 Dec 2022 22:15:46 +0500 Subject: [PATCH 05/12] Extend API. --- utils/curve/core.py | 13 +++++++++++++ utils/curve/nurbs.py | 25 ++++++++++++++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/utils/curve/core.py b/utils/curve/core.py index 20a2627ad6..bd40598728 100644 --- a/utils/curve/core.py +++ b/utils/curve/core.py @@ -214,6 +214,19 @@ def derivatives_array(self, n, ts, tangent_delta=None): return result + def nth_derivative(self, order, t, tangent_delta=None): + h = self.get_tangent_delta(tangent_delta) + if order == 0: + return self.evaluate(t) + elif order == 1: + return self.tangent(t, tangent_delta=tangent_delta) + elif order == 2: + return self.second_derivative(t, tangent_delta=tangent_delta) + elif order == 3: + return self.third_derivative(t, tangent_delta=tangent_delta) + else: + raise Exception(f"Unsupported derivative order: {order}") + def main_normal(self, t, normalize=True, tangent_delta=None): h = self.get_tangent_delta(tangent_delta) diff --git a/utils/curve/nurbs.py b/utils/curve/nurbs.py index f0f3f5a5d7..aa4b519daf 100644 --- a/utils/curve/nurbs.py +++ b/utils/curve/nurbs.py @@ -851,13 +851,24 @@ def get_polyline_vertices(self): points.append(segments[-1].get_end_points()[1]) return np.array(points) - def split_at_fracture_points(self, smooth=1, tangent_tolerance = 1e-6): + def split_at_fracture_points(self, order=1, direction_only = True, tangent_tolerance = 1e-6): + + if order not in {1,2,3}: + raise Exception(f"Unsupported discontinuity order: {order}") def is_fracture(segment1, segment2): - tangent1 = segment1.get_end_tangent() - tangent2 = segment2.get_start_tangent() - tangent1 = tangent1 / np.linalg.norm(tangent1) - tangent2 = tangent2 / np.linalg.norm(tangent2) + if order == 1: + tangent1 = segment1.get_end_tangent() + tangent2 = segment2.get_start_tangent() + else: + u1_max = segment1.get_u_bounds()[1] + u2_min = segment2.get_u_bounds()[0] + tangent1 = segment1.nth_derivative(order, u1_max) + tangent2 = segment2.nth_derivative(order, u2_min) + + if direction_only: + tangent1 = tangent1 / np.linalg.norm(tangent1) + tangent2 = tangent2 / np.linalg.norm(tangent2) delta = np.linalg.norm(tangent1 - tangent2) return delta >= tangent_tolerance @@ -876,8 +887,8 @@ def concatenate_non_fractured(segments): kv = self.get_knotvector() p = self.get_degree() - ms = sv_knotvector.to_multiplicity(kv) - possible_fracture_ts = [t for t, s in ms if s == p] + ms = sv_knotvector.to_multiplicity(kv)[1:-1] + possible_fracture_ts = [t for t, s in ms if s >= p-order+1] segments = self.split_at_ts(possible_fracture_ts) segments = concatenate_non_fractured(segments) return segments From c3a8d818c984906d5fd1c0f3b07384c09dd1b534 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 10 Dec 2022 19:45:50 +0500 Subject: [PATCH 06/12] Add keep_enum_reference. --- nodes/curve/fillet_curve.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nodes/curve/fillet_curve.py b/nodes/curve/fillet_curve.py index 0a01e1e2e2..dde0c0fc11 100644 --- a/nodes/curve/fillet_curve.py +++ b/nodes/curve/fillet_curve.py @@ -19,6 +19,7 @@ SMOOTH_POSITION, SMOOTH_TANGENT, SMOOTH_ARC, SMOOTH_BIARC, SMOOTH_QUAD, SMOOTH_NORMAL, SMOOTH_CURVATURE, fillet_polyline_from_curve, fillet_nurbs_curve ) +from sverchok.utils.handle_python_data import keep_enum_reference class SvFilletCurveNode(SverchCustomTreeNode, bpy.types.Node): """ @@ -60,6 +61,7 @@ class SvFilletCurveNode(SverchCustomTreeNode, bpy.types.Node): default = False, update = updateNode) + @keep_enum_reference def get_smooth_modes(self, context): items = [] items.append((SMOOTH_POSITION, "0 - Position", "Connect segments with straight line segment", 0)) From a8836f5b32006b6c536f83753e72187b09fc434e Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 10 Dec 2022 20:19:36 +0500 Subject: [PATCH 07/12] Add documentation. --- docs/nodes/curve/fillet_curve.rst | 135 ++++++++++++++++++++++++++++++ nodes/curve/fillet_curve.py | 2 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 docs/nodes/curve/fillet_curve.rst diff --git a/docs/nodes/curve/fillet_curve.rst b/docs/nodes/curve/fillet_curve.rst new file mode 100644 index 0000000000..57f6f913b9 --- /dev/null +++ b/docs/nodes/curve/fillet_curve.rst @@ -0,0 +1,135 @@ +Fillet Curve +============ + +Functionality +------------- + +This node takes a NURBS (or NURBS-like) Curve object, finds it's "fracture" +points (i.e. points where tangent of the curve does not change continuously), +and makes a smooth fillet in such points. + +For polyline curves, it is possible to make a fillet made of circular arc, and +it is possible to make the arc of user-provided radius. +For other types of curves, it is not possible to automatically calculate +circular fillet based on radius. So, for other types of curves, this node makes +fillets by use of Bezier curves or biarcs. Points where original curve is glued +with the fillet curve are in such case specified in terms of curve's T +parameter space, instead of fillet radius. + +More specifically, what this node does is follows: +* If it is known that the curve is a polyline: replace all corners of the + polyline with a circular arc of a specified radius. +* For other types of NURBS curves, + + * Find fracture points of the curve. + * Split the curve into segments at these fracture points. + * Of each segment, cut a small (or not small) piece at each end, based on + **CutOffset** input and segment's T parameter span. For example, if + CutOffset is 0.05, and segment's T parameter span is 1.0 - 2.0, then this + will cut the segment at points 1.05 and 1.95, leaving only span of 1.05 - + 1.95. So at this step, there will be gaps between segments. + * Place a fillet curve (Bezier curve or BiArc) at each gap. + +This node will automatically detect if the input curve is closed, and, if +necessary, add a fillet at closing point. + +You can also want to take a look at **Blend Curves** node. + +Inputs +------ + +This node has the following inputs: + +* **Curve**. The curve to make fillets for. This input is mandatory. +* **Radius**. This input is only available when **Polylines only** parameter is + checked. This specifies the fillet radius. It is possible to provide a + separate fillet radius for each vertex of the polyline curve. The default + value is 0.2. +* **CutOffset**. This input is only available when **Polylines only** parameter + is not checked. This specifies the proportion of curve segment T parameter + span, which will be cut off of each segment in order to make a place for the + fillet curve. The default value is 0.05. +* **BulgeFactor**. This input is only available when **Polylines only** + parameter is not checked, and **Continuity** parameter is set to **1 - + Tangency**. This defines the strength with which the tangent vector of the + second curve at it's starting point will affect the generated blending curve. + The default value is 0.5. + +Parameters +---------- + +This node has the following parameters: + +* **Polylines only**. If this is checked, the curve will process only + polylines. If you feed it with any other curve, the node will fail. In this + mode, it is possible to make fillets in form of circular arc, and provide the + fillet radius. If this is not checked, the node will process any NURBS or + NURBS-like curve, but it will not be able to make circular arc fillets based + on fillet radius. +* **Continuity**. This defines the order of continuity of the resulting curve, + and the algorithm used to calculate the fillet curves. The available options are: + + * **0 - Position**. This will connect curve segments with a straight line + segment. So, effectively, this does a bevel instead of smooth fillet. + * **1 - Tangency**. The fillet curves are generated so that the tangent of + the curves are equal at points where fillet curves are joined with + original curve segments. The generated fillet curves are cubic Bezier + curves. + * **1 - BiArc**. This option is not available when **Polylines only** + parameter is checked. The fillet curves are generated as biarc_ curves, + i.e. pairs of circular arcs; they are generated so that the tanent + vectors of the segments are equal at their meeting points. Biarc parameter + will be set to 1.0. Note that this option works only when tangents of the + curve at points where it is replaced with fillet are coplanar. I.e., this + will work fine for planar curves, but may fail in other cases. + * **2 - Normals**. This option is not available when **Polylines only** + parameter is checked. The fillet curves are generated so that 1) tangent + vectors of the curves are equal at the meeting points; 2) second + derivatives of the curves are also equal at the meeting points. Thus, + normal and binormal vectors of the curves are equal at their meeting + points. The generated curves are Bezier curves of fifth order. + * **3 - Curvature**. This option is not available when **Polylines only** + parameter is checked. The fillet curves are generated so that 1) tangent + vectors of the curves are equal at the meeting points; 2) second and third + derivatives of the curves are also equal at the meeting points. Thus, + normal and binormal vectors of the curves, as well as curvatures of the + curves, are equal at their meeting points. The generated curves are Bezier + curves of order 7. + * **1 - Circular Arc**. This option is only available when **Polylines + only** parameter is not checked. Fillet curves are calculated as circular + arc of radiuses provided in the **Radius** input. + + The default value is **0 - Position**. + +* **Concatenate**. If checked, then the node will output all segments of + initial curve together with generated fillet curves, concatenated into one + curve. Otherwise, original curve segments and fillet curves will be output + as separate Curve objects. Checked by default. +* **Even domains**. If checked, give each segment a domain of length 1. This + parameter is only available if **Concatenate** parameter is checked. + Unchecked by default. + +.. _biarc: https://en.wikipedia.org/wiki/Biarc + +Outputs +------- + +This node has the following outputs: + +* **Curve**. Generated Curve objects. +* **Centers**. This output is only available when **Polylines only** parameter + is checked, and **Continuity** parameter is set to **1 - Circular Arc**. + Centers of circles used to make fillet arcs. These are matrices, since this + output provides not only centers, but also orientation of the circles. + +Examples of Usage +----------------- + +Make fillets on some curve: + +.. image:: https://user-images.githubusercontent.com/284644/205504044-bdaa43c8-f13f-4ff4-92f4-aca8100c989b.png + +Make circular arc fillets on a polyline: + +.. image:: https://user-images.githubusercontent.com/284644/205504045-aab871b9-c851-484c-a908-230cd463e060.png + diff --git a/nodes/curve/fillet_curve.py b/nodes/curve/fillet_curve.py index dde0c0fc11..e97d5827ac 100644 --- a/nodes/curve/fillet_curve.py +++ b/nodes/curve/fillet_curve.py @@ -88,7 +88,7 @@ def update_sockets(self, context): update = update_sockets) is_polyline : BoolProperty( - name = "Polylines", + name = "Polylines only", description = "If checked, the node will work with polylines only, but `Circular Arc' option will be available", default = False, update = update_sockets) From e7b83ffcd1df09d2f3b0799eba1e2e8211bea737 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 10 Dec 2022 20:21:31 +0500 Subject: [PATCH 08/12] Fix the list. --- docs/nodes/curve/fillet_curve.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/nodes/curve/fillet_curve.rst b/docs/nodes/curve/fillet_curve.rst index 57f6f913b9..e685a58cff 100644 --- a/docs/nodes/curve/fillet_curve.rst +++ b/docs/nodes/curve/fillet_curve.rst @@ -17,6 +17,7 @@ with the fillet curve are in such case specified in terms of curve's T parameter space, instead of fillet radius. More specifically, what this node does is follows: + * If it is known that the curve is a polyline: replace all corners of the polyline with a circular arc of a specified radius. * For other types of NURBS curves, From 0d95c23bbed467a3e0b37bc78a9062c6a3c27fc9 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 10 Dec 2022 21:04:37 +0500 Subject: [PATCH 09/12] Add radius output. --- nodes/curve/fillet_curve.py | 15 +++++++++++---- utils/curve/bezier.py | 4 ++-- utils/curve/fillet.py | 20 ++++++++------------ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/nodes/curve/fillet_curve.py b/nodes/curve/fillet_curve.py index e97d5827ac..ffdb3b5076 100644 --- a/nodes/curve/fillet_curve.py +++ b/nodes/curve/fillet_curve.py @@ -19,7 +19,7 @@ SMOOTH_POSITION, SMOOTH_TANGENT, SMOOTH_ARC, SMOOTH_BIARC, SMOOTH_QUAD, SMOOTH_NORMAL, SMOOTH_CURVATURE, fillet_polyline_from_curve, fillet_nurbs_curve ) -from sverchok.utils.handle_python_data import keep_enum_reference +from sverchok.utils.handle_blender_data import keep_enum_reference class SvFilletCurveNode(SverchCustomTreeNode, bpy.types.Node): """ @@ -47,7 +47,6 @@ class SvFilletCurveNode(SverchCustomTreeNode, bpy.types.Node): name = "Bulge Factor", default = 0.5, min = 0.0, - max = 1.0, update = updateNode) concat : BoolProperty( @@ -78,7 +77,8 @@ def update_sockets(self, context): self.inputs['Radius'].hide_safe = not self.is_polyline self.inputs['CutOffset'].hide_safe = self.is_polyline self.inputs['BulgeFactor'].hide_safe = self.is_polyline or self.smooth_mode != SMOOTH_TANGENT - self.outputs['Centers'].hide_safe = not self.is_polyline + self.outputs['Centers'].hide_safe = not (self.is_polyline and self.smooth_mode == SMOOTH_ARC) + self.outputs['Radius'].hide_safe = not (self.is_polyline and self.smooth_mode == SMOOTH_ARC) updateNode(self, context) smooth_mode : EnumProperty( @@ -108,6 +108,7 @@ def sv_init(self, context): self.inputs.new('SvStringsSocket', "BulgeFactor").prop_name = 'bulge_factor' self.outputs.new('SvCurveSocket', "Curve") self.outputs.new('SvMatrixSocket', "Centers") + self.outputs.new('SvStringsSocket', "Radius") self.update_sockets(context) def process(self): @@ -132,20 +133,23 @@ def process(self): curves_out = [] centers_out = [] + radius_out = [] for params in zip_long_repeat(curves_s, radius_s, cut_offset_s, bulge_factor_s): new_curves = [] new_centers = [] + new_radiuses = [] for curve, radiuses, cut_offset, bulge_factor in zip_long_repeat(*params): curve = SvNurbsCurve.to_nurbs(curve) if curve is None: raise Exception("One of curves is not a NURBS") if self.is_polyline: - curve, centers = fillet_polyline_from_curve(curve, radiuses, + curve, centers, radiuses = fillet_polyline_from_curve(curve, radiuses, smooth = self.smooth_mode, concat = self.concat, scale_to_unit = self.scale_to_unit) new_centers.append(centers) new_curves.append(curve) + new_radiuses.append(radiuses) else: curve = fillet_nurbs_curve(curve, smooth = self.smooth_mode, @@ -155,12 +159,15 @@ def process(self): if nested_output: curves_out.append(new_curves) centers_out.append(new_centers) + radius_out.append(new_radiuses) else: curves_out.extend(new_curves) centers_out.extend(new_centers) + radius_out.extend(new_radiuses) self.outputs['Curve'].sv_set(curves_out) self.outputs['Centers'].sv_set(centers_out) + self.outputs['Radius'].sv_set(radius_out) def register(): bpy.utils.register_class(SvFilletCurveNode) diff --git a/utils/curve/bezier.py b/utils/curve/bezier.py index d142a50f5a..37d84c1713 100644 --- a/utils/curve/bezier.py +++ b/utils/curve/bezier.py @@ -112,8 +112,8 @@ def blend_second_derivatives(cls, p0, v0, a0, p5, v5, a5): """ p1 = p0 + v0 / 5.0 p4 = p5 - v5 / 5.0 - p2 = a0/20.0 + 2*p1 - p0 - p3 = a5/20.0 + 2*p4 - p5 + p2 = p0 + 0.4*v0 + a0/20.0 + p3 = p5 - 0.4*v5 + a5/20.0 return SvBezierCurve([p0, p1, p2, p3, p4, p5]) @classmethod diff --git a/utils/curve/fillet.py b/utils/curve/fillet.py index 27e7cd4ff0..8d59773455 100644 --- a/utils/curve/fillet.py +++ b/utils/curve/fillet.py @@ -40,10 +40,10 @@ def calc_single_fillet(smooth, curve1, curve2, bulge_factor = 0.5, biarc_paramet if smooth == SMOOTH_POSITION: return SvLine.from_two_points(curve1_end, curve2_begin) elif smooth == SMOOTH_TANGENT: - tangent1 = tangent1_end / np.linalg.norm(tangent1_end) - tangent2 = tangent2_begin / np.linalg.norm(tangent2_begin) - tangent1 = bulge_factor * tangent1_end - tangent2 = bulge_factor * tangent2_begin + tangent1 = tangent1_end # / np.linalg.norm(tangent1_end) + tangent2 = tangent2_begin # / np.linalg.norm(tangent2_begin) + tangent1 = bulge_factor * tangent1 + tangent2 = bulge_factor * tangent2 return SvCubicBezierCurve( curve1_end, curve1_end + tangent1 / 3.0, @@ -79,16 +79,12 @@ def cut_ends(curve, cut_offset, cut_start=True, cut_end=True): p1, p2 = curve.get_end_points() l = np.linalg.norm(p1 - p2) dt = u_max - u_min - if l < 1e-6: - k = 1.0 - else: - k = dt / l if cut_start: - u1 = u_min + cut_offset * k + u1 = u_min + cut_offset*dt else: u1 = u_min if cut_end: - u2 = u_max - cut_offset * k + u2 = u_max - cut_offset*dt else: u2 = u_max #print(f"cut: {u_min} - {u_max} * cut_offset => {u1} - {u2}") @@ -231,9 +227,9 @@ def fillet_polyline_from_vertices(vertices, radiuses, curves = [curve.to_nurbs() for curve in curves] if concat: concat = concatenate_curves(curves, scale_to_unit = scale_to_unit) - return concat, centers + return concat, centers, radiuses else: - return curves, centers + return curves, centers, radiuses def fillet_polyline_from_curve(curve, radiuses, smooth = SMOOTH_ARC, From 9ce6f0a634d93cb7589e72b45c65b1373885faaa Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sat, 10 Dec 2022 23:18:50 +0500 Subject: [PATCH 10/12] Some tuning. --- tests/bezier_tests.py | 27 ++++++++++++++++++++++++--- utils/curve/fillet.py | 39 +++++++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/tests/bezier_tests.py b/tests/bezier_tests.py index 018dc07279..087bbfaeca 100644 --- a/tests/bezier_tests.py +++ b/tests/bezier_tests.py @@ -87,11 +87,32 @@ def test_blend_second(self): a0r = curve.second_derivative(0) a1r = curve.second_derivative(1) - self.assert_numpy_arrays_equal(v0r, v0) - self.assert_numpy_arrays_equal(v1r, v1) - self.assert_numpy_arrays_equal(a0r, a0) + self.assert_numpy_arrays_equal(v0r, v0, precision=6) + self.assert_numpy_arrays_equal(v1r, v1, precision=6) + self.assert_numpy_arrays_equal(a0r, a0, precision=6) self.assert_numpy_arrays_equal(a1r, a1, precision=8) + def test_blend_second_2(self): + p0 = np.array([-0.12, -1.75, 0]) + v0 = np.array([0.69, -0.05, 0]) + a0 = np.array([-2.75, 42.64, 0]) + p1 = np.array([0.57, -1.5, 0]) + v1 = np.array([-0.31, 0.07, 0]) + a1 = np.array([0.15, 1.05, 0]) + + curve = SvBezierCurve.blend_second_derivatives(p0, v0, a0, p1, v1, a1) + + v0r = curve.tangent(0) + v1r = curve.tangent(1) + + a0r = curve.second_derivative(0) + a1r = curve.second_derivative(1) + + self.assert_numpy_arrays_equal(v0r, v0, precision=6) + self.assert_numpy_arrays_equal(v1r, v1, precision=6) + self.assert_numpy_arrays_equal(a0r, a0, precision=6) + self.assert_numpy_arrays_equal(a1r, a1, precision=6) + def test_blend_third(self): p0 = np.array([0, 0, 0]) p1 = np.array([3, 0, 0]) diff --git a/utils/curve/fillet.py b/utils/curve/fillet.py index 8d59773455..8bda8a8ee5 100644 --- a/utils/curve/fillet.py +++ b/utils/curve/fillet.py @@ -29,13 +29,14 @@ SMOOTH_NORMAL = '2' SMOOTH_CURVATURE = '3' -def calc_single_fillet(smooth, curve1, curve2, bulge_factor = 0.5, biarc_parameter = 1.0, planar_tolerance = 1e-6): +def calc_single_fillet(smooth, curve1, curve2, t_span, bulge_factor = 0.5, biarc_parameter = 1.0, planar_tolerance = 1e-6): + #t_span = 1.0 u1_max = curve1.get_u_bounds()[1] u2_min = curve2.get_u_bounds()[0] curve1_end = curve1.evaluate(u1_max) curve2_begin = curve2.evaluate(u2_min) - tangent1_end = curve1.get_end_tangent() - tangent2_begin = curve2.get_start_tangent() + tangent1_end = t_span * curve1.get_end_tangent() + tangent2_begin = t_span * curve2.get_start_tangent() if smooth == SMOOTH_POSITION: return SvLine.from_two_points(curve1_end, curve2_begin) @@ -57,16 +58,19 @@ def calc_single_fillet(smooth, curve1, curve2, bulge_factor = 0.5, biarc_paramet biarc_parameter, planar_tolerance = planar_tolerance) elif smooth == SMOOTH_NORMAL: - second_1_end = curve1.second_derivative(u1_max) - second_2_begin = curve2.second_derivative(u2_min) + second_1_end = t_span**2 * curve1.second_derivative(u1_max) + second_2_begin = t_span**2 * curve2.second_derivative(u2_min) + #print(f"T: {t_span**2}") + #print(f"E: {curve1_end}, {tangent1_end}, {second_1_end}") + #print(f"B: {curve2_begin}, {tangent2_begin}, {second_2_begin}") return SvBezierCurve.blend_second_derivatives( curve1_end, tangent1_end, second_1_end, curve2_begin, tangent2_begin, second_2_begin) elif smooth == SMOOTH_CURVATURE: - second_1_end = curve1.second_derivative(u1_max) - second_2_begin = curve2.second_derivative(u2_min) - third_1_end = curve1.third_derivative_array(np.array([u1_max]))[0] - third_2_begin = curve2.third_derivative_array(np.array([u2_min]))[0] + second_1_end = t_span**2 * curve1.second_derivative(u1_max) + second_2_begin = t_span**2 * curve2.second_derivative(u2_min) + third_1_end = t_span**3 * curve1.third_derivative_array(np.array([u1_max]))[0] + third_2_begin = t_span**3 * curve2.third_derivative_array(np.array([u2_min]))[0] return SvBezierCurve.blend_third_derivatives( curve1_end, tangent1_end, second_1_end, third_1_end, @@ -78,17 +82,17 @@ def cut_ends(curve, cut_offset, cut_start=True, cut_end=True): u_min, u_max = curve.get_u_bounds() p1, p2 = curve.get_end_points() l = np.linalg.norm(p1 - p2) - dt = u_max - u_min + dt = cut_offset * (u_max - u_min) if cut_start: - u1 = u_min + cut_offset*dt + u1 = u_min + dt else: u1 = u_min if cut_end: - u2 = u_max - cut_offset*dt + u2 = u_max - dt else: u2 = u_max #print(f"cut: {u_min} - {u_max} * cut_offset => {u1} - {u2}") - return curve.cut_segment(u1, u2) + return dt, curve.cut_segment(u1, u2) def limit_filet_radiuses(vertices, radiuses, cyclic=False): factor = 0.999 @@ -145,11 +149,14 @@ def fillet_nurbs_curve(curve, smooth, cut_offset, cyclic = curve.is_closed() segments = curve.split_at_fracture_points(tangent_tolerance = tangent_tolerance) n = len(segments) - segments = [cut_ends(s, cut_offset, cut_start = (i > 0 or cyclic), cut_end = (i < n-1 or cyclic)) for i, s in enumerate(segments)] - fillets = [calc_single_fillet(smooth, s1, s2, bulge_factor, biarc_parameter, planar_tolerance) for s1, s2 in zip(segments, segments[1:])] + cuts = [cut_ends(s, cut_offset, cut_start = (i > 0 or cyclic), cut_end = (i < n-1 or cyclic)) for i, s in enumerate(segments)] + fillets = [calc_single_fillet(smooth, s1, s2, dt1+dt2, bulge_factor, biarc_parameter, planar_tolerance) for (dt1,s1), (dt2,s2) in zip(cuts, cuts[1:])] if cyclic: - fillet = calc_single_fillet(smooth, segments[-1], segments[0], bulge_factor, biarc_parameter, planar_tolerance) + dt1, s1 = cuts[-1] + dt2, s2 = cuts[0] + fillet = calc_single_fillet(smooth, s1, s2, dt1+dt2, bulge_factor, biarc_parameter, planar_tolerance) fillets.append(fillet) + segments = [cut[1] for cut in cuts] new_segments = [[segment, fillet] for segment, fillet in zip(segments, fillets)] if not cyclic: new_segments.append([segments[-1]]) From f61ac771b060d5bcdaa330343c075fa539ef4a71 Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 11 Dec 2022 11:40:56 +0500 Subject: [PATCH 11/12] Hide "normals" and "curvature" options as I do not understand how to use them correctly :) --- docs/nodes/curve/fillet_curve.rst | 13 ------------- nodes/curve/fillet_curve.py | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/docs/nodes/curve/fillet_curve.rst b/docs/nodes/curve/fillet_curve.rst index e685a58cff..823b561b03 100644 --- a/docs/nodes/curve/fillet_curve.rst +++ b/docs/nodes/curve/fillet_curve.rst @@ -83,19 +83,6 @@ This node has the following parameters: will be set to 1.0. Note that this option works only when tangents of the curve at points where it is replaced with fillet are coplanar. I.e., this will work fine for planar curves, but may fail in other cases. - * **2 - Normals**. This option is not available when **Polylines only** - parameter is checked. The fillet curves are generated so that 1) tangent - vectors of the curves are equal at the meeting points; 2) second - derivatives of the curves are also equal at the meeting points. Thus, - normal and binormal vectors of the curves are equal at their meeting - points. The generated curves are Bezier curves of fifth order. - * **3 - Curvature**. This option is not available when **Polylines only** - parameter is checked. The fillet curves are generated so that 1) tangent - vectors of the curves are equal at the meeting points; 2) second and third - derivatives of the curves are also equal at the meeting points. Thus, - normal and binormal vectors of the curves, as well as curvatures of the - curves, are equal at their meeting points. The generated curves are Bezier - curves of order 7. * **1 - Circular Arc**. This option is only available when **Polylines only** parameter is not checked. Fillet curves are calculated as circular arc of radiuses provided in the **Radius** input. diff --git a/nodes/curve/fillet_curve.py b/nodes/curve/fillet_curve.py index ffdb3b5076..2bfc9242d0 100644 --- a/nodes/curve/fillet_curve.py +++ b/nodes/curve/fillet_curve.py @@ -67,8 +67,8 @@ def get_smooth_modes(self, context): items.append((SMOOTH_TANGENT, "1 - Tangency", "Connect segments such that their tangents are smoothly joined", 1)) if not self.is_polyline: items.append((SMOOTH_BIARC, "1 - Bi Arc", "Connect segments with Bi Arc, such that tangents are smoothly joined", 2)) - items.append((SMOOTH_NORMAL, "2 - Normals", "Connect segments such that their normals (second derivatives) are smoothly joined", 3)) - items.append((SMOOTH_CURVATURE, "3 - Curvature", "Connect segments such that their curvatures (third derivatives) are smoothly joined", 4)) + #items.append((SMOOTH_NORMAL, "2 - Normals", "Connect segments such that their normals (second derivatives) are smoothly joined", 3)) + #items.append((SMOOTH_CURVATURE, "3 - Curvature", "Connect segments such that their curvatures (third derivatives) are smoothly joined", 4)) else: items.append((SMOOTH_ARC, "1 - Circular Arc", "Connect segments with circular arcs", 5)) return items From 448f11476b544eb16674e9111adcbc490aebbfce Mon Sep 17 00:00:00 2001 From: Ilya Portnov Date: Sun, 11 Dec 2022 11:45:23 +0500 Subject: [PATCH 12/12] Hide "even domain" parameter to N panel. --- docs/nodes/curve/fillet_curve.rst | 6 +++--- nodes/curve/fillet_curve.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/nodes/curve/fillet_curve.rst b/docs/nodes/curve/fillet_curve.rst index 823b561b03..9da66693f9 100644 --- a/docs/nodes/curve/fillet_curve.rst +++ b/docs/nodes/curve/fillet_curve.rst @@ -93,9 +93,9 @@ This node has the following parameters: initial curve together with generated fillet curves, concatenated into one curve. Otherwise, original curve segments and fillet curves will be output as separate Curve objects. Checked by default. -* **Even domains**. If checked, give each segment a domain of length 1. This - parameter is only available if **Concatenate** parameter is checked. - Unchecked by default. +* **Even domains**. This parameter is available in the N panel only. If + checked, give each segment a domain of length 1. This parameter is only + available if **Concatenate** parameter is checked. Unchecked by default. .. _biarc: https://en.wikipedia.org/wiki/Biarc diff --git a/nodes/curve/fillet_curve.py b/nodes/curve/fillet_curve.py index 2bfc9242d0..32aba5c287 100644 --- a/nodes/curve/fillet_curve.py +++ b/nodes/curve/fillet_curve.py @@ -98,6 +98,9 @@ def draw_buttons(self, context, layout): layout.label(text="Continuity") layout.prop(self, 'smooth_mode', text='') layout.prop(self, "concat") + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) if self.concat: layout.prop(self, "scale_to_unit")