# Pr3tz — Architecture Overview This document describes the internal design of the add-on for contributors and developers who want to extend or debug it. --- ## High-Level Flow ``` Blender Timeline scrub / playback │ ▼ frame_change_post handler ── handler.py :: knot_frame_handler() │ ├── compute playlist position (which knot is active, transition blend weight) │ ├── geometry.py :: _make_torus_knot(config) │ └── writes NURBS control points into the shared "AnimKnot" object │ └── materials.py :: apply shader / blend material to "AnimKnot" ``` **Key design choice:** The handler never calls `bpy.ops` and never touches `bpy.context.space_data`. This makes it safe to run headlessly (command-line renders, bake loops) without crashes. --- ## Module Descriptions ### `compat.py` Applied once at addon load. Monkey-patches `align_matrix` in `add_curve_torus_knots` so it doesn't crash when there is no `space_data` (headless / baked renders). ### `constants.py` Pure Python — no `bpy` imports. Contains: - `KNOT_CONFIGS` — the default 10-entry demo playlist. - Scene defaults (camera position, light energy). - String constants for well-known object/material names. ### `types.py` A `TypedDict` called `KnotConfig` that documents every key the geometry and materials systems expect. Used for type-checking; not enforced at runtime. ### `properties.py` All Blender `PropertyGroup` subclasses: - `KnotAllowedMaterial` — an item in the generator's allowed-material list. - `KnotGlobalSettings` — scene-level animation controls. - `KnotItem` — one entry in the knot playlist. Includes `to_dict()` to serialize to a plain Python dict. - `KnotGeneratorSettings` — the random playlist generator's configuration. ### `geometry.py` `_make_torus_knot(config)` — the core parametric geometry function. - Accepts a `KnotConfig` dict. - Finds or creates the `AnimKnot` NURBS curve object. - Writes control points directly into `curve.splines[0].points`, bypassing `bpy.ops.curve.torus_knot_plus` for headless safety. - Returns immediately if the config would produce a degenerate knot. ### `materials.py` - 20 shader builder functions, each returning a `bpy.types.Material`. - A material **cache** (`KnotShader_`) avoids rebuilding identical materials every frame. - **Preset materials per KnotItem** (`KnotItem_Preset_`) allow per-item color/roughness overrides without polluting the global cache. - **Blend materials** (`KnotBlend__`) mix two adjacent presets during transition windows using a `MixShader` node driven by a `Value` node that the handler updates each frame. - `prewarm_materials_and_blends(scene)` — builds all materials upfront so the first frame has no stutter. - `prebuild_playlist_blend_materials(scene)` — called whenever the playlist changes (add/remove/reorder) to keep blend materials in sync. ### `handler.py` `knot_frame_handler(scene)` — the `@persistent` callback registered to `frame_change_post`. Timeline math: ``` effective_frame = (current_frame × global_speed) + animation_phase for each knot_i in playlist: duration_i = frames_per_knot × cycle_rate_i cumulative_start_i = sum(duration_j for j < i) + transition_frames_i/2 active_knot, blend_weight = resolve(effective_frame, playlist) ``` During a transition window the handler interpolates geometry config dicts and sets the blend material's mix factor. ### `operators.py` All `KNOT_OT_*` operator classes: - `Add / Remove / Move` — playlist CRUD. - `Populate` — loads `KNOT_CONFIGS` into the playlist. - `BakePreview` — runs `_make_torus_knot` once for the selected item (useful while scrubbing). - `SyncGeneratorMaterials` — rebuilds the generator's allowed-materials list from live `bpy.data.materials`. - `GenerateRandom` — fills the playlist with randomly configured knots. - `FitTimeline / FitPlaylist` — synchronise frame range and `frames_per_knot`. ### `ui.py` - `KNOT_UL_List` — the playlist UIList. - `KNOT_UL_AllowedMaterialsList` — the generator's material filter UIList. - `draw_knot_properties(layout, item)` — renders the full KnotItem editor (topology, animation, material). - `KNOT_PT_Panel` — the root N-panel. ### `scene_setup.py` `setup_scene()` — called once when the add-on runs as a script. Creates or repositions the camera, area light, and sets sensible render defaults (Cycles, HDRI world, denoising). ### `bake_export.py` `KNOT_OT_BakeExport` — seven-phase bake operator. See [PARAMETERS.md](PARAMETERS.md#bake--export-options) for user-facing options. `_get_action_fcurves(obj)` — walks the Blender 5 layered Action tree (`action → layers → strips → channelbag(slot) → fcurves`) to return an iterable of FCurves without touching the removed `action.fcurves` attribute. Phases: 1. **Bake geometry** — iterate every frame, convert NURBS → mesh, fingerprint for deduplication. 2. **Visibility keyframes** — insert `hide_render` / `hide_viewport` with CONSTANT interpolation. 3. **Camera shake bake** — convert procedural camera shake to explicit `delta_location` keyframes. 4. **Cleanup** — remove the live `AnimKnot` NURBS object. 5. **Pack** — make paths relative, pack external textures, purge zero-user data blocks (first chunk only). 6. **Save copy** — `wm.save_as_mainfile(copy=True, compress=True)`. 7. **Restore session** — remove all baked objects, re-register the handler, trigger one handler call to rebuild the live knot. --- ## Data Flow Diagram ``` Scene properties (knot_list, knot_globals) │ │ read by ▼ handler.py ──────────────────────────────────────────────────────────────────┐ │ │ │ _make_torus_knot(config) apply_material(knot_obj, material) │ ▼ ▼ │ geometry.py materials.py │ │ │ │ └── writes into ──► "AnimKnot" ◄───┘ │ (NURBS curve object in scene) │ │ bake_export.py ◄──────────────────────────────────────┘ (iterates frames, calls frame_set which triggers handler, then converts knot_obj to mesh) ``` --- ## Adding a New Shader Preset 1. Open `materials.py`. 2. Add a builder function following the pattern of existing builders (e.g. `_build_gloss_blue`). The function receives `color`, `roughness`, `metallic`, `emission_strength` keyword arguments. 3. Add the new ID string to `SHADER_IDS` in `constants.py` **and** `materials.py`'s dispatch dict. 4. The EnumProperty in `KnotItem` is auto-generated from `SHADER_IDS`, so the UI will pick it up automatically. ## Adding a New KnotItem Property 1. Add the `bpy.props.*` declaration to `KnotItem` in `properties.py`. 2. Add the corresponding key to `KnotItem.to_dict()`. 3. If the property should affect geometry, read it in `geometry.py :: _make_torus_knot()`. 4. If it should be randomisable, add `r_`, `min_`, `max_` entries to `KnotGeneratorSettings` in `properties.py` and wire them in `operators.py :: KNOT_OT_GenerateRandom.execute()`. 5. Add UI for it in `ui.py :: draw_knot_properties()`.