Fix: Node Wrangler: error when previewing without Geo output socket
[blender-addons.git] / add_curve_extra_objects / add_curve_torus_knots.py
blob6bd6f0e34b0271a518a4bc8a829d49209ca12a25
1 # SPDX-FileCopyrightText: 2010-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 """
6 bl_info = {
7 "name": "Torus Knots",
8 "author": "Marius Giurgi (DolphinDream), testscreenings",
9 "version": (0, 3),
10 "blender": (2, 80, 0),
11 "location": "View3D > Add > Curve",
12 "description": "Adds many types of (torus) knots",
13 "warning": "",
14 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
15 "category": "Add Curve",
17 """
19 import bpy
20 from bpy.props import (
21 BoolProperty,
22 EnumProperty,
23 FloatProperty,
24 IntProperty
26 from math import (
27 sin, cos,
28 pi, sqrt
30 from mathutils import (
31 Vector,
32 Matrix,
34 from bpy_extras.object_utils import (
35 AddObjectHelper,
36 object_data_add
38 from random import random
39 from bpy.types import Operator
41 # Globals:
42 DEBUG = False
45 # greatest common denominator
46 def gcd(a, b):
47 if b == 0:
48 return a
49 else:
50 return gcd(b, a % b)
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
63 if self.options_plus:
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
69 u = 1
70 v = 1
71 h = 1
72 s = 1
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
90 if self.options_plus:
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
94 rPhase = 0
95 sPhase = 0
97 rPhase += linkPhase # total revolution phase of the current link
99 if DEBUG:
100 print("")
101 print("Link: %i of %i" % (linkIndex, links))
102 print("gcd = %i" % links)
103 print("p = %i" % p)
104 print("q = %i" % q)
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
109 if self.flip_p:
110 p *= -1
111 if self.flip_q:
112 q *= -1
114 # create the 3D point array for the current link
115 newPoints = []
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
118 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)
124 z = r * sin(phi) * h
126 # append 3D point
127 # NOTE : the array is adjusted later as needed to 4D for POLY and NURBS
128 newPoints.append([x, y, z])
130 return newPoints
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)))
139 else:
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()
149 else:
150 rot = Matrix()
152 align_matrix = userLoc @ loc @ rot @ userRot
153 return align_matrix
156 # ------------------------------------------------------------------------------
157 # Set curve BEZIER handles to auto
159 def setBezierHandles(obj, mode='AUTO'):
160 scene = bpy.context.scene
161 if obj.type != 'CURVE':
162 return
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):
174 # main vars
175 vertArray = []
177 # array for BEZIER spline output (V3)
178 if splineType == 'BEZIER':
179 for v in Verts:
180 vertArray += v
182 # array for non-BEZIER output (V4)
183 else:
184 for v in Verts:
185 vertArray += v
186 if splineType == 'NURBS':
187 vertArray.append(1) # for NURBS w=1
188 else: # for POLY w=0
189 vertArray.append(0)
191 return vertArray
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)
201 # create curve
202 curve_data = bpy.data.curves.new(name=aName, type='CURVE')
204 # setup materials to be used for the TK links
205 if self.use_colors:
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)
211 else:
212 links = 1
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
232 else:
233 spline.points.add(int(len(vertArray) * 1.0 / 4 - 1))
234 spline.points.foreach_set('co', vertArray)
235 spline.use_endpoint_u = True
237 # set curve options
238 spline.use_cyclic_u = True
239 spline.order_u = 4
241 # set a color per link
242 if self.use_colors:
243 spline.material_index = l
245 curve_data.dimensions = '3D'
246 curve_data.resolution_u = self.segment_res
248 # create surface ?
249 if self.geo_surface:
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()
264 return
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
272 colors = []
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]]
283 else: # RainBow
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]]
294 me = curveData
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:
302 if DEBUG:
303 print("Creating new material : %s" % matName)
304 mat = bpy.data.materials.new(matName)
305 else:
306 if DEBUG:
307 print("Material %s already exists" % matName)
308 mat = bpy.data.materials[matName]
310 # set material color
311 if self.options_plus and self.random_colors:
312 mat.diffuse_color = (random(), random(), random(), 1.0)
313 else:
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)
319 else:
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
342 align_matrix = None
344 # GENERAL options
345 options_plus : BoolProperty(
346 name="Extra Options",
347 default=False,
348 description="Show more options (the plus part)",
350 absolute_location : BoolProperty(
351 name="Absolute Location",
352 default=False,
353 description="Set absolute location instead of relative to 3D cursor",
355 # COLOR options
356 use_colors : BoolProperty(
357 name="Use Colors",
358 default=False,
359 description="Show torus links in colors",
361 colorSet : EnumProperty(
362 name="Color Set",
363 items=(('1', "RGBish", "RGBsish ordered colors"),
364 ('2', "Rainbow", "Rainbow ordered colors")),
366 random_colors : BoolProperty(
367 name="Randomize Colors",
368 default=False,
369 description="Randomize link colors",
371 saturation : FloatProperty(
372 name="Saturation",
373 default=0.75,
374 min=0.0, max=1.0,
375 description="Color saturation",
377 # SURFACE Options
378 geo_surface : BoolProperty(
379 name="Surface",
380 default=True,
381 description="Create surface",
383 geo_bDepth : FloatProperty(
384 name="Bevel Depth",
385 default=0.04,
386 min=0, soft_min=0,
387 description="Bevel Depth",
389 geo_bRes : IntProperty(
390 name="Bevel Resolution",
391 default=2,
392 min=0, soft_min=0,
393 max=5, soft_max=5,
394 description="Bevel Resolution"
396 geo_extrude : FloatProperty(
397 name="Extrude",
398 default=0.0,
399 min=0, soft_min=0,
400 description="Amount of curve extrusion"
402 geo_offset : FloatProperty(
403 name="Offset",
404 default=0.0,
405 min=0, soft_min=0,
406 description="Offset the surface relative to the curve"
408 # TORUS KNOT Options
409 torus_p : IntProperty(
410 name="p",
411 default=2,
412 min=1, soft_min=1,
413 description="Number of Revolutions around the torus hole before closing the knot"
415 torus_q : IntProperty(
416 name="q",
417 default=3,
418 min=1, soft_min=1,
419 description="Number of Spins through the torus hole before closing the knot"
421 flip_p : BoolProperty(
422 name="Flip p",
423 default=False,
424 description="Flip Revolution direction"
426 flip_q : BoolProperty(
427 name="Flip q",
428 default=False,
429 description="Flip Spin direction"
431 multiple_links : BoolProperty(
432 name="Multiple Links",
433 default=True,
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",
438 default=1,
439 min=1, soft_min=1,
440 description="Revolutions Multiplier"
442 torus_v : IntProperty(
443 name="Spin Multiplier",
444 default=1,
445 min=1, soft_min=1,
446 description="Spin multiplier"
448 torus_rP : FloatProperty(
449 name="Revolution Phase",
450 default=0.0,
451 min=0.0, soft_min=0.0,
452 description="Phase revolutions by this radian amount"
454 torus_sP : FloatProperty(
455 name="Spin Phase",
456 default=0.0,
457 min=0.0, soft_min=0.0,
458 description="Phase spins by this radian amount"
460 # TORUS DIMENSIONS options
461 mode : EnumProperty(
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(
470 name="Major Radius",
471 min=0.00, max=100.0,
472 default=1.0,
473 subtype='DISTANCE',
474 unit='LENGTH',
475 description="Radius from the torus origin to the center of the cross section"
477 torus_r : FloatProperty(
478 name="Minor Radius",
479 min=0.00, max=100.0,
480 default=.25,
481 subtype='DISTANCE',
482 unit='LENGTH',
483 description="Radius of the torus' cross section"
485 torus_iR : FloatProperty(
486 name="Interior Radius",
487 min=0.00, max=100.0,
488 default=.75,
489 subtype='DISTANCE',
490 unit='LENGTH',
491 description="Interior radius of the torus (closest to the torus center)"
493 torus_eR : FloatProperty(
494 name="Exterior Radius",
495 min=0.00, max=100.0,
496 default=1.25,
497 subtype='DISTANCE',
498 unit='LENGTH',
499 description="Exterior radius of the torus (farthest from the torus center)"
501 torus_s : FloatProperty(
502 name="Scale",
503 min=0.01, max=100.0,
504 default=1.00,
505 description="Scale factor to multiply the radii"
507 torus_h : FloatProperty(
508 name="Height",
509 default=1.0,
510 min=0.0, max=100.0,
511 description="Scale along the local Z axis"
513 # CURVE options
514 torus_res : IntProperty(
515 name="Curve Resolution",
516 default=100,
517 min=3, soft_min=3,
518 description="Number of control vertices in the curve"
520 segment_res : IntProperty(
521 name="Segment Resolution",
522 default=12,
523 min=1, soft_min=1,
524 description="Curve subdivisions per segment"
526 SplineTypes = [
527 ('POLY', "Poly", "Poly type"),
528 ('NURBS', "Nurbs", "Nurbs type"),
529 ('BEZIER', "Bezier", "Bezier type")]
530 outputType : EnumProperty(
531 name="Output splines",
532 default='BEZIER',
533 description="Type of splines to output",
534 items=SplineTypes,
536 bezierHandles = [
537 ('VECTOR', "Vector", "Bezier Handles type - Vector"),
538 ('AUTO', "Auto", "Bezier Handles type - Automatic"),
540 handleType : EnumProperty(
541 name="Handle type",
542 default='AUTO',
543 items=bezierHandles,
544 description="Bezier handle type",
546 adaptive_resolution : BoolProperty(
547 name="Adaptive Resolution",
548 default=False,
549 description="Auto adjust curve resolution based on TK length",
551 edit_mode : BoolProperty(
552 name="Show in edit mode",
553 default=True,
554 description="Show in edit mode"
557 def draw(self, context):
558 layout = self.layout
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:")
567 box = layout.box()
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"
581 if links > 1:
582 info += " ( " + str(links) + " )"
583 box.prop(self, 'multiple_links', text=info)
585 if self.options_plus:
586 box = box.box()
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:")
598 box = layout.box()
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:
612 box = box.box()
613 col = box.column(align=True)
614 col.prop(self, "torus_s")
615 col.prop(self, "torus_h")
617 # CURVE options
618 col = layout.column(align=True)
619 col.label(text="Curve Options:")
620 box = layout.box()
622 col = box.column()
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")
634 # SURFACE options
635 col = layout.column()
636 col.label(text="Geometry Options:")
637 box = layout.box()
638 box.prop(self, "geo_surface")
639 if 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")
648 # COLOR options
649 col = layout.column()
650 col.label(text="Color Options:")
651 box = layout.box()
652 box.prop(self, "use_colors")
653 if self.use_colors and self.options_plus:
654 box = box.box()
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)
662 # TRANSFORM options
663 col = layout.column()
664 col.label(text="Transform Options:")
665 box = col.box()
666 box.prop(self, "location")
667 box.prop(self, "absolute_location")
668 box.prop(self, "rotation")
670 @classmethod
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
686 p = self.torus_p
687 q = self.torus_q
688 R = self.torus_R
689 r = self.torus_r
690 links = gcd(p, q)
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
699 if DEBUG:
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)
708 # create the curve
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
717 if self.edit_mode:
718 bpy.ops.object.mode_set(mode = 'EDIT')
719 else:
720 bpy.ops.object.mode_set(mode = 'OBJECT')
722 return {'FINISHED'}
724 def invoke(self, context, event):
725 self.execute(context)
727 return {'FINISHED'}
729 # Register
730 classes = [
731 torus_knot_plus
734 def register():
735 from bpy.utils import register_class
736 for cls in classes:
737 register_class(cls)
739 def unregister():
740 from bpy.utils import unregister_class
741 for cls in reversed(classes):
742 unregister_class(cls)
744 if __name__ == "__main__":
745 register()