diff --git a/docs/nodes/curve/fillet_curve.rst b/docs/nodes/curve/fillet_curve.rst new file mode 100644 index 0000000000..9da66693f9 --- /dev/null +++ b/docs/nodes/curve/fillet_curve.rst @@ -0,0 +1,123 @@ +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. + * **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**. 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 + +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/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..32aba5c287 --- /dev/null +++ b/nodes/curve/fillet_curve.py @@ -0,0 +1,180 @@ +# 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 + ) +from sverchok.utils.handle_blender_data import keep_enum_reference + +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, + 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) + + @keep_enum_reference + 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 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( + 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 only", + 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") + + def draw_buttons_ext(self, context, layout): + self.draw_buttons(context, layout) + 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.outputs.new('SvStringsSocket', "Radius") + 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 = [] + 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, 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, + 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) + 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) + +def unregister(): + bpy.utils.unregister_class(SvFilletCurveNode) + 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/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/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/core.py b/utils/curve/core.py index 348a9c42b6..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) @@ -443,6 +456,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..8bda8a8ee5 --- /dev/null +++ b/utils/curve/fillet.py @@ -0,0 +1,278 @@ +# 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_QUAD = '1q' +SMOOTH_NORMAL = '2' +SMOOTH_CURVATURE = '3' + +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 = 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) + elif smooth == SMOOTH_TANGENT: + 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, + 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 = 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 = 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, + 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 = cut_offset * (u_max - u_min) + if cut_start: + u1 = u_min + dt + else: + u1 = u_min + if cut_end: + u2 = u_max - dt + else: + u2 = u_max + #print(f"cut: {u_min} - {u_max} * cut_offset => {u1} - {u2}") + return dt, 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) + 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: + 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]]) + 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, radiuses + else: + return curves, centers, radiuses + +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().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) + + if smooth == SMOOTH_POSITION: + arc_mode = FILLET_BEVEL + elif smooth == SMOOTH_TANGENT: + 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}") + + 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..aa4b519daf 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: @@ -560,6 +562,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 +581,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 +838,61 @@ 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, 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): + 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 + + 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)[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 + 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)