""" ui.py ----- 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 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. """ from __future__ import annotations import bpy from .handler import compute_playlist_duration # --------------------------------------------------------------------------- # UILists # --------------------------------------------------------------------------- class KNOT_UL_List(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): layout.label(text=item.name, icon='CURVE_PATH') class KNOT_UL_AllowedMaterialsList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): row = layout.row(align=True) row.prop(item, "enabled", text="") if item.is_preset: row.label(text=f"Preset: {item.name}", icon='SHADING_RENDERED') else: row.label(text=f"Material: {item.name}", icon='MATERIAL') # --------------------------------------------------------------------------- # Shared property layout helper # --------------------------------------------------------------------------- def draw_knot_properties(layout, item) -> None: """Draw all KnotItem properties into *layout*. 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') layout.prop(item, "mode") if item.mode == 'MAJOR_MINOR': row = layout.row(align=True) row.prop(item, "torus_R") row.prop(item, "torus_r") 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") 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") 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") # --------------------------------------------------------------------------- # Main panel # --------------------------------------------------------------------------- class KNOT_PT_Panel(bpy.types.Panel): bl_label = "AnimKnots Configuration" bl_idname = "KNOT_PT_panel" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'AnimKnots' def draw(self, context): layout = self.layout scene = context.scene # ── 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') 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') # ── Global Settings ───────────────────────────────────────────────── layout.separator() 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") 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") 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') row = gb.row(align=True) row.prop(scene, "frame_end", text="Total Frames") row.prop(scene.render, "fps", text="FPS") # ── Random Generator ──────────────────────────────────────────────── layout.separator() gen_box = layout.box() gen_box.label(text="Random Knot Generator", icon='GROUP') gen = scene.knot_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) rand_box = gen_box.box() rand_box.label(text="Randomize 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}") 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.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") 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") row = rand_box.row(align=True) row.prop(gen, "r_material") if gen.r_material: row.prop(gen, "r_preset_params", text="Random Parameters") filter_box = rand_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) col_btn = row_sync.column(align=True) col_btn.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') gen_box.operator("knot.generate_random", icon='PLAY')