Files
Pr3tz/pr3tz/properties.py
T
2026-06-04 06:04:45 -04:00

558 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
properties.py
-------------
All Blender PropertyGroup classes for knot_animation.
P4 fix — KnotGeneratorSettings
--------------------------------
Instead of 70+ individual property declarations, three helper functions
(_add_rand_int, _add_rand_float, _add_rand_bool) append range-triplets
directly to the class's __annotations__ dict before registration.
_add_rand_int → r_{prop} (Bool) + min_{prop} / max_{prop} (Int)
_add_rand_float→ r_{prop} (Bool) + min_{prop} / max_{prop} (Float)
_add_rand_bool → r_{prop} (Bool) + prob_{prop} (Float factor)
The property *identifiers* are identical to the monolith, so existing
.blend files deserialise without changes.
"""
from __future__ import annotations
import random
import bpy
from .constants import KNOT_OBJ_NAME
from .materials import (
SHADER_IDS,
_ensure_item_preset_material,
prebuild_playlist_blend_materials,
_update_viewport_knot_material,
)
# ---------------------------------------------------------------------------
# Range-property registration helpers (P4)
# ---------------------------------------------------------------------------
def _add_rand_int(cls, prop: str, label: str, min_d: int, max_d: int,
rand_default: bool = False, val_min: int | None = None) -> None:
"""Add r_{prop} (Bool) + min_{prop}/max_{prop} (Int) to a PropertyGroup."""
cls.__annotations__[f"r_{prop}"] = bpy.props.BoolProperty(name=label, default=rand_default)
int_kw: dict = {"name": "Min", "default": min_d}
if val_min is not None:
int_kw["min"] = val_min
cls.__annotations__[f"min_{prop}"] = bpy.props.IntProperty(**int_kw)
cls.__annotations__[f"max_{prop}"] = bpy.props.IntProperty(
name="Max", default=max_d, **({} if val_min is None else {"min": val_min})
)
def _add_rand_float(cls, prop: str, label: str, min_d: float, max_d: float,
rand_default: bool = False, val_min: float | None = None) -> None:
"""Add r_{prop} (Bool) + min_{prop}/max_{prop} (Float) to a PropertyGroup."""
cls.__annotations__[f"r_{prop}"] = bpy.props.BoolProperty(name=label, default=rand_default)
float_kw: dict = {"name": "Min", "default": min_d}
if val_min is not None:
float_kw["min"] = val_min
cls.__annotations__[f"min_{prop}"] = bpy.props.FloatProperty(**float_kw)
cls.__annotations__[f"max_{prop}"] = bpy.props.FloatProperty(
name="Max", default=max_d, **({} if val_min is None else {"min": val_min})
)
def _add_rand_bool(cls, prop: str, label: str, rand_default: bool = False,
prob_default: float = 0.5, prob_label: str = "% True") -> None:
"""Add r_{prop} (Bool) + prob_{prop} (Float factor) to a PropertyGroup."""
cls.__annotations__[f"r_{prop}"] = bpy.props.BoolProperty(name=label, default=rand_default)
cls.__annotations__[f"prob_{prop}"] = bpy.props.FloatProperty(
name=prob_label, default=prob_default, min=0.0, max=1.0, subtype='FACTOR'
)
# ---------------------------------------------------------------------------
# Property update callback
# ---------------------------------------------------------------------------
def _update_knot_material_cb(self, context) -> None:
"""Called whenever a material-related KnotItem property changes."""
if not self.uid:
self.uid = f"knot_{random.randint(100000, 999999)}"
try:
scene = getattr(context, "scene", None)
if scene is None:
return
if self.material_mode == 'PRESET':
_ensure_item_preset_material(self)
# Rebuild blend materials to keep transitions up to date
prebuild_playlist_blend_materials(scene)
# Instantly update the active viewport knot's material
_update_viewport_knot_material(scene)
# Force redraw of 3-D viewport if screen exists
screen = getattr(context, "screen", None)
if screen:
for area in screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
except Exception:
# Safe fallback: if this callback fires during depsgraph evaluation
# (e.g., driven by an F-curve), context accesses will throw internal state bugs.
pass
# ---------------------------------------------------------------------------
# PropertyGroups
# ---------------------------------------------------------------------------
class KnotAllowedMaterial(bpy.types.PropertyGroup):
"""Entry in the generator's allowed-materials filter list."""
material: bpy.props.PointerProperty(type=bpy.types.Material, name="Material")
preset_id: bpy.props.StringProperty(name="Preset ID", default="")
is_preset: bpy.props.BoolProperty(name="Is Preset", default=False)
enabled: bpy.props.BoolProperty(name="Enabled", default=True)
name: bpy.props.StringProperty(name="Name", default="")
class KnotGlobalSettings(bpy.types.PropertyGroup):
"""Scene-level playback and rendering settings."""
frames_per_knot: bpy.props.IntProperty(
name="Frames per Knot", default=12, min=1,
description="Base display duration (in frames) for each playlist entry. Each knot's effective duration is frames_per_knot × cycle_rate")
preview_resolution: bpy.props.IntProperty(
name="Preview Curve Res", default=64, min=3, max=1024,
description="NURBS subdivision used in the viewport. Higher values produce a smoother curve but update more slowly")
render_resolution: bpy.props.IntProperty(
name="Render Curve Res", default=128, min=3, max=2048,
description="NURBS subdivision used during renders and baking. Higher values produce higher quality output")
preview_bevel_resolution: bpy.props.IntProperty(
name="Preview Bevel Res", default=4, min=0, max=64,
description="Number of sides on the tube cross-section in the viewport")
render_bevel_resolution: bpy.props.IntProperty(
name="Render Bevel Res", default=8, min=0, max=64,
description="Number of sides on the tube cross-section during renders and baking")
knot_scale: bpy.props.FloatProperty(
name="Global Scale", default=1.0, min=0.01,
description="Uniform scale multiplier applied to all knots")
global_speed: bpy.props.FloatProperty(
name="Global Speed", default=1.0, min=0.01,
description="Multiplies the effective frame counter for all knots. Keyframeable.")
animation_phase: bpy.props.FloatProperty(
name="Animation Phase", default=0.0,
description="Frame offset added before all calculations. Keyframe or drive this for reactive control.")
reactivity_factor: bpy.props.FloatProperty(
name="Reactivity", default=1.0, min=0.0, max=10.0,
description="Scales all per-knot animation rates. Wire a driver here for audio-reactive animation.")
# New 8 Global Variables
global_turbulence: bpy.props.FloatProperty(
name="Turbulence (Noise)", default=0.0, min=0.0,
description="Injects 3D noise/displacement into the final curve vertices.")
global_emission_multiplier: bpy.props.FloatProperty(
name="Emission Multiplier", default=1.0, min=0.0,
description="Master scalar for the shader's emission strength.")
global_master_thickness: bpy.props.FloatProperty(
name="Master Thickness", default=1.0, min=0.0,
description="Multiplier for the knot's bevel depth.")
global_hue_shift: bpy.props.FloatProperty(
name="Hue Shift", default=0.0,
description="Offset applied to the material's color hue.")
camera_shake_amplitude: bpy.props.FloatProperty(
name="Camera Shake Amp", default=0.0, min=0.0,
description="Injects X/Y/Z jitter into the active camera's location.")
auto_turntable_speed: bpy.props.FloatProperty(
name="Turntable Speed", default=0.0,
description="Constant Z-axis rotation speed applied to the master knot object.")
smooth_shading: bpy.props.BoolProperty(
name="Smooth Shading", default=True,
description="Enforce Smooth shading on the generated mesh.")
viewport_wireframe: bpy.props.BoolProperty(
name="Viewport Wireframe", default=False,
description="Force the object to display as wireframe in the 3D viewport.")
# UI expand state
ui_show_global: bpy.props.BoolProperty(name="Global Settings", default=True)
ui_show_playback: bpy.props.BoolProperty(name="Playback", default=True)
ui_show_playlist: bpy.props.BoolProperty(name="Playlist", default=True)
ui_show_knot_edit: bpy.props.BoolProperty(name="Selected Knot", default=True)
class KnotItem(bpy.types.PropertyGroup):
"""One entry in the knot playlist."""
name: bpy.props.StringProperty(name="Name", default="Knot")
uid: bpy.props.StringProperty(name="Unique ID", default="")
# Shape Type
shape_type: bpy.props.EnumProperty(
name="Shape",
description="Parametric curve type used to generate this knot",
items=[
('TORUS_KNOT', "Torus Knot", "Classic (p,q) torus knot wound around a torus surface"),
('MOBIUS', "Mobius Strip", "One-sided surface with a half-twist — non-orientable loop"),
('LISSAJOUS', "Lissajous 3D", "3-axis frequency-ratio figure trace"),
('SPIRAL', "Spherical Spiral", "Spherical spiral (loxodrome) with configurable turns and radius"),
],
default='TORUS_KNOT'
)
# Topology (Torus Knot)
torus_p: bpy.props.IntProperty(
name="Revolutions (p)", default=2, min=1,
description="Number of revolutions around the torus axis. Must be coprime with q for a true knot")
mod_torus_p: bpy.props.FloatProperty(
name="Mod p", default=0.0,
description="Per-frame attenuverter offset applied to p before rendering")
torus_q: bpy.props.IntProperty(
name="Spins (q)", default=3, min=1,
description="Number of spins around the torus tube. Must be coprime with p for a true knot")
mod_torus_q: bpy.props.FloatProperty(
name="Mod q", default=0.0,
description="Per-frame attenuverter offset applied to q before rendering")
# Topology (Mobius)
mobius_twists: bpy.props.IntProperty(name="Half Twists", default=1, min=1)
mod_mobius_twists: bpy.props.FloatProperty(name="Mod Twists", default=0.0)
mobius_width: bpy.props.FloatProperty(name="Width", default=1.0, min=0.1)
mod_mobius_width: bpy.props.FloatProperty(name="Mod Width", default=0.0)
# Topology (Lissajous 3D)
liss_kx: bpy.props.IntProperty(name="kx (Freq X)", default=3, min=1)
mod_liss_kx: bpy.props.FloatProperty(name="Mod kx", default=0.0)
liss_ky: bpy.props.IntProperty(name="ky (Freq Y)", default=2, min=1)
mod_liss_ky: bpy.props.FloatProperty(name="Mod ky", default=0.0)
liss_kz: bpy.props.IntProperty(name="kz (Freq Z)", default=4, min=1)
mod_liss_kz: bpy.props.FloatProperty(name="Mod kz", default=0.0)
liss_amp: bpy.props.FloatProperty(name="Amplitude", default=2.0, min=0.1)
mod_liss_amp: bpy.props.FloatProperty(name="Mod Amp", default=0.0)
# Topology (Spherical Spiral)
spiral_turns: bpy.props.IntProperty(name="Turns", default=10, min=1)
mod_spiral_turns: bpy.props.FloatProperty(name="Mod Turns", default=0.0)
spiral_R: bpy.props.FloatProperty(name="Radius", default=2.0, min=0.1)
mod_spiral_R: bpy.props.FloatProperty(name="Mod Radius", default=0.0)
# Radii (Torus Knot)
torus_R: bpy.props.FloatProperty(
name="Major", default=2.0, min=0.0,
description="Distance from the centre of the torus to the centre of the tube")
mod_torus_R: bpy.props.FloatProperty(
name="Mod Major", default=0.0,
description="Per-frame attenuverter offset applied to the major radius")
torus_r: bpy.props.FloatProperty(
name="Minor", default=1.0, min=0.0,
description="Radius of the tube itself")
mod_torus_r: bpy.props.FloatProperty(
name="Mod Minor", default=0.0,
description="Per-frame attenuverter offset applied to the minor radius")
# Colors (legacy TKP path)
multiple_links: bpy.props.BoolProperty(
name="Multiple Links", default=False,
description="Render all gcd(p,q) link components as separate curves")
use_colors: bpy.props.BoolProperty(
name="Use Colors", default=False,
description="Apply per-link vertex colors to the curve (legacy TKP color mode)")
colorSet: bpy.props.EnumProperty(
name="Color Set",
description="Which built-in vertex color palette to use",
items=[('1', "RGBish", "Red/Green/Blue-based palette"), ('2', "Rainbow", "Full-spectrum rainbow palette")],
default='1')
random_colors: bpy.props.BoolProperty(
name="Randomize Colors", default=False,
description="Shuffle the vertex color assignment randomly each frame")
# Multipliers & phases
torus_u: bpy.props.IntProperty(
name="Rev. Multiplier", default=1, min=1,
description="Repeats the revolution pattern — creates multiple overlapping copies of the knot")
torus_v: bpy.props.IntProperty(
name="Spin Multiplier", default=1, min=1,
description="Repeats the spin pattern around the tube")
torus_rP: bpy.props.FloatProperty(
name="Rev. Phase", default=0.0,
description="Revolution phase offset in radians (orbit rotation)")
torus_sP: bpy.props.FloatProperty(
name="Spin Phase", default=0.0,
description="Spin phase offset in radians (tube rotation)")
# Animation rates
spin_phase_rate: bpy.props.FloatProperty(
name="Spin Phase Rate", default=0.0,
description="Animate spin phase (tube rotation) over time. Scaled by Reactivity.")
rev_phase_rate: bpy.props.FloatProperty(
name="Rev. Phase Rate", default=0.0,
description="Animate revolution phase (orbit rotation) over time. Scaled by Reactivity.")
height_rate: bpy.props.FloatProperty(
name="Height Pulse Rate", default=0.0,
description="Oscillates torus height over time. Scaled by Reactivity.")
scale_rate: bpy.props.FloatProperty(
name="Scale Oscillation Rate", default=0.0,
description="Frequency of per-knot scale oscillation. Scaled by Reactivity.")
scale_amplitude: bpy.props.FloatProperty(
name="Scale Oscillation Amplitude", default=0.0, min=0.0,
description="Amplitude of per-knot scale oscillation (0 = none).")
cycle_rate: bpy.props.FloatProperty(
name="Cycle Rate", default=1.0, min=0.01,
description="Multiplies frames_per_knot for this knot only.")
# Transition
transition_frames: bpy.props.IntProperty(
name="Transition Frames", default=0, min=0,
description="Number of frames to morph into the next knot (0 = instant)",
update=_update_knot_material_cb)
transition_easing: bpy.props.EnumProperty(
name="Easing",
description="Interpolation curve applied during the morph window",
items=[
('LINEAR', "Linear", "Constant-rate blend between the two knots"),
('QUAD_IN_OUT', "Quadratic In/Out", "Slow start and end, faster in the middle"),
('SMOOTHSTEP', "Smoothstep", "S-curve blend — smooth at both endpoints"),
],
default='QUAD_IN_OUT')
# Geometry
geo_extrude: bpy.props.FloatProperty(
name="Extrude", default=0.0, min=0.0,
description="Extrude the curve profile outward — creates a ribbon effect when combined with Offset")
mod_geo_extrude: bpy.props.FloatProperty(
name="Mod Extrude", default=0.0,
description="Per-frame attenuverter offset applied to Extrude")
geo_offset: bpy.props.FloatProperty(
name="Offset", default=0.0,
description="Offset the extruded profile from the curve centreline")
mod_geo_offset: bpy.props.FloatProperty(
name="Mod Offset", default=0.0,
description="Per-frame attenuverter offset applied to Offset")
geo_bDepth: bpy.props.FloatProperty(
name="Bevel Depth", default=0.04, min=0.0,
description="Tube thickness — controls how wide the knot strand is")
mod_geo_bDepth: bpy.props.FloatProperty(
name="Mod BDepth", default=0.0,
description="Per-frame attenuverter offset applied to Bevel Depth")
# Dimensions mode
mode: bpy.props.EnumProperty(
name="Dimensions Mode",
description="Choose how to specify the torus radii",
items=[
('MAJOR_MINOR', "Major/Minor", "Specify inner and outer radius as Major and Minor"),
('EXT_INT', "Exterior/Interior", "Specify the outer edge and the hole as Exterior and Interior radii"),
],
default='MAJOR_MINOR')
torus_eR: bpy.props.FloatProperty(
name="Exterior", default=3.0, min=0.0,
description="Outer edge radius of the torus (Exterior/Interior mode)")
mod_torus_eR: bpy.props.FloatProperty(
name="Mod Ext", default=0.0,
description="Per-frame attenuverter offset applied to Exterior radius")
torus_iR: bpy.props.FloatProperty(
name="Interior", default=1.0, min=0.0,
description="Inner hole radius of the torus (Exterior/Interior mode)")
mod_torus_iR: bpy.props.FloatProperty(
name="Mod Int", default=0.0,
description="Per-frame attenuverter offset applied to Interior radius")
torus_h: bpy.props.FloatProperty(
name="Height", default=1.0, min=0.0,
description="Vertical stretch of the torus — values above 1.0 elongate the knot")
mod_torus_h: bpy.props.FloatProperty(
name="Mod Height", default=0.0,
description="Per-frame attenuverter offset applied to Height")
# Direction
flip_p: bpy.props.BoolProperty(
name="Flip p", default=False,
description="Reverse the direction of the p (revolution) winding")
flip_q: bpy.props.BoolProperty(
name="Flip q", default=False,
description="Reverse the direction of the q (spin) winding")
# UI expand state (display-only, not serialised to animation)
ui_show_shape: bpy.props.BoolProperty(name="Shape", default=True)
ui_show_geometry: bpy.props.BoolProperty(name="Geometry", default=False)
ui_show_links: bpy.props.BoolProperty(name="Links & Phases", default=False)
ui_show_anim: bpy.props.BoolProperty(name="Animation Rates", default=True)
ui_show_material: bpy.props.BoolProperty(name="Material", default=True)
ui_show_colors: bpy.props.BoolProperty(name="Colors", default=False)
ui_show_trans: bpy.props.BoolProperty(name="Transition", default=False)
# Material
material_mode: bpy.props.EnumProperty(
name="Material Mode",
description="Choose between a built-in shader preset or an existing material from the project",
items=[
('PRESET', "Shader Preset", "Use one of the 20 built-in procedural shader presets"),
('PROJECT', "Project Material", "Use any material already in this .blend file"),
],
default='PRESET',
update=_update_knot_material_cb)
project_material: bpy.props.PointerProperty(
name="Material", type=bpy.types.Material,
description="Referenced material from the project",
update=_update_knot_material_cb)
shader_id: bpy.props.EnumProperty(
name="Shader Preset",
description="Material preset applied to this knot",
items=[(s, s.replace('_', ' ').title(), '') for s in SHADER_IDS],
default='GLOSS_BLUE',
update=_update_knot_material_cb)
preset_color: bpy.props.FloatVectorProperty(
name="Preset Color", subtype='COLOR', size=3,
default=(0.2, 0.6, 1.0), min=0.0, max=1.0,
description="Base colour tint passed into the selected shader preset",
update=_update_knot_material_cb)
preset_roughness: bpy.props.FloatProperty(
name="Preset Roughness", default=0.1, min=0.0, max=1.0,
description="Roughness override for the active preset (0 = mirror-smooth, 1 = fully diffuse)",
update=_update_knot_material_cb)
preset_metallic: bpy.props.FloatProperty(
name="Preset Metallic", default=0.0, min=0.0, max=1.0,
description="Metallic factor override for the active preset (0 = dielectric, 1 = metal)",
update=_update_knot_material_cb)
preset_emission_strength: bpy.props.FloatProperty(
name="Preset Emission Strength", default=1.0, min=0.0, max=100.0,
description="Emission strength override for presets that emit light",
update=_update_knot_material_cb)
def to_dict(self) -> dict:
return {
"shape_type": self.shape_type,
"torus_p": self.torus_p,
"mod_torus_p": self.mod_torus_p,
"torus_q": self.torus_q,
"mod_torus_q": self.mod_torus_q,
"mobius_twists": self.mobius_twists,
"mod_mobius_twists": self.mod_mobius_twists,
"mobius_width": self.mobius_width,
"mod_mobius_width": self.mod_mobius_width,
"liss_kx": self.liss_kx,
"mod_liss_kx": self.mod_liss_kx,
"liss_ky": self.liss_ky,
"mod_liss_ky": self.mod_liss_ky,
"liss_kz": self.liss_kz,
"mod_liss_kz": self.mod_liss_kz,
"liss_amp": self.liss_amp,
"mod_liss_amp": self.mod_liss_amp,
"spiral_turns": self.spiral_turns,
"mod_spiral_turns": self.mod_spiral_turns,
"spiral_R": self.spiral_R,
"mod_spiral_R": self.mod_spiral_R,
"torus_R": self.torus_R,
"mod_torus_R": self.mod_torus_R,
"torus_r": self.torus_r,
"mod_torus_r": self.mod_torus_r,
"multiple_links": self.multiple_links,
"use_colors": self.use_colors,
"colorSet": self.colorSet,
"random_colors": self.random_colors,
"torus_u": self.torus_u,
"torus_v": self.torus_v,
"torus_rP": self.torus_rP,
"torus_sP": self.torus_sP,
"spin_phase_rate": self.spin_phase_rate,
"rev_phase_rate": self.rev_phase_rate,
"height_rate": self.height_rate,
"scale_rate": self.scale_rate,
"scale_amplitude": self.scale_amplitude,
"cycle_rate": self.cycle_rate,
"transition_frames": self.transition_frames,
"transition_easing": self.transition_easing,
"geo_extrude": self.geo_extrude,
"geo_offset": self.geo_offset,
"geo_bDepth": self.geo_bDepth,
"mode": self.mode,
"torus_eR": self.torus_eR,
"torus_iR": self.torus_iR,
"torus_h": self.torus_h,
"flip_p": self.flip_p,
"flip_q": self.flip_q,
"material_mode": self.material_mode,
"project_material": self.project_material,
"shader_id": self.shader_id,
"preset_color": self.preset_color,
"preset_roughness": self.preset_roughness,
"preset_metallic": self.preset_metallic,
"preset_emission_strength": self.preset_emission_strength,
"uid": self.uid,
}
class KnotGeneratorSettings(bpy.types.PropertyGroup):
"""Settings for the random knot generator.
Range triplets (r_*/min_*/max_*) and bool/prob pairs are added below the
class definition via _add_rand_* helpers — see P4 in the refactor notes.
"""
num_knots: bpy.props.IntProperty(name="Number of Knots", default=10, min=1, max=100)
base_knot: bpy.props.PointerProperty(type=KnotItem)
# Standalone booleans without an associated range or prob
r_shape_type: bpy.props.BoolProperty(name="Randomize Shape Type", default=True)
r_modulations: bpy.props.BoolProperty(name="Randomize Mods", default=True, description="Add random attenuverter modulations to the generated knots")
r_transition_easing: bpy.props.BoolProperty(name="Easing", default=False)
r_material: bpy.props.BoolProperty(
name="Randomize Material", default=True,
description="Randomize the material/preset for each generated knot")
r_preset_params: bpy.props.BoolProperty(
name="Randomize Preset Parameters", default=True,
description="Randomize color, roughness, metallic, etc. for preset materials")
# Generator allowed-materials filter
allowed_materials: bpy.props.CollectionProperty(type=KnotAllowedMaterial)
allowed_materials_index: bpy.props.IntProperty(name="Index", default=0)
# UI expand state for generator rand sub-boxes
ui_show_rand_shape: bpy.props.BoolProperty(name="Shape", default=True)
ui_show_rand_geo: bpy.props.BoolProperty(name="Geometry", default=False)
ui_show_rand_anim: bpy.props.BoolProperty(name="Animation", default=True)
ui_show_rand_mat: bpy.props.BoolProperty(name="Material", default=True)
ui_show_base: bpy.props.BoolProperty(name="Base Knot Defaults", default=False)
ui_show_generator: bpy.props.BoolProperty(name="Random Generator", default=True)
ui_show_rand_toggles: bpy.props.BoolProperty(name="Randomise Toggles", default=True)
# ── Integer range triplets ────────────────────────────────────────────────────
_add_rand_int(KnotGeneratorSettings, "torus_p", "Revolutions (p)", 1, 8, rand_default=True, val_min=1)
_add_rand_int(KnotGeneratorSettings, "torus_q", "Spins (q)", 1, 8, rand_default=True, val_min=1)
_add_rand_int(KnotGeneratorSettings, "mobius_twists", "Mobius Twists", 1, 5, val_min=1)
_add_rand_int(KnotGeneratorSettings, "liss_kx", "Lissajous kx", 1, 5, val_min=1)
_add_rand_int(KnotGeneratorSettings, "liss_ky", "Lissajous ky", 1, 5, val_min=1)
_add_rand_int(KnotGeneratorSettings, "liss_kz", "Lissajous kz", 1, 5, val_min=1)
_add_rand_int(KnotGeneratorSettings, "spiral_turns", "Spiral Turns", 5, 20, val_min=1)
_add_rand_int(KnotGeneratorSettings, "torus_u", "Rev. Multiplier", 1, 4, val_min=1)
_add_rand_int(KnotGeneratorSettings, "torus_v", "Spin Multiplier", 1, 4, val_min=1)
_add_rand_int(KnotGeneratorSettings, "transition_frames","Transition Frames",0, 36, val_min=0)
# ── Float range triplets ──────────────────────────────────────────────────────
_add_rand_float(KnotGeneratorSettings, "torus_R", "Major Radius", 1.0, 4.0, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "torus_r", "Minor Radius", 0.1, 1.5, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "mobius_width", "Mobius Width", 0.5, 2.0, val_min=0.1)
_add_rand_float(KnotGeneratorSettings, "liss_amp", "Lissajous Amp", 1.0, 4.0, val_min=0.1)
_add_rand_float(KnotGeneratorSettings, "spiral_R", "Spiral Radius", 1.0, 4.0, val_min=0.1)
_add_rand_float(KnotGeneratorSettings, "torus_eR", "Exterior Radius", 2.0, 5.0, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "torus_iR", "Interior Radius", 0.5, 2.0, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "geo_extrude", "Extrude", 0.0, 0.5, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "geo_offset", "Offset", 0.0, 0.5)
_add_rand_float(KnotGeneratorSettings, "geo_bDepth", "Bevel Depth", 0.01, 0.2, rand_default=True, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "torus_h", "Height", 0.5, 3.0, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "torus_rP", "Rev. Phase", 0.0, 2.0)
_add_rand_float(KnotGeneratorSettings, "torus_sP", "Spin Phase", 0.0, 2.0)
_add_rand_float(KnotGeneratorSettings, "spin_phase_rate", "Spin Phase Rate", -0.5, 0.5)
_add_rand_float(KnotGeneratorSettings, "rev_phase_rate", "Rev. Phase Rate", -0.5, 0.5)
_add_rand_float(KnotGeneratorSettings, "height_rate", "Height Pulse Rate", 0.0, 0.2, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "scale_rate", "Scale Osc. Rate", 0.0, 0.3, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "scale_amplitude", "Scale Osc. Amplitude", 0.0, 0.4, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "cycle_rate", "Cycle Rate", 0.5, 2.0, val_min=0.01)
# ── Bool/prob pairs ───────────────────────────────────────────────────────────
_add_rand_bool(KnotGeneratorSettings, "multiple_links", "Multiple Links")
_add_rand_bool(KnotGeneratorSettings, "use_colors", "Use Colors", rand_default=True)
_add_rand_bool(KnotGeneratorSettings, "colorSet", "Color Set", rand_default=True, prob_label="% Set 2")
_add_rand_bool(KnotGeneratorSettings, "random_colors", "Randomize Colors", rand_default=True)
_add_rand_bool(KnotGeneratorSettings, "flip_p", "Flip p")
_add_rand_bool(KnotGeneratorSettings, "flip_q", "Flip q")
_add_rand_bool(KnotGeneratorSettings, "mode", "Mode", prob_label="% Ext/Int")