Merge branch 'blender-v3.6-release'
[blender-addons.git] / curve_tools / operators.py
blobd4ada3187e32bd39feed3ce1d84e9c4de6ac6971
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"
148 bl_options = {'UNDO'}
151 @classmethod
152 def poll(cls, context):
153 return util.Selected1Curve()
156 def execute(self, context):
159 blCurve = context.active_object
160 blSpline = blCurve.data.splines[0]
161 newOrigin = blCurve.matrix_world @ blSpline.bezier_points[0].co
163 origOrigin = bpy.context.scene.cursor.location.copy()
164 self.report({'INFO'}, "origOrigin: %.6f, %.6f, %.6f" % (origOrigin.x, origOrigin.y, origOrigin.z))
165 self.report({'INFO'}, "newOrigin: %.6f, %.6f, %.6f" % (newOrigin.x, newOrigin.y, newOrigin.z))
167 current_mode = bpy.context.object.mode
169 bpy.ops.object.mode_set(mode = 'OBJECT')
170 bpy.context.scene.cursor.location = newOrigin
171 bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
172 bpy.context.scene.cursor.location = origOrigin
174 bpy.ops.object.mode_set (mode = current_mode)
176 return {'FINISHED'}
180 # 2 CURVES SELECTED
181 # #################
182 class OperatorIntersectCurves(bpy.types.Operator):
183 bl_idname = "curvetools.operatorintersectcurves"
184 bl_label = "Intersect"
185 bl_description = "Intersects selected curves"
186 bl_options = {'UNDO'}
189 @classmethod
190 def poll(cls, context):
191 return util.Selected2OrMoreCurves()
194 def execute(self, context):
195 print("### TODO: OperatorIntersectcurves.execute()")
197 algo = context.scene.curvetools.IntersectCurvesAlgorithm
198 print("-- algo:", algo)
201 mode = context.scene.curvetools.IntersectCurvesMode
202 print("-- mode:", mode)
203 # if mode == 'Split':
204 # self.report({'WARNING'}, "'Split' mode is not implemented yet -- <<STOPPING>>")
205 # return {'CANCELLED'}
207 affect = context.scene.curvetools.IntersectCurvesAffect
208 print("-- affect:", affect)
210 selected_objects = context.selected_objects
211 lenodjs = len(selected_objects)
212 print('lenodjs:', lenodjs)
213 for i in range(0, lenodjs):
214 for j in range(0, lenodjs):
215 if j != i:
216 bpy.ops.object.select_all(action='DESELECT')
217 selected_objects[i].select_set(True)
218 selected_objects[j].select_set(True)
220 if selected_objects[i].type == 'CURVE' and selected_objects[j].type == 'CURVE':
221 curveIntersector = intersections.CurvesIntersector.FromSelection()
222 rvIntersectionNrs = curveIntersector.CalcAndApplyIntersections()
224 self.report({'INFO'}, "Active curve points: %d; other curve points: %d" % (rvIntersectionNrs[0], rvIntersectionNrs[1]))
226 for obj in selected_objects:
227 obj.select_set(True)
229 return {'FINISHED'}
231 # ------------------------------------------------------------
232 # OperatorLoftCurves
234 class OperatorLoftCurves(bpy.types.Operator):
235 bl_idname = "curvetools.operatorloftcurves"
236 bl_label = "Loft"
237 bl_description = "Lofts selected curves"
238 bl_options = {'UNDO'}
241 @classmethod
242 def poll(cls, context):
243 return util.Selected2Curves()
246 def execute(self, context):
247 #print("### TODO: OperatorLoftcurves.execute()")
249 loftedSurface = surfaces.LoftedSurface.FromSelection()
250 loftedSurface.AddToScene()
252 self.report({'INFO'}, "OperatorLoftcurves.execute()")
254 return {'FINISHED'}
257 # ------------------------------------------------------------
258 # OperatorSweepCurves
260 class OperatorSweepCurves(bpy.types.Operator):
261 bl_idname = "curvetools.operatorsweepcurves"
262 bl_label = "Sweep"
263 bl_description = "Sweeps the active curve along to other curve (rail)"
264 bl_options = {'UNDO'}
267 @classmethod
268 def poll(cls, context):
269 return util.Selected2Curves()
272 def execute(self, context):
273 #print("### TODO: OperatorSweepcurves.execute()")
275 sweptSurface = surfaces.SweptSurface.FromSelection()
276 sweptSurface.AddToScene()
278 self.report({'INFO'}, "OperatorSweepcurves.execute()")
280 return {'FINISHED'}
284 # 3 CURVES SELECTED
285 # #################
286 class OperatorBirail(bpy.types.Operator):
287 bl_idname = "curvetools.operatorbirail"
288 bl_label = "Birail"
289 bl_description = "Generates a birailed surface from 3 selected curves -- in order: rail1, rail2 and profile"
290 bl_options = {'UNDO'}
293 @classmethod
294 def poll(cls, context):
295 return util.Selected3Curves()
298 def execute(self, context):
299 birailedSurface = surfaces.BirailedSurface.FromSelection()
300 birailedSurface.AddToScene()
302 self.report({'INFO'}, "OperatorBirail.execute()")
304 return {'FINISHED'}
308 # 1 OR MORE CURVES SELECTED
309 # #########################
310 class OperatorSplinesSetResolution(bpy.types.Operator):
311 bl_idname = "curvetools.operatorsplinessetresolution"
312 bl_label = "SplinesSetResolution"
313 bl_description = "Sets the resolution of all splines"
314 bl_options = {'UNDO'}
317 @classmethod
318 def poll(cls, context):
319 return util.Selected1OrMoreCurves()
322 def execute(self, context):
323 splRes = context.scene.curvetools.SplineResolution
324 selCurves = util.GetSelectedCurves()
326 for blCurve in selCurves:
327 for spline in blCurve.data.splines:
328 spline.resolution_u = splRes
330 return {'FINISHED'}
332 # ------------------------------------------------------------
333 # OperatorSplinesRemoveZeroSegment
335 class OperatorSplinesRemoveZeroSegment(bpy.types.Operator):
336 bl_idname = "curvetools.operatorsplinesremovezerosegment"
337 bl_label = "SplinesRemoveZeroSegment"
338 bl_description = "Removes splines with no segments -- they seem to creep up, sometimes"
339 bl_options = {'UNDO'}
342 @classmethod
343 def poll(cls, context):
344 return util.Selected1OrMoreCurves()
347 def execute(self, context):
348 selCurves = util.GetSelectedCurves()
350 for blCurve in selCurves:
351 curve = curves.Curve(blCurve)
352 nrSplines = curve.nrSplines
354 splinesToRemove = []
355 for spline in curve.splines:
356 if len(spline.segments) < 1: splinesToRemove.append(spline)
357 nrRemovedSplines = len(splinesToRemove)
359 for spline in splinesToRemove: curve.splines.remove(spline)
361 if nrRemovedSplines > 0: curve.RebuildInScene()
363 self.report({'INFO'}, "Removed %d of %d splines" % (nrRemovedSplines, nrSplines))
365 return {'FINISHED'}
367 # ------------------------------------------------------------
368 # OperatorSplinesRemoveShort
370 class OperatorSplinesRemoveShort(bpy.types.Operator):
371 bl_idname = "curvetools.operatorsplinesremoveshort"
372 bl_label = "SplinesRemoveShort"
373 bl_description = "Removes splines with a length smaller than the threshold"
374 bl_options = {'UNDO'}
377 @classmethod
378 def poll(cls, context):
379 return util.Selected1OrMoreCurves()
382 def execute(self, context):
383 threshold = context.scene.curvetools.SplineRemoveLength
384 selCurves = util.GetSelectedCurves()
386 for blCurve in selCurves:
387 curve = curves.Curve(blCurve)
388 nrSplines = curve.nrSplines
390 nrRemovedSplines = curve.RemoveShortSplines(threshold)
391 if nrRemovedSplines > 0: curve.RebuildInScene()
393 self.report({'INFO'}, "Removed %d of %d splines" % (nrRemovedSplines, nrSplines))
395 return {'FINISHED'}
397 # ------------------------------------------------------------
398 # OperatorSplinesJoinNeighbouring
400 class OperatorSplinesJoinNeighbouring(bpy.types.Operator):
401 bl_idname = "curvetools.operatorsplinesjoinneighbouring"
402 bl_label = "SplinesJoinNeighbouring"
403 bl_description = "Joins neighbouring splines within a distance smaller than the threshold"
404 bl_options = {'UNDO'}
407 @classmethod
408 def poll(cls, context):
409 return util.Selected1OrMoreCurves()
412 def execute(self, context):
413 selCurves = util.GetSelectedCurves()
415 for blCurve in selCurves:
416 curve = curves.Curve(blCurve)
417 nrSplines = curve.nrSplines
419 threshold = context.scene.curvetools.SplineJoinDistance
420 startEnd = context.scene.curvetools.SplineJoinStartEnd
421 mode = context.scene.curvetools.SplineJoinMode
423 nrJoins = curve.JoinNeighbouringSplines(startEnd, threshold, mode)
424 if nrJoins > 0: curve.RebuildInScene()
426 self.report({'INFO'}, "Applied %d joins on %d splines; resulting nrSplines: %d" % (nrJoins, nrSplines, curve.nrSplines))
428 return {'FINISHED'}
430 # ------------------------------------------------------------
431 # SurfaceFromBezier
433 def SurfaceFromBezier(surfacedata, points, center):
435 len_points = len(points) - 1
437 if len_points % 2 == 0:
438 h = mathematics.subdivide_cubic_bezier(
439 points[len_points].co, points[len_points].handle_right,
440 points[0].handle_left, points[0].co, 0.5
442 points.add(1)
443 len_points = len(points) - 1
444 points[len_points - 1].handle_right = h[0]
445 points[len_points].handle_left = h[1]
446 points[len_points].co = h[2]
447 points[len_points].handle_right = h[3]
448 points[0].handle_left = h[4]
450 half = round((len_points + 1)/2) - 1
452 surfacespline1 = surfacedata.splines.new(type='NURBS')
453 surfacespline1.points.add(3)
454 surfacespline1.points[0].co = [points[0].co.x, points[0].co.y, points[0].co.z, 1]
455 surfacespline1.points[1].co = [points[0].handle_left.x, points[0].handle_left.y, points[0].handle_left.z, 1]
456 surfacespline1.points[2].co = [points[len_points].handle_right.x,points[len_points].handle_right.y, points[len_points].handle_right.z, 1]
457 surfacespline1.points[3].co = [points[len_points].co.x, points[len_points].co.y, points[len_points].co.z, 1]
458 for p in surfacespline1.points:
459 p.select = True
460 surfacespline1.use_endpoint_u = True
461 surfacespline1.use_endpoint_v = True
463 for i in range(0, half):
465 if center:
467 surfacespline2 = surfacedata.splines.new(type='NURBS')
468 surfacespline2.points.add(3)
469 surfacespline2.points[0].co = [points[i].co.x, points[i].co.y, points[i].co.z, 1]
470 surfacespline2.points[1].co = [(points[i].co.x + points[len_points - i].co.x)/2,
471 (points[i].co.y + points[len_points - i].co.y)/2,
472 (points[i].co.z + points[len_points - i].co.z)/2, 1]
473 surfacespline2.points[2].co = [(points[len_points - i].co.x + points[i].co.x)/2,
474 (points[len_points - i].co.y + points[i].co.y)/2,
475 (points[len_points - i].co.z + points[i].co.z)/2, 1]
476 surfacespline2.points[3].co = [points[len_points - i].co.x, points[len_points - i].co.y, points[len_points - i].co.z, 1]
477 for p in surfacespline2.points:
478 p.select = True
479 surfacespline2.use_endpoint_u = True
480 surfacespline2.use_endpoint_v = True
483 surfacespline3 = surfacedata.splines.new(type='NURBS')
484 surfacespline3.points.add(3)
485 surfacespline3.points[0].co = [points[i].handle_right.x, points[i].handle_right.y, points[i].handle_right.z, 1]
486 surfacespline3.points[1].co = [(points[i].handle_right.x + points[len_points - i].handle_left.x)/2,
487 (points[i].handle_right.y + points[len_points - i].handle_left.y)/2,
488 (points[i].handle_right.z + points[len_points - i].handle_left.z)/2, 1]
489 surfacespline3.points[2].co = [(points[len_points - i].handle_left.x + points[i].handle_right.x)/2,
490 (points[len_points - i].handle_left.y + points[i].handle_right.y)/2,
491 (points[len_points - i].handle_left.z + points[i].handle_right.z)/2, 1]
492 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]
493 for p in surfacespline3.points:
494 p.select = True
495 surfacespline3.use_endpoint_u = True
496 surfacespline3.use_endpoint_v = True
499 surfacespline4 = surfacedata.splines.new(type='NURBS')
500 surfacespline4.points.add(3)
501 surfacespline4.points[0].co = [points[i + 1].handle_left.x, points[i + 1].handle_left.y, points[i + 1].handle_left.z, 1]
502 surfacespline4.points[1].co = [(points[i + 1].handle_left.x + points[len_points - i - 1].handle_right.x)/2,
503 (points[i + 1].handle_left.y + points[len_points - i - 1].handle_right.y)/2,
504 (points[i + 1].handle_left.z + points[len_points - i - 1].handle_right.z)/2, 1]
505 surfacespline4.points[2].co = [(points[len_points - i - 1].handle_right.x + points[i + 1].handle_left.x)/2,
506 (points[len_points - i - 1].handle_right.y + points[i + 1].handle_left.y)/2,
507 (points[len_points - i - 1].handle_right.z + points[i + 1].handle_left.z)/2, 1]
508 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]
509 for p in surfacespline4.points:
510 p.select = True
511 surfacespline4.use_endpoint_u = True
512 surfacespline4.use_endpoint_v = True
514 if center:
516 surfacespline5 = surfacedata.splines.new(type='NURBS')
517 surfacespline5.points.add(3)
518 surfacespline5.points[0].co = [points[i + 1].co.x, points[i + 1].co.y, points[i + 1].co.z, 1]
519 surfacespline5.points[1].co = [(points[i + 1].co.x + points[len_points - i - 1].co.x)/2,
520 (points[i + 1].co.y + points[len_points - i - 1].co.y)/2,
521 (points[i + 1].co.z + points[len_points - i - 1].co.z)/2, 1]
522 surfacespline5.points[2].co = [(points[len_points - i - 1].co.x + points[i + 1].co.x)/2,
523 (points[len_points - i - 1].co.y + points[i + 1].co.y)/2,
524 (points[len_points - i - 1].co.z + points[i + 1].co.z)/2, 1]
525 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]
526 for p in surfacespline5.points:
527 p.select = True
528 surfacespline5.use_endpoint_u = True
529 surfacespline5.use_endpoint_v = True
532 surfacespline6 = surfacedata.splines.new(type='NURBS')
533 surfacespline6.points.add(3)
534 surfacespline6.points[0].co = [points[half].co.x, points[half].co.y, points[half].co.z, 1]
535 surfacespline6.points[1].co = [points[half].handle_right.x, points[half].handle_right.y, points[half].handle_right.z, 1]
536 surfacespline6.points[2].co = [points[half+1].handle_left.x, points[half+1].handle_left.y, points[half+1].handle_left.z, 1]
537 surfacespline6.points[3].co = [points[half+1].co.x, points[half+1].co.y, points[half+1].co.z, 1]
538 for p in surfacespline6.points:
539 p.select = True
540 surfacespline6.use_endpoint_u = True
541 surfacespline6.use_endpoint_v = True
543 bpy.ops.object.mode_set(mode = 'EDIT')
544 bpy.ops.curve.make_segment()
546 for s in surfacedata.splines:
547 s.resolution_u = 4
548 s.resolution_v = 4
549 s.order_u = 4
550 s.order_v = 4
551 for p in s.points:
552 p.select = False
554 # ------------------------------------------------------------
555 # Convert selected faces to Bezier
557 class ConvertSelectedFacesToBezier(bpy.types.Operator):
558 bl_idname = "curvetools.convert_selected_face_to_bezier"
559 bl_label = "Convert selected faces to Bezier"
560 bl_description = "Convert selected faces to Bezier"
561 bl_options = {'REGISTER', 'UNDO'}
563 @classmethod
564 def poll(cls, context):
565 return util.Selected1Mesh()
567 def execute(self, context):
568 # main function
569 bpy.ops.object.mode_set(mode = 'OBJECT')
570 active_object = context.active_object
571 meshdata = active_object.data
572 curvedata = bpy.data.curves.new('Curve' + active_object.name, type='CURVE')
573 curveobject = object_utils.object_data_add(context, curvedata)
574 curvedata.dimensions = '3D'
576 for poly in meshdata.polygons:
577 if poly.select:
578 newSpline = curvedata.splines.new(type='BEZIER')
579 newSpline.use_cyclic_u = True
580 newSpline.bezier_points.add(poly.loop_total - 1)
581 npoint = 0
582 for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total):
583 newSpline.bezier_points[npoint].co = meshdata.vertices[meshdata.loops[loop_index].vertex_index].co
584 newSpline.bezier_points[npoint].handle_left_type = 'VECTOR'
585 newSpline.bezier_points[npoint].handle_right_type = 'VECTOR'
586 newSpline.bezier_points[npoint].select_control_point = True
587 newSpline.bezier_points[npoint].select_left_handle = True
588 newSpline.bezier_points[npoint].select_right_handle = True
589 npoint += 1
591 return {'FINISHED'}
593 # ------------------------------------------------------------
594 # Convert Bezier to Surface
596 class ConvertBezierToSurface(bpy.types.Operator):
597 bl_idname = "curvetools.convert_bezier_to_surface"
598 bl_label = "Convert Bezier to Surface"
599 bl_description = "Convert Bezier to Surface"
600 bl_options = {'REGISTER', 'UNDO'}
602 Center : BoolProperty(
603 name="Center",
604 default=False,
605 description="Consider center points"
608 Resolution_U: IntProperty(
609 name="Resolution_U",
610 default=4,
611 min=1, max=64,
612 soft_min=1,
613 description="Surface resolution U"
616 Resolution_V: IntProperty(
617 name="Resolution_V",
618 default=4,
619 min=1, max=64,
620 soft_min=1,
621 description="Surface resolution V"
624 def draw(self, context):
625 layout = self.layout
627 # general options
628 col = layout.column()
629 col.prop(self, 'Center')
630 col.prop(self, 'Resolution_U')
631 col.prop(self, 'Resolution_V')
633 @classmethod
634 def poll(cls, context):
635 return util.Selected1OrMoreCurves()
637 def execute(self, context):
638 # main function
639 bpy.ops.object.mode_set(mode = 'OBJECT')
640 active_object = context.active_object
641 curvedata = active_object.data
643 surfacedata = bpy.data.curves.new('Surface', type='SURFACE')
644 surfaceobject = object_utils.object_data_add(context, surfacedata)
645 surfaceobject.matrix_world = active_object.matrix_world
646 surfaceobject.rotation_euler = active_object.rotation_euler
647 surfacedata.dimensions = '3D'
648 surfaceobject.show_wire = True
649 surfaceobject.show_in_front = True
651 for spline in curvedata.splines:
652 SurfaceFromBezier(surfacedata, spline.bezier_points, self.Center)
654 for spline in surfacedata.splines:
655 len_p = len(spline.points)
656 len_devide_4 = round(len_p / 4) + 1
657 len_devide_2 = round(len_p / 2)
658 bpy.ops.object.mode_set(mode = 'EDIT')
659 for point_index in range(len_devide_4, len_p - len_devide_4):
660 if point_index != len_devide_2 and point_index != len_devide_2 - 1:
661 spline.points[point_index].select = True
663 surfacedata.resolution_u = self.Resolution_U
664 surfacedata.resolution_v = self.Resolution_V
666 return {'FINISHED'}
668 # ------------------------------------------------------------
669 # Fillet
671 class BezierPointsFillet(bpy.types.Operator):
672 bl_idname = "curvetools.bezier_points_fillet"
673 bl_label = "Bezier points Fillet"
674 bl_description = "Bezier points Fillet"
675 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
677 Fillet_radius : FloatProperty(
678 name="Radius",
679 default=0.25,
680 unit='LENGTH',
681 description="Radius"
683 Types = [('Round', "Round", "Round"),
684 ('Chamfer', "Chamfer", "Chamfer")]
685 Fillet_Type : EnumProperty(
686 name="Type",
687 description="Fillet type",
688 items=Types
691 def draw(self, context):
692 layout = self.layout
694 # general options
695 col = layout.column()
696 col.prop(self, "Fillet_radius")
697 col.prop(self, "Fillet_Type", expand=True)
699 @classmethod
700 def poll(cls, context):
701 return util.Selected1OrMoreCurves()
703 def execute(self, context):
704 # main function
705 if bpy.ops.object.mode_set.poll():
706 bpy.ops.object.mode_set(mode='EDIT')
708 splines = bpy.context.object.data.splines
709 bpy.ops.curve.spline_type_set(type='BEZIER')
711 bpy.ops.curve.handle_type_set(type='VECTOR')
712 s = []
713 for spline in splines:
714 n = 0
715 ii = []
716 for p in spline.bezier_points:
717 if p.select_control_point:
718 ii.append(n)
719 n += 1
720 else:
721 n += 1
722 s.append(ii)
724 sn = 0
725 for spline in splines:
726 ii = s[sn]
727 bezier_points = spline.bezier_points
728 n = len(bezier_points)
729 if n > 2:
730 jn = 0
731 for j in ii:
732 j += jn
734 bpy.ops.curve.select_all(action='DESELECT')
736 if j != 0 and j != n - 1:
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[j - 1], bezier_points[j],
741 bezier_points[j + 1], bezier_points[j + 2]]
742 jn += 1
743 n += 1
745 elif j == 0:
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[n], bezier_points[0],
750 bezier_points[1], bezier_points[2]]
751 jn += 1
752 n += 1
754 elif j == n - 1:
755 bezier_points[j].select_control_point = True
756 bezier_points[j - 1].select_control_point = True
757 bpy.ops.curve.subdivide()
758 selected4 = [bezier_points[0], bezier_points[n],
759 bezier_points[n - 1], bezier_points[n - 2]]
761 selected4[2].co = selected4[1].co
762 s1 = Vector(selected4[0].co) - Vector(selected4[1].co)
763 s2 = Vector(selected4[3].co) - Vector(selected4[2].co)
764 s1.normalize()
765 s11 = Vector(selected4[1].co) + s1 * self.Fillet_radius
766 selected4[1].co = s11
767 s2.normalize()
768 s22 = Vector(selected4[2].co) + s2 * self.Fillet_radius
769 selected4[2].co = s22
771 if self.Fillet_Type == 'Round':
772 if j != n - 1:
773 selected4[2].handle_right_type = 'VECTOR'
774 selected4[1].handle_left_type = 'VECTOR'
775 selected4[1].handle_right_type = 'ALIGNED'
776 selected4[2].handle_left_type = 'ALIGNED'
777 else:
778 selected4[1].handle_right_type = 'VECTOR'
779 selected4[2].handle_left_type = 'VECTOR'
780 selected4[2].handle_right_type = 'ALIGNED'
781 selected4[1].handle_left_type = 'ALIGNED'
782 if self.Fillet_Type == 'Chamfer':
783 selected4[2].handle_right_type = 'VECTOR'
784 selected4[1].handle_left_type = 'VECTOR'
785 selected4[1].handle_right_type = 'VECTOR'
786 selected4[2].handle_left_type = 'VECTOR'
787 sn += 1
789 return {'FINISHED'}
791 # ------------------------------------------------------------
792 # BezierDivide Operator
794 class BezierDivide(bpy.types.Operator):
795 bl_idname = "curvetools.bezier_spline_divide"
796 bl_label = "Bezier Spline Divide"
797 bl_description = "Bezier Divide (enters edit mode) for Fillet Curves"
798 bl_options = {'REGISTER', 'UNDO'}
800 # align_matrix for the invoke
801 align_matrix : Matrix()
803 Bezier_t : FloatProperty(
804 name="t (0% - 100%)",
805 default=50.0,
806 min=0.0, soft_min=0.0,
807 max=100.0, soft_max=100.0,
808 description="t (0% - 100%)"
811 @classmethod
812 def poll(cls, context):
813 return util.Selected1OrMoreCurves()
815 def execute(self, context):
816 # main function
817 if bpy.ops.object.mode_set.poll():
818 bpy.ops.object.mode_set(mode='EDIT')
820 splines = bpy.context.object.data.splines
821 s = []
822 for spline in splines:
823 bpy.ops.curve.spline_type_set(type='BEZIER')
825 n = 0
826 ii = []
827 for p in spline.bezier_points:
828 if p.select_control_point:
829 ii.append(n)
830 n += 1
831 else:
832 n += 1
833 s.append(ii)
835 sn = 0
836 for spline in splines:
837 ii = s[sn]
838 bezier_points = spline.bezier_points
839 n = len(bezier_points)
840 if n > 2:
841 jn = 0
842 for j in ii:
844 bpy.ops.curve.select_all(action='DESELECT')
846 if (j in ii) and (j + 1 in ii):
847 bezier_points[j + jn].select_control_point = True
848 bezier_points[j + 1 + jn].select_control_point = True
849 h = mathematics.subdivide_cubic_bezier(
850 bezier_points[j + jn].co, bezier_points[j + jn].handle_right,
851 bezier_points[j + 1 + jn].handle_left, bezier_points[j + 1 + jn].co, self.Bezier_t / 100
853 bpy.ops.curve.subdivide(1)
854 bezier_points[j + jn].handle_right_type = 'FREE'
855 bezier_points[j + jn].handle_right = h[0]
856 bezier_points[j + 1 + jn].co = h[2]
857 bezier_points[j + 1 + jn].handle_left_type = 'FREE'
858 bezier_points[j + 1 + jn].handle_left = h[1]
859 bezier_points[j + 1 + jn].handle_right_type = 'FREE'
860 bezier_points[j + 1 + jn].handle_right = h[3]
861 bezier_points[j + 2 + jn].handle_left_type = 'FREE'
862 bezier_points[j + 2 + jn].handle_left = h[4]
863 jn += 1
865 if j == n - 1 and (0 in ii) and spline.use_cyclic_u:
866 bezier_points[j + jn].select_control_point = True
867 bezier_points[0].select_control_point = True
868 h = mathematics.subdivide_cubic_bezier(
869 bezier_points[j + jn].co, bezier_points[j + jn].handle_right,
870 bezier_points[0].handle_left, bezier_points[0].co, self.Bezier_t / 100
872 bpy.ops.curve.subdivide(1)
873 bezier_points[j + jn].handle_right_type = 'FREE'
874 bezier_points[j + jn].handle_right = h[0]
875 bezier_points[j + 1 + jn].co = h[2]
876 bezier_points[j + 1 + jn].handle_left_type = 'FREE'
877 bezier_points[j + 1 + jn].handle_left = h[1]
878 bezier_points[j + 1 + jn].handle_right_type = 'FREE'
879 bezier_points[j + 1 + jn].handle_right = h[3]
880 bezier_points[0].handle_left_type = 'FREE'
881 bezier_points[0].handle_left = h[4]
883 sn += 1
885 return {'FINISHED'}
887 # ------------------------------------------------------------
888 # CurveScaleReset Operator
890 class CurveScaleReset(bpy.types.Operator):
891 bl_idname = "curvetools.scale_reset"
892 bl_label = "Curve Scale Reset"
893 bl_description = "Curve Scale Reset"
894 bl_options = {'REGISTER', 'UNDO'}
896 @classmethod
897 def poll(cls, context):
898 return (context.object is not None and
899 context.object.type == 'CURVE')
901 def execute(self, context):
902 # main function
903 current_mode = bpy.context.object.mode
905 bpy.ops.object.mode_set(mode = 'OBJECT')
907 oldCurve = context.active_object
908 oldCurveName = oldCurve.name
910 bpy.ops.object.duplicate_move(OBJECT_OT_duplicate=None, TRANSFORM_OT_translate=None)
911 newCurve = context.active_object
912 newCurve.data.splines.clear()
913 newCurve.scale = (1.0, 1.0, 1.0)
915 oldCurve.select_set(True)
916 newCurve.select_set(True)
917 bpy.context.view_layer.objects.active = newCurve
918 bpy.ops.object.join()
920 joinCurve = context.active_object
921 joinCurve.name = oldCurveName
923 bpy.ops.object.mode_set (mode = current_mode)
925 return {'FINISHED'}
927 # ------------------------------------------------------------
928 # Split Operator
930 class Split(bpy.types.Operator):
931 bl_idname = "curvetools.split"
932 bl_label = "Split"
933 bl_options = {'REGISTER', 'UNDO'}
935 @classmethod
936 def poll(cls, context):
937 return util.Selected1OrMoreCurves()
939 def execute(self, context):
940 selected_Curves = util.GetSelectedCurves()
942 for curve in selected_Curves:
943 spline_points = []
944 select_points = {}
945 bezier_spline_points = []
946 select_bezier_points = {}
947 i_bp = 0
948 i_p = 0
949 for spline in curve.data.splines:
950 if spline.type == 'BEZIER':
951 points = {}
952 select_bezier_points[i_bp] = [len(spline.bezier_points)]
953 for i in range(len(spline.bezier_points)):
954 bezier_point = spline.bezier_points[i]
955 points[i]=[bezier_point.co[:], bezier_point.handle_left[:], bezier_point.handle_right[:]]
957 if spline.bezier_points[i].select_control_point:
958 select_bezier_points[i_bp].append(i)
959 i_bp+=1
960 bezier_spline_points.append(points)
961 else:
962 points = {}
963 select_points[i_p] = [len(spline.points)]
964 for i in range(len(spline.points)):
965 point = spline.points[i]
966 points[i]=[point.co[:], spline.type]
967 if spline.points[i].select:
968 select_points[i_p].append(i)
969 i_p+=1
970 spline_points.append(points)
972 curve.data.splines.clear()
974 for key in select_bezier_points:
976 num=0
978 if select_bezier_points[key][-1] == select_bezier_points[key][0]-1:
979 select_bezier_points[key].pop()
981 for i in select_bezier_points[key][1:]+[select_bezier_points[key][0]-1]:
982 if i != 0:
983 spline = curve.data.splines.new('BEZIER')
984 spline.bezier_points.add(i-num)
986 for j in range(num, i):
987 bezier_point = spline.bezier_points[j-num]
989 bezier_point.co = bezier_spline_points[key][j][0]
990 bezier_point.handle_left = bezier_spline_points[key][j][1]
991 bezier_point.handle_right = bezier_spline_points[key][j][2]
992 bezier_point = spline.bezier_points[-1]
993 bezier_point.co = bezier_spline_points[key][i][0]
994 bezier_point.handle_left = bezier_spline_points[key][i][1]
995 bezier_point.handle_right = bezier_spline_points[key][i][2]
996 num=i
998 for key in select_points:
1000 num=0
1002 if select_points[key][-1] == select_points[key][0]-1:
1003 select_points[key].pop()
1005 for i in select_points[key][1:]+[select_points[key][0]-1]:
1006 if i != 0:
1007 spline = curve.data.splines.new(spline_points[key][i][1])
1008 spline.points.add(i-num)
1010 for j in range(num, i):
1011 point = spline.points[j-num]
1013 point.co = spline_points[key][j][0]
1014 point = spline.points[-1]
1015 point.co = spline_points[key][i][0]
1016 num=i
1018 return {'FINISHED'}
1020 class SeparateOutline(bpy.types.Operator):
1021 bl_idname = "curvetools.sep_outline"
1022 bl_label = "Separate Outline"
1023 bl_options = {'REGISTER', 'UNDO'}
1024 bl_description = "Makes 'Outline' separate mesh"
1026 @classmethod
1027 def poll(cls, context):
1028 return util.Selected1OrMoreCurves()
1030 def execute(self, context):
1031 bpy.ops.object.mode_set(mode = 'EDIT')
1032 bpy.ops.curve.separate()
1034 return {'FINISHED'}
1036 class CurveBoolean(bpy.types.Operator):
1037 bl_idname = "curvetools.bezier_curve_boolean"
1038 bl_description = "Curve Boolean"
1039 bl_label = "Curve Boolean"
1040 bl_options = {'REGISTER', 'UNDO'}
1042 operation: bpy.props.EnumProperty(name='Type', items=[
1043 ('UNION', 'Union', 'Boolean OR', 0),
1044 ('INTERSECTION', 'Intersection', 'Boolean AND', 1),
1045 ('DIFFERENCE', 'Difference', 'Active minus Selected', 2),
1047 number : IntProperty(
1048 name="Spline Number",
1049 default=1,
1050 min=1,
1051 description="Spline Number"
1054 @classmethod
1055 def poll(cls, context):
1056 return util.Selected1OrMoreCurves()
1058 def draw(self, context):
1059 layout = self.layout
1061 # general options
1062 col = layout.column()
1063 col.prop(self, "operation")
1064 if self.operation == 'DIFFERENCE':
1065 col.prop(self, "number")
1067 def execute(self, context):
1068 current_mode = bpy.context.object.mode
1070 if bpy.ops.object.mode_set.poll():
1071 bpy.ops.object.mode_set(mode = 'OBJECT')
1073 selected_Curves = util.GetSelectedCurves()
1074 len_selected_curves = len(selected_Curves)
1075 if len_selected_curves < 2:
1076 return {'FINISHED'}
1078 min_number = 1
1080 max_number = 0
1081 for iCurve in range(0, len_selected_curves):
1082 len_splines = len(selected_Curves[iCurve].data.splines)
1083 max_number += len_splines
1085 if self.number < min_number:
1086 self.number = min_number
1087 if self.number > max_number:
1088 self.number = max_number
1090 j = 0
1091 first_curve = 0
1092 first_spline = 0
1093 for iCurve in range(0, len_selected_curves):
1094 len_splines = len(selected_Curves[iCurve].data.splines)
1095 for iSpline in range(0, len_splines):
1096 if j == self.number:
1097 first_curve = iCurve
1098 first_spline = iSpline
1099 j += 1
1101 bpy.ops.object.select_all(action='DESELECT')
1103 spline1 = selected_Curves[first_curve].data.splines[first_spline]
1104 matrix_world1 = selected_Curves[first_curve].matrix_world
1106 len_spline1 = len(spline1.bezier_points)
1108 dataCurve = bpy.data.curves.new(self.operation, type='CURVE')
1109 dataCurve.dimensions = '2D'
1110 newSpline1 = dataCurve.splines.new(type='BEZIER')
1111 newSpline1.use_cyclic_u = True
1112 newSpline1.bezier_points.add(len_spline1 - 1)
1113 for n in range(0, len_spline1):
1114 newSpline1.bezier_points[n].co = matrix_world1 @ spline1.bezier_points[n].co
1115 newSpline1.bezier_points[n].handle_left_type = spline1.bezier_points[n].handle_left_type
1116 newSpline1.bezier_points[n].handle_left = matrix_world1 @ spline1.bezier_points[n].handle_left
1117 newSpline1.bezier_points[n].handle_right_type = spline1.bezier_points[n].handle_right_type
1118 newSpline1.bezier_points[n].handle_right = matrix_world1 @ spline1.bezier_points[n].handle_right
1120 Curve = object_utils.object_data_add(context, dataCurve)
1121 bpy.context.view_layer.objects.active = Curve
1122 Curve.select_set(True)
1123 Curve.location = (0.0, 0.0, 0.0)
1125 j = 0
1126 for iCurve in range(0, len_selected_curves):
1127 matrix_world = selected_Curves[iCurve].matrix_world
1128 len_splines = len(selected_Curves[iCurve].data.splines)
1129 for iSpline in range(0, len_splines):
1130 if iCurve == first_curve and iSpline == first_spline:
1131 continue
1132 spline = selected_Curves[iCurve].data.splines[iSpline]
1133 len_spline = len(spline.bezier_points)
1134 newSpline = dataCurve.splines.new(type='BEZIER')
1135 newSpline.use_cyclic_u = True
1136 newSpline.bezier_points.add(len_spline - 1)
1137 for n in range(0, len_spline):
1138 newSpline.bezier_points[n].co = matrix_world @ spline.bezier_points[n].co
1139 newSpline.bezier_points[n].handle_left_type = spline.bezier_points[n].handle_left_type
1140 newSpline.bezier_points[n].handle_left = matrix_world @ spline.bezier_points[n].handle_left
1141 newSpline.bezier_points[n].handle_right_type = spline.bezier_points[n].handle_right_type
1142 newSpline.bezier_points[n].handle_right = matrix_world @ spline.bezier_points[n].handle_right
1144 bpy.ops.object.mode_set(mode = 'EDIT')
1145 bpy.ops.curve.select_all(action='SELECT')
1146 splines = internal.getSelectedSplines(True, True)
1147 if len(splines) < 2:
1148 continue
1149 splineA = splines[0]
1150 splineB = splines[1]
1151 dataCurve.splines.active = newSpline1
1153 if not internal.bezierBooleanGeometry(splineA, splineB, self.operation):
1154 self.report({'WARNING'}, 'Invalid selection.')
1155 return {'CANCELLED'}
1157 j += 1
1159 bpy.ops.object.mode_set(mode = 'EDIT')
1160 bpy.ops.curve.select_all(action='SELECT')
1162 return {'FINISHED'}
1164 # ----------------------------
1165 # Set first points operator
1166 class SetFirstPoints(bpy.types.Operator):
1167 bl_idname = "curvetools.set_first_points"
1168 bl_label = "Set first points"
1169 bl_description = "Set the selected points as the first point of each spline"
1170 bl_options = {'REGISTER', 'UNDO'}
1172 @classmethod
1173 def poll(cls, context):
1174 return util.Selected1OrMoreCurves()
1176 def execute(self, context):
1177 splines_to_invert = []
1179 curve = bpy.context.object
1181 bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
1183 # Check non-cyclic splines to invert
1184 for i in range(len(curve.data.splines)):
1185 b_points = curve.data.splines[i].bezier_points
1187 if i not in self.cyclic_splines: # Only for non-cyclic splines
1188 if b_points[len(b_points) - 1].select_control_point:
1189 splines_to_invert.append(i)
1191 # Reorder points of cyclic splines, and set all handles to "Automatic"
1193 # Check first selected point
1194 cyclic_splines_new_first_pt = {}
1195 for i in self.cyclic_splines:
1196 sp = curve.data.splines[i]
1198 for t in range(len(sp.bezier_points)):
1199 bp = sp.bezier_points[t]
1200 if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
1201 cyclic_splines_new_first_pt[i] = t
1202 break # To take only one if there are more
1204 # Reorder
1205 for spline_idx in cyclic_splines_new_first_pt:
1206 sp = curve.data.splines[spline_idx]
1208 spline_old_coords = []
1209 for bp_old in sp.bezier_points:
1210 coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
1212 left_handle_type = str(bp_old.handle_left_type)
1213 left_handle_length = float(bp_old.handle_left.length)
1214 left_handle_xyz = (
1215 float(bp_old.handle_left.x),
1216 float(bp_old.handle_left.y),
1217 float(bp_old.handle_left.z)
1219 right_handle_type = str(bp_old.handle_right_type)
1220 right_handle_length = float(bp_old.handle_right.length)
1221 right_handle_xyz = (
1222 float(bp_old.handle_right.x),
1223 float(bp_old.handle_right.y),
1224 float(bp_old.handle_right.z)
1226 spline_old_coords.append(
1227 [coords, left_handle_type,
1228 right_handle_type, left_handle_length,
1229 right_handle_length, left_handle_xyz,
1230 right_handle_xyz]
1233 for t in range(len(sp.bezier_points)):
1234 bp = sp.bezier_points
1236 if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
1237 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
1238 else:
1239 new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
1241 bp[t].co = Vector(spline_old_coords[new_index][0])
1243 bp[t].handle_left.length = spline_old_coords[new_index][3]
1244 bp[t].handle_right.length = spline_old_coords[new_index][4]
1246 bp[t].handle_left_type = "FREE"
1247 bp[t].handle_right_type = "FREE"
1249 bp[t].handle_left.x = spline_old_coords[new_index][5][0]
1250 bp[t].handle_left.y = spline_old_coords[new_index][5][1]
1251 bp[t].handle_left.z = spline_old_coords[new_index][5][2]
1253 bp[t].handle_right.x = spline_old_coords[new_index][6][0]
1254 bp[t].handle_right.y = spline_old_coords[new_index][6][1]
1255 bp[t].handle_right.z = spline_old_coords[new_index][6][2]
1257 bp[t].handle_left_type = spline_old_coords[new_index][1]
1258 bp[t].handle_right_type = spline_old_coords[new_index][2]
1260 # Invert the non-cyclic splines designated above
1261 for i in range(len(splines_to_invert)):
1262 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1264 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1265 curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
1266 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1268 bpy.ops.curve.switch_direction()
1270 bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
1272 # Keep selected the first vert of each spline
1273 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1274 for i in range(len(curve.data.splines)):
1275 if not curve.data.splines[i].use_cyclic_u:
1276 bp = curve.data.splines[i].bezier_points[0]
1277 else:
1278 bp = curve.data.splines[i].bezier_points[
1279 len(curve.data.splines[i].bezier_points) - 1
1282 bp.select_control_point = True
1283 bp.select_right_handle = True
1284 bp.select_left_handle = True
1286 bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
1288 return {'FINISHED'}
1290 def invoke(self, context, event):
1291 curve = bpy.context.object
1293 # Check if all curves are Bezier, and detect which ones are cyclic
1294 self.cyclic_splines = []
1295 for i in range(len(curve.data.splines)):
1296 if curve.data.splines[i].type != "BEZIER":
1297 self.report({'WARNING'}, "All splines must be Bezier type")
1299 return {'CANCELLED'}
1300 else:
1301 if curve.data.splines[i].use_cyclic_u:
1302 self.cyclic_splines.append(i)
1304 self.execute(context)
1305 self.report({'INFO'}, "First points have been set")
1307 return {'FINISHED'}
1309 def register():
1310 for cls in classes:
1311 bpy.utils.register_class(operators)
1313 def unregister():
1314 for cls in classes:
1315 bpy.utils.unregister_class(operators)
1317 if __name__ == "__main__":
1318 register()
1320 operators = [
1321 OperatorCurveInfo,
1322 OperatorCurveLength,
1323 OperatorSplinesInfo,
1324 OperatorSegmentsInfo,
1325 OperatorOriginToSpline0Start,
1326 OperatorIntersectCurves,
1327 OperatorLoftCurves,
1328 OperatorSweepCurves,
1329 OperatorBirail,
1330 OperatorSplinesSetResolution,
1331 OperatorSplinesRemoveZeroSegment,
1332 OperatorSplinesRemoveShort,
1333 OperatorSplinesJoinNeighbouring,
1334 ConvertSelectedFacesToBezier,
1335 ConvertBezierToSurface,
1336 BezierPointsFillet,
1337 BezierDivide,
1338 CurveScaleReset,
1339 Split,
1340 SeparateOutline,
1341 CurveBoolean,
1342 SetFirstPoints,