1 # ***** GPL LICENSE BLOCK *****
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # All rights reserved.
17 # ***** GPL LICENSE BLOCK *****
20 'name': 'Curve CAD Tools',
21 'author': 'Alexander Meißner',
23 'blender': (2, 80, 0),
25 'doc_url': 'https://github.com/Lichtso/curve_cad',
26 'tracker_url': 'https://github.com/lichtso/curve_cad/issues'
30 from . import internal
33 class Fillet(bpy
.types
.Operator
):
34 bl_idname
= 'curvetools.bezier_cad_fillet'
35 bl_description
= bl_label
= 'Fillet'
36 bl_options
= {'REGISTER', 'UNDO'}
38 radius
: bpy
.props
.FloatProperty(name
='Radius', description
='Radius of the rounded corners', unit
='LENGTH', min=0.0, default
=0.1)
39 chamfer_mode
: bpy
.props
.BoolProperty(name
='Chamfer', description
='Cut off sharp without rounding', default
=False)
40 limit_half_way
: bpy
.props
.BoolProperty(name
='Limit Half Way', description
='Limits the segements to half their length in order to prevent collisions', default
=False)
43 def poll(cls
, context
):
44 return util
.Selected1OrMoreCurves()
46 def execute(self
, context
):
47 splines
= internal
.getSelectedSplines(True, True, True)
49 self
.report({'WARNING'}, 'Nothing selected')
51 for spline
in splines
:
52 internal
.filletSpline(spline
, self
.radius
, self
.chamfer_mode
, self
.limit_half_way
)
53 bpy
.context
.object.data
.splines
.remove(spline
)
56 class Boolean(bpy
.types
.Operator
):
57 bl_idname
= 'curvetools.bezier_cad_boolean'
58 bl_description
= bl_label
= 'Boolean'
59 bl_options
= {'REGISTER', 'UNDO'}
61 operation
: bpy
.props
.EnumProperty(name
='Type', items
=[
62 ('UNION', 'Union', 'Boolean OR', 0),
63 ('INTERSECTION', 'Intersection', 'Boolean AND', 1),
64 ('DIFFERENCE', 'Difference', 'Active minus Selected', 2)
68 def poll(cls
, context
):
69 return util
.Selected1Curve()
71 def execute(self
, context
):
72 current_mode
= bpy
.context
.object.mode
74 bpy
.ops
.object.mode_set(mode
= 'EDIT')
75 if bpy
.context
.object.data
.dimensions
!= '2D':
76 self
.report({'WARNING'}, 'Can only be applied in 2D')
78 splines
= internal
.getSelectedSplines(True, True)
80 self
.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
82 bpy
.ops
.curve
.spline_type_set(type='BEZIER')
83 splineA
= bpy
.context
.object.data
.splines
.active
84 splineB
= splines
[0] if (splines
[1] == splineA
) else splines
[1]
85 if not internal
.bezierBooleanGeometry(splineA
, splineB
, self
.operation
):
86 self
.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
89 bpy
.ops
.object.mode_set (mode
= current_mode
)
93 class Intersection(bpy
.types
.Operator
):
94 bl_idname
= 'curvetools.bezier_cad_intersection'
95 bl_description
= bl_label
= 'Intersection'
96 bl_options
= {'REGISTER', 'UNDO'}
99 def poll(cls
, context
):
100 return util
.Selected1OrMoreCurves()
102 def execute(self
, context
):
103 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
104 if len(segments
) < 2:
105 self
.report({'WARNING'}, 'Invalid selection')
108 internal
.bezierMultiIntersection(segments
)
111 class HandleProjection(bpy
.types
.Operator
):
112 bl_idname
= 'curvetools.bezier_cad_handle_projection'
113 bl_description
= bl_label
= 'Handle Projection'
114 bl_options
= {'REGISTER', 'UNDO'}
117 def poll(cls
, context
):
118 return util
.Selected1OrMoreCurves()
120 def execute(self
, context
):
121 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
122 if len(segments
) < 1:
123 self
.report({'WARNING'}, 'Nothing selected')
126 internal
.bezierProjectHandles(segments
)
129 class MergeEnds(bpy
.types
.Operator
):
130 bl_idname
= 'curvetools.bezier_cad_merge_ends'
131 bl_description
= bl_label
= 'Merge Ends'
132 bl_options
= {'REGISTER', 'UNDO'}
134 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)
137 def poll(cls
, context
):
138 return util
.Selected1OrMoreCurves()
140 def execute(self
, context
):
141 splines
= [spline
for spline
in internal
.getSelectedSplines(True, False) if spline
.use_cyclic_u
== False]
143 while len(splines
) > 0:
144 spline
= splines
.pop()
145 closest_pair
= ([spline
, spline
], [spline
.bezier_points
[0], spline
.bezier_points
[-1]], [False, True])
146 min_dist
= (spline
.bezier_points
[0].co
-spline
.bezier_points
[-1].co
).length
147 for other_spline
in splines
:
148 for j
in range(-1, 1):
149 for i
in range(-1, 1):
150 dist
= (spline
.bezier_points
[i
].co
-other_spline
.bezier_points
[j
].co
).length
153 closest_pair
= ([spline
, other_spline
], [spline
.bezier_points
[i
], other_spline
.bezier_points
[j
]], [i
== -1, j
== -1])
154 if min_dist
> self
.max_dist
:
156 if closest_pair
[0][0] != closest_pair
[0][1]:
157 splines
.remove(closest_pair
[0][1])
158 spline
= internal
.mergeEnds(closest_pair
[0], closest_pair
[1], closest_pair
[2])
159 if spline
.use_cyclic_u
== False:
160 splines
.append(spline
)
164 class Subdivide(bpy
.types
.Operator
):
165 bl_idname
= 'curvetools.bezier_cad_subdivide'
166 bl_description
= bl_label
= 'Subdivide'
167 bl_options
= {'REGISTER', 'UNDO'}
169 params
: bpy
.props
.StringProperty(name
='Params', default
='0.25 0.5 0.75')
172 def poll(cls
, context
):
173 return util
.Selected1OrMoreCurves()
175 def execute(self
, context
):
176 current_mode
= bpy
.context
.object.mode
178 bpy
.ops
.object.mode_set(mode
= 'EDIT')
180 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
181 if len(segments
) == 0:
182 self
.report({'WARNING'}, 'Nothing selected')
186 for param
in self
.params
.split(' '):
187 cuts
.append({'param': max(0.0, min(float(param
), 1.0))})
188 cuts
.sort(key
=(lambda cut
: cut
['param']))
189 for segment
in segments
:
190 segment
['cuts'].extend(cuts
)
191 internal
.subdivideBezierSegments(segments
)
193 bpy
.ops
.object.mode_set (mode
= current_mode
)
196 class Array(bpy
.types
.Operator
):
197 bl_idname
= 'curvetools.bezier_cad_array'
198 bl_description
= bl_label
= 'Array'
199 bl_options
= {'REGISTER', 'UNDO'}
201 offset
: bpy
.props
.FloatVectorProperty(name
='Offset', unit
='LENGTH', description
='Vector between to copies', subtype
='DIRECTION', default
=(0.0, 0.0, -1.0), size
=3)
202 count
: bpy
.props
.IntProperty(name
='Count', description
='Number of copies', min=1, default
=2)
203 connect
: bpy
.props
.BoolProperty(name
='Connect', description
='Concatenate individual copies', default
=False)
204 serpentine
: bpy
.props
.BoolProperty(name
='Serpentine', description
='Switch direction of every second copy', default
=False)
207 def poll(cls
, context
):
208 return util
.Selected1OrMoreCurves()
210 def execute(self
, context
):
211 splines
= internal
.getSelectedSplines(True, True)
212 if len(splines
) == 0:
213 self
.report({'WARNING'}, 'Nothing selected')
215 internal
.arrayModifier(splines
, self
.offset
, self
.count
, self
.connect
, self
.serpentine
)
218 class Circle(bpy
.types
.Operator
):
219 bl_idname
= 'curvetools.bezier_cad_circle'
220 bl_description
= bl_label
= 'Circle'
221 bl_options
= {'REGISTER', 'UNDO'}
224 def poll(cls
, context
):
225 return util
.Selected1OrMoreCurves()
227 def execute(self
, context
):
228 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
229 if len(segments
) != 1:
230 self
.report({'WARNING'}, 'Invalid selection')
233 segment
= internal
.bezierSegmentPoints(segments
[0]['beginPoint'], segments
[0]['endPoint'])
234 circle
= internal
.circleOfBezier(segment
)
236 self
.report({'WARNING'}, 'Not a circle')
238 bpy
.context
.scene
.cursor
.location
= circle
.center
239 bpy
.context
.scene
.cursor
.rotation_mode
= 'QUATERNION'
240 bpy
.context
.scene
.cursor
.rotation_quaternion
= circle
.orientation
.to_quaternion()
243 class Length(bpy
.types
.Operator
):
244 bl_idname
= 'curvetools.bezier_cad_length'
245 bl_description
= bl_label
= 'Length'
248 def poll(cls
, context
):
249 return util
.Selected1OrMoreCurves()
251 def execute(self
, context
):
252 segments
= internal
.bezierSegments(bpy
.context
.object.data
.splines
, True)
253 if len(segments
) == 0:
254 self
.report({'WARNING'}, 'Nothing selected')
258 for segment
in segments
:
259 length
+= internal
.bezierLength(internal
.bezierSegmentPoints(segment
['beginPoint'], segment
['endPoint']))
260 self
.report({'INFO'}, bpy
.utils
.units
.to_string(bpy
.context
.scene
.unit_settings
.system
, 'LENGTH', length
))
265 bpy
.utils
.register_class(operators
)
269 bpy
.utils
.unregister_class(operators
)
271 if __name__
== "__main__":
274 operators
= [Fillet
, Boolean
, Intersection
, HandleProjection
, MergeEnds
, Subdivide
, Array
, Circle
, Length
]