1 # SPDX-FileCopyrightText: 2010-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
8 "author": "Marius Giurgi (DolphinDream), testscreenings",
10 "blender": (2, 80, 0),
11 "location": "View3D > Add > Curve",
12 "description": "Adds many types of (torus) knots",
14 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
15 "category": "Add Curve",
20 from bpy
.props
import (
30 from mathutils
import (
34 from bpy_extras
.object_utils
import (
38 from random
import random
39 from bpy
.types
import Operator
45 # greatest common denominator
53 # #######################################################################
54 # ###################### Knot Definitions ###############################
55 # #######################################################################
56 def Torus_Knot(self
, linkIndex
=0):
57 p
= self
.torus_p
# revolution count (around the torus center)
58 q
= self
.torus_q
# spin count (around the torus tube)
60 N
= self
.torus_res
# curve resolution (number of control points)
62 # use plus options only when they are enabled
64 u
= self
.torus_u
# p multiplier
65 v
= self
.torus_v
# q multiplier
66 h
= self
.torus_h
# height (scale along Z)
67 s
= self
.torus_s
# torus scale (radii scale factor)
68 else: # don't use plus settings
74 R
= self
.torus_R
* s
# major radius (scaled)
75 r
= self
.torus_r
* s
# minor radius (scaled)
77 # number of decoupled links when (p,q) are NOT co-primes
78 links
= gcd(p
, q
) # = 1 when (p,q) are co-primes
80 # parametrized angle increment (cached outside of the loop for performance)
81 # NOTE: the total angle is divided by number of decoupled links to ensure
82 # the curve does not overlap with itself when (p,q) are not co-primes
83 da
= 2 * pi
/ links
/ (N
- 1)
85 # link phase : each decoupled link is phased equally around the torus center
86 # NOTE: linkIndex value is in [0, links-1]
87 linkPhase
= 2 * pi
/ q
* linkIndex
# = 0 when there is just ONE link
89 # user defined phasing
91 rPhase
= self
.torus_rP
# user defined revolution phase
92 sPhase
= self
.torus_sP
# user defined spin phase
93 else: # don't use plus settings
97 rPhase
+= linkPhase
# total revolution phase of the current link
101 print("Link: %i of %i" % (linkIndex
, links
))
102 print("gcd = %i" % links
)
105 print("link phase = %.2f deg" % (linkPhase
* 180 / pi
))
106 print("link phase = %.2f rad" % linkPhase
)
108 # flip directions ? NOTE: flipping both is equivalent to no flip
114 # create the 3D point array for the current link
116 for n
in range(N
- 1):
117 # t = 2 * pi / links * n/(N-1) with: da = 2*pi/links/(N-1) => t = n * da
119 theta
= p
* t
* u
+ rPhase
# revolution angle
120 phi
= q
* t
* v
+ sPhase
# spin angle
122 x
= (R
+ r
* cos(phi
)) * cos(theta
)
123 y
= (R
+ r
* cos(phi
)) * sin(theta
)
127 # NOTE : the array is adjusted later as needed to 4D for POLY and NURBS
128 newPoints
.append([x
, y
, z
])
133 # ------------------------------------------------------------------------------
134 # Calculate the align matrix for the new object (based on user preferences)
136 def align_matrix(self
, context
):
137 if self
.absolute_location
:
138 loc
= Matrix
.Translation(Vector((0, 0, 0)))
140 loc
= Matrix
.Translation(context
.scene
.cursor
.location
)
142 # user defined location & translation
143 userLoc
= Matrix
.Translation(self
.location
)
144 userRot
= self
.rotation
.to_matrix().to_4x4()
146 obj_align
= context
.preferences
.edit
.object_align
147 if (context
.space_data
.type == 'VIEW_3D' and obj_align
== 'VIEW'):
148 rot
= context
.space_data
.region_3d
.view_matrix
.to_3x3().inverted().to_4x4()
152 align_matrix
= userLoc
@ loc
@ rot
@ userRot
156 # ------------------------------------------------------------------------------
157 # Set curve BEZIER handles to auto
159 def setBezierHandles(obj
, mode
='AUTO'):
160 scene
= bpy
.context
.scene
161 if obj
.type != 'CURVE':
163 #scene.objects.active = obj
164 #bpy.ops.object.mode_set(mode='EDIT', toggle=True)
165 #bpy.ops.curve.select_all(action='SELECT')
166 #bpy.ops.curve.handle_type_set(type=mode)
167 #bpy.ops.object.mode_set(mode='OBJECT', toggle=True)
170 # ------------------------------------------------------------------------------
171 # Convert array of vert coordinates to points according to spline type
173 def vertsToPoints(Verts
, splineType
):
177 # array for BEZIER spline output (V3)
178 if splineType
== 'BEZIER':
182 # array for non-BEZIER output (V4)
186 if splineType
== 'NURBS':
187 vertArray
.append(1) # for NURBS w=1
194 # ------------------------------------------------------------------------------
195 # Create the Torus Knot curve and object and add it to the scene
197 def create_torus_knot(self
, context
):
198 # pick a name based on (p,q) parameters
199 aName
= "Torus Knot %i x %i" % (self
.torus_p
, self
.torus_q
)
202 curve_data
= bpy
.data
.curves
.new(name
=aName
, type='CURVE')
204 # setup materials to be used for the TK links
206 addLinkColors(self
, curve_data
)
208 # create torus knot link(s)
209 if self
.multiple_links
:
210 links
= gcd(self
.torus_p
, self
.torus_q
)
214 for l
in range(links
):
215 # get vertices for the current link
216 verts
= Torus_Knot(self
, l
)
218 # output splineType 'POLY' 'NURBS' or 'BEZIER'
219 splineType
= self
.outputType
221 # turn verts into proper array (based on spline type)
222 vertArray
= vertsToPoints(verts
, splineType
)
224 # create spline from vertArray (based on spline type)
225 spline
= curve_data
.splines
.new(type=splineType
)
226 if splineType
== 'BEZIER':
227 spline
.bezier_points
.add(int(len(vertArray
) * 1.0 / 3 - 1))
228 spline
.bezier_points
.foreach_set('co', vertArray
)
229 for point
in spline
.bezier_points
:
230 point
.handle_right_type
= self
.handleType
231 point
.handle_left_type
= self
.handleType
233 spline
.points
.add(int(len(vertArray
) * 1.0 / 4 - 1))
234 spline
.points
.foreach_set('co', vertArray
)
235 spline
.use_endpoint_u
= True
238 spline
.use_cyclic_u
= True
241 # set a color per link
243 spline
.material_index
= l
245 curve_data
.dimensions
= '3D'
246 curve_data
.resolution_u
= self
.segment_res
250 curve_data
.fill_mode
= 'FULL'
251 curve_data
.bevel_depth
= self
.geo_bDepth
252 curve_data
.bevel_resolution
= self
.geo_bRes
253 curve_data
.extrude
= self
.geo_extrude
254 curve_data
.offset
= self
.geo_offset
256 # set object in the scene
257 new_obj
= object_data_add(context
, curve_data
) # place in active scene
258 bpy
.ops
.object.select_all(action
='DESELECT')
259 new_obj
.select_set(True) # set as selected
260 bpy
.context
.view_layer
.objects
.active
= new_obj
261 new_obj
.matrix_world
= self
.align_matrix
# apply matrix
262 bpy
.context
.view_layer
.update()
267 # ------------------------------------------------------------------------------
268 # Create materials to be assigned to each TK link
270 def addLinkColors(self
, curveData
):
271 # some predefined colors for the torus knot links
273 if self
.colorSet
== "1": # RGBish
274 colors
+= [[0.0, 0.0, 1.0]]
275 colors
+= [[0.0, 1.0, 0.0]]
276 colors
+= [[1.0, 0.0, 0.0]]
277 colors
+= [[1.0, 1.0, 0.0]]
278 colors
+= [[0.0, 1.0, 1.0]]
279 colors
+= [[1.0, 0.0, 1.0]]
280 colors
+= [[1.0, 0.5, 0.0]]
281 colors
+= [[0.0, 1.0, 0.5]]
282 colors
+= [[0.5, 0.0, 1.0]]
284 colors
+= [[0.0, 0.0, 1.0]]
285 colors
+= [[0.0, 0.5, 1.0]]
286 colors
+= [[0.0, 1.0, 1.0]]
287 colors
+= [[0.0, 1.0, 0.5]]
288 colors
+= [[0.0, 1.0, 0.0]]
289 colors
+= [[0.5, 1.0, 0.0]]
290 colors
+= [[1.0, 1.0, 0.0]]
291 colors
+= [[1.0, 0.5, 0.0]]
292 colors
+= [[1.0, 0.0, 0.0]]
295 links
= gcd(self
.torus_p
, self
.torus_q
)
297 for i
in range(links
):
298 matName
= "TorusKnot-Link-%i" % i
299 matListNames
= bpy
.data
.materials
.keys()
300 # create the material
301 if matName
not in matListNames
:
303 print("Creating new material : %s" % matName
)
304 mat
= bpy
.data
.materials
.new(matName
)
307 print("Material %s already exists" % matName
)
308 mat
= bpy
.data
.materials
[matName
]
311 if self
.options_plus
and self
.random_colors
:
312 mat
.diffuse_color
= (random(), random(), random(), 1.0)
314 cID
= i
% (len(colors
)) # cycle through predefined colors
315 mat
.diffuse_color
= (*colors
[cID
], 1.0)
317 if self
.options_plus
:
318 mat
.diffuse_color
= (mat
.diffuse_color
[0] * self
.saturation
, mat
.diffuse_color
[1] * self
.saturation
, mat
.diffuse_color
[2] * self
.saturation
, 1.0)
320 mat
.diffuse_color
= (mat
.diffuse_color
[0] * 0.75, mat
.diffuse_color
[1] * 0.75, mat
.diffuse_color
[2] * 0.75, 1.0)
322 me
.materials
.append(mat
)
325 # ------------------------------------------------------------------------------
326 # Main Torus Knot class
328 class torus_knot_plus(Operator
, AddObjectHelper
):
329 bl_idname
= "curve.torus_knot_plus"
330 bl_label
= "Torus Knot +"
331 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
332 bl_description
= "Adds many types of tours knots"
333 bl_context
= "object"
335 def mode_update_callback(self
, context
):
336 # keep the equivalent radii sets (R,r)/(eR,iR) in sync
337 if self
.mode
== 'EXT_INT':
338 self
.torus_eR
= self
.torus_R
+ self
.torus_r
339 self
.torus_iR
= self
.torus_R
- self
.torus_r
341 # align_matrix for the invoke
345 options_plus
: BoolProperty(
346 name
="Extra Options",
348 description
="Show more options (the plus part)",
350 absolute_location
: BoolProperty(
351 name
="Absolute Location",
353 description
="Set absolute location instead of relative to 3D cursor",
356 use_colors
: BoolProperty(
359 description
="Show torus links in colors",
361 colorSet
: EnumProperty(
363 items
=(('1', "RGBish", "RGBsish ordered colors"),
364 ('2', "Rainbow", "Rainbow ordered colors")),
366 random_colors
: BoolProperty(
367 name
="Randomize Colors",
369 description
="Randomize link colors",
371 saturation
: FloatProperty(
375 description
="Color saturation",
378 geo_surface
: BoolProperty(
381 description
="Create surface",
383 geo_bDepth
: FloatProperty(
387 description
="Bevel Depth",
389 geo_bRes
: IntProperty(
390 name
="Bevel Resolution",
394 description
="Bevel Resolution"
396 geo_extrude
: FloatProperty(
400 description
="Amount of curve extrusion"
402 geo_offset
: FloatProperty(
406 description
="Offset the surface relative to the curve"
409 torus_p
: IntProperty(
413 description
="Number of Revolutions around the torus hole before closing the knot"
415 torus_q
: IntProperty(
419 description
="Number of Spins through the torus hole before closing the knot"
421 flip_p
: BoolProperty(
424 description
="Flip Revolution direction"
426 flip_q
: BoolProperty(
429 description
="Flip Spin direction"
431 multiple_links
: BoolProperty(
432 name
="Multiple Links",
434 description
="Generate all links or just one link when q and q are not co-primes"
436 torus_u
: IntProperty(
437 name
="Rev. Multiplier",
440 description
="Revolutions Multiplier"
442 torus_v
: IntProperty(
443 name
="Spin Multiplier",
446 description
="Spin multiplier"
448 torus_rP
: FloatProperty(
449 name
="Revolution Phase",
451 min=0.0, soft_min
=0.0,
452 description
="Phase revolutions by this radian amount"
454 torus_sP
: FloatProperty(
457 min=0.0, soft_min
=0.0,
458 description
="Phase spins by this radian amount"
460 # TORUS DIMENSIONS options
462 name
="Torus Dimensions",
463 items
=(("MAJOR_MINOR", "Major/Minor",
464 "Use the Major/Minor radii for torus dimensions."),
465 ("EXT_INT", "Exterior/Interior",
466 "Use the Exterior/Interior radii for torus dimensions.")),
467 update
=mode_update_callback
,
469 torus_R
: FloatProperty(
475 description
="Radius from the torus origin to the center of the cross section"
477 torus_r
: FloatProperty(
483 description
="Radius of the torus' cross section"
485 torus_iR
: FloatProperty(
486 name
="Interior Radius",
491 description
="Interior radius of the torus (closest to the torus center)"
493 torus_eR
: FloatProperty(
494 name
="Exterior Radius",
499 description
="Exterior radius of the torus (farthest from the torus center)"
501 torus_s
: FloatProperty(
505 description
="Scale factor to multiply the radii"
507 torus_h
: FloatProperty(
511 description
="Scale along the local Z axis"
514 torus_res
: IntProperty(
515 name
="Curve Resolution",
518 description
="Number of control vertices in the curve"
520 segment_res
: IntProperty(
521 name
="Segment Resolution",
524 description
="Curve subdivisions per segment"
527 ('POLY', "Poly", "Poly type"),
528 ('NURBS', "Nurbs", "Nurbs type"),
529 ('BEZIER', "Bezier", "Bezier type")]
530 outputType
: EnumProperty(
531 name
="Output splines",
533 description
="Type of splines to output",
537 ('VECTOR', "Vector", "Bezier Handles type - Vector"),
538 ('AUTO', "Auto", "Bezier Handles type - Automatic"),
540 handleType
: EnumProperty(
544 description
="Bezier handle type",
546 adaptive_resolution
: BoolProperty(
547 name
="Adaptive Resolution",
549 description
="Auto adjust curve resolution based on TK length",
551 edit_mode
: BoolProperty(
552 name
="Show in edit mode",
554 description
="Show in edit mode"
557 def draw(self
, context
):
560 # extra parameters toggle
561 layout
.prop(self
, "options_plus")
563 # TORUS KNOT Parameters
564 col
= layout
.column()
565 col
.label(text
="Torus Knot Parameters:")
568 split
= box
.split(factor
=0.85, align
=True)
569 split
.prop(self
, "torus_p", text
="Revolutions")
570 split
.prop(self
, "flip_p", toggle
=True, text
="",
571 icon
='ARROW_LEFTRIGHT')
573 split
= box
.split(factor
=0.85, align
=True)
574 split
.prop(self
, "torus_q", text
="Spins")
575 split
.prop(self
, "flip_q", toggle
=True, text
="",
576 icon
='ARROW_LEFTRIGHT')
578 links
= gcd(self
.torus_p
, self
.torus_q
)
579 info
= "Multiple Links"
582 info
+= " ( " + str(links
) + " )"
583 box
.prop(self
, 'multiple_links', text
=info
)
585 if self
.options_plus
:
587 col
= box
.column(align
=True)
588 col
.prop(self
, "torus_u")
589 col
.prop(self
, "torus_v")
591 col
= box
.column(align
=True)
592 col
.prop(self
, "torus_rP")
593 col
.prop(self
, "torus_sP")
595 # TORUS DIMENSIONS options
596 col
= layout
.column(align
=True)
597 col
.label(text
="Torus Dimensions:")
599 col
= box
.column(align
=True)
600 col
.row().prop(self
, "mode", expand
=True)
602 if self
.mode
== "MAJOR_MINOR":
603 col
= box
.column(align
=True)
604 col
.prop(self
, "torus_R")
605 col
.prop(self
, "torus_r")
606 else: # EXTERIOR-INTERIOR
607 col
= box
.column(align
=True)
608 col
.prop(self
, "torus_eR")
609 col
.prop(self
, "torus_iR")
611 if self
.options_plus
:
613 col
= box
.column(align
=True)
614 col
.prop(self
, "torus_s")
615 col
.prop(self
, "torus_h")
618 col
= layout
.column(align
=True)
619 col
.label(text
="Curve Options:")
623 col
.label(text
="Output Curve Type:")
624 col
.row().prop(self
, "outputType", expand
=True)
626 depends
= box
.column()
627 depends
.prop(self
, "torus_res")
628 # deactivate the "curve resolution" if "adaptive resolution" is enabled
629 depends
.enabled
= not self
.adaptive_resolution
631 box
.prop(self
, "adaptive_resolution")
632 box
.prop(self
, "segment_res")
635 col
= layout
.column()
636 col
.label(text
="Geometry Options:")
638 box
.prop(self
, "geo_surface")
640 col
= box
.column(align
=True)
641 col
.prop(self
, "geo_bDepth")
642 col
.prop(self
, "geo_bRes")
644 col
= box
.column(align
=True)
645 col
.prop(self
, "geo_extrude")
646 col
.prop(self
, "geo_offset")
649 col
= layout
.column()
650 col
.label(text
="Color Options:")
652 box
.prop(self
, "use_colors")
653 if self
.use_colors
and self
.options_plus
:
655 box
.prop(self
, "colorSet")
656 box
.prop(self
, "random_colors")
657 box
.prop(self
, "saturation")
659 col
= layout
.column()
660 col
.row().prop(self
, "edit_mode", expand
=True)
663 col
= layout
.column()
664 col
.label(text
="Transform Options:")
666 box
.prop(self
, "location")
667 box
.prop(self
, "absolute_location")
668 box
.prop(self
, "rotation")
671 def poll(cls
, context
):
672 return context
.scene
is not None
674 def execute(self
, context
):
675 # turn off 'Enter Edit Mode'
676 use_enter_edit_mode
= bpy
.context
.preferences
.edit
.use_enter_edit_mode
677 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= False
679 if self
.mode
== 'EXT_INT':
680 # adjust the equivalent radii pair : (R,r) <=> (eR,iR)
681 self
.torus_R
= (self
.torus_eR
+ self
.torus_iR
) * 0.5
682 self
.torus_r
= (self
.torus_eR
- self
.torus_iR
) * 0.5
684 if self
.adaptive_resolution
:
685 # adjust curve resolution automatically based on (p,q,R,r) values
692 # get an approximate length of the whole TK curve
693 # upper bound approximation
694 maxTKLen
= 2 * pi
* sqrt(p
* p
* (R
+ r
) * (R
+ r
) + q
* q
* r
* r
)
695 # lower bound approximation
696 minTKLen
= 2 * pi
* sqrt(p
* p
* (R
- r
) * (R
- r
) + q
* q
* r
* r
)
697 avgTKLen
= (minTKLen
+ maxTKLen
) / 2 # average approximation
700 print("Approximate average TK length = %.2f" % avgTKLen
)
702 # x N factor = control points per unit length
703 self
.torus_res
= max(3, int(avgTKLen
/ links
) * 8)
705 # update align matrix
706 self
.align_matrix
= align_matrix(self
, context
)
709 create_torus_knot(self
, context
)
711 if use_enter_edit_mode
:
712 bpy
.ops
.object.mode_set(mode
= 'EDIT')
714 # restore pre operator state
715 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= use_enter_edit_mode
718 bpy
.ops
.object.mode_set(mode
= 'EDIT')
720 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
724 def invoke(self
, context
, event
):
725 self
.execute(context
)
735 from bpy
.utils
import register_class
740 from bpy
.utils
import unregister_class
741 for cls
in reversed(classes
):
742 unregister_class(cls
)
744 if __name__
== "__main__":