Pose library: fix asset creation operator poll when no object active
[blender-addons.git] / curve_tools / cad.py
blobda68a0a52a58e1fc717e6f2d361792bb99b4c768
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 'name': 'Curve CAD Tools',
5 'author': 'Alexander Meißner',
6 'version': (1, 0, 0),
7 'blender': (2, 80, 0),
8 'category': 'Curve',
9 'doc_url': 'https://github.com/Lichtso/curve_cad',
10 'tracker_url': 'https://github.com/lichtso/curve_cad/issues'
13 import bpy
14 from . import internal
15 from . import util
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)
26 @classmethod
27 def poll(cls, context):
28 return util.Selected1OrMoreCurves()
30 def execute(self, context):
31 splines = internal.getSelectedSplines(True, True, True)
32 if len(splines) == 0:
33 self.report({'WARNING'}, 'Nothing selected')
34 return {'CANCELLED'}
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)
38 return {'FINISHED'}
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)
51 @classmethod
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')
61 return {'CANCELLED'}
62 splines = internal.getSelectedSplines(True, True)
63 if len(splines) != 2:
64 self.report({'WARNING'}, 'Invalid selection. Only work to selected two spline.')
65 return {'CANCELLED'}
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.')
71 return {'CANCELLED'}
73 bpy.ops.object.mode_set (mode = current_mode)
75 return {'FINISHED'}
77 class Intersection(bpy.types.Operator):
78 bl_idname = 'curvetools.bezier_cad_intersection'
79 bl_description = bl_label = 'Intersection'
80 bl_options = {'REGISTER', 'UNDO'}
82 @classmethod
83 def poll(cls, context):
84 return util.Selected1OrMoreCurves()
86 def execute(self, context):
87 segments = internal.bezierSegments(bpy.context.object.data.splines, True)
88 if len(segments) < 2:
89 self.report({'WARNING'}, 'Invalid selection')
90 return {'CANCELLED'}
92 internal.bezierMultiIntersection(segments)
93 return {'FINISHED'}
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'}
100 @classmethod
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')
108 return {'CANCELLED'}
110 internal.bezierProjectHandles(segments)
111 return {'FINISHED'}
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)
120 @classmethod
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
135 if min_dist > dist:
136 min_dist = dist
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:
139 continue
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)
146 return {'FINISHED'}
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')
155 @classmethod
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')
167 return {'CANCELLED'}
169 cuts = []
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)
178 return {'FINISHED'}
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)
190 @classmethod
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')
198 return {'CANCELLED'}
199 internal.arrayModifier(splines, self.offset, self.count, self.connect, self.serpentine)
200 return {'FINISHED'}
202 class Circle(bpy.types.Operator):
203 bl_idname = 'curvetools.bezier_cad_circle'
204 bl_description = bl_label = 'Circle'
205 bl_options = {'REGISTER', 'UNDO'}
207 @classmethod
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')
215 return {'CANCELLED'}
217 segment = internal.bezierSegmentPoints(segments[0]['beginPoint'], segments[0]['endPoint'])
218 circle = internal.circleOfBezier(segment)
219 if circle == None:
220 self.report({'WARNING'}, 'Not a circle')
221 return {'CANCELLED'}
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()
225 return {'FINISHED'}
227 class Length(bpy.types.Operator):
228 bl_idname = 'curvetools.bezier_cad_length'
229 bl_description = bl_label = 'Length'
231 @classmethod
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')
239 return {'CANCELLED'}
241 length = 0
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))
245 return {'FINISHED'}
247 def register():
248 for cls in classes:
249 bpy.utils.register_class(operators)
251 def unregister():
252 for cls in classes:
253 bpy.utils.unregister_class(operators)
255 if __name__ == "__main__":
256 register()
258 operators = [Fillet, Boolean, Intersection, HandleProjection, MergeEnds, Subdivide, Array, Circle, Length]