Merge branch 'blender-v3.6-release'
[blender-addons.git] / add_curve_extra_objects / add_curve_torus_knots.py
blob891754c5f5859cad1f2cb2f7fde039beae18b415
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 """
4 bl_info = {
5 "name": "Torus Knots",
6 "author": "Marius Giurgi (DolphinDream), testscreenings",
7 "version": (0, 3),
8 "blender": (2, 80, 0),
9 "location": "View3D > Add > Curve",
10 "description": "Adds many types of (torus) knots",
11 "warning": "",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
13 "category": "Add Curve",
15 """
17 import bpy
18 from bpy.props import (
19 BoolProperty,
20 EnumProperty,
21 FloatProperty,
22 IntProperty
24 from math import (
25 sin, cos,
26 pi, sqrt
28 from mathutils import (
29 Vector,
30 Matrix,
32 from bpy_extras.object_utils import (
33 AddObjectHelper,
34 object_data_add
36 from random import random
37 from bpy.types import Operator
39 # Globals:
40 DEBUG = False
43 # greatest common denominator
44 def gcd(a, b):
45 if b == 0:
46 return a
47 else:
48 return gcd(b, a % b)
51 # #######################################################################
52 # ###################### Knot Definitions ###############################
53 # #######################################################################
54 def Torus_Knot(self, linkIndex=0):
55 p = self.torus_p # revolution count (around the torus center)
56 q = self.torus_q # spin count (around the torus tube)
58 N = self.torus_res # curve resolution (number of control points)
60 # use plus options only when they are enabled
61 if self.options_plus:
62 u = self.torus_u # p multiplier
63 v = self.torus_v # q multiplier
64 h = self.torus_h # height (scale along Z)
65 s = self.torus_s # torus scale (radii scale factor)
66 else: # don't use plus settings
67 u = 1
68 v = 1
69 h = 1
70 s = 1
72 R = self.torus_R * s # major radius (scaled)
73 r = self.torus_r * s # minor radius (scaled)
75 # number of decoupled links when (p,q) are NOT co-primes
76 links = gcd(p, q) # = 1 when (p,q) are co-primes
78 # parametrized angle increment (cached outside of the loop for performance)
79 # NOTE: the total angle is divided by number of decoupled links to ensure
80 # the curve does not overlap with itself when (p,q) are not co-primes
81 da = 2 * pi / links / (N - 1)
83 # link phase : each decoupled link is phased equally around the torus center
84 # NOTE: linkIndex value is in [0, links-1]
85 linkPhase = 2 * pi / q * linkIndex # = 0 when there is just ONE link
87 # user defined phasing
88 if self.options_plus:
89 rPhase = self.torus_rP # user defined revolution phase
90 sPhase = self.torus_sP # user defined spin phase
91 else: # don't use plus settings
92 rPhase = 0
93 sPhase = 0
95 rPhase += linkPhase # total revolution phase of the current link
97 if DEBUG:
98 print("")
99 print("Link: %i of %i" % (linkIndex, links))
100 print("gcd = %i" % links)
101 print("p = %i" % p)
102 print("q = %i" % q)
103 print("link phase = %.2f deg" % (linkPhase * 180 / pi))
104 print("link phase = %.2f rad" % linkPhase)
106 # flip directions ? NOTE: flipping both is equivalent to no flip
107 if self.flip_p:
108 p *= -1
109 if self.flip_q:
110 q *= -1
112 # create the 3D point array for the current link
113 newPoints = []
114 for n in range(N - 1):
115 # t = 2 * pi / links * n/(N-1) with: da = 2*pi/links/(N-1) => t = n * da
116 t = n * da
117 theta = p * t * u + rPhase # revolution angle
118 phi = q * t * v + sPhase # spin angle
120 x = (R + r * cos(phi)) * cos(theta)
121 y = (R + r * cos(phi)) * sin(theta)
122 z = r * sin(phi) * h
124 # append 3D point
125 # NOTE : the array is adjusted later as needed to 4D for POLY and NURBS
126 newPoints.append([x, y, z])
128 return newPoints
131 # ------------------------------------------------------------------------------
132 # Calculate the align matrix for the new object (based on user preferences)
134 def align_matrix(self, context):
135 if self.absolute_location:
136 loc = Matrix.Translation(Vector((0, 0, 0)))
137 else:
138 loc = Matrix.Translation(context.scene.cursor.location)
140 # user defined location & translation
141 userLoc = Matrix.Translation(self.location)
142 userRot = self.rotation.to_matrix().to_4x4()
144 obj_align = context.preferences.edit.object_align
145 if (context.space_data.type == 'VIEW_3D' and obj_align == 'VIEW'):
146 rot = context.space_data.region_3d.view_matrix.to_3x3().inverted().to_4x4()
147 else:
148 rot = Matrix()
150 align_matrix = userLoc @ loc @ rot @ userRot
151 return align_matrix
154 # ------------------------------------------------------------------------------
155 # Set curve BEZIER handles to auto
157 def setBezierHandles(obj, mode='AUTO'):
158 scene = bpy.context.scene
159 if obj.type != 'CURVE':
160 return
161 #scene.objects.active = obj
162 #bpy.ops.object.mode_set(mode='EDIT', toggle=True)
163 #bpy.ops.curve.select_all(action='SELECT')
164 #bpy.ops.curve.handle_type_set(type=mode)
165 #bpy.ops.object.mode_set(mode='OBJECT', toggle=True)
168 # ------------------------------------------------------------------------------
169 # Convert array of vert coordinates to points according to spline type
171 def vertsToPoints(Verts, splineType):
172 # main vars
173 vertArray = []
175 # array for BEZIER spline output (V3)
176 if splineType == 'BEZIER':
177 for v in Verts:
178 vertArray += v
180 # array for non-BEZIER output (V4)
181 else:
182 for v in Verts:
183 vertArray += v
184 if splineType == 'NURBS':
185 vertArray.append(1) # for NURBS w=1
186 else: # for POLY w=0
187 vertArray.append(0)
189 return vertArray
192 # ------------------------------------------------------------------------------
193 # Create the Torus Knot curve and object and add it to the scene
195 def create_torus_knot(self, context):
196 # pick a name based on (p,q) parameters
197 aName = "Torus Knot %i x %i" % (self.torus_p, self.torus_q)
199 # create curve
200 curve_data = bpy.data.curves.new(name=aName, type='CURVE')
202 # setup materials to be used for the TK links
203 if self.use_colors:
204 addLinkColors(self, curve_data)
206 # create torus knot link(s)
207 if self.multiple_links:
208 links = gcd(self.torus_p, self.torus_q)
209 else:
210 links = 1
212 for l in range(links):
213 # get vertices for the current link
214 verts = Torus_Knot(self, l)
216 # output splineType 'POLY' 'NURBS' or 'BEZIER'
217 splineType = self.outputType
219 # turn verts into proper array (based on spline type)
220 vertArray = vertsToPoints(verts, splineType)
222 # create spline from vertArray (based on spline type)
223 spline = curve_data.splines.new(type=splineType)
224 if splineType == 'BEZIER':
225 spline.bezier_points.add(int(len(vertArray) * 1.0 / 3 - 1))
226 spline.bezier_points.foreach_set('co', vertArray)
227 for point in spline.bezier_points:
228 point.handle_right_type = self.handleType
229 point.handle_left_type = self.handleType
230 else:
231 spline.points.add(int(len(vertArray) * 1.0 / 4 - 1))
232 spline.points.foreach_set('co', vertArray)
233 spline.use_endpoint_u = True
235 # set curve options
236 spline.use_cyclic_u = True
237 spline.order_u = 4
239 # set a color per link
240 if self.use_colors:
241 spline.material_index = l
243 curve_data.dimensions = '3D'
244 curve_data.resolution_u = self.segment_res
246 # create surface ?
247 if self.geo_surface:
248 curve_data.fill_mode = 'FULL'
249 curve_data.bevel_depth = self.geo_bDepth
250 curve_data.bevel_resolution = self.geo_bRes
251 curve_data.extrude = self.geo_extrude
252 curve_data.offset = self.geo_offset
254 # set object in the scene
255 new_obj = object_data_add(context, curve_data) # place in active scene
256 bpy.ops.object.select_all(action='DESELECT')
257 new_obj.select_set(True) # set as selected
258 bpy.context.view_layer.objects.active = new_obj
259 new_obj.matrix_world = self.align_matrix # apply matrix
260 bpy.context.view_layer.update()
262 return
265 # ------------------------------------------------------------------------------
266 # Create materials to be assigned to each TK link
268 def addLinkColors(self, curveData):
269 # some predefined colors for the torus knot links
270 colors = []
271 if self.colorSet == "1": # RGBish
272 colors += [[0.0, 0.0, 1.0]]
273 colors += [[0.0, 1.0, 0.0]]
274 colors += [[1.0, 0.0, 0.0]]
275 colors += [[1.0, 1.0, 0.0]]
276 colors += [[0.0, 1.0, 1.0]]
277 colors += [[1.0, 0.0, 1.0]]
278 colors += [[1.0, 0.5, 0.0]]
279 colors += [[0.0, 1.0, 0.5]]
280 colors += [[0.5, 0.0, 1.0]]
281 else: # RainBow
282 colors += [[0.0, 0.0, 1.0]]
283 colors += [[0.0, 0.5, 1.0]]
284 colors += [[0.0, 1.0, 1.0]]
285 colors += [[0.0, 1.0, 0.5]]
286 colors += [[0.0, 1.0, 0.0]]
287 colors += [[0.5, 1.0, 0.0]]
288 colors += [[1.0, 1.0, 0.0]]
289 colors += [[1.0, 0.5, 0.0]]
290 colors += [[1.0, 0.0, 0.0]]
292 me = curveData
293 links = gcd(self.torus_p, self.torus_q)
295 for i in range(links):
296 matName = "TorusKnot-Link-%i" % i
297 matListNames = bpy.data.materials.keys()
298 # create the material
299 if matName not in matListNames:
300 if DEBUG:
301 print("Creating new material : %s" % matName)
302 mat = bpy.data.materials.new(matName)
303 else:
304 if DEBUG:
305 print("Material %s already exists" % matName)
306 mat = bpy.data.materials[matName]
308 # set material color
309 if self.options_plus and self.random_colors:
310 mat.diffuse_color = (random(), random(), random(), 1.0)
311 else:
312 cID = i % (len(colors)) # cycle through predefined colors
313 mat.diffuse_color = (*colors[cID], 1.0)
315 if self.options_plus:
316 mat.diffuse_color = (mat.diffuse_color[0] * self.saturation, mat.diffuse_color[1] * self.saturation, mat.diffuse_color[2] * self.saturation, 1.0)
317 else:
318 mat.diffuse_color = (mat.diffuse_color[0] * 0.75, mat.diffuse_color[1] * 0.75, mat.diffuse_color[2] * 0.75, 1.0)
320 me.materials.append(mat)
323 # ------------------------------------------------------------------------------
324 # Main Torus Knot class
326 class torus_knot_plus(Operator, AddObjectHelper):
327 bl_idname = "curve.torus_knot_plus"
328 bl_label = "Torus Knot +"
329 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
330 bl_description = "Adds many types of tours knots"
331 bl_context = "object"
333 def mode_update_callback(self, context):
334 # keep the equivalent radii sets (R,r)/(eR,iR) in sync
335 if self.mode == 'EXT_INT':
336 self.torus_eR = self.torus_R + self.torus_r
337 self.torus_iR = self.torus_R - self.torus_r
339 # align_matrix for the invoke
340 align_matrix = None
342 # GENERAL options
343 options_plus : BoolProperty(
344 name="Extra Options",
345 default=False,
346 description="Show more options (the plus part)",
348 absolute_location : BoolProperty(
349 name="Absolute Location",
350 default=False,
351 description="Set absolute location instead of relative to 3D cursor",
353 # COLOR options
354 use_colors : BoolProperty(
355 name="Use Colors",
356 default=False,
357 description="Show torus links in colors",
359 colorSet : EnumProperty(
360 name="Color Set",
361 items=(('1', "RGBish", "RGBsish ordered colors"),
362 ('2', "Rainbow", "Rainbow ordered colors")),
364 random_colors : BoolProperty(
365 name="Randomize Colors",
366 default=False,
367 description="Randomize link colors",
369 saturation : FloatProperty(
370 name="Saturation",
371 default=0.75,
372 min=0.0, max=1.0,
373 description="Color saturation",
375 # SURFACE Options
376 geo_surface : BoolProperty(
377 name="Surface",
378 default=True,
379 description="Create surface",
381 geo_bDepth : FloatProperty(
382 name="Bevel Depth",
383 default=0.04,
384 min=0, soft_min=0,
385 description="Bevel Depth",
387 geo_bRes : IntProperty(
388 name="Bevel Resolution",
389 default=2,
390 min=0, soft_min=0,
391 max=5, soft_max=5,
392 description="Bevel Resolution"
394 geo_extrude : FloatProperty(
395 name="Extrude",
396 default=0.0,
397 min=0, soft_min=0,
398 description="Amount of curve extrusion"
400 geo_offset : FloatProperty(
401 name="Offset",
402 default=0.0,
403 min=0, soft_min=0,
404 description="Offset the surface relative to the curve"
406 # TORUS KNOT Options
407 torus_p : IntProperty(
408 name="p",
409 default=2,
410 min=1, soft_min=1,
411 description="Number of Revolutions around the torus hole before closing the knot"
413 torus_q : IntProperty(
414 name="q",
415 default=3,
416 min=1, soft_min=1,
417 description="Number of Spins through the torus hole before closing the knot"
419 flip_p : BoolProperty(
420 name="Flip p",
421 default=False,
422 description="Flip Revolution direction"
424 flip_q : BoolProperty(
425 name="Flip q",
426 default=False,
427 description="Flip Spin direction"
429 multiple_links : BoolProperty(
430 name="Multiple Links",
431 default=True,
432 description="Generate all links or just one link when q and q are not co-primes"
434 torus_u : IntProperty(
435 name="Rev. Multiplier",
436 default=1,
437 min=1, soft_min=1,
438 description="Revolutions Multiplier"
440 torus_v : IntProperty(
441 name="Spin Multiplier",
442 default=1,
443 min=1, soft_min=1,
444 description="Spin multiplier"
446 torus_rP : FloatProperty(
447 name="Revolution Phase",
448 default=0.0,
449 min=0.0, soft_min=0.0,
450 description="Phase revolutions by this radian amount"
452 torus_sP : FloatProperty(
453 name="Spin Phase",
454 default=0.0,
455 min=0.0, soft_min=0.0,
456 description="Phase spins by this radian amount"
458 # TORUS DIMENSIONS options
459 mode : EnumProperty(
460 name="Torus Dimensions",
461 items=(("MAJOR_MINOR", "Major/Minor",
462 "Use the Major/Minor radii for torus dimensions."),
463 ("EXT_INT", "Exterior/Interior",
464 "Use the Exterior/Interior radii for torus dimensions.")),
465 update=mode_update_callback,
467 torus_R : FloatProperty(
468 name="Major Radius",
469 min=0.00, max=100.0,
470 default=1.0,
471 subtype='DISTANCE',
472 unit='LENGTH',
473 description="Radius from the torus origin to the center of the cross section"
475 torus_r : FloatProperty(
476 name="Minor Radius",
477 min=0.00, max=100.0,
478 default=.25,
479 subtype='DISTANCE',
480 unit='LENGTH',
481 description="Radius of the torus' cross section"
483 torus_iR : FloatProperty(
484 name="Interior Radius",
485 min=0.00, max=100.0,
486 default=.75,
487 subtype='DISTANCE',
488 unit='LENGTH',
489 description="Interior radius of the torus (closest to the torus center)"
491 torus_eR : FloatProperty(
492 name="Exterior Radius",
493 min=0.00, max=100.0,
494 default=1.25,
495 subtype='DISTANCE',
496 unit='LENGTH',
497 description="Exterior radius of the torus (farthest from the torus center)"
499 torus_s : FloatProperty(
500 name="Scale",
501 min=0.01, max=100.0,
502 default=1.00,
503 description="Scale factor to multiply the radii"
505 torus_h : FloatProperty(
506 name="Height",
507 default=1.0,
508 min=0.0, max=100.0,
509 description="Scale along the local Z axis"
511 # CURVE options
512 torus_res : IntProperty(
513 name="Curve Resolution",
514 default=100,
515 min=3, soft_min=3,
516 description="Number of control vertices in the curve"
518 segment_res : IntProperty(
519 name="Segment Resolution",
520 default=12,
521 min=1, soft_min=1,
522 description="Curve subdivisions per segment"
524 SplineTypes = [
525 ('POLY', "Poly", "Poly type"),
526 ('NURBS', "Nurbs", "Nurbs type"),
527 ('BEZIER', "Bezier", "Bezier type")]
528 outputType : EnumProperty(
529 name="Output splines",
530 default='BEZIER',
531 description="Type of splines to output",
532 items=SplineTypes,
534 bezierHandles = [
535 ('VECTOR', "Vector", "Bezier Handles type - Vector"),
536 ('AUTO', "Auto", "Bezier Handles type - Automatic"),
538 handleType : EnumProperty(
539 name="Handle type",
540 default='AUTO',
541 items=bezierHandles,
542 description="Bezier handle type",
544 adaptive_resolution : BoolProperty(
545 name="Adaptive Resolution",
546 default=False,
547 description="Auto adjust curve resolution based on TK length",
549 edit_mode : BoolProperty(
550 name="Show in edit mode",
551 default=True,
552 description="Show in edit mode"
555 def draw(self, context):
556 layout = self.layout
558 # extra parameters toggle
559 layout.prop(self, "options_plus")
561 # TORUS KNOT Parameters
562 col = layout.column()
563 col.label(text="Torus Knot Parameters:")
565 box = layout.box()
566 split = box.split(factor=0.85, align=True)
567 split.prop(self, "torus_p", text="Revolutions")
568 split.prop(self, "flip_p", toggle=True, text="",
569 icon='ARROW_LEFTRIGHT')
571 split = box.split(factor=0.85, align=True)
572 split.prop(self, "torus_q", text="Spins")
573 split.prop(self, "flip_q", toggle=True, text="",
574 icon='ARROW_LEFTRIGHT')
576 links = gcd(self.torus_p, self.torus_q)
577 info = "Multiple Links"
579 if links > 1:
580 info += " ( " + str(links) + " )"
581 box.prop(self, 'multiple_links', text=info)
583 if self.options_plus:
584 box = box.box()
585 col = box.column(align=True)
586 col.prop(self, "torus_u")
587 col.prop(self, "torus_v")
589 col = box.column(align=True)
590 col.prop(self, "torus_rP")
591 col.prop(self, "torus_sP")
593 # TORUS DIMENSIONS options
594 col = layout.column(align=True)
595 col.label(text="Torus Dimensions:")
596 box = layout.box()
597 col = box.column(align=True)
598 col.row().prop(self, "mode", expand=True)
600 if self.mode == "MAJOR_MINOR":
601 col = box.column(align=True)
602 col.prop(self, "torus_R")
603 col.prop(self, "torus_r")
604 else: # EXTERIOR-INTERIOR
605 col = box.column(align=True)
606 col.prop(self, "torus_eR")
607 col.prop(self, "torus_iR")
609 if self.options_plus:
610 box = box.box()
611 col = box.column(align=True)
612 col.prop(self, "torus_s")
613 col.prop(self, "torus_h")
615 # CURVE options
616 col = layout.column(align=True)
617 col.label(text="Curve Options:")
618 box = layout.box()
620 col = box.column()
621 col.label(text="Output Curve Type:")
622 col.row().prop(self, "outputType", expand=True)
624 depends = box.column()
625 depends.prop(self, "torus_res")
626 # deactivate the "curve resolution" if "adaptive resolution" is enabled
627 depends.enabled = not self.adaptive_resolution
629 box.prop(self, "adaptive_resolution")
630 box.prop(self, "segment_res")
632 # SURFACE options
633 col = layout.column()
634 col.label(text="Geometry Options:")
635 box = layout.box()
636 box.prop(self, "geo_surface")
637 if self.geo_surface:
638 col = box.column(align=True)
639 col.prop(self, "geo_bDepth")
640 col.prop(self, "geo_bRes")
642 col = box.column(align=True)
643 col.prop(self, "geo_extrude")
644 col.prop(self, "geo_offset")
646 # COLOR options
647 col = layout.column()
648 col.label(text="Color Options:")
649 box = layout.box()
650 box.prop(self, "use_colors")
651 if self.use_colors and self.options_plus:
652 box = box.box()
653 box.prop(self, "colorSet")
654 box.prop(self, "random_colors")
655 box.prop(self, "saturation")
657 col = layout.column()
658 col.row().prop(self, "edit_mode", expand=True)
660 # TRANSFORM options
661 col = layout.column()
662 col.label(text="Transform Options:")
663 box = col.box()
664 box.prop(self, "location")
665 box.prop(self, "absolute_location")
666 box.prop(self, "rotation")
668 @classmethod
669 def poll(cls, context):
670 return context.scene is not None
672 def execute(self, context):
673 # turn off 'Enter Edit Mode'
674 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
675 bpy.context.preferences.edit.use_enter_edit_mode = False
677 if self.mode == 'EXT_INT':
678 # adjust the equivalent radii pair : (R,r) <=> (eR,iR)
679 self.torus_R = (self.torus_eR + self.torus_iR) * 0.5
680 self.torus_r = (self.torus_eR - self.torus_iR) * 0.5
682 if self.adaptive_resolution:
683 # adjust curve resolution automatically based on (p,q,R,r) values
684 p = self.torus_p
685 q = self.torus_q
686 R = self.torus_R
687 r = self.torus_r
688 links = gcd(p, q)
690 # get an approximate length of the whole TK curve
691 # upper bound approximation
692 maxTKLen = 2 * pi * sqrt(p * p * (R + r) * (R + r) + q * q * r * r)
693 # lower bound approximation
694 minTKLen = 2 * pi * sqrt(p * p * (R - r) * (R - r) + q * q * r * r)
695 avgTKLen = (minTKLen + maxTKLen) / 2 # average approximation
697 if DEBUG:
698 print("Approximate average TK length = %.2f" % avgTKLen)
700 # x N factor = control points per unit length
701 self.torus_res = max(3, avgTKLen / links * 8)
703 # update align matrix
704 self.align_matrix = align_matrix(self, context)
706 # create the curve
707 create_torus_knot(self, context)
709 if use_enter_edit_mode:
710 bpy.ops.object.mode_set(mode = 'EDIT')
712 # restore pre operator state
713 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
715 if self.edit_mode:
716 bpy.ops.object.mode_set(mode = 'EDIT')
717 else:
718 bpy.ops.object.mode_set(mode = 'OBJECT')
720 return {'FINISHED'}
722 def invoke(self, context, event):
723 self.execute(context)
725 return {'FINISHED'}
727 # Register
728 classes = [
729 torus_knot_plus
732 def register():
733 from bpy.utils import register_class
734 for cls in classes:
735 register_class(cls)
737 def unregister():
738 from bpy.utils import unregister_class
739 for cls in reversed(classes):
740 unregister_class(cls)
742 if __name__ == "__main__":
743 register()