Switch to Blender addon

This commit is contained in:
Stefan Cepko
2026-05-31 17:35:06 -04:00
parent 88811218fc
commit cb79e028be
11 changed files with 2693 additions and 0 deletions
+205
View File
@@ -0,0 +1,205 @@
"""
knot_animation/__init__.py
--------------------------
Blender add-on entry-point for the AnimKnots procedural animation system.
What this add-on does
---------------------
1. Registers a VIEW_3D N-panel ("AnimKnots") 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
----------
Option A Blender Text Editor:
Open knot_animation/__init__.py, click Run Script (Alt+P).
Option B Command line:
blender.exe --python knot_animation/__init__.py
Option C Persistent add-on:
Copy the knot_animation/ folder to Blender's addons directory and enable
it from Edit → Preferences → Add-ons. The panel appears under the
"AnimKnots" tab in the 3-D viewport N-panel.
"""
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)
else:
import bpy
from . import compat, constants, types, materials, geometry, handler, properties, operators, ui, scene_setup
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 .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": "AnimKnots",
"author": "knot_animation project",
"version": (2, 0, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > AnimKnots",
"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_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.")