Files
Pr3tz/docs/ARCHITECTURE.md
T
2026-06-04 03:49:56 -04:00

152 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_<id>`) avoids rebuilding identical materials every frame.
- **Preset materials per KnotItem** (`KnotItem_Preset_<uid>`) allow per-item color/roughness overrides without polluting the global cache.
- **Blend materials** (`KnotBlend_<uid_a>_<uid_b>`) 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_<name>`, `min_<name>`, `max_<name>` 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()`.