Newer
Older
# SPDX-License-Identifier: GPL-2.0-or-later
Marius Giurgi
committed
"author": "Marius Giurgi (DolphinDream), testscreenings",
"version": (0, 3),
"blender": (2, 80, 0),
"location": "View3D > Add > Curve",
"description": "Adds many types of (torus) knots",
"warning": "",
"doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
"category": "Add Curve",
}
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty
)
from math import (
sin, cos,
pi, sqrt
)
from mathutils import (
Vector,
Matrix,
from bpy_extras.object_utils import (
AddObjectHelper,
object_data_add
)
Marius Giurgi
committed
from random import random
from bpy.types import Operator
Marius Giurgi
committed
Marius Giurgi
committed
DEBUG = False
Marius Giurgi
committed
# greatest common denominator
def gcd(a, b):
if b == 0:
return a
else:
return gcd(b, a % b)
# #######################################################################
# ###################### Knot Definitions ###############################
# #######################################################################
Marius Giurgi
committed
def Torus_Knot(self, linkIndex=0):
p = self.torus_p # revolution count (around the torus center)
q = self.torus_q # spin count (around the torus tube)
N = self.torus_res # curve resolution (number of control points)
Marius Giurgi
committed
# use plus options only when they are enabled
Marius Giurgi
committed
if self.options_plus:
u = self.torus_u # p multiplier
v = self.torus_v # q multiplier
h = self.torus_h # height (scale along Z)
s = self.torus_s # torus scale (radii scale factor)
else: # don't use plus settings
Marius Giurgi
committed
u = 1
v = 1
h = 1
s = 1
R = self.torus_R * s # major radius (scaled)
r = self.torus_r * s # minor radius (scaled)
Marius Giurgi
committed
# number of decoupled links when (p,q) are NOT co-primes
links = gcd(p, q) # = 1 when (p,q) are co-primes
Marius Giurgi
committed
# parametrized angle increment (cached outside of the loop for performance)
# NOTE: the total angle is divided by number of decoupled links to ensure
# the curve does not overlap with itself when (p,q) are not co-primes
da = 2 * pi / links / (N - 1)
# link phase : each decoupled link is phased equally around the torus center
# NOTE: linkIndex value is in [0, links-1]
linkPhase = 2 * pi / q * linkIndex # = 0 when there is just ONE link
Marius Giurgi
committed
if self.options_plus:
rPhase = self.torus_rP # user defined revolution phase
sPhase = self.torus_sP # user defined spin phase
else: # don't use plus settings
Marius Giurgi
committed
rPhase = 0
sPhase = 0
rPhase += linkPhase # total revolution phase of the current link
Marius Giurgi
committed
if DEBUG:
print("")
print("Link: %i of %i" % (linkIndex, links))
print("gcd = %i" % links)
print("p = %i" % p)
print("q = %i" % q)
print("link phase = %.2f deg" % (linkPhase * 180 / pi))
Marius Giurgi
committed
print("link phase = %.2f rad" % linkPhase)
# flip directions ? NOTE: flipping both is equivalent to no flip
if self.flip_p:
p *= -1
if self.flip_q:
q *= -1
Marius Giurgi
committed
# create the 3D point array for the current link
Marius Giurgi
committed
newPoints = []
for n in range(N - 1):
# t = 2 * pi / links * n/(N-1) with: da = 2*pi/links/(N-1) => t = n * da
Marius Giurgi
committed
t = n * da
theta = p * t * u + rPhase # revolution angle
phi = q * t * v + sPhase # spin angle
Marius Giurgi
committed
x = (R + r * cos(phi)) * cos(theta)
y = (R + r * cos(phi)) * sin(theta)
z = r * sin(phi) * h
Marius Giurgi
committed
# append 3D point
# NOTE : the array is adjusted later as needed to 4D for POLY and NURBS
newPoints.append([x, y, z])
Marius Giurgi
committed
# ------------------------------------------------------------------------------
# Calculate the align matrix for the new object (based on user preferences)
Marius Giurgi
committed
def align_matrix(self, context):
if self.absolute_location:
loc = Matrix.Translation(Vector((0, 0, 0)))
Marius Giurgi
committed
else:
loc = Matrix.Translation(context.scene.cursor.location)
Marius Giurgi
committed
# user defined location & translation
Marius Giurgi
committed
userLoc = Matrix.Translation(self.location)
userRot = self.rotation.to_matrix().to_4x4()
obj_align = context.preferences.edit.object_align
Marius Giurgi
committed
if (context.space_data.type == 'VIEW_3D' and obj_align == 'VIEW'):
rot = context.space_data.region_3d.view_matrix.to_3x3().inverted().to_4x4()
else:
rot = Matrix()
align_matrix = userLoc @ loc @ rot @ userRot
Marius Giurgi
committed
return align_matrix
Marius Giurgi
committed
# ------------------------------------------------------------------------------
# Set curve BEZIER handles to auto
def setBezierHandles(obj, mode='AUTO'):
Marius Giurgi
committed
scene = bpy.context.scene
if obj.type != 'CURVE':
return
#scene.objects.active = obj
#bpy.ops.object.mode_set(mode='EDIT', toggle=True)
#bpy.ops.curve.select_all(action='SELECT')
#bpy.ops.curve.handle_type_set(type=mode)
#bpy.ops.object.mode_set(mode='OBJECT', toggle=True)
Marius Giurgi
committed
Marius Giurgi
committed
# ------------------------------------------------------------------------------
# Convert array of vert coordinates to points according to spline type
Marius Giurgi
committed
def vertsToPoints(Verts, splineType):
Marius Giurgi
committed
vertArray = []
# array for BEZIER spline output (V3)
Marius Giurgi
committed
if splineType == 'BEZIER':
for v in Verts:
vertArray += v
# array for non-BEZIER output (V4)
Marius Giurgi
committed
else:
for v in Verts:
vertArray += v
if splineType == 'NURBS':
vertArray.append(1) # for NURBS w=1
else: # for POLY w=0
Marius Giurgi
committed
vertArray.append(0)
return vertArray
Marius Giurgi
committed
# ------------------------------------------------------------------------------
# Create the Torus Knot curve and object and add it to the scene
# pick a name based on (p,q) parameters
Marius Giurgi
committed
aName = "Torus Knot %i x %i" % (self.torus_p, self.torus_q)
Marius Giurgi
committed
curve_data = bpy.data.curves.new(name=aName, type='CURVE')
# setup materials to be used for the TK links
Marius Giurgi
committed
if self.use_colors:
addLinkColors(self, curve_data)
# create torus knot link(s)
Marius Giurgi
committed
if self.multiple_links:
links = gcd(self.torus_p, self.torus_q)
Marius Giurgi
committed
else:
Marius Giurgi
committed
for l in range(links):
# get vertices for the current link
Marius Giurgi
committed
verts = Torus_Knot(self, l)
# output splineType 'POLY' 'NURBS' or 'BEZIER'
Marius Giurgi
committed
splineType = self.outputType
# turn verts into proper array (based on spline type)
Marius Giurgi
committed
vertArray = vertsToPoints(verts, splineType)
# create spline from vertArray (based on spline type)
Marius Giurgi
committed
spline = curve_data.splines.new(type=splineType)
if splineType == 'BEZIER':
spline.bezier_points.add(int(len(vertArray) * 1.0 / 3 - 1))
Marius Giurgi
committed
spline.bezier_points.foreach_set('co', vertArray)
for point in spline.bezier_points:
point.handle_right_type = self.handleType
point.handle_left_type = self.handleType
Marius Giurgi
committed
else:
spline.points.add(int(len(vertArray) * 1.0 / 4 - 1))
Marius Giurgi
committed
spline.points.foreach_set('co', vertArray)
spline.use_endpoint_u = True
Marius Giurgi
committed
spline.use_cyclic_u = True
spline.order_u = 4
# set a color per link
Marius Giurgi
committed
if self.use_colors:
spline.material_index = l
Marius Giurgi
committed
curve_data.resolution_u = self.segment_res
Marius Giurgi
committed
if self.geo_surface:
curve_data.fill_mode = 'FULL'
curve_data.bevel_depth = self.geo_bDepth
curve_data.bevel_resolution = self.geo_bRes
curve_data.extrude = self.geo_extrude
Marius Giurgi
committed
curve_data.offset = self.geo_offset
# set object in the scene
new_obj = object_data_add(context, curve_data) # place in active scene
Spivak Vladimir (cwolf3d)
committed
bpy.ops.object.select_all(action='DESELECT')
new_obj.select_set(True) # set as selected
Spivak Vladimir (cwolf3d)
committed
bpy.context.view_layer.objects.active = new_obj
new_obj.matrix_world = self.align_matrix # apply matrix
Spivak Vladimir (cwolf3d)
committed
bpy.context.view_layer.update()
Marius Giurgi
committed
return
Marius Giurgi
committed
# ------------------------------------------------------------------------------
# Create materials to be assigned to each TK link
Marius Giurgi
committed
def addLinkColors(self, curveData):
# some predefined colors for the torus knot links
Marius Giurgi
committed
colors = []
if self.colorSet == "1": # RGBish
colors += [[0.0, 0.0, 1.0]]
colors += [[0.0, 1.0, 0.0]]
colors += [[1.0, 0.0, 0.0]]
colors += [[1.0, 1.0, 0.0]]
colors += [[0.0, 1.0, 1.0]]
colors += [[1.0, 0.0, 1.0]]
colors += [[1.0, 0.5, 0.0]]
colors += [[0.0, 1.0, 0.5]]
colors += [[0.5, 0.0, 1.0]]
else: # RainBow
colors += [[0.0, 0.0, 1.0]]
colors += [[0.0, 0.5, 1.0]]
colors += [[0.0, 1.0, 1.0]]
colors += [[0.0, 1.0, 0.5]]
colors += [[0.0, 1.0, 0.0]]
colors += [[0.5, 1.0, 0.0]]
colors += [[1.0, 1.0, 0.0]]
colors += [[1.0, 0.5, 0.0]]
colors += [[1.0, 0.0, 0.0]]
Marius Giurgi
committed
me = curveData
links = gcd(self.torus_p, self.torus_q)
Marius Giurgi
committed
for i in range(links):
matName = "TorusKnot-Link-%i" % i
matListNames = bpy.data.materials.keys()
Marius Giurgi
committed
if matName not in matListNames:
if DEBUG:
print("Creating new material : %s" % matName)
Marius Giurgi
committed
mat = bpy.data.materials.new(matName)
else:
if DEBUG:
print("Material %s already exists" % matName)
Marius Giurgi
committed
mat = bpy.data.materials[matName]
Marius Giurgi
committed
if self.options_plus and self.random_colors:
mat.diffuse_color = (random(), random(), random(), 1.0)
Marius Giurgi
committed
else:
cID = i % (len(colors)) # cycle through predefined colors
mat.diffuse_color = (*colors[cID], 1.0)
Marius Giurgi
committed
if self.options_plus:
Spivak Vladimir (cwolf3d)
committed
mat.diffuse_color = (mat.diffuse_color[0] * self.saturation, mat.diffuse_color[1] * self.saturation, mat.diffuse_color[2] * self.saturation, 1.0)
Marius Giurgi
committed
else:
Spivak Vladimir (cwolf3d)
committed
mat.diffuse_color = (mat.diffuse_color[0] * 0.75, mat.diffuse_color[1] * 0.75, mat.diffuse_color[2] * 0.75, 1.0)
Marius Giurgi
committed
me.materials.append(mat)
Marius Giurgi
committed
# ------------------------------------------------------------------------------
# Main Torus Knot class
class torus_knot_plus(Operator, AddObjectHelper):
bl_idname = "curve.torus_knot_plus"
bl_label = "Torus Knot +"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
Marius Giurgi
committed
bl_description = "Adds many types of tours knots"
bl_context = "object"
def mode_update_callback(self, context):
# keep the equivalent radii sets (R,r)/(eR,iR) in sync
Marius Giurgi
committed
if self.mode == 'EXT_INT':
self.torus_eR = self.torus_R + self.torus_r
self.torus_iR = self.torus_R - self.torus_r
# align_matrix for the invoke
Marius Giurgi
committed
align_matrix = None
name="Extra Options",
default=False,
description="Show more options (the plus part)",
)
absolute_location : BoolProperty(
name="Absolute Location",
default=False,
description="Set absolute location instead of relative to 3D cursor",
)
name="Use Colors",
default=False,
description="Show torus links in colors",
)
items=(('1', "RGBish", "RGBsish ordered colors"),
('2', "Rainbow", "Rainbow ordered colors")),
name="Randomize Colors",
default=False,
description="Randomize link colors",
)
name="Saturation",
default=0.75,
min=0.0, max=1.0,
description="Color saturation",
)
name="Surface",
default=True,
description="Create surface",
)
name="Bevel Depth",
default=0.04,
min=0, soft_min=0,
description="Bevel Depth",
)
name="Bevel Resolution",
default=2,
min=0, soft_min=0,
max=5, soft_max=5,
description="Bevel Resolution"
)
name="Extrude",
default=0.0,
min=0, soft_min=0,
description="Amount of curve extrusion"
)
name="Offset",
default=0.0,
min=0, soft_min=0,
description="Offset the surface relative to the curve"
)
name="p",
default=2,
min=1, soft_min=1,
description="Number of Revolutions around the torus hole before closing the knot"
name="q",
default=3,
min=1, soft_min=1,
description="Number of Spins through the torus hole before closing the knot"
name="Flip p",
default=False,
description="Flip Revolution direction"
name="Flip q",
default=False,
description="Flip Spin direction"
name="Multiple Links",
default=True,
description="Generate all links or just one link when q and q are not co-primes"
name="Rev. Multiplier",
default=1,
min=1, soft_min=1,
description="Revolutions Multiplier"
name="Spin Multiplier",
default=1,
min=1, soft_min=1,
description="Spin multiplier"
name="Revolution Phase",
default=0.0,
min=0.0, soft_min=0.0,
description="Phase revolutions by this radian amount"
)
name="Spin Phase",
default=0.0,
min=0.0, soft_min=0.0,
description="Phase spins by this radian amount"
)
# TORUS DIMENSIONS options
name="Torus Dimensions",
items=(("MAJOR_MINOR", "Major/Minor",
"Use the Major/Minor radii for torus dimensions."),
("EXT_INT", "Exterior/Interior",
"Use the Exterior/Interior radii for torus dimensions.")),
update=mode_update_callback,
)
name="Major Radius",
min=0.00, max=100.0,
default=1.0,
subtype='DISTANCE',
unit='LENGTH',
description="Radius from the torus origin to the center of the cross section"
)
name="Minor Radius",
min=0.00, max=100.0,
default=.25,
subtype='DISTANCE',
unit='LENGTH',
description="Radius of the torus' cross section"
)
name="Interior Radius",
min=0.00, max=100.0,
default=.75,
subtype='DISTANCE',
unit='LENGTH',
description="Interior radius of the torus (closest to the torus center)"
)
name="Exterior Radius",
min=0.00, max=100.0,
default=1.25,
subtype='DISTANCE',
unit='LENGTH',
description="Exterior radius of the torus (farthest from the torus center)"
)
name="Scale",
min=0.01, max=100.0,
default=1.00,
description="Scale factor to multiply the radii"
)
name="Height",
default=1.0,
min=0.0, max=100.0,
description="Scale along the local Z axis"
)
name="Curve Resolution",
default=100,
min=3, soft_min=3,
description="Number of control vertices in the curve"
)
name="Segment Resolution",
default=12,
min=1, soft_min=1,
description="Curve subdivisions per segment"
)
Marius Giurgi
committed
SplineTypes = [
('POLY', "Poly", "Poly type"),
('NURBS', "Nurbs", "Nurbs type"),
('BEZIER', "Bezier", "Bezier type")]
name="Output splines",
default='BEZIER',
description="Type of splines to output",
items=SplineTypes,
)
Marius Giurgi
committed
bezierHandles = [
('VECTOR', "Vector", "Bezier Handles type - Vector"),
('AUTO', "Auto", "Bezier Handles type - Automatic"),
items=bezierHandles,
description="Bezier handle type",
)
adaptive_resolution : BoolProperty(
name="Adaptive Resolution",
default=False,
description="Auto adjust curve resolution based on TK length",
)
Spivak Vladimir (cwolf3d)
committed
edit_mode : BoolProperty(
name="Show in edit mode",
default=True,
description="Show in edit mode"
)
# extra parameters toggle
layout.prop(self, "options_plus")
# TORUS KNOT Parameters
Marius Giurgi
committed
col = layout.column()
col.label(text="Torus Knot Parameters:")
split = box.split(factor=0.85, align=True)
split.prop(self, "torus_p", text="Revolutions")
split.prop(self, "flip_p", toggle=True, text="",
split = box.split(factor=0.85, align=True)
split.prop(self, "torus_q", text="Spins")
split.prop(self, "flip_q", toggle=True, text="",
Marius Giurgi
committed
links = gcd(self.torus_p, self.torus_q)
info = "Multiple Links"
if links > 1:
info += " ( " + str(links) + " )"
Marius Giurgi
committed
box.prop(self, 'multiple_links', text=info)
Marius Giurgi
committed
box = box.box()
col = box.column(align=True)
col.prop(self, "torus_u")
col.prop(self, "torus_v")
col = box.column(align=True)
col.prop(self, "torus_rP")
col.prop(self, "torus_sP")
Marius Giurgi
committed
# TORUS DIMENSIONS options
Marius Giurgi
committed
col = layout.column(align=True)
col.label(text="Torus Dimensions:")
box = layout.box()
col = box.column(align=True)
col.row().prop(self, "mode", expand=True)
if self.mode == "MAJOR_MINOR":
Marius Giurgi
committed
col = box.column(align=True)
col.prop(self, "torus_R")
col.prop(self, "torus_r")
else: # EXTERIOR-INTERIOR
Marius Giurgi
committed
col = box.column(align=True)
col.prop(self, "torus_eR")
col.prop(self, "torus_iR")
if self.options_plus:
box = box.box()
col = box.column(align=True)
col.prop(self, "torus_s")
col.prop(self, "torus_h")
Marius Giurgi
committed
Marius Giurgi
committed
col = layout.column(align=True)
col.label(text="Curve Options:")
box = layout.box()
col = box.column()
col.label(text="Output Curve Type:")
col.row().prop(self, "outputType", expand=True)
depends = box.column()
depends.prop(self, "torus_res")
# deactivate the "curve resolution" if "adaptive resolution" is enabled
Marius Giurgi
committed
depends.enabled = not self.adaptive_resolution
box.prop(self, "adaptive_resolution")
box.prop(self, "segment_res")
Marius Giurgi
committed
col = layout.column()
col.label(text="Geometry Options:")
box = layout.box()
box.prop(self, "geo_surface")
Marius Giurgi
committed
if self.geo_surface:
col = box.column(align=True)
col.prop(self, "geo_bDepth")
col.prop(self, "geo_bRes")
col = box.column(align=True)
col.prop(self, "geo_extrude")
col.prop(self, "geo_offset")
# COLOR options
Marius Giurgi
committed
col.label(text="Color Options:")
box = layout.box()
box.prop(self, "use_colors")
Marius Giurgi
committed
if self.use_colors and self.options_plus:
box = box.box()
box.prop(self, "colorSet")
box.prop(self, "random_colors")
box.prop(self, "saturation")
Spivak Vladimir (cwolf3d)
committed
col = layout.column()
col.row().prop(self, "edit_mode", expand=True)
Marius Giurgi
committed
Marius Giurgi
committed
col = layout.column()
col.label(text="Transform Options:")
box = col.box()
box.prop(self, "location")
box.prop(self, "absolute_location")
box.prop(self, "rotation")
return context.scene is not None
# turn off 'Enter Edit Mode'
use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
bpy.context.preferences.edit.use_enter_edit_mode = False
Marius Giurgi
committed
if self.mode == 'EXT_INT':
# adjust the equivalent radii pair : (R,r) <=> (eR,iR)
self.torus_R = (self.torus_eR + self.torus_iR) * 0.5
self.torus_r = (self.torus_eR - self.torus_iR) * 0.5
Marius Giurgi
committed
if self.adaptive_resolution:
# adjust curve resolution automatically based on (p,q,R,r) values
Marius Giurgi
committed
p = self.torus_p
q = self.torus_q
R = self.torus_R
r = self.torus_r
# get an approximate length of the whole TK curve
# upper bound approximation
maxTKLen = 2 * pi * sqrt(p * p * (R + r) * (R + r) + q * q * r * r)
# lower bound approximation
minTKLen = 2 * pi * sqrt(p * p * (R - r) * (R - r) + q * q * r * r)
avgTKLen = (minTKLen + maxTKLen) / 2 # average approximation
if DEBUG:
print("Approximate average TK length = %.2f" % avgTKLen)
Marius Giurgi
committed
# x N factor = control points per unit length
self.torus_res = max(3, avgTKLen / links * 8)
# update align matrix
Marius Giurgi
committed
self.align_matrix = align_matrix(self, context)
if use_enter_edit_mode:
bpy.ops.object.mode_set(mode = 'EDIT')
# restore pre operator state
bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
Spivak Vladimir (cwolf3d)
committed
if self.edit_mode:
bpy.ops.object.mode_set(mode = 'EDIT')
else:
bpy.ops.object.mode_set(mode = 'OBJECT')
Marius Giurgi
committed
def invoke(self, context, event):
self.execute(context)
# Register
classes = [
torus_knot_plus
]
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
if __name__ == "__main__":