From 39a4725334d7825bdc3cd3ec552ab067f8b4c864 Mon Sep 17 00:00:00 2001 From: Stefan Cepko Date: Tue, 2 Jun 2026 05:19:38 -0400 Subject: [PATCH] Big update --- .gitignore | 1 + knot_animation/geometry.py | 32 ++- knot_animation/handler.py | 121 ++++++-- knot_animation/operators.py | 14 + knot_animation/properties.py | 127 ++++++++- knot_animation/types.py | 18 ++ knot_animation/ui.py | 523 ++++++++++++++++++++++------------- 7 files changed, 593 insertions(+), 243 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee00eb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +knot_animation/__pycache__ \ No newline at end of file diff --git a/knot_animation/geometry.py b/knot_animation/geometry.py index d157ea4..05321cf 100644 --- a/knot_animation/geometry.py +++ b/knot_animation/geometry.py @@ -45,6 +45,8 @@ def _make_torus_knot( 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. @@ -99,6 +101,9 @@ def _make_torus_knot( # 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) @@ -117,8 +122,6 @@ def _make_torus_knot( u = config.get('torus_u', 1) v = config.get('torus_v', 1) - rP = config.get('torus_rP', 0.0) * math.pi * 2.0 - sP = config.get('torus_sP', 0.0) * math.pi * 2.0 h = config.get('torus_h', 1.0) multiple_links = config.get('multiple_links', False) @@ -197,20 +200,23 @@ def _make_torus_knot( t_param = (i / steps) * t_max w_eff = w if link == 0 else -w - x = (R + w_eff * math.cos(twists * t_param / 2.0)) * math.cos(t_param) - y = (R + w_eff * math.cos(twists * t_param / 2.0)) * math.sin(t_param) - z = w_eff * math.sin(twists * t_param / 2.0) + 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) - y = amp * math.sin(ky * t_param + (TAU / 4.0)) - z = amp * math.sin(kz * t_param) + 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 + 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) @@ -218,6 +224,14 @@ def _make_torus_knot( 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 diff --git a/knot_animation/handler.py b/knot_animation/handler.py index e4c4db8..79c2c00 100644 --- a/knot_animation/handler.py +++ b/knot_animation/handler.py @@ -139,33 +139,33 @@ def knot_frame_handler(scene, depsgraph=None) -> None: config[key] = val_b # Animated spin phase - sP_a = item.torus_sP + effective_f * item.spin_phase_rate * reactivity - sP_b = next_item.torus_sP + effective_f * next_item.spin_phase_rate * reactivity + sP_a = item.torus_sP + effective_f * item.spin_phase_rate + sP_b = next_item.torus_sP + effective_f * next_item.spin_phase_rate config["torus_sP"] = sP_a + (sP_b - sP_a) * t # Animated revolution phase - rP_a = item.torus_rP + effective_f * item.rev_phase_rate * reactivity - rP_b = next_item.torus_rP + effective_f * next_item.rev_phase_rate * reactivity + rP_a = item.torus_rP + effective_f * item.rev_phase_rate + rP_b = next_item.torus_rP + effective_f * next_item.rev_phase_rate config["torus_rP"] = rP_a + (rP_b - rP_a) * t # Animated height pulse h_a = item.torus_h + ( - math.sin(effective_f * item.height_rate * reactivity) * abs(item.height_rate) * 5.0 + math.sin(effective_f * item.height_rate) * abs(item.height_rate) * 5.0 * reactivity if item.height_rate != 0.0 else 0.0 ) h_b = next_item.torus_h + ( - math.sin(effective_f * next_item.height_rate * reactivity) * abs(next_item.height_rate) * 5.0 + math.sin(effective_f * next_item.height_rate) * abs(next_item.height_rate) * 5.0 * reactivity if next_item.height_rate != 0.0 else 0.0 ) config["torus_h"] = h_a + (h_b - h_a) * t # Animated scale oscillation sc_a = ( - 1.0 + item.scale_amplitude * math.sin(effective_f * item.scale_rate * reactivity) + 1.0 + item.scale_amplitude * math.sin(effective_f * item.scale_rate) * reactivity if item.scale_amplitude > 0.0 and item.scale_rate != 0.0 else None ) sc_b = ( - 1.0 + next_item.scale_amplitude * math.sin(effective_f * next_item.scale_rate * reactivity) + 1.0 + next_item.scale_amplitude * math.sin(effective_f * next_item.scale_rate) * reactivity if next_item.scale_amplitude > 0.0 and next_item.scale_rate != 0.0 else None ) if sc_a is not None or sc_b is not None: @@ -176,21 +176,39 @@ def knot_frame_handler(scene, depsgraph=None) -> None: else: # Apply animated rates with reactivity (no transition) if item.spin_phase_rate != 0.0: - config["torus_sP"] += effective_f * item.spin_phase_rate * reactivity + config["torus_sP"] += effective_f * item.spin_phase_rate if item.rev_phase_rate != 0.0: - config["torus_rP"] += effective_f * item.rev_phase_rate * reactivity + config["torus_rP"] += effective_f * item.rev_phase_rate if item.height_rate != 0.0: config["torus_h"] += ( - math.sin(effective_f * item.height_rate * reactivity) * abs(item.height_rate) * 5.0 + math.sin(effective_f * item.height_rate) * abs(item.height_rate) * 5.0 * reactivity ) if item.scale_amplitude > 0.0 and item.scale_rate != 0.0: config["_scale_override"] = ( - 1.0 + item.scale_amplitude * math.sin(effective_f * item.scale_rate * reactivity) + 1.0 + item.scale_amplitude * math.sin(effective_f * item.scale_rate) * reactivity ) + # ── Apply Attenuverter Modulations ─────────────────────────────────────── + # Float mods + for key in [ + "torus_R", "torus_r", "torus_eR", "torus_iR", "torus_h", + "mobius_width", "liss_amp", "spiral_R", + "geo_extrude", "geo_offset", "geo_bDepth" + ]: + if f"mod_{key}" in config and config[f"mod_{key}"] != 0.0: + config[key] += config[f"mod_{key}"] * reactivity + + # Integer mods (clamp to minimum 1 for topology safety) + for key in [ + "torus_p", "torus_q", "mobius_twists", + "liss_kx", "liss_ky", "liss_kz", "spiral_turns" + ]: + if f"mod_{key}" in config and config[f"mod_{key}"] != 0.0: + config[key] = max(1, config[key] + int(config[f"mod_{key}"] * reactivity)) + # ── Cross-material blend ───────────────────────────────────────────────── cross_material = False if transition_active: @@ -200,26 +218,87 @@ def knot_frame_handler(scene, depsgraph=None) -> None: if cross_material: config["_skip_material"] = True + # ── Apply Global Settings & Overrides ──────────────────────────────────── + is_render = False + if depsgraph is not None and getattr(depsgraph, "mode", 'VIEWPORT') == 'RENDER': + is_render = True + + res = glob.render_resolution if is_render else glob.preview_resolution + bevel_res = glob.render_bevel_resolution if is_render else glob.preview_bevel_resolution + + if "geo_bDepth" in config: + config["geo_bDepth"] *= glob.global_master_thickness + + if "preset_emission_strength" in config: + config["preset_emission_strength"] *= glob.global_emission_multiplier + + if "preset_color" in config and glob.global_hue_shift != 0.0: + import colorsys + color_val = config["preset_color"] + if len(color_val) == 4: + r, g, b, a = color_val + else: + r, g, b = color_val + a = 1.0 + + h_hsv, s, v = colorsys.rgb_to_hsv(r, g, b) + h_hsv = (h_hsv + glob.global_hue_shift) % 1.0 + nr, ng, nb = colorsys.hsv_to_rgb(h_hsv, s, v) + + if len(color_val) == 4: + config["preset_color"] = (nr, ng, nb, a) + else: + config["preset_color"] = (nr, ng, nb) + # Resolve globals once and pass explicitly — no bpy.context inside geometry _make_torus_knot( config, - resolution=glob.resolution, - bevel_resolution=glob.bevel_resolution, + resolution=res, + bevel_resolution=bevel_res, knot_scale=glob.knot_scale, scene=scene, + turbulence=glob.global_turbulence, + smooth_shading=glob.smooth_shading, ) if cross_material: blend_name = f"KnotBlend_Item_{idx}" blend_mat = bpy.data.materials.get(blend_name) - if blend_mat: - mix_node = blend_mat.node_tree.nodes.get("_KnotBlendMix") - if mix_node: - mix_node.inputs["Fac"].default_value = float(t) - - blend_obj = bpy.data.objects.get(KNOT_OBJ_NAME) - if blend_obj: + blend_obj = bpy.data.objects.get(KNOT_OBJ_NAME) + if blend_obj: + if blend_mat: + mix_node = blend_mat.node_tree.nodes.get("_KnotBlendMix") + if mix_node: + mix_node.inputs["Fac"].default_value = float(t) if len(blend_obj.data.materials) == 0: blend_obj.data.materials.append(blend_mat) else: blend_obj.data.materials[0] = blend_mat + elif mat_a: + # Fallback if blend material wasn't generated + if len(blend_obj.data.materials) == 0: + blend_obj.data.materials.append(mat_a) + else: + blend_obj.data.materials[0] = mat_a + + # ── Post-Geometry Object Overrides ─────────────────────────────────────── + knot_obj = bpy.data.objects.get(KNOT_OBJ_NAME) + if knot_obj: + knot_obj.display_type = 'WIRE' if glob.viewport_wireframe else 'TEXTURED' + knot_obj.rotation_euler[2] = effective_f * glob.auto_turntable_speed + + # ── Camera Shake ───────────────────────────────────────────────────────── + cam = scene.camera + if cam: + shake = glob.camera_shake_amplitude + if shake > 0.0: + import random + # Deterministic noise based on frame for stable rendering + random.seed(int(f * 1000)) + cam.delta_location = ( + random.uniform(-shake, shake), + random.uniform(-shake, shake), + random.uniform(-shake, shake) + ) + else: + cam.delta_location = (0.0, 0.0, 0.0) diff --git a/knot_animation/operators.py b/knot_animation/operators.py index d986486..469fe01 100644 --- a/knot_animation/operators.py +++ b/knot_animation/operators.py @@ -250,6 +250,20 @@ class KNOT_OT_GenerateRandom(bpy.types.Operator): _rand_apply_float(gen, item, "scale_amplitude") _rand_apply_float(gen, item, "cycle_rate") + if getattr(gen, "r_modulations", False): + import random + for k in [ + "mod_torus_R", "mod_torus_r", "mod_torus_eR", "mod_torus_iR", "mod_torus_h", + "mod_mobius_width", "mod_liss_amp", "mod_spiral_R", + "mod_geo_extrude", "mod_geo_offset", "mod_geo_bDepth" + ]: + setattr(item, k, random.uniform(-0.5, 0.5)) + for k in [ + "mod_torus_p", "mod_torus_q", "mod_mobius_twists", + "mod_liss_kx", "mod_liss_ky", "mod_liss_kz", "mod_spiral_turns" + ]: + setattr(item, k, random.uniform(-2.0, 2.0)) + _rand_apply_bool(gen, item, "multiple_links") _rand_apply_bool(gen, item, "use_colors") _rand_apply_bool(gen, item, "random_colors") diff --git a/knot_animation/properties.py b/knot_animation/properties.py index 708d41b..0562819 100644 --- a/knot_animation/properties.py +++ b/knot_animation/properties.py @@ -79,19 +79,30 @@ def _update_knot_material_cb(self, context) -> None: if not self.uid: self.uid = f"knot_{random.randint(100000, 999999)}" - if self.material_mode == 'PRESET': - _ensure_item_preset_material(self) + try: + scene = getattr(context, "scene", None) + if scene is None: + return - # Rebuild blend materials to keep transitions up to date - prebuild_playlist_blend_materials(context.scene) + if self.material_mode == 'PRESET': + _ensure_item_preset_material(self) - # Force redraw of 3-D viewport - for area in context.screen.areas: - if area.type == 'VIEW_3D': - area.tag_redraw() + # 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(context.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 # --------------------------------------------------------------------------- @@ -111,10 +122,14 @@ 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) - resolution: bpy.props.IntProperty( - name="Curve Resolution", default=128, min=3, max=1024) - bevel_resolution: bpy.props.IntProperty( - name="Bevel Resolution", default=8, min=0, max=64) + 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( @@ -127,6 +142,38 @@ class KnotGlobalSettings(bpy.types.PropertyGroup): 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.""" @@ -147,25 +194,37 @@ class KnotItem(bpy.types.PropertyGroup): # 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) @@ -205,7 +264,8 @@ class KnotItem(bpy.types.PropertyGroup): # 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)") + 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=[ @@ -217,8 +277,11 @@ class KnotItem(bpy.types.PropertyGroup): # 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( @@ -226,13 +289,25 @@ class KnotItem(bpy.types.PropertyGroup): 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", @@ -267,17 +342,29 @@ class KnotItem(bpy.types.PropertyGroup): 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, @@ -325,6 +412,7 @@ class KnotGeneratorSettings(bpy.types.PropertyGroup): # 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, @@ -337,6 +425,15 @@ class KnotGeneratorSettings(bpy.types.PropertyGroup): 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) diff --git a/knot_animation/types.py b/knot_animation/types.py index 45bcde8..c9f6d71 100644 --- a/knot_animation/types.py +++ b/knot_animation/types.py @@ -24,33 +24,47 @@ class KnotConfig(TypedDict, total=False): # ── Topology (Torus Knot) ──────────────────────────────────────────────── torus_p: int + mod_torus_p: float torus_q: int + mod_torus_q: float flip_p: bool flip_q: bool multiple_links: bool # ── Topology (Mobius) ──────────────────────────────────────────────────── mobius_twists: int + mod_mobius_twists: float mobius_width: float + mod_mobius_width: float # ── Topology (Lissajous 3D) ────────────────────────────────────────────── liss_kx: int + mod_liss_kx: float liss_ky: int + mod_liss_ky: float liss_kz: int + mod_liss_kz: float liss_amp: float + mod_liss_amp: float # ── Topology (Spherical Spiral) ────────────────────────────────────────── spiral_turns: int + mod_spiral_turns: float spiral_R: float + mod_spiral_R: float # ── Radii (Major/Minor mode) ────────────────────────────────────────────── torus_R: float + mod_torus_R: float torus_r: float + mod_torus_r: float # ── Radii (Ext/Int mode) ───────────────────────────────────────────────── mode: str # 'MAJOR_MINOR' | 'EXT_INT' torus_eR: float + mod_torus_eR: float torus_iR: float + mod_torus_iR: float # ── Multipliers & phases ────────────────────────────────────────────────── torus_u: int @@ -58,6 +72,7 @@ class KnotConfig(TypedDict, total=False): torus_rP: float torus_sP: float torus_h: float + mod_torus_h: float # ── Per-knot animation rates ────────────────────────────────────────────── spin_phase_rate: float @@ -69,8 +84,11 @@ class KnotConfig(TypedDict, total=False): # ── Geometry ────────────────────────────────────────────────────────────── geo_extrude: float + mod_geo_extrude: float geo_offset: float + mod_geo_offset: float geo_bDepth: float + mod_geo_bDepth: float # ── Transition ──────────────────────────────────────────────────────────── transition_frames: int diff --git a/knot_animation/ui.py b/knot_animation/ui.py index b979314..3082abc 100644 --- a/knot_animation/ui.py +++ b/knot_animation/ui.py @@ -5,12 +5,11 @@ All Blender UI classes for knot_animation: KNOT_UL_List — playlist UIList KNOT_UL_AllowedMaterialsList— generator filter UIList - draw_knot_properties() — shared property layout helper + draw_knot_properties() — shared property layout helper (collapsible sections) KNOT_PT_Panel — main N-panel (VIEW_3D > UI > AnimKnots) -This module contains only layout code. It imports compute_playlist_duration -from handler.py for the live duration readout; it does not call any geometry -or material functions directly. +Every major block is collapsible via per-item or per-settings ui_show_* flags. +KnotItem flags are per-item so each playlist entry remembers its own state. """ from __future__ import annotations @@ -19,6 +18,38 @@ import bpy from .handler import compute_playlist_duration +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _section(layout, item, flag: str, label: str, icon: str = 'NONE'): + """Draw a collapsible box section. Returns (box, is_open).""" + box = layout.box() + row = box.row() + row.prop(item, flag, + icon='TRIA_DOWN' if getattr(item, flag) else 'TRIA_RIGHT', + icon_only=True, emboss=False) + row.label(text=label, icon=icon) + return box, getattr(item, flag) + + +def _subsection(layout, item, flag: str, label: str, icon: str = 'NONE'): + """Like _section but without an outer box — for nesting inside existing boxes.""" + row = layout.row() + row.prop(item, flag, + icon='TRIA_DOWN' if getattr(item, flag) else 'TRIA_RIGHT', + icon_only=True, emboss=False) + row.label(text=label, icon=icon) + return getattr(item, flag) + + +def draw_prop_with_mod(layout, item, prop: str, mod_prop: str): + """Draw a split row with the base property and its attenuverter (Mod).""" + split = layout.split(factor=0.6) + split.prop(item, prop) + split.prop(item, mod_prop) + + # --------------------------------------------------------------------------- # UILists # --------------------------------------------------------------------------- @@ -45,98 +76,105 @@ class KNOT_UL_AllowedMaterialsList(bpy.types.UIList): # --------------------------------------------------------------------------- def draw_knot_properties(layout, item) -> None: - """Draw all KnotItem properties into *layout*. + """Draw all KnotItem properties using collapsible per-item sections.""" - Used by both the main panel (selected item) and the generator base-knot - section so the layout stays in sync automatically. - """ - layout.prop(item, "shape_type") - - if item.shape_type == 'TORUS_KNOT': - row = layout.row(align=True) - row.prop(item, "torus_p") - row.prop(item, "torus_q") - row = layout.row(align=True) - row.prop(item, "flip_p", toggle=True, icon='ARROW_LEFTRIGHT') - row.prop(item, "flip_q", toggle=True, icon='ARROW_LEFTRIGHT') + # ── Shape ────────────────────────────────────────────────────────────── + box, open_ = _section(layout, item, "ui_show_shape", "Shape", 'CURVE_PATH') + if open_: + box.prop(item, "shape_type") - layout.prop(item, "mode") - if item.mode == 'MAJOR_MINOR': - row = layout.row(align=True) - row.prop(item, "torus_R") - row.prop(item, "torus_r") + if item.shape_type == 'TORUS_KNOT': + draw_prop_with_mod(box, item, "torus_p", "mod_torus_p") + draw_prop_with_mod(box, item, "torus_q", "mod_torus_q") + row = box.row(align=True) + row.prop(item, "flip_p", toggle=True, icon='ARROW_LEFTRIGHT') + row.prop(item, "flip_q", toggle=True, icon='ARROW_LEFTRIGHT') + box.prop(item, "mode") + if item.mode == 'MAJOR_MINOR': + draw_prop_with_mod(box, item, "torus_R", "mod_torus_R") + draw_prop_with_mod(box, item, "torus_r", "mod_torus_r") + else: + draw_prop_with_mod(box, item, "torus_eR", "mod_torus_eR") + draw_prop_with_mod(box, item, "torus_iR", "mod_torus_iR") + + elif item.shape_type == 'MOBIUS': + draw_prop_with_mod(box, item, "mobius_twists", "mod_mobius_twists") + draw_prop_with_mod(box, item, "mobius_width", "mod_mobius_width") + + elif item.shape_type == 'LISSAJOUS': + draw_prop_with_mod(box, item, "liss_kx", "mod_liss_kx") + draw_prop_with_mod(box, item, "liss_ky", "mod_liss_ky") + draw_prop_with_mod(box, item, "liss_kz", "mod_liss_kz") + draw_prop_with_mod(box, item, "liss_amp", "mod_liss_amp") + + elif item.shape_type == 'SPIRAL': + draw_prop_with_mod(box, item, "spiral_turns", "mod_spiral_turns") + draw_prop_with_mod(box, item, "spiral_R", "mod_spiral_R") + + # ── Geometry ──────────────────────────────────────────────────────────── + box, open_ = _section(layout, item, "ui_show_geometry", "Geometry", 'MESH_DATA') + if open_: + draw_prop_with_mod(box, item, "geo_extrude", "mod_geo_extrude") + draw_prop_with_mod(box, item, "geo_offset", "mod_geo_offset") + draw_prop_with_mod(box, item, "geo_bDepth", "mod_geo_bDepth") + draw_prop_with_mod(box, item, "torus_h", "mod_torus_h") + + # ── Links & Phases ─────────────────────────────────────────────────────── + box, open_ = _section(layout, item, "ui_show_links", "Links & Phases", 'LINKED') + if open_: + box.prop(item, "multiple_links") + col = box.column(align=True) + row = col.row(align=True) + row.prop(item, "torus_u") + row.prop(item, "torus_v") + row = col.row(align=True) + row.prop(item, "torus_rP") + row.prop(item, "torus_sP") + + # ── Animation Rates ────────────────────────────────────────────────────── + box, open_ = _section(layout, item, "ui_show_anim", "Animation Rates", 'ANIM') + if open_: + box.prop(item, "cycle_rate") + col = box.column(align=True) + row = col.row(align=True) + row.prop(item, "spin_phase_rate") + row.prop(item, "rev_phase_rate") + row = col.row(align=True) + row.prop(item, "height_rate") + row = col.row(align=True) + row.prop(item, "scale_rate") + row.prop(item, "scale_amplitude") + + # ── Material ───────────────────────────────────────────────────────────── + box, open_ = _section(layout, item, "ui_show_material", "Material", 'MATERIAL') + if open_: + box.prop(item, "material_mode", expand=True) + if item.material_mode == 'PRESET': + box.prop(item, "shader_id") + col = box.column(align=True) + col.prop(item, "preset_color") + row = col.row(align=True) + row.prop(item, "preset_roughness", slider=True) + row.prop(item, "preset_metallic", slider=True) + col.prop(item, "preset_emission_strength") else: - row = layout.row(align=True) - row.prop(item, "torus_eR") - row.prop(item, "torus_iR") - - elif item.shape_type == 'MOBIUS': - row = layout.row(align=True) - row.prop(item, "mobius_twists") - row.prop(item, "mobius_width") - - elif item.shape_type == 'LISSAJOUS': - row = layout.row(align=True) - row.prop(item, "liss_kx") - row.prop(item, "liss_ky") - row.prop(item, "liss_kz") - layout.prop(item, "liss_amp") - - elif item.shape_type == 'SPIRAL': - row = layout.row(align=True) - row.prop(item, "spiral_turns") - row.prop(item, "spiral_R") + box.prop(item, "project_material", text="Material") - row = layout.row(align=True) - row.prop(item, "geo_extrude") - row.prop(item, "geo_offset") - layout.prop(item, "geo_bDepth") - layout.prop(item, "torus_h") + # ── Colors ─────────────────────────────────────────────────────────────── + box, open_ = _section(layout, item, "ui_show_colors", "Colors", 'COLOR') + if open_: + box.prop(item, "use_colors") + if item.use_colors: + row = box.row(align=True) + row.prop(item, "colorSet") + row.prop(item, "random_colors") - col = layout.column(align=True) - col.prop(item, "multiple_links") - row = col.row(align=True) - row.prop(item, "torus_u") - row.prop(item, "torus_v") - row = col.row(align=True) - row.prop(item, "torus_rP") - row.prop(item, "torus_sP") - - anim_box = layout.box() - anim_box.label(text="Animation Rates", icon='ANIM') - anim_box.prop(item, "cycle_rate") - col = anim_box.column(align=True) - col.prop(item, "spin_phase_rate") - col.prop(item, "rev_phase_rate") - col.prop(item, "height_rate") - row = anim_box.row(align=True) - row.prop(item, "scale_rate") - row.prop(item, "scale_amplitude") - - mat_box = layout.box() - mat_box.label(text="Material Options", icon='MATERIAL') - mat_box.prop(item, "material_mode", expand=True) - if item.material_mode == 'PRESET': - mat_box.prop(item, "shader_id") - col = mat_box.column(align=True) - col.prop(item, "preset_color") - col.prop(item, "preset_roughness", slider=True) - col.prop(item, "preset_metallic", slider=True) - col.prop(item, "preset_emission_strength") - else: - mat_box.prop(item, "project_material", text="Material") - - layout.prop(item, "use_colors") - if item.use_colors: - row = layout.row(align=True) - row.prop(item, "colorSet") - row.prop(item, "random_colors") - - trans_box = layout.box() - trans_box.label(text="Transition to Next Knot", icon='MOD_TIME') - trans_box.prop(item, "transition_frames") - if item.transition_frames > 0: - trans_box.prop(item, "transition_easing") + # ── Transition ─────────────────────────────────────────────────────────── + box, open_ = _section(layout, item, "ui_show_trans", "Transition", 'MOD_TIME') + if open_: + box.prop(item, "transition_frames") + if item.transition_frames > 0: + box.prop(item, "transition_easing") # --------------------------------------------------------------------------- @@ -144,7 +182,7 @@ def draw_knot_properties(layout, item) -> None: # --------------------------------------------------------------------------- class KNOT_PT_Panel(bpy.types.Panel): - bl_label = "AnimKnots Configuration" + bl_label = "AnimKnots" bl_idname = "KNOT_PT_panel" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' @@ -153,131 +191,220 @@ class KNOT_PT_Panel(bpy.types.Panel): def draw(self, context): layout = self.layout scene = context.scene + glob = scene.knot_globals + gen = scene.knot_generator - # ── Playlist ──────────────────────────────────────────────────────── - row = layout.row() - row.template_list("KNOT_UL_List", "", scene, "knot_list", - scene, "knot_list_index") - col = row.column(align=True) - col.operator("knot.add_item", text="", icon='ADD') - col.operator("knot.remove_item",text="", icon='REMOVE') - col.separator() - col.operator("knot.move_item", text="", icon='TRIA_UP').direction = 'UP' - col.operator("knot.move_item", text="", icon='TRIA_DOWN').direction = 'DOWN' - - layout.operator("knot.populate", icon='FILE_REFRESH') + # ── Playlist ───────────────────────────────────────────────────────── + pl_box = layout.box() + pl_row = pl_box.row() + pl_row.prop(glob, "ui_show_playlist", + icon='TRIA_DOWN' if glob.ui_show_playlist else 'TRIA_RIGHT', + icon_only=True, emboss=False) + pl_row.label(text="Playlist", icon='NLA') + if glob.ui_show_playlist: + row = pl_box.row() + row.template_list("KNOT_UL_List", "", scene, "knot_list", + scene, "knot_list_index") + col = row.column(align=True) + col.operator("knot.add_item", text="", icon='ADD') + col.operator("knot.remove_item", text="", icon='REMOVE') + col.separator() + col.operator("knot.move_item", text="", icon='TRIA_UP').direction = 'UP' + col.operator("knot.move_item", text="", icon='TRIA_DOWN').direction = 'DOWN' + pl_box.operator("knot.populate", icon='FILE_REFRESH') + # ── Selected Knot ───────────────────────────────────────────────────── if 0 <= scene.knot_list_index < len(scene.knot_list): item = scene.knot_list[scene.knot_list_index] - box = layout.box() - box.prop(item, "name") - draw_knot_properties(box, item) - layout.operator("knot.bake_preview", icon='FILE_TICK') + ke_box = layout.box() + ke_row = ke_box.row() + ke_row.prop(glob, "ui_show_knot_edit", + icon='TRIA_DOWN' if glob.ui_show_knot_edit else 'TRIA_RIGHT', + icon_only=True, emboss=False) + ke_row.label(text=f"Knot: {item.name}", icon='CURVE_PATH') + if glob.ui_show_knot_edit: + ke_box.prop(item, "name", text="") + draw_knot_properties(ke_box, item) + ke_box.operator("knot.bake_preview", icon='FILE_TICK') - # ── Global Settings ───────────────────────────────────────────────── - layout.separator() + # ── Global Settings ─────────────────────────────────────────────────── gb = layout.box() - gb.label(text="Global Settings", icon='WORLD') - glob = scene.knot_globals - gb.prop(glob, "frames_per_knot") - gb.prop(glob, "resolution") - gb.prop(glob, "bevel_resolution") - gb.prop(glob, "knot_scale") + gb_row = gb.row() + gb_row.prop(glob, "ui_show_global", + icon='TRIA_DOWN' if glob.ui_show_global else 'TRIA_RIGHT', + icon_only=True, emboss=False) + gb_row.label(text="Global Settings", icon='WORLD') - n_knots = len(scene.knot_list) - if n_knots > 0: - total_frames = compute_playlist_duration(scene) - fps = max(1, scene.render.fps) - secs = total_frames / fps - dur_row = gb.row(align=True) - dur_row.label( - text=f"Playlist: {total_frames} fr / {secs:.1f}s ({n_knots} knots)", - icon='TIME') - fit_row = gb.row(align=True) - fit_row.operator("knot.fit_timeline", icon='PREVIEW_RANGE', text="Fit Timeline") - fit_row.operator("knot.fit_playlist", icon='NLA_PUSHDOWN', text="Fit Playlist") + if glob.ui_show_global: + gb.prop(glob, "knot_scale") + gb.prop(glob, "frames_per_knot") + + # Resolutions + res_box = gb.box() + res_box.label(text="Resolutions (Preview / Render)", icon='RESTRICT_VIEW_OFF') + row = res_box.row(align=True) + row.prop(glob, "preview_resolution", text="Curve") + row.prop(glob, "render_resolution", text="") + row = res_box.row(align=True) + row.prop(glob, "preview_bevel_resolution", text="Bevel") + row.prop(glob, "render_bevel_resolution", text="") - gb.separator() - gb.label(text="Playback Control", icon='PLAY') - gb.prop(glob, "global_speed") - gb.prop(glob, "animation_phase") - gb.prop(glob, "reactivity_factor", slider=True) - gb.label(text="↑ Keyframe or drive for reactivity", icon='INFO') + # Visual Effects & Mods + fx_box = gb.box() + fx_box.label(text="Visual Effects & Tweaks", icon='MODIFIER') + fx_box.prop(glob, "global_master_thickness", slider=True) + fx_box.prop(glob, "global_emission_multiplier", slider=True) + fx_box.prop(glob, "global_hue_shift", slider=True) + fx_box.prop(glob, "global_turbulence", slider=True) + fx_box.prop(glob, "camera_shake_amplitude", slider=True) + fx_box.prop(glob, "auto_turntable_speed") + + row = fx_box.row(align=True) + row.prop(glob, "smooth_shading", toggle=True, icon='SHADING_RENDERED') + row.prop(glob, "viewport_wireframe", toggle=True, icon='SHADING_WIRE') - row = gb.row(align=True) - row.prop(scene, "frame_end", text="Total Frames") - row.prop(scene.render, "fps", text="FPS") + n_knots = len(scene.knot_list) + if n_knots > 0: + total_frames = compute_playlist_duration(scene) + fps = max(1, scene.render.fps) + secs = total_frames / fps + gb.label( + text=f"Playlist: {total_frames} fr / {secs:.1f}s ({n_knots} knots)", + icon='TIME') + row = gb.row(align=True) + row.operator("knot.fit_timeline", icon='PREVIEW_RANGE', text="Fit Timeline") + row.operator("knot.fit_playlist", icon='NLA_PUSHDOWN', text="Fit Playlist") - # ── Random Generator ──────────────────────────────────────────────── - layout.separator() + # · Playback sub-section · + gb.separator() + pb_open = _subsection(gb, glob, "ui_show_playback", "Playback", 'PLAY') + if pb_open: + pb = gb.column(align=True) + pb.prop(glob, "global_speed") + pb.prop(glob, "animation_phase") + pb.prop(glob, "reactivity_factor", slider=True) + gb.label(text="↑ Keyframe or drive for reactivity", icon='INFO') + row = gb.row(align=True) + row.prop(scene, "frame_end", text="Total Frames") + row.prop(scene.render, "fps", text="FPS") + + # ── Random Generator ────────────────────────────────────────────────── gen_box = layout.box() - gen_box.label(text="Random Knot Generator", icon='GROUP') - gen = scene.knot_generator + gen_hdr = gen_box.row() + gen_hdr.prop(gen, "ui_show_generator", + icon='TRIA_DOWN' if gen.ui_show_generator else 'TRIA_RIGHT', + icon_only=True, emboss=False) + gen_hdr.label(text="Random Generator", icon='GROUP') - gen_box.prop(gen, "num_knots") + if gen.ui_show_generator: + gen_box.prop(gen, "num_knots") - base_box = gen_box.box() - base_box.label(text="Base Defaults", icon='PRESET') - draw_knot_properties(base_box, gen.base_knot) + # · Base Knot Defaults · + base_box = gen_box.box() + base_row = base_box.row() + base_row.prop(gen, "ui_show_base", + icon='TRIA_DOWN' if gen.ui_show_base else 'TRIA_RIGHT', + icon_only=True, emboss=False) + base_row.label(text="Base Defaults", icon='PRESET') + if gen.ui_show_base: + draw_knot_properties(base_box, gen.base_knot) - rand_box = gen_box.box() - rand_box.label(text="Randomize Toggles", icon='MODIFIER') + # · Randomise Toggles (collapsible container) · + rand_box = gen_box.box() + rand_hdr = rand_box.row() + rand_hdr.prop(gen, "ui_show_rand_toggles", + icon='TRIA_DOWN' if gen.ui_show_rand_toggles else 'TRIA_RIGHT', + icon_only=True, emboss=False) + rand_hdr.label(text="Randomise Toggles", icon='MODIFIER') - def draw_rand(prop): - row = rand_box.row(align=True) - row.prop(gen, f"r_{prop}") - if getattr(gen, f"r_{prop}"): - row.prop(gen, f"min_{prop}") - row.prop(gen, f"max_{prop}") + if gen.ui_show_rand_toggles: - def draw_rand_bool(prop): - row = rand_box.row(align=True) - row.prop(gen, f"r_{prop}") - if getattr(gen, f"r_{prop}") and hasattr(gen, f"prob_{prop}"): - row.prop(gen, f"prob_{prop}") + def draw_rand(prop): + row = rand_box.row(align=True) + row.prop(gen, f"r_{prop}") + if getattr(gen, f"r_{prop}"): + row.prop(gen, f"min_{prop}") + row.prop(gen, f"max_{prop}") - rand_box.prop(gen, "r_shape_type") - draw_rand("torus_p"); draw_rand("torus_q") - draw_rand_bool("flip_p"); draw_rand_bool("flip_q") - draw_rand_bool("mode") - draw_rand("torus_R"); draw_rand("torus_r") - draw_rand("torus_eR"); draw_rand("torus_iR") - draw_rand("mobius_twists"); draw_rand("mobius_width") - draw_rand("liss_kx"); draw_rand("liss_ky"); draw_rand("liss_kz"); draw_rand("liss_amp") - draw_rand("spiral_turns"); draw_rand("spiral_R") - draw_rand("geo_extrude"); draw_rand("geo_offset"); draw_rand("geo_bDepth") - draw_rand("torus_h") - draw_rand_bool("multiple_links") - draw_rand("torus_u"); draw_rand("torus_v") - draw_rand("torus_rP"); draw_rand("torus_sP") - draw_rand("spin_phase_rate") - draw_rand_bool("use_colors"); draw_rand_bool("colorSet"); draw_rand_bool("random_colors") - draw_rand("transition_frames"); draw_rand_bool("transition_easing") + def draw_rand_bool(prop): + row = rand_box.row(align=True) + row.prop(gen, f"r_{prop}") + if getattr(gen, f"r_{prop}") and hasattr(gen, f"prob_{prop}"): + row.prop(gen, f"prob_{prop}") - rand_box.separator() - rand_box.label(text="Animation Rates", icon='ANIM') - draw_rand("cycle_rate"); draw_rand("spin_phase_rate") - draw_rand("rev_phase_rate"); draw_rand("height_rate") - draw_rand("scale_rate"); draw_rand("scale_amplitude") + # ·· Shape ·· + s_box = rand_box.box() + s_row = s_box.row() + s_row.prop(gen, "ui_show_rand_shape", + icon='TRIA_DOWN' if gen.ui_show_rand_shape else 'TRIA_RIGHT', + icon_only=True, emboss=False) + s_row.label(text="Shape", icon='CURVE_PATH') + if gen.ui_show_rand_shape: + s_box.prop(gen, "r_shape_type") + draw_rand("torus_p"); draw_rand("torus_q") + draw_rand_bool("flip_p"); draw_rand_bool("flip_q") + draw_rand_bool("mode") + draw_rand("torus_R"); draw_rand("torus_r") + draw_rand("torus_eR"); draw_rand("torus_iR") + draw_rand("mobius_twists"); draw_rand("mobius_width") + draw_rand("liss_kx"); draw_rand("liss_ky") + draw_rand("liss_kz"); draw_rand("liss_amp") + draw_rand("spiral_turns"); draw_rand("spiral_R") - row = rand_box.row(align=True) - row.prop(gen, "r_material") - if gen.r_material: - row.prop(gen, "r_preset_params", text="Random Parameters") + # ·· Geometry & Links ·· + g_box = rand_box.box() + g_row = g_box.row() + g_row.prop(gen, "ui_show_rand_geo", + icon='TRIA_DOWN' if gen.ui_show_rand_geo else 'TRIA_RIGHT', + icon_only=True, emboss=False) + g_row.label(text="Geometry & Links", icon='MESH_DATA') + if gen.ui_show_rand_geo: + draw_rand("geo_extrude"); draw_rand("geo_offset") + draw_rand("geo_bDepth"); draw_rand("torus_h") + draw_rand_bool("multiple_links") + draw_rand("torus_u"); draw_rand("torus_v") + draw_rand("torus_rP"); draw_rand("torus_sP") + draw_rand("transition_frames") + draw_rand_bool("transition_easing") - filter_box = rand_box.box() - filter_box.label(text="Allowed Materials & Presets:") + # ·· Animation Rates ·· + a_box = rand_box.box() + a_row = a_box.row() + a_row.prop(gen, "ui_show_rand_anim", + icon='TRIA_DOWN' if gen.ui_show_rand_anim else 'TRIA_RIGHT', + icon_only=True, emboss=False) + a_row.label(text="Animation Rates", icon='ANIM') + if gen.ui_show_rand_anim: + draw_rand("cycle_rate") + draw_rand("spin_phase_rate"); draw_rand("rev_phase_rate") + draw_rand("height_rate") + draw_rand("scale_rate"); draw_rand("scale_amplitude") - row_sync = filter_box.row() - row_sync.template_list( - "KNOT_UL_AllowedMaterialsList", "", - gen, "allowed_materials", - gen, "allowed_materials_index", - rows=5) - col_btn = row_sync.column(align=True) - col_btn.operator("knot.sync_generator_materials", icon='FILE_REFRESH', text="") + # ·· Material ·· + m_box = rand_box.box() + m_row = m_box.row() + m_row.prop(gen, "ui_show_rand_mat", + icon='TRIA_DOWN' if gen.ui_show_rand_mat else 'TRIA_RIGHT', + icon_only=True, emboss=False) + m_row.label(text="Material", icon='MATERIAL') + if gen.ui_show_rand_mat: + row = m_box.row(align=True) + row.prop(gen, "r_material") + if gen.r_material: + row.prop(gen, "r_preset_params", text="Rnd Params") + filter_box = m_box.box() + filter_box.label(text="Allowed Materials & Presets:") + row_sync = filter_box.row() + row_sync.template_list( + "KNOT_UL_AllowedMaterialsList", "", + gen, "allowed_materials", + gen, "allowed_materials_index", + rows=5) + row_sync.column(align=True).operator( + "knot.sync_generator_materials", icon='FILE_REFRESH', text="") + if len(gen.allowed_materials) == 0: + filter_box.label( + text="Click Sync to load project materials & presets", + icon='INFO') - if len(gen.allowed_materials) == 0: - filter_box.label(text="Click Sync to load project materials & presets", icon='INFO') - - gen_box.operator("knot.generate_random", icon='PLAY') + gen_box.operator("knot.generate_random", icon='PLAY')