rename knot_animation folder
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
geometry.py
|
||||
-----------
|
||||
Procedural torus-knot geometry engine.
|
||||
|
||||
Key design choice (P2 fix)
|
||||
--------------------------
|
||||
`_make_torus_knot` no longer reads from `bpy.context`. The three
|
||||
scene-global parameters that change between calls —
|
||||
resolution, bevel_resolution, knot_scale
|
||||
— are passed in as explicit keyword arguments by the caller (either
|
||||
`knot_frame_handler` or `KNOT_OT_BakePreview.execute`). This makes
|
||||
the function independently testable and keeps geometry logic separate
|
||||
from scene-state queries.
|
||||
|
||||
Torus-knot parametric equations
|
||||
--------------------------------
|
||||
For each link l of gcd total links:
|
||||
|
||||
t ∈ [0, 2π) (steps = resolution)
|
||||
rev = (p/gcd)·t + rP + (l·2π/gcd)
|
||||
spin = (q/gcd)·t + sP
|
||||
rad = R + r·cos(spin·v)
|
||||
x = rad·cos(rev·u)
|
||||
y = rad·sin(rev·u)
|
||||
z = r·sin(spin·v)·h
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import bpy
|
||||
|
||||
from .constants import KNOT_OBJ_NAME
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_torus_knot(
|
||||
config: dict,
|
||||
*,
|
||||
resolution: int,
|
||||
bevel_resolution: int,
|
||||
knot_scale: float,
|
||||
scene=None,
|
||||
turbulence: float = 0.0,
|
||||
smooth_shading: bool = True,
|
||||
) -> bpy.types.Object:
|
||||
"""Procedurally build / update the AnimKnot NURBS curve in-place.
|
||||
|
||||
This avoids ALL crashes associated with bpy.ops, edit-mode switching,
|
||||
and dependency-graph race conditions during playback or rendering.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
config:
|
||||
KnotConfig dict produced by ``KnotItem.to_dict()`` (optionally
|
||||
augmented by the frame handler with ``_scale_override`` /
|
||||
``_skip_material`` keys).
|
||||
resolution:
|
||||
Number of NURBS control points per spline.
|
||||
bevel_resolution:
|
||||
Curve bevel resolution (controls tube facets).
|
||||
knot_scale:
|
||||
Global scale factor from ``KnotGlobalSettings.knot_scale``.
|
||||
scene:
|
||||
The active Blender scene. Used only when the AnimKnot object does
|
||||
not yet exist and needs to be linked to the scene collection.
|
||||
Falls back to ``bpy.context.scene`` if not provided.
|
||||
"""
|
||||
if scene is None:
|
||||
scene = bpy.context.scene
|
||||
|
||||
# 1. Find or create the reusable curve object
|
||||
obj = bpy.data.objects.get(KNOT_OBJ_NAME)
|
||||
if not obj:
|
||||
curve_data = bpy.data.curves.new(name=KNOT_OBJ_NAME, type='CURVE')
|
||||
obj = bpy.data.objects.new(KNOT_OBJ_NAME, curve_data)
|
||||
scene.collection.objects.link(obj)
|
||||
else:
|
||||
curve_data = obj.data
|
||||
# NOTE: Do NOT call curve_data.splines.clear() here.
|
||||
# The rendered viewport keeps an evaluated depsgraph copy alive during
|
||||
# frame_change_post. Clearing splines frees the underlying C SplinePoint
|
||||
# arrays while the evaluator's GeomCache still holds a pointer to them.
|
||||
# TBB's allocator then detects the use-after-free on the next malloc and
|
||||
# raises EXCEPTION_ACCESS_VIOLATION inside tbbmalloc.dll.
|
||||
# Instead we reconcile the spline count in-place (add/remove from tail)
|
||||
# and only reallocate point arrays when the count actually changes.
|
||||
|
||||
# 2. Curve geometry properties
|
||||
curve_data.dimensions = '3D'
|
||||
curve_data.fill_mode = 'FULL'
|
||||
curve_data.extrude = config.get('geo_extrude', 0.0)
|
||||
curve_data.offset = config.get('geo_offset', 0.0)
|
||||
curve_data.bevel_depth = config.get('geo_bDepth', 0.04)
|
||||
curve_data.bevel_resolution = bevel_resolution # ← no bpy.context read
|
||||
|
||||
# 3. Resolve shape parameters
|
||||
shape_type = config.get('shape_type', 'TORUS_KNOT')
|
||||
|
||||
rP = config.get('torus_rP', 0.0) * math.pi * 2.0
|
||||
sP = config.get('torus_sP', 0.0) * math.pi * 2.0
|
||||
|
||||
if shape_type == 'TORUS_KNOT':
|
||||
p = config.get('torus_p', 2)
|
||||
q = config.get('torus_q', 3)
|
||||
if config.get('flip_p', False): p = -p
|
||||
if config.get('flip_q', False): q = -q
|
||||
|
||||
mode = config.get('mode', 'MAJOR_MINOR')
|
||||
if mode == 'EXT_INT':
|
||||
eR = config.get('torus_eR', 3.0)
|
||||
iR = config.get('torus_iR', 1.0)
|
||||
R = (eR + iR) / 2.0
|
||||
r = (eR - iR) / 2.0
|
||||
else:
|
||||
R = config.get('torus_R', 2.0)
|
||||
r = config.get('torus_r', 1.0)
|
||||
|
||||
u = config.get('torus_u', 1)
|
||||
v = config.get('torus_v', 1)
|
||||
h = config.get('torus_h', 1.0)
|
||||
|
||||
multiple_links = config.get('multiple_links', False)
|
||||
gcd = math.gcd(abs(p), abs(q)) if p and q else 1
|
||||
if not multiple_links:
|
||||
gcd = 1
|
||||
is_cyclic = True
|
||||
|
||||
elif shape_type == 'MOBIUS':
|
||||
twists = config.get('mobius_twists', 1)
|
||||
w = config.get('mobius_width', 1.0)
|
||||
R = config.get('torus_R', 2.0) # Use torus major radius for the ring size
|
||||
gcd = 2 if twists % 2 == 0 else 1
|
||||
is_cyclic = True
|
||||
|
||||
elif shape_type == 'LISSAJOUS':
|
||||
kx = config.get('liss_kx', 3)
|
||||
ky = config.get('liss_ky', 2)
|
||||
kz = config.get('liss_kz', 4)
|
||||
amp = config.get('liss_amp', 2.0)
|
||||
gcd = 1
|
||||
is_cyclic = True
|
||||
|
||||
elif shape_type == 'SPIRAL':
|
||||
turns = config.get('spiral_turns', 10)
|
||||
R = config.get('spiral_R', 2.0)
|
||||
gcd = 1
|
||||
is_cyclic = False
|
||||
|
||||
else:
|
||||
gcd = 1
|
||||
is_cyclic = True
|
||||
|
||||
|
||||
# 4. Reconcile spline count without freeing existing splines.
|
||||
steps = resolution # ← no bpy.context read
|
||||
current_count = len(curve_data.splines)
|
||||
if current_count < gcd:
|
||||
for _ in range(gcd - current_count):
|
||||
sp = curve_data.splines.new('NURBS')
|
||||
sp.use_cyclic_u = is_cyclic
|
||||
sp.use_endpoint_u = not is_cyclic
|
||||
elif current_count > gcd:
|
||||
for _ in range(current_count - gcd):
|
||||
curve_data.splines.remove(curve_data.splines[-1])
|
||||
|
||||
# 5. Write point coordinates in-place for each spline.
|
||||
TAU = 2.0 * math.pi
|
||||
for link, spline in enumerate(curve_data.splines):
|
||||
spline.use_cyclic_u = is_cyclic
|
||||
spline.use_endpoint_u = not is_cyclic
|
||||
|
||||
# Grow or shrink the point array only when the size changes.
|
||||
current_pts = len(spline.points)
|
||||
if current_pts < steps:
|
||||
spline.points.add(steps - current_pts)
|
||||
elif current_pts > steps:
|
||||
curve_data.splines.remove(spline)
|
||||
spline = curve_data.splines.new('NURBS')
|
||||
spline.use_cyclic_u = is_cyclic
|
||||
spline.use_endpoint_u = not is_cyclic
|
||||
spline.points.add(steps - 1)
|
||||
|
||||
for i in range(steps):
|
||||
if shape_type == 'TORUS_KNOT':
|
||||
t_param = (i / steps) * TAU
|
||||
rev = (p / gcd) * t_param + rP + (link * TAU / gcd)
|
||||
spin = (q / gcd) * t_param + sP
|
||||
rad = R + r * math.cos(spin * v)
|
||||
x = rad * math.cos(rev * u)
|
||||
y = rad * math.sin(rev * u)
|
||||
z = r * math.sin(spin * v) * h
|
||||
|
||||
elif shape_type == 'MOBIUS':
|
||||
t_max = 2.0 * TAU if gcd == 1 else TAU
|
||||
t_param = (i / steps) * t_max
|
||||
w_eff = w if link == 0 else -w
|
||||
|
||||
t_param_rot = t_param + rP
|
||||
twist_angle = twists * t_param_rot / 2.0 + sP
|
||||
|
||||
x = (R + w_eff * math.cos(twist_angle)) * math.cos(t_param_rot)
|
||||
y = (R + w_eff * math.cos(twist_angle)) * math.sin(t_param_rot)
|
||||
z = w_eff * math.sin(twist_angle)
|
||||
|
||||
elif shape_type == 'LISSAJOUS':
|
||||
t_param = (i / steps) * TAU
|
||||
x = amp * math.sin(kx * t_param + rP)
|
||||
y = amp * math.sin(ky * t_param + (TAU / 4.0) + sP)
|
||||
z = amp * math.sin(kz * t_param + rP + sP)
|
||||
|
||||
elif shape_type == 'SPIRAL':
|
||||
t_param = -math.pi / 2.0 + (i / max(1, steps - 1)) * math.pi
|
||||
theta = t_param
|
||||
phi = turns * 2.0 * t_param + rP + sP
|
||||
x = R * math.cos(theta) * math.cos(phi)
|
||||
y = R * math.cos(theta) * math.sin(phi)
|
||||
z = R * math.sin(theta)
|
||||
|
||||
else:
|
||||
x, y, z = 0.0, 0.0, 0.0
|
||||
|
||||
if turbulence > 0.0:
|
||||
nx = math.sin(i * 1.345 + rP) * turbulence
|
||||
ny = math.cos(i * 0.932 - sP) * turbulence
|
||||
nz = math.sin(i * 1.777 + rP + sP) * turbulence
|
||||
x += nx
|
||||
y += ny
|
||||
z += nz
|
||||
|
||||
spline.points[i].co = (x, y, z, 1.0)
|
||||
|
||||
# 6. Transform and material
|
||||
obj.location = (0.0, 0.0, 0.0)
|
||||
|
||||
# Apply per-knot scale oscillation override if set by the handler
|
||||
scale_override = config.get("_scale_override", None)
|
||||
effective_scale = knot_scale if scale_override is None else knot_scale * scale_override
|
||||
obj.scale = (effective_scale, effective_scale, effective_scale)
|
||||
|
||||
# Assign material unless the handler has already set up a cross-shader blend
|
||||
if not config.get("_skip_material", False):
|
||||
if config.get("material_mode") == 'PROJECT':
|
||||
mat = config.get("project_material")
|
||||
else:
|
||||
uid = config.get("uid")
|
||||
mat = bpy.data.materials.get(f"KnotItem_Preset_{uid}")
|
||||
|
||||
if mat:
|
||||
if len(obj.data.materials) == 0:
|
||||
obj.data.materials.append(mat)
|
||||
else:
|
||||
obj.data.materials[0] = mat
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def _remove_existing_knot() -> None:
|
||||
"""Remove the AnimKnot object and its curve data block from the scene.
|
||||
|
||||
Called by ``unregister()`` to clean up when the script is reloaded or
|
||||
disabled. ``_make_torus_knot()`` reuses the object in-place during
|
||||
animation playback, so this function is intentionally NOT called there.
|
||||
"""
|
||||
obj = bpy.data.objects.get(KNOT_OBJ_NAME)
|
||||
if obj is not None:
|
||||
curve_data = obj.data
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
if curve_data and curve_data.users == 0:
|
||||
bpy.data.curves.remove(curve_data)
|
||||
Reference in New Issue
Block a user