rename knot_animation folder

This commit is contained in:
Stefan Cepko
2026-06-04 06:04:45 -04:00
parent d6d6348ce6
commit b6d0eeffbb
12 changed files with 0 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
"""
operators.py
------------
All KNOT_OT_* operator classes for knot_animation.
P3 fix — module-level randomisation helpers
--------------------------------------------
The four helpers that were previously defined as nested closures inside
``KNOT_OT_GenerateRandom.execute()`` are now module-level functions. They
take ``gen`` (the KnotGeneratorSettings) explicitly, making them independently
callable and testable.
"""
from __future__ import annotations
import random
import bpy
from .constants import KNOT_CONFIGS, SHADER_BLEND_MAT_NAME
from .materials import (
SHADER_IDS,
prebuild_playlist_blend_materials,
prewarm_materials_and_blends,
_ensure_item_preset_material,
)
from .geometry import _make_torus_knot
from .handler import compute_playlist_duration
# ---------------------------------------------------------------------------
# Module-level randomisation helpers (P3)
# ---------------------------------------------------------------------------
def _rand_apply_int(gen, item, prop: str) -> None:
"""Set item.{prop} to a random int in [gen.min_{prop}, gen.max_{prop}]
if gen.r_{prop} is True."""
if getattr(gen, f"r_{prop}"):
setattr(item, prop, random.randint(
getattr(gen, f"min_{prop}"),
getattr(gen, f"max_{prop}"),
))
def _rand_apply_float(gen, item, prop: str) -> None:
"""Set item.{prop} to a random float in [gen.min_{prop}, gen.max_{prop}]
if gen.r_{prop} is True."""
if getattr(gen, f"r_{prop}"):
setattr(item, prop, random.uniform(
getattr(gen, f"min_{prop}"),
getattr(gen, f"max_{prop}"),
))
def _rand_apply_bool(gen, item, prop: str) -> None:
"""Set item.{prop} to True/False with probability gen.prob_{prop}
if gen.r_{prop} is True."""
if getattr(gen, f"r_{prop}"):
prob = getattr(gen, f"prob_{prop}", 0.5)
setattr(item, prop, random.random() < prob)
def _rand_apply_choice(gen, item, prop: str, choices: list) -> None:
"""Set item.{prop} to a random element of choices if gen.r_{prop} is True."""
if getattr(gen, f"r_{prop}"):
setattr(item, prop, random.choice(choices))
# ---------------------------------------------------------------------------
# Operators
# ---------------------------------------------------------------------------
class KNOT_OT_Add(bpy.types.Operator):
bl_idname = "knot.add_item"
bl_label = "Add Knot"
bl_description = "Add a new knot entry to the end of the playlist"
def execute(self, context):
item = context.scene.knot_list.add()
item.uid = f"knot_{random.randint(100000, 999999)}"
item.name = f"Knot {len(context.scene.knot_list)}"
item.material_mode = 'PRESET'
context.scene.knot_list_index = len(context.scene.knot_list) - 1
prebuild_playlist_blend_materials(context.scene)
return {'FINISHED'}
class KNOT_OT_Remove(bpy.types.Operator):
bl_idname = "knot.remove_item"
bl_label = "Remove Knot"
bl_description = "Remove the selected knot entry from the playlist"
def execute(self, context):
idx = context.scene.knot_list_index
if 0 <= idx < len(context.scene.knot_list):
context.scene.knot_list.remove(idx)
if idx > 0:
context.scene.knot_list_index = idx - 1
prebuild_playlist_blend_materials(context.scene)
return {'FINISHED'}
class KNOT_OT_Move(bpy.types.Operator):
bl_idname = "knot.move_item"
bl_label = "Move Knot"
bl_description = "Move the selected knot up or down in the playlist order"
direction: bpy.props.EnumProperty(items=[('UP', 'Up', 'Move knot earlier in the playlist'), ('DOWN', 'Down', 'Move knot later in the playlist')])
def execute(self, context):
idx = context.scene.knot_list_index
new_idx = idx - 1 if self.direction == 'UP' else idx + 1
if 0 <= new_idx < len(context.scene.knot_list):
context.scene.knot_list.move(idx, new_idx)
context.scene.knot_list_index = new_idx
prebuild_playlist_blend_materials(context.scene)
return {'FINISHED'}
class KNOT_OT_Populate(bpy.types.Operator):
bl_idname = "knot.populate"
bl_label = "Populate Default List"
bl_description = "Clear the playlist and load the 10 built-in example knots (trefoil, cinquefoil, Möbius, Lissajous, and more)"
def execute(self, context):
context.scene.knot_list.clear()
for i, config in enumerate(KNOT_CONFIGS):
item = context.scene.knot_list.add()
item.uid = f"knot_{random.randint(100000, 999999)}"
item.name = config.get("name", f"Knot {i+1}")
item.material_mode = 'PRESET'
for k, v in config.items():
if k == "name": continue
if hasattr(item, k):
setattr(item, k, v)
context.scene.knot_list_index = 0
prewarm_materials_and_blends(context.scene)
return {'FINISHED'}
class KNOT_OT_BakePreview(bpy.types.Operator):
bl_idname = "knot.bake_preview"
bl_label = "Update Preview"
bl_description = "Immediately regenerate the viewport knot using the currently selected playlist entry's settings, without waiting for a frame change"
def execute(self, context):
idx = context.scene.knot_list_index
if 0 <= idx < len(context.scene.knot_list):
config = context.scene.knot_list[idx].to_dict()
glob = context.scene.knot_globals
_make_torus_knot(
config,
resolution=glob.resolution,
bevel_resolution=glob.bevel_resolution,
knot_scale=glob.knot_scale,
scene=context.scene,
)
return {'FINISHED'}
class KNOT_OT_SyncGeneratorMaterials(bpy.types.Operator):
bl_idname = "knot.sync_generator_materials"
bl_label = "Sync Materials & Presets"
bl_description = "Reload all available presets and project materials into the generator allowed list"
def execute(self, context):
scene = context.scene
gen = scene.knot_generator
# Preserve previously enabled states
enabled_presets = {}
enabled_mats = {}
for entry in gen.allowed_materials:
if entry.is_preset:
enabled_presets[entry.preset_id] = entry.enabled
elif entry.material:
enabled_mats[entry.material.name] = entry.enabled
gen.allowed_materials.clear()
# 1. All shader presets
for sid in SHADER_IDS:
entry = gen.allowed_materials.add()
entry.name = sid.replace('_', ' ').title()
entry.preset_id = sid
entry.is_preset = True
entry.enabled = enabled_presets.get(sid, True)
# 2. All project materials (excluding generated ones)
for mat in bpy.data.materials:
name = mat.name
if (name.startswith("KnotItem_Preset_") or name.startswith("KnotBlend_")
or name == SHADER_BLEND_MAT_NAME):
continue
entry = gen.allowed_materials.add()
entry.name = name
entry.material = mat
entry.is_preset = False
entry.enabled = enabled_mats.get(name, True)
return {'FINISHED'}
class KNOT_OT_GenerateRandom(bpy.types.Operator):
bl_idname = "knot.generate_random"
bl_label = "Generate Playlist"
bl_description = "Clear the playlist and fill it with randomly configured knots using the ranges and toggles set in the Randomise Toggles section below"
def execute(self, context):
scene = context.scene
gen = scene.knot_generator
base = gen.base_knot
scene.knot_list.clear()
for i in range(gen.num_knots):
item = scene.knot_list.add()
item.name = f"Random Knot {i+1}"
# Copy base defaults
for k, v in base.to_dict().items():
if hasattr(item, k):
setattr(item, k, v)
# Apply randomisations using module-level helpers (P3)
if gen.r_shape_type:
_rand_apply_choice(gen, item, "shape_type", ['TORUS_KNOT', 'MOBIUS', 'LISSAJOUS', 'SPIRAL'])
_rand_apply_int(gen, item, "torus_p")
_rand_apply_int(gen, item, "torus_q")
_rand_apply_int(gen, item, "mobius_twists")
_rand_apply_int(gen, item, "liss_kx")
_rand_apply_int(gen, item, "liss_ky")
_rand_apply_int(gen, item, "liss_kz")
_rand_apply_int(gen, item, "spiral_turns")
_rand_apply_int(gen, item, "torus_u")
_rand_apply_int(gen, item, "torus_v")
_rand_apply_int(gen, item, "transition_frames")
_rand_apply_float(gen, item, "torus_R")
_rand_apply_float(gen, item, "torus_r")
_rand_apply_float(gen, item, "mobius_width")
_rand_apply_float(gen, item, "liss_amp")
_rand_apply_float(gen, item, "spiral_R")
_rand_apply_float(gen, item, "torus_eR")
_rand_apply_float(gen, item, "torus_iR")
_rand_apply_float(gen, item, "geo_extrude")
_rand_apply_float(gen, item, "geo_offset")
_rand_apply_float(gen, item, "geo_bDepth")
_rand_apply_float(gen, item, "torus_h")
_rand_apply_float(gen, item, "torus_rP")
_rand_apply_float(gen, item, "torus_sP")
_rand_apply_float(gen, item, "spin_phase_rate")
_rand_apply_float(gen, item, "rev_phase_rate")
_rand_apply_float(gen, item, "height_rate")
_rand_apply_float(gen, item, "scale_rate")
_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")
_rand_apply_bool(gen, item, "flip_p")
_rand_apply_bool(gen, item, "flip_q")
if gen.r_colorSet:
item.colorSet = '2' if random.random() < gen.prob_colorSet else '1'
if gen.r_mode:
item.mode = 'EXT_INT' if random.random() < gen.prob_mode else 'MAJOR_MINOR'
_rand_apply_choice(gen, item, "transition_easing",
['LINEAR', 'QUAD_IN_OUT', 'SMOOTHSTEP'])
item.uid = f"knot_{random.randint(100000, 999999)}"
if gen.r_material:
enabled_items = [am for am in gen.allowed_materials if am.enabled]
if enabled_items:
chosen = random.choice(enabled_items)
if chosen.is_preset:
item.material_mode = 'PRESET'
item.shader_id = chosen.preset_id
if getattr(gen, "r_preset_params", True):
item.preset_color = (random.random(), random.random(), random.random())
item.preset_roughness = random.uniform(0.0, 1.0)
item.preset_metallic = random.uniform(0.0, 1.0)
item.preset_emission_strength = random.uniform(0.5, 5.0)
else:
item.material_mode = 'PROJECT'
item.project_material = chosen.material
else:
item.material_mode = 'PRESET'
item.shader_id = 'GLOSS_BLUE'
scene.knot_list_index = 0
prewarm_materials_and_blends(scene)
return {'FINISHED'}
class KNOT_OT_FitTimeline(bpy.types.Operator):
"""Set frame_end so the full playlist fits exactly once in the timeline."""
bl_idname = "knot.fit_timeline"
bl_label = "Fit Timeline to Playlist"
bl_description = "Extend Total Frames so every knot plays through once"
def execute(self, context):
scene = context.scene
glob = scene.knot_globals
total = compute_playlist_duration(scene) or glob.frames_per_knot
scene.frame_end = max(scene.frame_start, total)
return {'FINISHED'}
class KNOT_OT_FitPlaylist(bpy.types.Operator):
"""Adjust frames_per_knot so the playlist fills the current frame_end exactly."""
bl_idname = "knot.fit_playlist"
bl_label = "Fit Playlist to Timeline"
bl_description = "Shrink/grow Frames per Knot so all knots fill the current Total Frames"
def execute(self, context):
scene = context.scene
glob = scene.knot_globals
n = len(scene.knot_list)
if n < 1:
self.report({'WARNING'}, "Playlist is empty")
return {'CANCELLED'}
avg_rate = sum(scene.knot_list[k].cycle_rate for k in range(n)) / n
avg_rate = max(0.01, avg_rate)
duration = scene.frame_end - scene.frame_start + 1
glob.frames_per_knot = max(1, int(duration / (n * avg_rate)))
return {'FINISHED'}