juggler/utils.py
2025-10-29 14:16:40 -04:00

167 lines
5.0 KiB
Python

import bpy
from mathutils import Matrix
from typing import List, Optional
from dataclasses import dataclass
@dataclass
class Snapshot:
m1: Optional[Matrix];
m2: Optional[Matrix];
f1: int;
f2: int;
obj: Optional[bpy.types.Object];
@dataclass
class TimeCache:
obj: bpy.types.Object;
mat: Matrix;
cache: List[Snapshot];
cached_objects:List[TimeCache] = []
blocking = False
def sample_transform(obj:bpy.types.Object, frame: int|float)->Matrix:
"""Return the object's world transform matrix at a given frame number."""
if obj is None:
raise ValueError("No object provided")
scene = bpy.context.scene
if frame != scene.frame_current:
global blocking
blocking = True
depsgraph = bpy.context.evaluated_depsgraph_get()
current_frame = scene.frame_current
scene.frame_set(int(frame))
eval_obj = obj.evaluated_get(depsgraph)
matrix = eval_obj.matrix_world.copy()
scene.frame_set(current_frame)
blocking = False
return matrix
else:
return obj.matrix_world.copy()
def apply_accumulated_deltas(data:TimeCache, sample_time:int)->None:
"""
obj_data = (obj, base_matrix, cached_segments)
Applies deltas to obj based on sample_time.
"""
# obj, base_matrix, cached_data = obj_data
cumulative = Matrix.Identity(4)
last_range_end = None
for snapshot in data.cache:
if snapshot.obj is None:
continue
if sample_time >= snapshot.f2:
delta = snapshot.m2 @ snapshot.m1.inverted()
cumulative = delta @ cumulative
last_range_end = snapshot.f2
elif snapshot.f1 <= sample_time < snapshot.f2:
current = sample_transform(snapshot.obj, sample_time)
delta = current @ snapshot.m1.inverted()
cumulative = delta @ cumulative
last_range_end = sample_time
break
else:
break
if last_range_end is None:
return
data.obj.matrix_world = cumulative @ data.mat
def parse_scene_objects() -> None:
"""
Scans all objects in the current scene for Child Of constraints named like 'frame_<frame_number>'.
Records for each object:
- Base matrix captured one frame before the first control
- List of control segments (m1, m2, f1, f2, control)
"""
global cached_objects
cached_objects = []
for target in bpy.context.scene.objects:
parse_object(target)
def parse_object(target:bpy.types.Object)->Optional[TimeCache]:
frame_entries:List[Snapshot] = []
for constraint in target.constraints:
if constraint.type == 'CHILD_OF' and constraint.name.startswith("frame_"):
try:
frame = int(constraint.name.split("_")[1])
frame_entries.append(Snapshot(m1=None, m2=None, f1=frame, f2=frame, obj=constraint.target))
except Exception as e:
print(f"Invalid constraint name '{constraint.name}' on '{target.name}': {e}")
finally:
constraint.mute = True # disable constraint
if len(frame_entries) == 0:
return None
frame_entries.sort(key=lambda x: x.f1)
# capture baseline before first frame
first_frame:int = frame_entries[0].f1
base_frame:int = max(first_frame - 1, 0)
base_matrix:Matrix = sample_transform(target, base_frame)
#for i, (f1, control) in enumerate(frame_entries):
for i, snapshot in enumerate(frame_entries):
check_next = frame_entries[i + 1].f1 if i + 1 < len(frame_entries) else 999999
snapshot.f2 = check_next - 1 if check_next > snapshot.f1 else snapshot.f1
print("frames are:", snapshot.f1, snapshot.f2)
if snapshot.obj:
snapshot.m1 = sample_transform(snapshot.obj, snapshot.f1)
snapshot.m2 = sample_transform(snapshot.obj, snapshot.f2)
cached = TimeCache(obj=target, mat=base_matrix, cache=frame_entries)
global cached_objects
cached_objects.append(cached)
return cached
def release_object(obj:bpy.types.Object)->None:
global cached_objects
for i, record in enumerate(cached_objects):
if obj is record.obj:
del cached_objects[i]
return
def recompute()->None:
for obj_data in cached_objects:
apply_accumulated_deltas(obj_data, bpy.context.scene.frame_current)
def frame_change_handler(scene:bpy.types.Scene)->None:
if blocking:
print("blocked")
else:
recompute()
def unregister_existing_handler()->None:
# Remove any previously registered version of this handler
for h in bpy.app.handlers.frame_change_post:
if h.__name__ == "frame_change_handler":
bpy.app.handlers.frame_change_post.remove(h)
print("Removed old frame_change_handler")
def register_handler()->None:
unregister_existing_handler()
bpy.app.handlers.frame_change_post.append(frame_change_handler)
print("Registered new frame_change_handler")