Fix: Node Wrangler: new reroute locations on hidden nodes
[blender-addons.git] / curve_tools / cad.py
blobb67b43ad22589e5d4498a34adc24146ff97a4490
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 'name': 'Curve CAD Tools',
7 'author': 'Alexander Meißner',
8 'version': (1, 0, 0),
9 'blender': (2, 80, 0),
10 'category': 'Curve',
11 'doc_url': 'https://github.com/Lichtso/curve_cad',
12 'tracker_url': 'https://github.com/lichtso/curve_cad/issues'
15 import bpy
16 from . import internal
17 from . import util
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)
28 @classmethod
29 def poll(cls, context):
30 return util.Selected1OrMoreCurves()
32 def execute(self, context):
33 splines = internal.getSelectedSplines(True, True, True)
34 if len(splines) == 0:
35 self.report({'WARNING'}, 'Nothing selected')
36 return {'CANCELLED'}
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)
40 return {'FINISHED'}
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)
53 @classmethod
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')
63 return {'CANCELLED'}
64 splines = internal.getSelectedSplines(True, True)
65 if len(splines) != 2:
66 self.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
67 return {'CANCELLED'}
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.')
73 return {'CANCELLED'}
75 bpy.ops.object.mode_set (mode = current_mode)
77 return {'FINISHED'}
79 class Intersection(bpy.types.Operator):
80 bl_idname = 'curvetools.bezier_cad_intersection'
81 bl_description = bl_label = 'Intersection'
82 bl_options = {'REGISTER', 'UNDO'}
84 @classmethod
85 def poll(cls, context):
86 return util.Selected1OrMoreCurves()
88 def execute(self, context):
89 segments = internal.bezierSegments(bpy.context.object.data.splines, True)
90 if len(segments) < 2:
91 self.report({'WARNING'}, 'Invalid selection')
92 return {'CANCELLED'}
94 internal.bezierMultiIntersection(segments)
95 return {'FINISHED'}
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'}
102 @classmethod
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')
110 return {'CANCELLED'}
112 internal.bezierProjectHandles(segments)
113 return {'FINISHED'}
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)
122 @classmethod
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
137 if min_dist > dist:
138 min_dist = dist
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:
141 continue
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)
148 return {'FINISHED'}
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')
157 @classmethod
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')
169 return {'CANCELLED'}
171 cuts = []
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)
180 return {'FINISHED'}
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)
192 @classmethod
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')
200 return {'CANCELLED'}
201 internal.arrayModifier(splines, self.offset, self.count, self.connect, self.serpentine)
202 return {'FINISHED'}
204 class Circle(bpy.types.Operator):
205 bl_idname = 'curvetools.bezier_cad_circle'
206 bl_description = bl_label = 'Circle'
207 bl_options = {'REGISTER', 'UNDO'}
209 @classmethod
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')
217 return {'CANCELLED'}
219 segment = internal.bezierSegmentPoints(segments[0]['beginPoint'], segments[0]['endPoint'])
220 circle = internal.circleOfBezier(segment)
221 if circle == None:
222 self.report({'WARNING'}, 'Not a circle')
223 return {'CANCELLED'}
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()
227 return {'FINISHED'}
229 class Length(bpy.types.Operator):
230 bl_idname = 'curvetools.bezier_cad_length'
231 bl_description = bl_label = 'Length'
233 @classmethod
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')
241 return {'CANCELLED'}
243 length = 0
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))
247 return {'FINISHED'}
249 def register():
250 for cls in classes:
251 bpy.utils.register_class(operators)
253 def unregister():
254 for cls in classes:
255 bpy.utils.unregister_class(operators)
257 if __name__ == "__main__":
258 register()
260 operators = [Fillet, Boolean, Intersection, HandleProjection, MergeEnds, Subdivide, Array, Circle, Length]