File headers: use SPDX license identifiers
[blender-addons.git] / curve_tools / operators.py
blobe4480f882f3b351bc7fdd0f7f0d1b3c2c6507d74
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import time
4 import threading
6 import bpy
7 from bpy.props import *
8 from bpy_extras import object_utils, view3d_utils
9 from mathutils import *
10 from math import *
12 from . import properties
13 from . import curves
14 from . import intersections
15 from . import util
16 from . import surfaces
17 from . import mathematics
18 from . import internal
20 # 1 CURVE SELECTED
21 # ################
22 class OperatorCurveInfo(bpy.types.Operator):
23 bl_idname = "curvetools.operatorcurveinfo"
24 bl_label = "Info"
25 bl_description = "Displays general info about the active/selected curve"
28 @classmethod
29 def poll(cls, context):
30 return util.Selected1Curve()
33 def execute(self, context):
34 curve = curves.Curve(context.active_object)
36 nrSplines = len(curve.splines)
37 nrSegments = 0
38 nrEmptySplines = 0
39 for spline in curve.splines:
40 nrSegments += spline.nrSegments
41 if spline.nrSegments < 1: nrEmptySplines += 1
44 self.report({'INFO'}, "nrSplines: %d; nrSegments: %d; nrEmptySplines: %d" % (nrSplines, nrSegments, nrEmptySplines))
46 return {'FINISHED'}
50 class OperatorCurveLength(bpy.types.Operator):
51 bl_idname = "curvetools.operatorcurvelength"
52 bl_label = "Length"
53 bl_description = "Calculates the length of the active/selected curve"
56 @classmethod
57 def poll(cls, context):
58 return util.Selected1Curve()
61 def execute(self, context):
62 curve = curves.Curve(context.active_object)
64 context.scene.curvetools.CurveLength = curve.length
66 return {'FINISHED'}
70 class OperatorSplinesInfo(bpy.types.Operator):
71 bl_idname = "curvetools.operatorsplinesinfo"
72 bl_label = "Info"
73 bl_description = "Displays general info about the splines of the active/selected curve"
76 @classmethod
77 def poll(cls, context):
78 return util.Selected1Curve()
81 def execute(self, context):
82 curve = curves.Curve(context.active_object)
83 nrSplines = len(curve.splines)
85 print("")
86 print("OperatorSplinesInfo:", "nrSplines:", nrSplines)
88 nrEmptySplines = 0
89 for iSpline, spline in enumerate(curve.splines):
90 print("--", "spline %d of %d: nrSegments: %d" % (iSpline + 1, nrSplines, spline.nrSegments))
92 if spline.nrSegments < 1:
93 nrEmptySplines += 1
94 print("--", "--", "## WARNING: spline has no segments and will therefor be ignored in any further calculations")
97 self.report({'INFO'}, "nrSplines: %d; nrEmptySplines: %d" % (nrSplines, nrEmptySplines) + " -- more info: see console")
99 return {'FINISHED'}
103 class OperatorSegmentsInfo(bpy.types.Operator):
104 bl_idname = "curvetools.operatorsegmentsinfo"
105 bl_label = "Info"
106 bl_description = "Displays general info about the segments of the active/selected curve"
109 @classmethod
110 def poll(cls, context):
111 return util.Selected1Curve()
114 def execute(self, context):
115 curve = curves.Curve(context.active_object)
116 nrSplines = len(curve.splines)
117 nrSegments = 0
119 print("")
120 print("OperatorSegmentsInfo:", "nrSplines:", nrSplines)
122 nrEmptySplines = 0
123 for iSpline, spline in enumerate(curve.splines):
124 nrSegmentsSpline = spline.nrSegments
125 print("--", "spline %d of %d: nrSegments: %d" % (iSpline + 1, nrSplines, nrSegmentsSpline))
127 if nrSegmentsSpline < 1:
128 nrEmptySplines += 1
129 print("--", "--", "## WARNING: spline has no segments and will therefor be ignored in any further calculations")
130 continue
132 for iSegment, segment in enumerate(spline.segments):
133 print("--", "--", "segment %d of %d coefficients:" % (iSegment + 1, nrSegmentsSpline))
134 print("--", "--", "--", "C0: %.6f, %.6f, %.6f" % (segment.coeff0.x, segment.coeff0.y, segment.coeff0.z))
136 nrSegments += nrSegmentsSpline
138 self.report({'INFO'}, "nrSplines: %d; nrSegments: %d; nrEmptySplines: %d" % (nrSplines, nrSegments, nrEmptySplines))
140 return {'FINISHED'}
144 class OperatorOriginToSpline0Start(bpy.types.Operator):
145 bl_idname = "curvetools.operatororigintospline0start"
146 bl_label = "OriginToSpline0Start"
147 bl_description = "Sets the origin of the active/selected curve to the starting point of the (first) spline. Nice for curve modifiers"
150 @classmethod
151 def poll(cls, context):
152 return util.Selected1Curve()
155 def execute(self, context):
158 blCurve = context.active_object
159 blSpline = blCurve.data.splines[0]
160 newOrigin = blCurve.matrix_world @ blSpline.bezier_points[0].co
162 origOrigin = bpy.context.scene.cursor.location.copy()
163 self.report({'INFO'}, "origOrigin: %.6f, %.6f, %.6f" % (origOrigin.x, origOrigin.y, origOrigin.z))
164 self.report({'INFO'}, "newOrigin: %.6f, %.6f, %.6f" % (newOrigin.x, newOrigin.y, newOrigin.z))
166 current_mode = bpy.context.object.mode
168 bpy.ops.object.mode_set(mode = 'OBJECT')
169 bpy.context.scene.cursor.location = newOrigin
170 bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
171 bpy.context.scene.cursor.location = origOrigin
173 bpy.ops.object.mode_set (mode = current_mode)
175 return {'FINISHED'}
179 # 2 CURVES SELECTED
180 # #################
181 class OperatorIntersectCurves(bpy.types.Operator):
182 bl_idname = "curvetools.operatorintersectcurves"
183 bl_label = "Intersect"
184 bl_description = "Intersects selected curves"
187 @classmethod
188 def poll(cls, context):
189 return util.Selected2OrMoreCurves()
192 def execute(self, context):
193 print("### TODO: OperatorIntersectcurves.execute()")
195 algo = context.scene.curvetools.IntersectCurvesAlgorithm
196 print("-- algo:", algo)
199 mode = context.scene.curvetools.IntersectCurvesMode
200 print("-- mode:", mode)
201 # if mode == 'Split':
202 # self.report({'WARNING'}, "'Split' mode is not implemented yet -- <<STOPPING>>")
203 # return {'CANCELLED'}
205 affect = context.scene.curvetools.IntersectCurvesAffect
206 print("-- affect:", affect)
208 selected_objects = context.selected_objects
209 lenodjs = len(selected_objects)
210 print('lenodjs:', lenodjs)
211 for i in range(0, lenodjs):
212 for j in range(0, lenodjs):
213 if j != i:
214 bpy.ops.object.select_all(action='DESELECT')
215 selected_objects[i].select_set(True)
216 selected_objects[j].select_set(True)
218 if selected_objects[i].type == 'CURVE' and selected_objects[j].type == 'CURVE':
219 curveIntersector = intersections.CurvesIntersector.FromSelection()
220 rvIntersectionNrs = curveIntersector.CalcAndApplyIntersections()
222 self.report({'INFO'}, "Active curve points: %d; other curve points: %d" % (rvIntersectionNrs[0], rvIntersectionNrs[1]))
224 for obj in selected_objects:
225 obj.select_set(True)
227 return {'FINISHED'}
229 # ------------------------------------------------------------
230 # OperatorLoftCurves
232 class OperatorLoftCurves(bpy.types.Operator):
233 bl_idname = "curvetools.operatorloftcurves"
234 bl_label = "Loft"
235 bl_description = "Lofts selected curves"
238 @classmethod
239 def poll(cls, context):
240 return util.Selected2Curves()
243 def execute(self, context):
244 #print("### TODO: OperatorLoftcurves.execute()")
246 loftedSurface = surfaces.LoftedSurface.FromSelection()
247 loftedSurface.AddToScene()
249 self.report({'INFO'}, "OperatorLoftcurves.execute()")
251 return {'FINISHED'}
254 # ------------------------------------------------------------
255 # OperatorSweepCurves
257 class OperatorSweepCurves(bpy.types.Operator):
258 bl_idname = "curvetools.operatorsweepcurves"
259 bl_label = "Sweep"
260 bl_description = "Sweeps the active curve along to other curve (rail)"
263 @classmethod
264 def poll(cls, context):
265 return util.Selected2Curves()
268 def execute(self, context):
269 #print("### TODO: OperatorSweepcurves.execute()")
271 sweptSurface = surfaces.SweptSurface.FromSelection()
272 sweptSurface.AddToScene()
274 self.report({'INFO'}, "OperatorSweepcurves.execute()")
276 return {'FINISHED'}
280 # 3 CURVES SELECTED
281 # #################
282 class OperatorBirail(bpy.types.Operator):
283 bl_idname = "curvetools.operatorbirail"
284 bl_label = "Birail"
285 bl_description = "Generates a birailed surface from 3 selected curves -- in order: rail1, rail2 and profile"
288 @classmethod
289 def poll(cls, context):
290 return util.Selected3Curves()
293 def execute(self, context):
294 birailedSurface = surfaces.BirailedSurface.FromSelection()
295 birailedSurface.AddToScene()
297 self.report({'INFO'}, "OperatorBirail.execute()")
299 return {'FINISHED'}
303 # 1 OR MORE CURVES SELECTED
304 # #########################
305 class OperatorSplinesSetResolution(bpy.types.Operator):
306 bl_idname = "curvetools.operatorsplinessetresolution"
307 bl_label = "SplinesSetResolution"
308 bl_description = "Sets the resolution of all splines"
311 @classmethod
312 def poll(cls, context):
313 return util.Selected1OrMoreCurves()
316 def execute(self, context):
317 splRes = context.scene.curvetools.SplineResolution
318 selCurves = util.GetSelectedCurves()
320 for blCurve in selCurves:
321 for spline in blCurve.data.splines:
322 spline.resolution_u = splRes
324 return {'FINISHED'}
326 # ------------------------------------------------------------
327 # OperatorSplinesRemoveZeroSegment
329 class OperatorSplinesRemoveZeroSegment(bpy.types.Operator):
330 bl_idname = "curvetools.operatorsplinesremovezerosegment"
331 bl_label = "SplinesRemoveZeroSegment"
332 bl_description = "Removes splines with no segments -- they seem to creep up, sometimes"
335 @classmethod
336 def poll(cls, context):
337 return util.Selected1OrMoreCurves()
340 def execute(self, context):
341 selCurves = util.GetSelectedCurves()
343 for blCurve in selCurves:
344 curve = curves.Curve(blCurve)
345 nrSplines = curve.nrSplines
347 splinesToRemove = []
348 for spline in curve.splines:
349 if len(spline.segments) < 1: splinesToRemove.append(spline)
350 nrRemovedSplines = len(splinesToRemove)
352 for spline in splinesToRemove: curve.splines.remove(spline)
354 if nrRemovedSplines > 0: curve.RebuildInScene()
356 self.report({'INFO'}, "Removed %d of %d splines" % (nrRemovedSplines, nrSplines))
358 return {'FINISHED'}
360 # ------------------------------------------------------------
361 # OperatorSplinesRemoveShort
363 class OperatorSplinesRemoveShort(bpy.types.Operator):
364 bl_idname = "curvetools.operatorsplinesremoveshort"
365 bl_label = "SplinesRemoveShort"
366 bl_description = "Removes splines with a length smaller than the threshold"
369 @classmethod
370 def poll(cls, context):
371 return util.Selected1OrMoreCurves()
374 def execute(self, context):
375 threshold = context.scene.curvetools.SplineRemoveLength
376 selCurves = util.GetSelectedCurves()
378 for blCurve in selCurves:
379 curve = curves.Curve(blCurve)
380 nrSplines = curve.nrSplines
382 nrRemovedSplines = curve.RemoveShortSplines(threshold)
383 if nrRemovedSplines > 0: curve.RebuildInScene()
385 self.report({'INFO'}, "Removed %d of %d splines" % (nrRemovedSplines, nrSplines))
387 return {'FINISHED'}
389 # ------------------------------------------------------------
390 # OperatorSplinesJoinNeighbouring
392 class OperatorSplinesJoinNeighbouring(bpy.types.Operator):
393 bl_idname = "curvetools.operatorsplinesjoinneighbouring"
394 bl_label = "SplinesJoinNeighbouring"
395 bl_description = "Joins neighbouring splines within a distance smaller than the threshold"
398 @classmethod
399 def poll(cls, context):
400 return util.Selected1OrMoreCurves()
403 def execute(self, context):
404 selCurves = util.GetSelectedCurves()
406 for blCurve in selCurves:
407 curve = curves.Curve(blCurve)
408 nrSplines = curve.nrSplines
410 threshold = context.scene.curvetools.SplineJoinDistance
411 startEnd = context.scene.curvetools.SplineJoinStartEnd
412 mode = context.scene.curvetools.SplineJoinMode
414 nrJoins = curve.JoinNeighbouringSplines(startEnd, threshold, mode)
415 if nrJoins > 0: curve.RebuildInScene()
417 self.report({'INFO'}, "Applied %d joins on %d splines; resulting nrSplines: %d" % (nrJoins, nrSplines, curve.nrSplines))
419 return {'FINISHED'}
421 # ------------------------------------------------------------
422 # SurfaceFromBezier
424 def SurfaceFromBezier(surfacedata, points, center):
426 len_points = len(points) - 1
428 if len_points % 2 == 0:
429 h = mathematics.subdivide_cubic_bezier(
430 points[len_points].co, points[len_points].handle_right,
431 points[0].handle_left, points[0].co, 0.5
433 points.add(1)
434 len_points = len(points) - 1
435 points[len_points - 1].handle_right = h[0]
436 points[len_points].handle_left = h[1]
437 points[len_points].co = h[2]
438 points[len_points].handle_right = h[3]
439 points[0].handle_left = h[4]
441 half = round((len_points + 1)/2) - 1
443 surfacespline1 = surfacedata.splines.new(type='NURBS')
444 surfacespline1.points.add(3)
445 surfacespline1.points[0].co = [points[0].co.x, points[0].co.y, points[0].co.z, 1]
446 surfacespline1.points[1].co = [points[0].handle_left.x, points[0].handle_left.y, points[0].handle_left.z, 1]
447 surfacespline1.points[2].co = [points[len_points].handle_right.x,points[len_points].handle_right.y, points[len_points].handle_right.z, 1]
448 surfacespline1.points[3].co = [points[len_points].co.x, points[len_points].co.y, points[len_points].co.z, 1]
449 for p in surfacespline1.points:
450 p.select = True
451 surfacespline1.use_endpoint_u = True
452 surfacespline1.use_endpoint_v = True
454 for i in range(0, half):
456 if center:
458 surfacespline2 = surfacedata.splines.new(type='NURBS')
459 surfacespline2.points.add(3)
460 surfacespline2.points[0].co = [points[i].co.x, points[i].co.y, points[i].co.z, 1]
461 surfacespline2.points[1].co = [(points[i].co.x + points[len_points - i].co.x)/2,
462 (points[i].co.y + points[len_points - i].co.y)/2,
463 (points[i].co.z + points[len_points - i].co.z)/2, 1]
464 surfacespline2.points[2].co = [(points[len_points - i].co.x + points[i].co.x)/2,
465 (points[len_points - i].co.y + points[i].co.y)/2,
466 (points[len_points - i].co.z + points[i].co.z)/2, 1]
467 surfacespline2.points[3].co = [points[len_points - i].co.x, points[len_points - i].co.y, points[len_points - i].co.z, 1]
468 for p in surfacespline2.points:
469 p.select = True
470 surfacespline2.use_endpoint_u = True
471 surfacespline2.use_endpoint_v = True
474 surfacespline3 = surfacedata.splines.new(type='NURBS')
475 surfacespline3.points.add(3)
476 surfacespline3.points[0].co = [points[i].handle_right.x, points[i].handle_right.y, points[i].handle_right.z, 1]
477 surfacespline3.points[1].co = [(points[i].handle_right.x + points[len_points - i].handle_left.x)/2,
478 (points[i].handle_right.y + points[len_points - i].handle_left.y)/2,
479 (points[i].handle_right.z + points[len_points - i].handle_left.z)/2, 1]
480 surfacespline3.points[2].co = [(points[len_points - i].handle_left.x + points[i].handle_right.x)/2,
481 (points[len_points - i].handle_left.y + points[i].handle_right.y)/2,
482 (points[len_points - i].handle_left.z + points[i].handle_right.z)/2, 1]
483 surfacespline3.points[3].co = [points[len_points - i].handle_left.x, points[len_points - i].handle_left.y, points[len_points - i].handle_left.z, 1]
484 for p in surfacespline3.points:
485 p.select = True
486 surfacespline3.use_endpoint_u = True
487 surfacespline3.use_endpoint_v = True
490 surfacespline4 = surfacedata.splines.new(type='NURBS')
491 surfacespline4.points.add(3)
492 surfacespline4.points[0].co = [points[i + 1].handle_left.x, points[i + 1].handle_left.y, points[i + 1].handle_left.z, 1]
493 surfacespline4.points[1].co = [(points[i + 1].handle_left.x + points[len_points - i - 1].handle_right.x)/2,
494 (points[i + 1].handle_left.y + points[len_points - i - 1].handle_right.y)/2,
495 (points[i + 1].handle_left.z + points[len_points - i - 1].handle_right.z)/2, 1]
496 surfacespline4.points[2].co = [(points[len_points - i - 1].handle_right.x + points[i + 1].handle_left.x)/2,
497 (points[len_points - i - 1].handle_right.y + points[i + 1].handle_left.y)/2,
498 (points[len_points - i - 1].handle_right.z + points[i + 1].handle_left.z)/2, 1]
499 surfacespline4.points[3].co = [points[len_points - i - 1].handle_right.x, points[len_points - i - 1].handle_right.y, points[len_points - i - 1].handle_right.z, 1]
500 for p in surfacespline4.points:
501 p.select = True
502 surfacespline4.use_endpoint_u = True
503 surfacespline4.use_endpoint_v = True
505 if center:
507 surfacespline5 = surfacedata.splines.new(type='NURBS')
508 surfacespline5.points.add(3)
509 surfacespline5.points[0].co = [points[i + 1].co.x, points[i + 1].co.y, points[i + 1].co.z, 1]
510 surfacespline5.points[1].co = [(points[i + 1].co.x + points[len_points - i - 1].co.x)/2,
511 (points[i + 1].co.y + points[len_points - i - 1].co.y)/2,
512 (points[i + 1].co.z + points[len_points - i - 1].co.z)/2, 1]
513 surfacespline5.points[2].co = [(points[len_points - i - 1].co.x + points[i + 1].co.x)/2,
514 (points[len_points - i - 1].co.y + points[i + 1].co.y)/2,
515 (points[len_points - i - 1].co.z + points[i + 1].co.z)/2, 1]
516 surfacespline5.points[3].co = [points[len_points - i - 1].co.x, points[len_points - i - 1].co.y, points[len_points - i - 1].co.z, 1]
517 for p in surfacespline5.points:
518 p.select = True
519 surfacespline5.use_endpoint_u = True
520 surfacespline5.use_endpoint_v = True
523 surfacespline6 = surfacedata.splines.new(type='NURBS')
524 surfacespline6.points.add(3)
525 surfacespline6.points[0].co = [points[half].co.x, points[half].co.y, points[half].co.z, 1]
526 surfacespline6.points[1].co = [points[half].handle_right.x, points[half].handle_right.y, points[half].handle_right.z, 1]
527 surfacespline6.points[2].co = [points[half+1].handle_left.x, points[half+1].handle_left.y, points[half+1].handle_left.z, 1]
528 surfacespline6.points[3].co = [points[half+1].co.x, points[half+1].co.y, points[half+1].co.z, 1]
529 for p in surfacespline6.points:
530 p.select = True
531 surfacespline6.use_endpoint_u = True
532 surfacespline6.use_endpoint_v = True
534 bpy.ops.object.mode_set(mode = 'EDIT')
535 bpy.ops.curve.make_segment()
537 for s in surfacedata.splines:
538 s.resolution_u = 4
539 s.resolution_v = 4
540 s.order_u = 4
541 s.order_v = 4
542 for p in s.points:
543 p.select = False
545 # ------------------------------------------------------------
546 # Convert selected faces to Bezier
548 class ConvertSelectedFacesToBezier(bpy.types.Operator):
549 bl_idname = "curvetools.convert_selected_face_to_bezier"
550 bl_label = "Convert selected faces to Bezier"
551 bl_description = "Convert selected faces to Bezier"
552 bl_options = {'REGISTER', 'UNDO'}
554 @classmethod
555 def poll(cls, context):
556 return util.Selected1Mesh()
558 def execute(self, context):
559 # main function
560 bpy.ops.object.mode_set(mode = 'OBJECT')
561 active_object = context.active_object
562 meshdata = active_object.data
563 curvedata = bpy.data.curves.new('Curve' + active_object.name, type='CURVE')
564 curveobject = object_utils.object_data_add(context, curvedata)
565 curvedata.dimensions = '3D'
567 for poly in meshdata.polygons:
568 if poly.select:
569 newSpline = curvedata.splines.new(type='BEZIER')
570 newSpline.use_cyclic_u = True
571 newSpline.bezier_points.add(poly.loop_total - 1)
572 npoint = 0
573 for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total):
574 newSpline.bezier_points[npoint].co = meshdata.vertices[meshdata.loops[loop_index].vertex_index].co
575 newSpline.bezier_points[npoint].handle_left_type = 'VECTOR'
576 newSpline.bezier_points[npoint].handle_right_type = 'VECTOR'
577 newSpline.bezier_points[npoint].select_control_point = True
578 newSpline.bezier_points[npoint].select_left_handle = True
579 newSpline.bezier_points[npoint].select_right_handle = True
580 npoint += 1
582 return {'FINISHED'}
584 # ------------------------------------------------------------
585 # Convert Bezier to Surface
587 class ConvertBezierToSurface(bpy.types.Operator):
588 bl_idname = "curvetools.convert_bezier_to_surface"
589 bl_label = "Convert Bezier to Surface"
590 bl_description = "Convert Bezier to Surface"
591 bl_options = {'REGISTER', 'UNDO'}
593 Center : BoolProperty(
594 name="Center",
595 default=False,
596 description="Consider center points"
599 Resolution_U: IntProperty(
600 name="Resolution_U",
601 default=4,
602 min=1, max=64,
603 soft_min=1,
604 description="Surface resolution U"
607 Resolution_V: IntProperty(
608 name="Resolution_V",
609 default=4,
610 min=1, max=64,
611 soft_min=1,
612 description="Surface resolution V"
615 def draw(self, context):
616 layout = self.layout
618 # general options
619 col = layout.column()
620 col.prop(self, 'Center')
621 col.prop(self, 'Resolution_U')
622 col.prop(self, 'Resolution_V')
624 @classmethod
625 def poll(cls, context):
626 return util.Selected1OrMoreCurves()
628 def execute(self, context):
629 # main function
630 bpy.ops.object.mode_set(mode = 'OBJECT')
631 active_object = context.active_object
632 curvedata = active_object.data
634 surfacedata = bpy.data.curves.new('Surface', type='SURFACE')
635 surfaceobject = object_utils.object_data_add(context, surfacedata)
636 surfaceobject.matrix_world = active_object.matrix_world
637 surfaceobject.rotation_euler = active_object.rotation_euler
638 surfacedata.dimensions = '3D'
639 surfaceobject.show_wire = True
640 surfaceobject.show_in_front = True
642 for spline in curvedata.splines:
643 SurfaceFromBezier(surfacedata, spline.bezier_points, self.Center)
645 for spline in surfacedata.splines:
646 len_p = len(spline.points)
647 len_devide_4 = round(len_p / 4) + 1
648 len_devide_2 = round(len_p / 2)
649 bpy.ops.object.mode_set(mode = 'EDIT')
650 for point_index in range(len_devide_4, len_p - len_devide_4):
651 if point_index != len_devide_2 and point_index != len_devide_2 - 1:
652 spline.points[point_index].select = True
654 surfacedata.resolution_u = self.Resolution_U
655 surfacedata.resolution_v = self.Resolution_V
657 return {'FINISHED'}
659 # ------------------------------------------------------------
660 # Fillet
662 class BezierPointsFillet(bpy.types.Operator):
663 bl_idname = "curvetools.bezier_points_fillet"
664 bl_label = "Bezier points Fillet"
665 bl_description = "Bezier points Fillet"
666 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
668 Fillet_radius : FloatProperty(
669 name="Radius",
670 default=0.25,
671 unit='LENGTH',
672 description="Radius"
674 Types = [('Round', "Round", "Round"),
675 ('Chamfer', "Chamfer", "Chamfer")]
676 Fillet_Type : EnumProperty(
677 name="Type",
678 description="Fillet type",
679 items=Types
682 def draw(self, context):
683 layout = self.layout
685 # general options
686 col = layout.column()
687 col.prop(self, "Fillet_radius")
688 col.prop(self, "Fillet_Type", expand=True)
690 @classmethod
691 def poll(cls, context):
692 return util.Selected1OrMoreCurves()
694 def execute(self, context):
695 # main function
696 if bpy.ops.object.mode_set.poll():
697 bpy.ops.object.mode_set(mode='EDIT')
699 splines = bpy.context.object.data.splines
700 bpy.ops.curve.spline_type_set(type='BEZIER')
702 bpy.ops.curve.handle_type_set(type='VECTOR')
703 s = []
704 for spline in splines:
705 n = 0
706 ii = []
707 for p in spline.bezier_points:
708 if p.select_control_point:
709 ii.append(n)
710 n += 1
711 else:
712 n += 1
713 s.append(ii)
715 sn = 0
716 for spline in splines:
717 ii = s[sn]
718 bezier_points = spline.bezier_points
719 n = len(bezier_points)
720 if n > 2:
721 jn = 0
722 for j in ii:
723 j += jn
725 bpy.ops.curve.select_all(action='DESELECT')
727 if j != 0 and j != n - 1:
728 bezier_points[j].select_control_point = True
729 bezier_points[j + 1].select_control_point = True
730 bpy.ops.curve.subdivide()
731 selected4 = [bezier_points[j - 1], bezier_points[j],
732 bezier_points[j + 1], bezier_points[j + 2]]
733 jn += 1
734 n += 1
736 elif j == 0:
737 bezier_points[j].select_control_point = True
738 bezier_points[j + 1].select_control_point = True
739 bpy.ops.curve.subdivide()
740 selected4 = [bezier_points[n], bezier_points[0],
741 bezier_points[1], bezier_points[2]]
742 jn += 1
743 n += 1
745 elif j == n - 1:
746 bezier_points[j].select_control_point = True
747 bezier_points[j - 1].select_control_point = True
748 bpy.ops.curve.subdivide()
749 selected4 = [bezier_points[0], bezier_points[n],
750 bezier_points[n - 1], bezier_points[n - 2]]
752 selected4[2].co = selected4[1].co
753 s1 = Vector(selected4[0].co) - Vector(selected4[1].co)
754 s2 = Vector(selected4[3].co) - Vector(selected4[2].co)
755 s1.normalize()
756 s11 = Vector(selected4[1].co) + s1 * self.Fillet_radius
757 selected4[1].co = s11
758 s2.normalize()
759 s22 = Vector(selected4[2].co) + s2 * self.Fillet_radius
760 selected4[2].co = s22
762 if self.Fillet_Type == 'Round':
763 if j != n - 1:
764 selected4[2].handle_right_type = 'VECTOR'
765 selected4[1].handle_left_type = 'VECTOR'
766 selected4[1].handle_right_type = 'ALIGNED'
767 selected4[2].handle_left_type = 'ALIGNED'
768 else:
769 selected4[1].handle_right_type = 'VECTOR'
770 selected4[2].handle_left_type = 'VECTOR'
771 selected4[2].handle_right_type = 'ALIGNED'
772 selected4[1].handle_left_type = 'ALIGNED'
773 if self.Fillet_Type == 'Chamfer':
774 selected4[2].handle_right_type = 'VECTOR'
775 selected4[1].handle_left_type = 'VECTOR'
776 selected4[1].handle_right_type = 'VECTOR'
777 selected4[2].handle_left_type = 'VECTOR'
778 sn += 1
780 return {'FINISHED'}
782 # ------------------------------------------------------------
783 # BezierDivide Operator
785 class BezierDivide(bpy.types.Operator):
786 bl_idname = "curvetools.bezier_spline_divide"
787 bl_label = "Bezier Spline Divide"
788 bl_description = "Bezier Divide (enters edit mode) for Fillet Curves"
789 bl_options = {'REGISTER', 'UNDO'}
791 # align_matrix for the invoke
792 align_matrix : Matrix()
794 Bezier_t : FloatProperty(
795 name="t (0% - 100%)",
796 default=50.0,
797 min=0.0, soft_min=0.0,
798 max=100.0, soft_max=100.0,
799 description="t (0% - 100%)"
802 @classmethod
803 def poll(cls, context):
804 return util.Selected1OrMoreCurves()
806 def execute(self, context):
807 # main function
808 if bpy.ops.object.mode_set.poll():
809 bpy.ops.object.mode_set(mode='EDIT')
811 splines = bpy.context.object.data.splines
812 s = []
813 for spline in splines:
814 bpy.ops.curve.spline_type_set(type='BEZIER')
816 n = 0
817 ii = []
818 for p in spline.bezier_points:
819 if p.select_control_point:
820 ii.append(n)
821 n += 1
822 else:
823 n += 1
824 s.append(ii)
826 sn = 0
827 for spline in splines:
828 ii = s[sn]
829 bezier_points = spline.bezier_points
830 n = len(bezier_points)
831 if n > 2:
832 jn = 0
833 for j in ii:
835 bpy.ops.curve.select_all(action='DESELECT')
837 if (j in ii) and (j + 1 in ii):
838 bezier_points[j + jn].select_control_point = True
839 bezier_points[j + 1 + jn].select_control_point = True
840 h = mathematics.subdivide_cubic_bezier(
841 bezier_points[j + jn].co, bezier_points[j + jn].handle_right,
842 bezier_points[j + 1 + jn].handle_left, bezier_points[j + 1 + jn].co, self.Bezier_t / 100
844 bpy.ops.curve.subdivide(1)
845 bezier_points[j + jn].handle_right_type = 'FREE'
846 bezier_points[j + jn].handle_right = h[0]
847 bezier_points[j + 1 + jn].co = h[2]
848 bezier_points[j + 1 + jn].handle_left_type = 'FREE'
849 bezier_points[j + 1 + jn].handle_left = h[1]
850 bezier_points[j + 1 + jn].handle_right_type = 'FREE'
851 bezier_points[j + 1 + jn].handle_right = h[3]
852 bezier_points[j + 2 + jn].handle_left_type = 'FREE'
853 bezier_points[j + 2 + jn].handle_left = h[4]
854 jn += 1
856 if j == n - 1 and (0 in ii) and spline.use_cyclic_u:
857 bezier_points[j + jn].select_control_point = True
858 bezier_points[0].select_control_point = True
859 h = mathematics.subdivide_cubic_bezier(
860 bezier_points[j + jn].co, bezier_points[j + jn].handle_right,
861 bezier_points[0].handle_left, bezier_points[0].co, self.Bezier_t / 100
863 bpy.ops.curve.subdivide(1)
864 bezier_points[j + jn].handle_right_type = 'FREE'
865 bezier_points[j + jn].handle_right = h[0]
866 bezier_points[j + 1 + jn].co = h[2]
867 bezier_points[j + 1 + jn].handle_left_type = 'FREE'
868 bezier_points[j + 1 + jn].handle_left = h[1]
869 bezier_points[j + 1 + jn].handle_right_type = 'FREE'
870 bezier_points[j + 1 + jn].handle_right = h[3]
871 bezier_points[0].handle_left_type = 'FREE'
872 bezier_points[0].handle_left = h[4]
874 sn += 1
876 return {'FINISHED'}
878 # ------------------------------------------------------------
879 # CurveScaleReset Operator
881 class CurveScaleReset(bpy.types.Operator):
882 bl_idname = "curvetools.scale_reset"
883 bl_label = "Curve Scale Reset"
884 bl_description = "Curve Scale Reset"
885 bl_options = {'REGISTER', 'UNDO'}
887 @classmethod
888 def poll(cls, context):
889 return (context.object is not None and
890 context.object.type == 'CURVE')
892 def execute(self, context):
893 # main function
894 current_mode = bpy.context.object.mode
896 bpy.ops.object.mode_set(mode = 'OBJECT')
898 oldCurve = context.active_object
899 oldCurveName = oldCurve.name
901 bpy.ops.object.duplicate_move(OBJECT_OT_duplicate=None, TRANSFORM_OT_translate=None)
902 newCurve = context.active_object
903 newCurve.data.splines.clear()
904 newCurve.scale = (1.0, 1.0, 1.0)
906 oldCurve.select_set(True)
907 newCurve.select_set(True)
908 bpy.context.view_layer.objects.active = newCurve
909 bpy.ops.object.join()
911 joinCurve = context.active_object
912 joinCurve.name = oldCurveName
914 bpy.ops.object.mode_set (mode = current_mode)
916 return {'FINISHED'}
918 # ------------------------------------------------------------
919 # Split Operator
921 class Split(bpy.types.Operator):
922 bl_idname = "curvetools.split"
923 bl_label = "Split"
924 bl_options = {'REGISTER', 'UNDO'}
926 @classmethod
927 def poll(cls, context):
928 return util.Selected1OrMoreCurves()
930 def execute(self, context):
931 selected_Curves = util.GetSelectedCurves()
933 for curve in selected_Curves:
934 spline_points = []
935 select_points = {}
936 bezier_spline_points = []
937 select_bezier_points = {}
938 i_bp = 0
939 i_p = 0
940 for spline in curve.data.splines:
941 if spline.type == 'BEZIER':
942 points = {}
943 select_bezier_points[i_bp] = [len(spline.bezier_points)]
944 for i in range(len(spline.bezier_points)):
945 bezier_point = spline.bezier_points[i]
946 points[i]=[bezier_point.co[:], bezier_point.handle_left[:], bezier_point.handle_right[:]]
948 if spline.bezier_points[i].select_control_point:
949 select_bezier_points[i_bp].append(i)
950 i_bp+=1
951 bezier_spline_points.append(points)
952 else:
953 points = {}
954 select_points[i_p] = [len(spline.points)]
955 for i in range(len(spline.points)):
956 point = spline.points[i]
957 points[i]=[point.co[:], spline.type]
958 if spline.points[i].select:
959 select_points[i_p].append(i)
960 i_p+=1
961 spline_points.append(points)
963 curve.data.splines.clear()
965 for key in select_bezier_points:
967 num=0
969 if select_bezier_points[key][-1] == select_bezier_points[key][0]-1:
970 select_bezier_points[key].pop()
972 for i in select_bezier_points[key][1:]+[select_bezier_points[key][0]-1]:
973 if i != 0:
974 spline = curve.data.splines.new('BEZIER')
975 spline.bezier_points.add(i-num)
977 for j in range(num, i):
978 bezier_point = spline.bezier_points[j-num]
980 bezier_point.co = bezier_spline_points[key][j][0]
981 bezier_point.handle_left = bezier_spline_points[key][j][1]
982 bezier_point.handle_right = bezier_spline_points[key][j][2]
983 bezier_point = spline.bezier_points[-1]
984 bezier_point.co = bezier_spline_points[key][i][0]
985 bezier_point.handle_left = bezier_spline_points[key][i][1]
986 bezier_point.handle_right = bezier_spline_points[key][i][2]
987 num=i
989 for key in select_points:
991 num=0
993 if select_points[key][-1] == select_points[key][0]-1:
994 select_points[key].pop()
996 for i in select_points[key][1:]+[select_points[key][0]-1]:
997 if i != 0:
998 spline = curve.data.splines.new(spline_points[key][i][1])
999 spline.points.add(i-num)
1001 for j in range(num, i):
1002 point = spline.points[j-num]
1004 point.co = spline_points[key][j][0]
1005 point = spline.points[-1]
1006 point.co = spline_points[key][i][0]
1007 num=i
1009 return {'FINISHED'}
1011 class SeparateOutline(bpy.types.Operator):
1012 bl_idname = "curvetools.sep_outline"
1013 bl_label = "Separate Outline"
1014 bl_options = {'REGISTER', 'UNDO'}
1015 bl_description = "Makes 'Outline' separate mesh"
1017 @classmethod
1018 def poll(cls, context):
1019 return util.Selected1OrMoreCurves()
1021 def execute(self, context):
1022 bpy.ops.object.mode_set(mode = 'EDIT')
1023 bpy.ops.curve.separate()
1025 return {'FINISHED'}
1027 class CurveBoolean(bpy.types.Operator):
1028 bl_idname = "curvetools.bezier_curve_boolean"
1029 bl_description = "Curve Boolean"
1030 bl_label = "Curve Boolean"
1031 bl_options = {'REGISTER', 'UNDO'}
1033 operation: bpy.props.EnumProperty(name='Type', items=[
1034 ('UNION', 'Union', 'Boolean OR', 0),
1035 ('INTERSECTION', 'Intersection', 'Boolean AND', 1),
1036 ('DIFFERENCE', 'Difference', 'Active minus Selected', 2),
1038 number : IntProperty(
1039 name="Spline Number",
1040 default=1,
1041 min=1,
1042 description="Spline Number"
1045 @classmethod
1046 def poll(cls, context):
1047 return util.Selected1OrMoreCurves()
1049 def draw(self, context):
1050 layout = self.layout
1052 # general options
1053 col = layout.column()
1054 col.prop(self, "operation")
1055 if self.operation == 'DIFFERENCE':
1056 col.prop(self, "number")
1058 def execute(self, context):
1059 current_mode = bpy.context.object.mode
1061 if bpy.ops.object.mode_set.poll():
1062 bpy.ops.object.mode_set(mode = 'OBJECT')
1064 selected_Curves = util.GetSelectedCurves()
1065 len_selected_curves = len(selected_Curves)
1066 if len_selected_curves < 2:
1067 return {'FINISHED'}
1069 min_number = 1
1071 max_number = 0
1072 for iCurve in range(0, len_selected_curves):
1073 len_splines = len(selected_Curves[iCurve].data.splines)
1074 max_number += len_splines
1076 if self.number < min_number:
1077 self.number = min_number
1078 if self.number > max_number:
1079 self.number = max_number
1081 j = 0
1082 first_curve = 0
1083 first_spline = 0
1084 for iCurve in range(0, len_selected_curves):
1085 len_splines = len(selected_Curves[iCurve].data.splines)
1086 for iSpline in range(0, len_splines):
1087 if j == self.number:
1088 first_curve = iCurve
1089 first_spline = iSpline
1090 j += 1
1092 bpy.ops.object.select_all(action='DESELECT')
1094 spline1 = selected_Curves[first_curve].data.splines[first_spline]
1095 matrix_world1 = selected_Curves[first_curve].matrix_world
1097 len_spline1 = len(spline1.bezier_points)
1099 dataCurve = bpy.data.curves.new(self.operation, type='CURVE')
1100 dataCurve.dimensions = '2D'
1101 newSpline1 = dataCurve.splines.new(type='BEZIER')
1102 newSpline1.use_cyclic_u = True
1103 newSpline1.bezier_points.add(len_spline1 - 1)
1104 for n in range(0, len_spline1):
1105 newSpline1.bezier_points[n].co = matrix_world1 @ spline1.bezier_points[n].co
1106 newSpline1.bezier_points[n].handle_left_type = spline1.bezier_points[n].handle_left_type
1107 newSpline1.bezier_points[n].handle_left = matrix_world1 @ spline1.bezier_points[n].handle_left
1108 newSpline1.bezier_points[n].handle_right_type = spline1.bezier_points[n].handle_right_type
1109 newSpline1.bezier_points[n].handle_right = matrix_world1 @ spline1.bezier_points[n].handle_right
1111 Curve = object_utils.object_data_add(context, dataCurve)
1112 bpy.context.view_layer.objects.active = Curve
1113 Curve.select_set(True)
1114 Curve.location = (0.0, 0.0, 0.0)
1116 j = 0
1117 for iCurve in range(0, len_selected_curves):
1118 matrix_world = selected_Curves[iCurve].matrix_world
1119 len_splines = len(selected_Curves[iCurve].data.splines)
1120 for iSpline in range(0, len_splines):
1121 if iCurve == first_curve and iSpline == first_spline:
1122 continue
1123 spline = selected_Curves[iCurve].data.splines[iSpline]
1124 len_spline = len(spline.bezier_points)
1125 newSpline = dataCurve.splines.new(type='BEZIER')
1126 newSpline.use_cyclic_u = True
1127 newSpline.bezier_points.add(len_spline - 1)
1128 for n in range(0, len_spline):
1129 newSpline.bezier_points[n].co = matrix_world @ spline.bezier_points[n].co
1130 newSpline.bezier_points[n].handle_left_type = spline.bezier_points[n].handle_left_type
1131 newSpline.bezier_points[n].handle_left = matrix_world @ spline.bezier_points[n].handle_left
1132 newSpline.bezier_points[n].handle_right_type = spline.bezier_points[n].handle_right_type
1133 newSpline.bezier_points[n].handle_right = matrix_world @ spline.bezier_points[n].handle_right
1135 bpy.ops.object.mode_set(mode = 'EDIT')
1136 bpy.ops.curve.select_all(action='SELECT')
1137 splines = internal.getSelectedSplines(True, True)
1138 if len(splines) < 2:
1139 continue
1140 splineA = splines[0]
1141 splineB = splines[1]
1142 dataCurve.splines.active = newSpline1
1144 if not internal.bezierBooleanGeometry(splineA, splineB, self.operation):
1145 self.report({'WARNING'}, 'Invalid selection.')
1146 return {'CANCELLED'}
1148 j += 1
1150 bpy.ops.object.mode_set(mode = 'EDIT')
1151 bpy.ops.curve.select_all(action='SELECT')
1153 return {'FINISHED'}
1155 # ----------------------------
1156 # Set first points operator
1157 class SetFirstPoints(bpy.types.Operator):
1158 bl_idname = "curvetools.set_first_points"
1159 bl_label = "Set first points"
1160 bl_description = "Set the selected points as the first point of each spline"
1161 bl_options = {'REGISTER', 'UNDO'}
1163 @classmethod
1164 def poll(cls, context):
1165 return util.Selected1OrMoreCurves()
1167 def execute(self, context):
1168 splines_to_invert = []
1170 curve = bpy.context.object
1172 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1174 # Check non-cyclic splines to invert
1175 for i in range(len(curve.data.splines)):
1176 b_points = curve.data.splines[i].bezier_points
1178 if i not in self.cyclic_splines: # Only for non-cyclic splines
1179 if b_points[len(b_points) - 1].select_control_point:
1180 splines_to_invert.append(i)
1182 # Reorder points of cyclic splines, and set all handles to "Automatic"
1184 # Check first selected point
1185 cyclic_splines_new_first_pt = {}
1186 for i in self.cyclic_splines:
1187 sp = curve.data.splines[i]
1189 for t in range(len(sp.bezier_points)):
1190 bp = sp.bezier_points[t]
1191 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
1192 cyclic_splines_new_first_pt[i] = t
1193 break # To take only one if there are more
1195 # Reorder
1196 for spline_idx in cyclic_splines_new_first_pt:
1197 sp = curve.data.splines[spline_idx]
1199 spline_old_coords = []
1200 for bp_old in sp.bezier_points:
1201 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
1203 left_handle_type = str(bp_old.handle_left_type)
1204 left_handle_length = float(bp_old.handle_left.length)
1205 left_handle_xyz = (
1206 float(bp_old.handle_left.x),
1207 float(bp_old.handle_left.y),
1208 float(bp_old.handle_left.z)
1210 right_handle_type = str(bp_old.handle_right_type)
1211 right_handle_length = float(bp_old.handle_right.length)
1212 right_handle_xyz = (
1213 float(bp_old.handle_right.x),
1214 float(bp_old.handle_right.y),
1215 float(bp_old.handle_right.z)
1217 spline_old_coords.append(
1218 [coords, left_handle_type,
1219 right_handle_type, left_handle_length,
1220 right_handle_length, left_handle_xyz,
1221 right_handle_xyz]
1224 for t in range(len(sp.bezier_points)):
1225 bp = sp.bezier_points
1227 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
1228 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
1229 else:
1230 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
1232 bp[t].co = Vector(spline_old_coords[new_index][0])
1234 bp[t].handle_left.length = spline_old_coords[new_index][3]
1235 bp[t].handle_right.length = spline_old_coords[new_index][4]
1237 bp[t].handle_left_type = "FREE"
1238 bp[t].handle_right_type = "FREE"
1240 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
1241 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
1242 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
1244 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
1245 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
1246 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
1248 bp[t].handle_left_type = spline_old_coords[new_index][1]
1249 bp[t].handle_right_type = spline_old_coords[new_index][2]
1251 # Invert the non-cyclic splines designated above
1252 for i in range(len(splines_to_invert)):
1253 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1255 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1256 curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
1257 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1259 bpy.ops.curve.switch_direction()
1261 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1263 # Keep selected the first vert of each spline
1264 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1265 for i in range(len(curve.data.splines)):
1266 if not curve.data.splines[i].use_cyclic_u:
1267 bp = curve.data.splines[i].bezier_points[0]
1268 else:
1269 bp = curve.data.splines[i].bezier_points[
1270 len(curve.data.splines[i].bezier_points) - 1
1273 bp.select_control_point = True
1274 bp.select_right_handle = True
1275 bp.select_left_handle = True
1277 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1279 return {'FINISHED'}
1281 def invoke(self, context, event):
1282 curve = bpy.context.object
1284 # Check if all curves are Bezier, and detect which ones are cyclic
1285 self.cyclic_splines = []
1286 for i in range(len(curve.data.splines)):
1287 if curve.data.splines[i].type != "BEZIER":
1288 self.report({'WARNING'}, "All splines must be Bezier type")
1290 return {'CANCELLED'}
1291 else:
1292 if curve.data.splines[i].use_cyclic_u:
1293 self.cyclic_splines.append(i)
1295 self.execute(context)
1296 self.report({'INFO'}, "First points have been set")
1298 return {'FINISHED'}
1300 def register():
1301 for cls in classes:
1302 bpy.utils.register_class(operators)
1304 def unregister():
1305 for cls in classes:
1306 bpy.utils.unregister_class(operators)
1308 if __name__ == "__main__":
1309 register()
1311 operators = [
1312 OperatorCurveInfo,
1313 OperatorCurveLength,
1314 OperatorSplinesInfo,
1315 OperatorSegmentsInfo,
1316 OperatorOriginToSpline0Start,
1317 OperatorIntersectCurves,
1318 OperatorLoftCurves,
1319 OperatorSweepCurves,
1320 OperatorBirail,
1321 OperatorSplinesSetResolution,
1322 OperatorSplinesRemoveZeroSegment,
1323 OperatorSplinesRemoveShort,
1324 OperatorSplinesJoinNeighbouring,
1325 ConvertSelectedFacesToBezier,
1326 ConvertBezierToSurface,
1327 BezierPointsFillet,
1328 BezierDivide,
1329 CurveScaleReset,
1330 Split,
1331 SeparateOutline,
1332 CurveBoolean,
1333 SetFirstPoints,