""" 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) preview_resolution: bpy.props.IntProperty( name="Preview Curve Res", default=64, min=3, max=1024) render_resolution: bpy.props.IntProperty( name="Render Curve Res", default=128, min=3, max=2048) preview_bevel_resolution: bpy.props.IntProperty( name="Preview Bevel Res", default=4, min=0, max=64) render_bevel_resolution: bpy.props.IntProperty( name="Render Bevel Res", default=8, min=0, max=64) knot_scale: bpy.props.FloatProperty( name="Global Scale", default=1.0, min=0.01) 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", items=[ ('TORUS_KNOT', "Torus Knot", ""), ('MOBIUS', "Mobius Strip", ""), ('LISSAJOUS', "Lissajous 3D", ""), ('SPIRAL', "Spherical Spiral", ""), ], default='TORUS_KNOT' ) # Topology (Torus Knot) torus_p: bpy.props.IntProperty(name="Revolutions (p)", default=2, min=1) mod_torus_p: bpy.props.FloatProperty(name="Mod p", default=0.0) torus_q: bpy.props.IntProperty(name="Spins (q)", default=3, min=1) mod_torus_q: bpy.props.FloatProperty(name="Mod q", default=0.0) # 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) mod_torus_R: bpy.props.FloatProperty(name="Mod Major", default=0.0) torus_r: bpy.props.FloatProperty(name="Minor", default=1.0, min=0.0) mod_torus_r: bpy.props.FloatProperty(name="Mod Minor", default=0.0) # Colors (legacy TKP path) multiple_links: bpy.props.BoolProperty(name="Multiple Links", default=False) use_colors: bpy.props.BoolProperty(name="Use Colors", default=False) colorSet: bpy.props.EnumProperty( name="Color Set", items=[('1', "RGBish", ""), ('2', "Rainbow", "")], default='1') random_colors: bpy.props.BoolProperty(name="Randomize Colors", default=False) # Multipliers & phases torus_u: bpy.props.IntProperty( name="Rev. Multiplier", default=1, min=1) torus_v: bpy.props.IntProperty( name="Spin Multiplier", default=1, min=1) torus_rP: bpy.props.FloatProperty(name="Rev. Phase", default=0.0) torus_sP: bpy.props.FloatProperty(name="Spin Phase", default=0.0) # 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", items=[ ('LINEAR', "Linear", ""), ('QUAD_IN_OUT', "Quadratic In/Out", ""), ('SMOOTHSTEP', "Smoothstep", ""), ], default='QUAD_IN_OUT') # Geometry geo_extrude: bpy.props.FloatProperty(name="Extrude", default=0.0, min=0.0) mod_geo_extrude: bpy.props.FloatProperty(name="Mod Extrude", default=0.0) geo_offset: bpy.props.FloatProperty(name="Offset", default=0.0) mod_geo_offset: bpy.props.FloatProperty(name="Mod Offset", default=0.0) geo_bDepth: bpy.props.FloatProperty(name="Bevel Depth", default=0.04, min=0.0) mod_geo_bDepth: bpy.props.FloatProperty(name="Mod BDepth", default=0.0) # Dimensions mode mode: bpy.props.EnumProperty( name="Dimensions Mode", items=[('MAJOR_MINOR', "Major/Minor", ""), ('EXT_INT', "Exterior/Interior", "")], default='MAJOR_MINOR') torus_eR: bpy.props.FloatProperty(name="Exterior", default=3.0, min=0.0) mod_torus_eR: bpy.props.FloatProperty(name="Mod Ext", default=0.0) torus_iR: bpy.props.FloatProperty(name="Interior", default=1.0, min=0.0) mod_torus_iR: bpy.props.FloatProperty(name="Mod Int", default=0.0) torus_h: bpy.props.FloatProperty(name="Height", default=1.0, min=0.0) mod_torus_h: bpy.props.FloatProperty(name="Mod Height", default=0.0) # Direction flip_p: bpy.props.BoolProperty(name="Flip p", default=False) flip_q: bpy.props.BoolProperty(name="Flip q", default=False) # 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", items=[('PRESET', "Shader Preset", ""), ('PROJECT', "Project Material", "")], 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, update=_update_knot_material_cb) preset_roughness: bpy.props.FloatProperty( name="Preset Roughness", default=0.1, min=0.0, max=1.0, update=_update_knot_material_cb) preset_metallic: bpy.props.FloatProperty( name="Preset Metallic", default=0.0, min=0.0, max=1.0, update=_update_knot_material_cb) preset_emission_strength: bpy.props.FloatProperty( name="Preset Emission Strength", default=1.0, min=0.0, max=100.0, 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")