""" pr3tz/__init__.py ----------------- Blender add-on entry-point for the Pr3tz procedural animation system. What this add-on does --------------------- 1. Registers a VIEW_3D N-panel ("Pr3tz") with: - A per-scene playlist of KnotItem entries, each specifying topology, geometry, animation rates, transition settings, and a material preset. - Operators for adding, removing, reordering, and randomly generating playlist entries. - Global playback controls (speed, phase, reactivity) that can be keyframed or driven for audio-reactive animation. 2. On every frame change (frame_change_post handler), procedurally rebuilds a single reused NURBS curve object ("AnimKnot") in-place using the parametric torus-knot equations — no bpy.ops, no edit-mode, no depsgraph races. 3. Interpolates geometry and cross-fades shader materials between adjacent playlist entries during configurable transition windows. 4. Provides a catalogue of 20 built-in shader presets (GLOSS_BLUE, NEON_GLOW, METALLIC, GLASS, HOLOGRAM, LAVA, IRIDESCENT, …) plus support for arbitrary project materials. Module layout ------------- types.py — KnotConfig TypedDict (shared data contract) constants.py — pure-data defaults (KNOT_CONFIGS, camera/light positions, …) compat.py — one-shot compatibility patches (headless align_matrix fix) materials.py — 20 shader builders + material cache + blend system geometry.py — _make_torus_knot() (parametric NURBS, no bpy.context reads) handler.py — @persistent frame-change handler + easing + playlist timing properties.py — Blender PropertyGroups (KnotItem, KnotGlobalSettings, …) operators.py — KNOT_OT_* operators ui.py — KNOT_UL_* lists, draw_knot_properties(), KNOT_PT_Panel scene_setup.py — setup_scene() (camera, light, world, render settings) How to use ---------- Requires Blender 5.0 or later. Option A – Persistent add-on: Zip the pr3tz/ folder (using package_addon.py) and install it from Preferences. Alternatively, copy or symlink the pr3tz/ folder to Blender's addons directory and enable it from Edit → Preferences → Add-ons. The panel appears under the "Pr3tz" tab in the 3-D viewport N-panel. Option B – Run as a script (via bootstrap runner): Open run_script.py from the workspace root in Blender's Text Editor, and click Run Script (Alt+P). Option C – Command line: blender.exe --python run_script.py """ if "bpy" in locals(): import importlib importlib.reload(compat) importlib.reload(constants) importlib.reload(types) importlib.reload(materials) importlib.reload(geometry) importlib.reload(handler) importlib.reload(properties) importlib.reload(operators) importlib.reload(ui) importlib.reload(scene_setup) # bake_export may not exist from a prior session — import if needed if "bake_export" in locals(): importlib.reload(bake_export) else: from . import bake_export else: import bpy from . import compat, constants, types, materials, geometry, handler, properties, operators, ui, scene_setup, bake_export from .compat import apply_compat_patches from .properties import ( KnotAllowedMaterial, KnotGlobalSettings, KnotItem, KnotGeneratorSettings, ) from .operators import ( KNOT_OT_Add, KNOT_OT_Remove, KNOT_OT_Move, KNOT_OT_Populate, KNOT_OT_BakePreview, KNOT_OT_SyncGeneratorMaterials, KNOT_OT_FitTimeline, KNOT_OT_FitPlaylist, KNOT_OT_GenerateRandom, ) from .bake_export import KNOT_OT_BakeExport from .ui import ( KNOT_UL_List, KNOT_UL_AllowedMaterialsList, KNOT_PT_Panel, ) from .geometry import _remove_existing_knot from .handler import knot_frame_handler from .scene_setup import setup_scene from .constants import KNOT_MAT_NAME, SHADER_BLEND_MAT_NAME # --------------------------------------------------------------------------- # Add-on metadata # --------------------------------------------------------------------------- bl_info = { "name": "Pr3tz", "author": "knot_animation project", "version": (2, 0, 0), "blender": (5, 0, 0), "location": "View3D > Sidebar > Pr3tz", "description": "Procedural torus-knot animation with playlist, transitions, and 20 shader presets", "category": "Animation", } # --------------------------------------------------------------------------- # Registered Blender classes (order matters for PropertyGroup dependencies) # --------------------------------------------------------------------------- classes = ( KnotAllowedMaterial, KnotGlobalSettings, KnotItem, KnotGeneratorSettings, KNOT_UL_List, KNOT_UL_AllowedMaterialsList, KNOT_OT_SyncGeneratorMaterials, KNOT_OT_Add, KNOT_OT_Remove, KNOT_OT_Move, KNOT_OT_Populate, KNOT_OT_BakePreview, KNOT_OT_FitTimeline, KNOT_OT_FitPlaylist, KNOT_OT_GenerateRandom, KNOT_OT_BakeExport, KNOT_PT_Panel, ) # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- def register() -> None: apply_compat_patches() for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.knot_list = bpy.props.CollectionProperty(type=KnotItem) bpy.types.Scene.knot_list_index = bpy.props.IntProperty(name="Index", default=0) bpy.types.Scene.knot_generator = bpy.props.PointerProperty(type=KnotGeneratorSettings) bpy.types.Scene.knot_globals = bpy.props.PointerProperty(type=KnotGlobalSettings) # Register to frame_change_post so the handler fires *after* Blender has # evaluated the new frame, preventing the freshly-built mesh from being # overwritten by the depsgraph update. handlers = bpy.app.handlers.frame_change_post for h in list(handlers): if getattr(h, "__name__", "") == knot_frame_handler.__name__: handlers.remove(h) handlers.append(knot_frame_handler) print("[KnotScript] Handler and UI registered.") def unregister() -> None: for cls in reversed(classes): bpy.utils.unregister_class(cls) del bpy.types.Scene.knot_list del bpy.types.Scene.knot_list_index del bpy.types.Scene.knot_generator del bpy.types.Scene.knot_globals handlers = bpy.app.handlers.frame_change_post for h in list(handlers): if getattr(h, "__name__", "") == knot_frame_handler.__name__: handlers.remove(h) _remove_existing_knot() # Purge all cached shader and blend materials on unregister to avoid # data-block buildup on script reload. Only remove zero-user materials. for mat in list(bpy.data.materials): if (mat.name.startswith("KnotShader_") or mat.name.startswith("KnotBlend_") or mat.name.startswith("KnotItem_Preset_") or mat.name in (KNOT_MAT_NAME, SHADER_BLEND_MAT_NAME)): if mat.users == 0: bpy.data.materials.remove(mat) print("[KnotScript] Handler and UI unregistered.") # --------------------------------------------------------------------------- # Entry point (run as script or via --python flag) # --------------------------------------------------------------------------- if __name__ == "__main__": register() setup_scene() # Generate the knot for the current frame immediately so the viewport is # not empty when the script first runs. knot_frame_handler(bpy.context.scene) print("[KnotScript] Ready. Start scrubbing the timeline!") print(" Press Space to play, or render with Ctrl+F12.")