Files
Pr3tz/knot_animation/ui.py
T
2026-05-31 17:35:06 -04:00

284 lines
11 KiB
Python

"""
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')