Merge branch 'blender-v2.92-release'
[blender-addons.git] / curve_tools / cad.py
blobdd11b4c732f1a253f8e9be88c18995c090dbec8a
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 *****
19 bl_info = {
20 'name': 'Curve CAD Tools',
21 'author': 'Alexander Meißner',
22 'version': (1, 0, 0),
23 'blender': (2, 80, 0),
24 'category': 'Curve',
25 'doc_url': 'https://github.com/Lichtso/curve_cad',
26 'tracker_url': 'https://github.com/lichtso/curve_cad/issues'
29 import bpy
30 from . import internal
31 from . import util
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)
42 @classmethod
43 def poll(cls, context):
44 return util.Selected1OrMoreCurves()
46 def execute(self, context):
47 splines = internal.getSelectedSplines(True, True, True)
48 if len(splines) == 0:
49 self.report({'WARNING'}, 'Nothing selected')
50 return {'CANCELLED'}
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)
54 return {'FINISHED'}
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)
67 @classmethod
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')
77 return {'CANCELLED'}
78 splines = internal.getSelectedSplines(True, True)
79 if len(splines) != 2:
80 self.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
81 return {'CANCELLED'}
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.')
87 return {'CANCELLED'}
89 bpy.ops.object.mode_set (mode = current_mode)
91 return {'FINISHED'}
93 class Intersection(bpy.types.Operator):
94 bl_idname = 'curvetools.bezier_cad_intersection'
95 bl_description = bl_label = 'Intersection'
96 bl_options = {'REGISTER', 'UNDO'}
98 @classmethod
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')
106 return {'CANCELLED'}
108 internal.bezierMultiIntersection(segments)
109 return {'FINISHED'}
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'}
116 @classmethod
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')
124 return {'CANCELLED'}
126 internal.bezierProjectHandles(segments)
127 return {'FINISHED'}
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)
136 @classmethod
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
151 if min_dist > dist:
152 min_dist = dist
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:
155 continue
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)
162 return {'FINISHED'}
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')
171 @classmethod
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')
183 return {'CANCELLED'}
185 cuts = []
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)
194 return {'FINISHED'}
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)
206 @classmethod
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')
214 return {'CANCELLED'}
215 internal.arrayModifier(splines, self.offset, self.count, self.connect, self.serpentine)
216 return {'FINISHED'}
218 class Circle(bpy.types.Operator):
219 bl_idname = 'curvetools.bezier_cad_circle'
220 bl_description = bl_label = 'Circle'
221 bl_options = {'REGISTER', 'UNDO'}
223 @classmethod
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')
231 return {'CANCELLED'}
233 segment = internal.bezierSegmentPoints(segments[0]['beginPoint'], segments[0]['endPoint'])
234 circle = internal.circleOfBezier(segment)
235 if circle == None:
236 self.report({'WARNING'}, 'Not a circle')
237 return {'CANCELLED'}
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()
241 return {'FINISHED'}
243 class Length(bpy.types.Operator):
244 bl_idname = 'curvetools.bezier_cad_length'
245 bl_description = bl_label = 'Length'
247 @classmethod
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')
255 return {'CANCELLED'}
257 length = 0
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))
261 return {'FINISHED'}
263 def register():
264 for cls in classes:
265 bpy.utils.register_class(operators)
267 def unregister():
268 for cls in classes:
269 bpy.utils.unregister_class(operators)
271 if __name__ == "__main__":
272 register()
274 operators = [Fillet, Boolean, Intersection, HandleProjection, MergeEnds, Subdivide, Array, Circle, Length]