558 lines
30 KiB
Python
558 lines
30 KiB
Python
"""
|
||
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")
|