7.7 KiB
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. Includesto_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
KnotConfigdict. - Finds or creates the
AnimKnotNURBS curve object. - Writes control points directly into
curve.splines[0].points, bypassingbpy.ops.curve.torus_knot_plusfor 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 aMixShadernode driven by aValuenode 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— loadsKNOT_CONFIGSinto the playlist.BakePreview— runs_make_torus_knotonce for the selected item (useful while scrubbing).SyncGeneratorMaterials— rebuilds the generator's allowed-materials list from livebpy.data.materials.GenerateRandom— fills the playlist with randomly configured knots.FitTimeline / FitPlaylist— synchronise frame range andframes_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:
- Bake geometry — iterate every frame, convert NURBS → mesh, fingerprint for deduplication.
- Visibility keyframes — insert
hide_render/hide_viewportwith CONSTANT interpolation. - Camera shake bake — convert procedural camera shake to explicit
delta_locationkeyframes. - Cleanup — remove the live
AnimKnotNURBS object. - Pack — make paths relative, pack external textures, purge zero-user data blocks (first chunk only).
- Save copy —
wm.save_as_mainfile(copy=True, compress=True). - 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
- Open
materials.py. - Add a builder function following the pattern of existing builders (e.g.
_build_gloss_blue). The function receivescolor,roughness,metallic,emission_strengthkeyword arguments. - Add the new ID string to
SHADER_IDSinconstants.pyandmaterials.py's dispatch dict. - The EnumProperty in
KnotItemis auto-generated fromSHADER_IDS, so the UI will pick it up automatically.
Adding a New KnotItem Property
- Add the
bpy.props.*declaration toKnotIteminproperties.py. - Add the corresponding key to
KnotItem.to_dict(). - If the property should affect geometry, read it in
geometry.py :: _make_torus_knot(). - If it should be randomisable, add
r_<name>,min_<name>,max_<name>entries toKnotGeneratorSettingsinproperties.pyand wire them inoperators.py :: KNOT_OT_GenerateRandom.execute(). - Add UI for it in
ui.py :: draw_knot_properties().