1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 'name': 'Curve CAD Tools',
7 'author': 'Alexander Meißner',
11 'doc_url': 'https://github.com/Lichtso/curve_cad',
12 'tracker_url': 'https://github.com/lichtso/curve_cad/issues'
16 from . import internal
19 class Fillet(bpy
.types
.Operator
):
20 bl_idname
= 'curvetools.bezier_cad_fillet'
21 bl_description
= bl_label
= 'Fillet'
22 bl_options
= {'REGISTER', 'UNDO'}
24 radius
: bpy
.props
.FloatProperty(name
='Radius', description
='Radius of the rounded corners', unit
='LENGTH', min=0.0, default
=0.1)
25 chamfer_mode
: bpy
.props
.BoolProperty(name
='Chamfer', description
='Cut off sharp without rounding', default
=False)
26 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)
29 def poll(cls
, context
):
30 return util
.Selected1OrMoreCurves()
32 def execute(self
, context
):
33 splines
= internal
.getSelectedSplines(True, True, True)
35 self
.report({'WARNING'}, 'Nothing selected')
37 for spline
in splines
:
38 internal
.filletSpline(spline
, self
.radius
, self
.chamfer_mode
, self
.limit_half_way
)
39 bpy
.context
.object.data
.splines
.remove(spline
)
42 class Boolean(bpy
.types
.Operator
):
43 bl_idname
= 'curvetools.bezier_cad_boolean'
44 bl_description
= bl_label
= 'Boolean'
45 bl_options
= {'REGISTER', 'UNDO'}
47 operation
: bpy
.props
.EnumProperty(name
='Type', items
=[
48 ('UNION', 'Union', 'Boolean OR', 0),
49 ('INTERSECTION', 'Intersection', 'Boolean AND', 1),
50 ('DIFFERENCE', 'Difference', 'Active minus Selected', 2)
54 def poll(cls
, context
):
55 return util
.Selected1Curve()
57 def execute(self
, context
):
58 current_mode
= bpy
.context
.object.mode
60 bpy
.ops
.object.mode_set(mode
= 'EDIT')
61 if bpy
.context
.object.data
.dimensions
!= '2D':
62 self
.report({'WARNING'}, 'Can only be applied in 2D')
64 splines
= internal
.getSelectedSplines(True, True)
66 self
.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
68 bpy
.ops
.curve
.spline_type_set(type='BEZIER')
69 splineA
= bpy
.context
.object.data
.splines
.active
70 splineB
= splines
[0] if (splines
[1] == splineA
) else splines
[1]
71 if not internal
.bezierBooleanGeometry(splineA
, splineB
, self
.operation
):
72 self
.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
75 bpy
.ops
.object.mode_set (mode
= current_mode
)
79 class Intersection(bpy
.types
.Operator
):
80 bl_idname
= 'curvetools.bezier_cad_intersection'
81 bl_description
= bl_label
= 'Intersection'
82 bl_options
= {'REGISTER', 'UNDO'}
85 def poll(cls
, context
):
86 return util
.Selected1OrMoreCurves()
88 def execute(self
, context
):
89 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
91 self
.report({'WARNING'}, 'Invalid selection')
94 internal
.bezierMultiIntersection(segments
)
97 class HandleProjection(bpy
.types
.Operator
):
98 bl_idname
= 'curvetools.bezier_cad_handle_projection'
99 bl_description
= bl_label
= 'Handle Projection'
100 bl_options
= {'REGISTER', 'UNDO'}
103 def poll(cls
, context
):
104 return util
.Selected1OrMoreCurves()
106 def execute(self
, context
):
107 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
108 if len(segments
) < 1:
109 self
.report({'WARNING'}, 'Nothing selected')
112 internal
.bezierProjectHandles(segments
)
115 class MergeEnds(bpy
.types
.Operator
):
116 bl_idname
= 'curvetools.bezier_cad_merge_ends'
117 bl_description
= bl_label
= 'Merge Ends'
118 bl_options
= {'REGISTER', 'UNDO'}
120 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)
123 def poll(cls
, context
):
124 return util
.Selected1OrMoreCurves()
126 def execute(self
, context
):
127 splines
= [spline
for spline
in internal
.getSelectedSplines(True, False) if spline
.use_cyclic_u
== False]
129 while len(splines
) > 0:
130 spline
= splines
.pop()
131 closest_pair
= ([spline
, spline
], [spline
.bezier_points
[0], spline
.bezier_points
[-1]], [False, True])
132 min_dist
= (spline
.bezier_points
[0].co
-spline
.bezier_points
[-1].co
).length
133 for other_spline
in splines
:
134 for j
in range(-1, 1):
135 for i
in range(-1, 1):
136 dist
= (spline
.bezier_points
[i
].co
-other_spline
.bezier_points
[j
].co
).length
139 closest_pair
= ([spline
, other_spline
], [spline
.bezier_points
[i
], other_spline
.bezier_points
[j
]], [i
== -1, j
== -1])
140 if min_dist
> self
.max_dist
:
142 if closest_pair
[0][0] != closest_pair
[0][1]:
143 splines
.remove(closest_pair
[0][1])
144 spline
= internal
.mergeEnds(closest_pair
[0], closest_pair
[1], closest_pair
[2])
145 if spline
.use_cyclic_u
== False:
146 splines
.append(spline
)
150 class Subdivide(bpy
.types
.Operator
):
151 bl_idname
= 'curvetools.bezier_cad_subdivide'
152 bl_description
= bl_label
= 'Subdivide'
153 bl_options
= {'REGISTER', 'UNDO'}
155 params
: bpy
.props
.StringProperty(name
='Params', default
='0.25 0.5 0.75')
158 def poll(cls
, context
):
159 return util
.Selected1OrMoreCurves()
161 def execute(self
, context
):
162 current_mode
= bpy
.context
.object.mode
164 bpy
.ops
.object.mode_set(mode
= 'EDIT')
166 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
167 if len(segments
) == 0:
168 self
.report({'WARNING'}, 'Nothing selected')
172 for param
in self
.params
.split(' '):
173 cuts
.append({'param': max(0.0, min(float(param
), 1.0))})
174 cuts
.sort(key
=(lambda cut
: cut
['param']))
175 for segment
in segments
:
176 segment
['cuts'].extend(cuts
)
177 internal
.subdivideBezierSegments(segments
)
179 bpy
.ops
.object.mode_set (mode
= current_mode
)
182 class Array(bpy
.types
.Operator
):
183 bl_idname
= 'curvetools.bezier_cad_array'
184 bl_description
= bl_label
= 'Array'
185 bl_options
= {'REGISTER', 'UNDO'}
187 offset
: bpy
.props
.FloatVectorProperty(name
='Offset', unit
='LENGTH', description
='Vector between to copies', subtype
='DIRECTION', default
=(0.0, 0.0, -1.0), size
=3)
188 count
: bpy
.props
.IntProperty(name
='Count', description
='Number of copies', min=1, default
=2)
189 connect
: bpy
.props
.BoolProperty(name
='Connect', description
='Concatenate individual copies', default
=False)
190 serpentine
: bpy
.props
.BoolProperty(name
='Serpentine', description
='Switch direction of every second copy', default
=False)
193 def poll(cls
, context
):
194 return util
.Selected1OrMoreCurves()
196 def execute(self
, context
):
197 splines
= internal
.getSelectedSplines(True, True)
198 if len(splines
) == 0:
199 self
.report({'WARNING'}, 'Nothing selected')
201 internal
.arrayModifier(splines
, self
.offset
, self
.count
, self
.connect
, self
.serpentine
)
204 class Circle(bpy
.types
.Operator
):
205 bl_idname
= 'curvetools.bezier_cad_circle'
206 bl_description
= bl_label
= 'Circle'
207 bl_options
= {'REGISTER', 'UNDO'}
210 def poll(cls
, context
):
211 return util
.Selected1OrMoreCurves()
213 def execute(self
, context
):
214 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
215 if len(segments
) != 1:
216 self
.report({'WARNING'}, 'Invalid selection')
219 segment
= internal
.bezierSegmentPoints(segments
[0]['beginPoint'], segments
[0]['endPoint'])
220 circle
= internal
.circleOfBezier(segment
)
222 self
.report({'WARNING'}, 'Not a circle')
224 bpy
.context
.scene
.cursor
.location
= circle
.center
225 bpy
.context
.scene
.cursor
.rotation_mode
= 'QUATERNION'
226 bpy
.context
.scene
.cursor
.rotation_quaternion
= circle
.orientation
.to_quaternion()
229 class Length(bpy
.types
.Operator
):
230 bl_idname
= 'curvetools.bezier_cad_length'
231 bl_description
= bl_label
= 'Length'
234 def poll(cls
, context
):
235 return util
.Selected1OrMoreCurves()
237 def execute(self
, context
):
238 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
239 if len(segments
) == 0:
240 self
.report({'WARNING'}, 'Nothing selected')
244 for segment
in segments
:
245 length
+= internal
.bezierLength(internal
.bezierSegmentPoints(segment
['beginPoint'], segment
['endPoint']))
246 self
.report({'INFO'}, bpy
.utils
.units
.to_string(bpy
.context
.scene
.unit_settings
.system
, 'LENGTH', length
))
251 bpy
.utils
.register_class(operators
)
255 bpy
.utils
.unregister_class(operators
)
257 if __name__
== "__main__":
260 operators
= [Fillet
, Boolean
, Intersection
, HandleProjection
, MergeEnds
, Subdivide
, Array
, Circle
, Length
]