1 # SPDX-License-Identifier: GPL-2.0-or-later
6 "author": "Marius Giurgi (DolphinDream), testscreenings",
9 "location": "View3D > Add > Curve",
10 "description": "Adds many types of (torus) knots",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
13 "category": "Add Curve",
18 from bpy
.props
import (
28 from mathutils
import (
32 from bpy_extras
.object_utils
import (
36 from random
import random
37 from bpy
.types
import Operator
43 # greatest common denominator
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
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
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
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
95 rPhase
+= linkPhase
# total revolution phase of the current link
99 print("Link: %i of %i" % (linkIndex
, links
))
100 print("gcd = %i" % links
)
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
112 # create the 3D point array for the current link
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
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
)
125 # NOTE : the array is adjusted later as needed to 4D for POLY and NURBS
126 newPoints
.append([x
, y
, z
])
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)))
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()
150 align_matrix
= userLoc
@ loc
@ rot
@ userRot
154 # ------------------------------------------------------------------------------
155 # Set curve BEZIER handles to auto
157 def setBezierHandles(obj
, mode
='AUTO'):
158 scene
= bpy
.context
.scene
159 if obj
.type != 'CURVE':
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
):
175 # array for BEZIER spline output (V3)
176 if splineType
== 'BEZIER':
180 # array for non-BEZIER output (V4)
184 if splineType
== 'NURBS':
185 vertArray
.append(1) # for NURBS w=1
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
)
200 curve_data
= bpy
.data
.curves
.new(name
=aName
, type='CURVE')
202 # setup materials to be used for the TK links
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
)
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
231 spline
.points
.add(int(len(vertArray
) * 1.0 / 4 - 1))
232 spline
.points
.foreach_set('co', vertArray
)
233 spline
.use_endpoint_u
= True
236 spline
.use_cyclic_u
= True
239 # set a color per link
241 spline
.material_index
= l
243 curve_data
.dimensions
= '3D'
244 curve_data
.resolution_u
= self
.segment_res
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()
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
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]]
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]]
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
:
301 print("Creating new material : %s" % matName
)
302 mat
= bpy
.data
.materials
.new(matName
)
305 print("Material %s already exists" % matName
)
306 mat
= bpy
.data
.materials
[matName
]
309 if self
.options_plus
and self
.random_colors
:
310 mat
.diffuse_color
= (random(), random(), random(), 1.0)
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)
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
343 options_plus
: BoolProperty(
344 name
="Extra Options",
346 description
="Show more options (the plus part)",
348 absolute_location
: BoolProperty(
349 name
="Absolute Location",
351 description
="Set absolute location instead of relative to 3D cursor",
354 use_colors
: BoolProperty(
357 description
="Show torus links in colors",
359 colorSet
: EnumProperty(
361 items
=(('1', "RGBish", "RGBsish ordered colors"),
362 ('2', "Rainbow", "Rainbow ordered colors")),
364 random_colors
: BoolProperty(
365 name
="Randomize Colors",
367 description
="Randomize link colors",
369 saturation
: FloatProperty(
373 description
="Color saturation",
376 geo_surface
: BoolProperty(
379 description
="Create surface",
381 geo_bDepth
: FloatProperty(
385 description
="Bevel Depth",
387 geo_bRes
: IntProperty(
388 name
="Bevel Resolution",
392 description
="Bevel Resolution"
394 geo_extrude
: FloatProperty(
398 description
="Amount of curve extrusion"
400 geo_offset
: FloatProperty(
404 description
="Offset the surface relative to the curve"
407 torus_p
: IntProperty(
411 description
="Number of Revolutions around the torus hole before closing the knot"
413 torus_q
: IntProperty(
417 description
="Number of Spins through the torus hole before closing the knot"
419 flip_p
: BoolProperty(
422 description
="Flip Revolution direction"
424 flip_q
: BoolProperty(
427 description
="Flip Spin direction"
429 multiple_links
: BoolProperty(
430 name
="Multiple Links",
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",
438 description
="Revolutions Multiplier"
440 torus_v
: IntProperty(
441 name
="Spin Multiplier",
444 description
="Spin multiplier"
446 torus_rP
: FloatProperty(
447 name
="Revolution Phase",
449 min=0.0, soft_min
=0.0,
450 description
="Phase revolutions by this radian amount"
452 torus_sP
: FloatProperty(
455 min=0.0, soft_min
=0.0,
456 description
="Phase spins by this radian amount"
458 # TORUS DIMENSIONS options
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(
473 description
="Radius from the torus origin to the center of the cross section"
475 torus_r
: FloatProperty(
481 description
="Radius of the torus' cross section"
483 torus_iR
: FloatProperty(
484 name
="Interior Radius",
489 description
="Interior radius of the torus (closest to the torus center)"
491 torus_eR
: FloatProperty(
492 name
="Exterior Radius",
497 description
="Exterior radius of the torus (farthest from the torus center)"
499 torus_s
: FloatProperty(
503 description
="Scale factor to multiply the radii"
505 torus_h
: FloatProperty(
509 description
="Scale along the local Z axis"
512 torus_res
: IntProperty(
513 name
="Curve Resolution",
516 description
="Number of control vertices in the curve"
518 segment_res
: IntProperty(
519 name
="Segment Resolution",
522 description
="Curve subdivisions per segment"
525 ('POLY', "Poly", "Poly type"),
526 ('NURBS', "Nurbs", "Nurbs type"),
527 ('BEZIER', "Bezier", "Bezier type")]
528 outputType
: EnumProperty(
529 name
="Output splines",
531 description
="Type of splines to output",
535 ('VECTOR', "Vector", "Bezier Handles type - Vector"),
536 ('AUTO', "Auto", "Bezier Handles type - Automatic"),
538 handleType
: EnumProperty(
542 description
="Bezier handle type",
544 adaptive_resolution
: BoolProperty(
545 name
="Adaptive Resolution",
547 description
="Auto adjust curve resolution based on TK length",
549 edit_mode
: BoolProperty(
550 name
="Show in edit mode",
552 description
="Show in edit mode"
555 def draw(self
, context
):
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:")
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"
580 info
+= " ( " + str(links
) + " )"
581 box
.prop(self
, 'multiple_links', text
=info
)
583 if self
.options_plus
:
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:")
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
:
611 col
= box
.column(align
=True)
612 col
.prop(self
, "torus_s")
613 col
.prop(self
, "torus_h")
616 col
= layout
.column(align
=True)
617 col
.label(text
="Curve Options:")
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")
633 col
= layout
.column()
634 col
.label(text
="Geometry Options:")
636 box
.prop(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")
647 col
= layout
.column()
648 col
.label(text
="Color Options:")
650 box
.prop(self
, "use_colors")
651 if self
.use_colors
and self
.options_plus
:
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)
661 col
= layout
.column()
662 col
.label(text
="Transform Options:")
664 box
.prop(self
, "location")
665 box
.prop(self
, "absolute_location")
666 box
.prop(self
, "rotation")
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
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
698 print("Approximate average TK length = %.2f" % avgTKLen
)
700 # x N factor = control points per unit length
701 self
.torus_res
= max(3, int(avgTKLen
/ links
) * 8)
703 # update align matrix
704 self
.align_matrix
= align_matrix(self
, context
)
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
716 bpy
.ops
.object.mode_set(mode
= 'EDIT')
718 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
722 def invoke(self
, context
, event
):
723 self
.execute(context
)
733 from bpy
.utils
import register_class
738 from bpy
.utils
import unregister_class
739 for cls
in reversed(classes
):
740 unregister_class(cls
)
742 if __name__
== "__main__":