1 # SPDX-License-Identifier: GPL-2.0-or-later
4 'name': 'Curve CAD Tools',
5 'author': 'Alexander Meißner',
9 'doc_url': 'https://github.com/Lichtso/curve_cad',
10 'tracker_url': 'https://github.com/lichtso/curve_cad/issues'
14 from . import internal
17 class Fillet(bpy
.types
.Operator
):
18 bl_idname
= 'curvetools.bezier_cad_fillet'
19 bl_description
= bl_label
= 'Fillet'
20 bl_options
= {'REGISTER', 'UNDO'}
22 radius
: bpy
.props
.FloatProperty(name
='Radius', description
='Radius of the rounded corners', unit
='LENGTH', min=0.0, default
=0.1)
23 chamfer_mode
: bpy
.props
.BoolProperty(name
='Chamfer', description
='Cut off sharp without rounding', default
=False)
24 limit_half_way
: bpy
.props
.BoolProperty(name
='Limit Half Way', description
='Limits the segments to half their length in order to prevent collisions', default
=False)
27 def poll(cls
, context
):
28 return util
.Selected1OrMoreCurves()
30 def execute(self
, context
):
31 splines
= internal
.getSelectedSplines(True, True, True)
33 self
.report({'WARNING'}, 'Nothing selected')
35 for spline
in splines
:
36 internal
.filletSpline(spline
, self
.radius
, self
.chamfer_mode
, self
.limit_half_way
)
37 bpy
.context
.object.data
.splines
.remove(spline
)
40 class Boolean(bpy
.types
.Operator
):
41 bl_idname
= 'curvetools.bezier_cad_boolean'
42 bl_description
= bl_label
= 'Boolean'
43 bl_options
= {'REGISTER', 'UNDO'}
45 operation
: bpy
.props
.EnumProperty(name
='Type', items
=[
46 ('UNION', 'Union', 'Boolean OR', 0),
47 ('INTERSECTION', 'Intersection', 'Boolean AND', 1),
48 ('DIFFERENCE', 'Difference', 'Active minus Selected', 2)
52 def poll(cls
, context
):
53 return util
.Selected1Curve()
55 def execute(self
, context
):
56 current_mode
= bpy
.context
.object.mode
58 bpy
.ops
.object.mode_set(mode
= 'EDIT')
59 if bpy
.context
.object.data
.dimensions
!= '2D':
60 self
.report({'WARNING'}, 'Can only be applied in 2D')
62 splines
= internal
.getSelectedSplines(True, True)
64 self
.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
66 bpy
.ops
.curve
.spline_type_set(type='BEZIER')
67 splineA
= bpy
.context
.object.data
.splines
.active
68 splineB
= splines
[0] if (splines
[1] == splineA
) else splines
[1]
69 if not internal
.bezierBooleanGeometry(splineA
, splineB
, self
.operation
):
70 self
.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
73 bpy
.ops
.object.mode_set (mode
= current_mode
)
77 class Intersection(bpy
.types
.Operator
):
78 bl_idname
= 'curvetools.bezier_cad_intersection'
79 bl_description
= bl_label
= 'Intersection'
80 bl_options
= {'REGISTER', 'UNDO'}
83 def poll(cls
, context
):
84 return util
.Selected1OrMoreCurves()
86 def execute(self
, context
):
87 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
89 self
.report({'WARNING'}, 'Invalid selection')
92 internal
.bezierMultiIntersection(segments
)
95 class HandleProjection(bpy
.types
.Operator
):
96 bl_idname
= 'curvetools.bezier_cad_handle_projection'
97 bl_description
= bl_label
= 'Handle Projection'
98 bl_options
= {'REGISTER', 'UNDO'}
101 def poll(cls
, context
):
102 return util
.Selected1OrMoreCurves()
104 def execute(self
, context
):
105 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
106 if len(segments
) < 1:
107 self
.report({'WARNING'}, 'Nothing selected')
110 internal
.bezierProjectHandles(segments
)
113 class MergeEnds(bpy
.types
.Operator
):
114 bl_idname
= 'curvetools.bezier_cad_merge_ends'
115 bl_description
= bl_label
= 'Merge Ends'
116 bl_options
= {'REGISTER', 'UNDO'}
118 max_dist
: bpy
.props
.FloatProperty(name
='Distance', description
='Threshold of the maximum distance at which two control points are merged', unit
='LENGTH', min=0.0, default
=0.1)
121 def poll(cls
, context
):
122 return util
.Selected1OrMoreCurves()
124 def execute(self
, context
):
125 splines
= [spline
for spline
in internal
.getSelectedSplines(True, False) if spline
.use_cyclic_u
== False]
127 while len(splines
) > 0:
128 spline
= splines
.pop()
129 closest_pair
= ([spline
, spline
], [spline
.bezier_points
[0], spline
.bezier_points
[-1]], [False, True])
130 min_dist
= (spline
.bezier_points
[0].co
-spline
.bezier_points
[-1].co
).length
131 for other_spline
in splines
:
132 for j
in range(-1, 1):
133 for i
in range(-1, 1):
134 dist
= (spline
.bezier_points
[i
].co
-other_spline
.bezier_points
[j
].co
).length
137 closest_pair
= ([spline
, other_spline
], [spline
.bezier_points
[i
], other_spline
.bezier_points
[j
]], [i
== -1, j
== -1])
138 if min_dist
> self
.max_dist
:
140 if closest_pair
[0][0] != closest_pair
[0][1]:
141 splines
.remove(closest_pair
[0][1])
142 spline
= internal
.mergeEnds(closest_pair
[0], closest_pair
[1], closest_pair
[2])
143 if spline
.use_cyclic_u
== False:
144 splines
.append(spline
)
148 class Subdivide(bpy
.types
.Operator
):
149 bl_idname
= 'curvetools.bezier_cad_subdivide'
150 bl_description
= bl_label
= 'Subdivide'
151 bl_options
= {'REGISTER', 'UNDO'}
153 params
: bpy
.props
.StringProperty(name
='Params', default
='0.25 0.5 0.75')
156 def poll(cls
, context
):
157 return util
.Selected1OrMoreCurves()
159 def execute(self
, context
):
160 current_mode
= bpy
.context
.object.mode
162 bpy
.ops
.object.mode_set(mode
= 'EDIT')
164 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
165 if len(segments
) == 0:
166 self
.report({'WARNING'}, 'Nothing selected')
170 for param
in self
.params
.split(' '):
171 cuts
.append({'param': max(0.0, min(float(param
), 1.0))})
172 cuts
.sort(key
=(lambda cut
: cut
['param']))
173 for segment
in segments
:
174 segment
['cuts'].extend(cuts
)
175 internal
.subdivideBezierSegments(segments
)
177 bpy
.ops
.object.mode_set (mode
= current_mode
)
180 class Array(bpy
.types
.Operator
):
181 bl_idname
= 'curvetools.bezier_cad_array'
182 bl_description
= bl_label
= 'Array'
183 bl_options
= {'REGISTER', 'UNDO'}
185 offset
: bpy
.props
.FloatVectorProperty(name
='Offset', unit
='LENGTH', description
='Vector between to copies', subtype
='DIRECTION', default
=(0.0, 0.0, -1.0), size
=3)
186 count
: bpy
.props
.IntProperty(name
='Count', description
='Number of copies', min=1, default
=2)
187 connect
: bpy
.props
.BoolProperty(name
='Connect', description
='Concatenate individual copies', default
=False)
188 serpentine
: bpy
.props
.BoolProperty(name
='Serpentine', description
='Switch direction of every second copy', default
=False)
191 def poll(cls
, context
):
192 return util
.Selected1OrMoreCurves()
194 def execute(self
, context
):
195 splines
= internal
.getSelectedSplines(True, True)
196 if len(splines
) == 0:
197 self
.report({'WARNING'}, 'Nothing selected')
199 internal
.arrayModifier(splines
, self
.offset
, self
.count
, self
.connect
, self
.serpentine
)
202 class Circle(bpy
.types
.Operator
):
203 bl_idname
= 'curvetools.bezier_cad_circle'
204 bl_description
= bl_label
= 'Circle'
205 bl_options
= {'REGISTER', 'UNDO'}
208 def poll(cls
, context
):
209 return util
.Selected1OrMoreCurves()
211 def execute(self
, context
):
212 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
213 if len(segments
) != 1:
214 self
.report({'WARNING'}, 'Invalid selection')
217 segment
= internal
.bezierSegmentPoints(segments
[0]['beginPoint'], segments
[0]['endPoint'])
218 circle
= internal
.circleOfBezier(segment
)
220 self
.report({'WARNING'}, 'Not a circle')
222 bpy
.context
.scene
.cursor
.location
= circle
.center
223 bpy
.context
.scene
.cursor
.rotation_mode
= 'QUATERNION'
224 bpy
.context
.scene
.cursor
.rotation_quaternion
= circle
.orientation
.to_quaternion()
227 class Length(bpy
.types
.Operator
):
228 bl_idname
= 'curvetools.bezier_cad_length'
229 bl_description
= bl_label
= 'Length'
232 def poll(cls
, context
):
233 return util
.Selected1OrMoreCurves()
235 def execute(self
, context
):
236 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
237 if len(segments
) == 0:
238 self
.report({'WARNING'}, 'Nothing selected')
242 for segment
in segments
:
243 length
+= internal
.bezierLength(internal
.bezierSegmentPoints(segment
['beginPoint'], segment
['endPoint']))
244 self
.report({'INFO'}, bpy
.utils
.units
.to_string(bpy
.context
.scene
.unit_settings
.system
, 'LENGTH', length
))
249 bpy
.utils
.register_class(operators
)
253 bpy
.utils
.unregister_class(operators
)
255 if __name__
== "__main__":
258 operators
= [Fillet
, Boolean
, Intersection
, HandleProjection
, MergeEnds
, Subdivide
, Array
, Circle
, Length
]