feat(bake): bake catalog item clips (fan spin) into the GLB + play in viewer

A catalog GLB ships its own animation clips (a ceiling fan's spin), but those
clips aren't in the editor scene graph, so the bake couldn't see them. Add an
`itemClipRegistry` (core, type-only three) that the item renderer fills with the
resolved clip per node while the scene is live; the GLB export reads it and
re-emits each item's clip onto the baked subtree, rebinding tracks to the cloned
spinning node's uuid. Catalog node names repeat across instances and the glTF
roundtrip rebinds by name, so the targeted node is uniquified per item
(`<id>__lamp_018`) — every fan animates independently.

The baked viewer plays these as looping ambient motion: `<id>: loop` clips are
set to LoopRepeat (not the door/window LoopOnce) and GlbItemAnimation drives
them off each item's toggle (lit/spinning by default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-24 18:13:48 -04:00
co-authored by Claude Opus 4.8
parent d80ffd67d5
commit 0514ec1858
6 changed files with 145 additions and 6 deletions
+15
View File
@@ -8,6 +8,7 @@ import {
type Interactive,
type ItemNode,
isSlotMaterialName,
itemClipRegistry,
LIBRARY_MATERIAL_REF_PREFIX,
type LightEffect,
SCENE_MATERIAL_REF_PREFIX,
@@ -387,6 +388,20 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
const lightEffects =
interactive?.effects.filter((e): e is LightEffect => e.kind === 'light') ?? []
// Expose this item's ambient clip (e.g. a fan's spin) to the GLB bake. The
// catalog GLB owns the clip; it isn't in the scene graph, so the export can't
// find it without this registry. The bake retargets it onto the baked subtree.
useEffect(() => {
if (!animEffect) return
const clipName = animEffect.clips.on ?? animEffect.clips.loop
const clip = clipName ? animations.find((c) => c.name === clipName) : undefined
if (!clip) return
itemClipRegistry.set(node.id, { clip, loop: true })
return () => {
itemClipRegistry.delete(node.id)
}
}, [node.id, animEffect, animations])
// useGLTF caches scenes, and Clone shares child geometry/material references.
// Undo can unmount one item while another clone of the same asset still needs them.
return (