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

7.7 KiB
Raw Permalink Blame History

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 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 copywm.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().