Skip to content

Commit 4a9a177

Browse files
committed
persist imported animations
1 parent 711f619 commit 4a9a177

7 files changed

Lines changed: 318 additions & 23 deletions

File tree

src/components/object/model.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function Based({ uuid, ...props }: { uuid: string }) {
9494
clearRuntimeModel(uuid);
9595
setObject(null);
9696
setModelInventory(uuid, EMPTY_MATERIAL_INVENTORY);
97-
setClips(uuid, []);
97+
setClips(uuid, [], { applyPersistedImports: false });
9898
setMixerRef(uuid, null);
9999
mixerRef.current = null;
100100
setLoadState(uuid, "loading");
@@ -123,8 +123,14 @@ export function Based({ uuid, ...props }: { uuid: string }) {
123123
mixer: parsed.mixer,
124124
clips: parsed.clips,
125125
});
126-
setClips(uuid, parsed.clips);
127126
setMixerRef(uuid, parsed.mixer);
127+
setClips(uuid, parsed.clips);
128+
const loadedState = useModelsStore.getState();
129+
setOriginalRuntimeModel(uuid, {
130+
object: parsed.object,
131+
mixer: loadedState.mixerRef[uuid] ?? parsed.mixer,
132+
clips: loadedState.clips[uuid] ?? parsed.clips,
133+
});
128134
setLoadState(uuid, "loaded");
129135
PubSub.emit(EventType.MODEL_READY, { uuid });
130136

@@ -140,7 +146,7 @@ export function Based({ uuid, ...props }: { uuid: string }) {
140146
err instanceof Error ? err.message : "Unknown model parse error";
141147
setObject(null);
142148
setModelInventory(uuid, EMPTY_MATERIAL_INVENTORY);
143-
setClips(uuid, []);
149+
setClips(uuid, [], { applyPersistedImports: false });
144150
setMixerRef(uuid, null);
145151
setLoadState(uuid, "error", message);
146152
clearRuntimeModel(uuid);
@@ -197,7 +203,13 @@ export function Based({ uuid, ...props }: { uuid: string }) {
197203
clips: [],
198204
});
199205
setClips(uuid, []);
200-
setMixerRef(uuid, null);
206+
const loadedState = useModelsStore.getState();
207+
setMixerRef(uuid, loadedState.mixerRef[uuid] ?? null);
208+
setOriginalRuntimeModel(uuid, {
209+
object: built.object,
210+
mixer: loadedState.mixerRef[uuid] ?? null,
211+
clips: loadedState.clips[uuid] ?? [],
212+
});
201213
setLoadState(uuid, "loaded");
202214
PubSub.emit(EventType.MODEL_READY, { uuid });
203215
}, [
@@ -215,7 +227,7 @@ export function Based({ uuid, ...props }: { uuid: string }) {
215227
if (!runtime) return;
216228
setObject(runtime.object);
217229
mixerRef.current = runtime.mixer;
218-
setClips(uuid, runtime.clips);
230+
setClips(uuid, runtime.clips, { applyPersistedImports: false });
219231
setMixerRef(uuid, runtime.mixer);
220232
}, [activeVariant, downgradeRevision, setClips, setMixerRef, uuid]);
221233

src/store/next/models/index.ts

Lines changed: 204 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import type {
88
import * as THREE from "three";
99
import { saveFileToFS } from "@/utils/file-system/fs.web";
1010
import { disposeParsedModel, parseModel } from "@/utils/model";
11-
import { getRuntimeModel } from "@/utils/model-downgrade-runtime";
11+
import {
12+
clearDowngradedRuntimeModel,
13+
getRuntimeModel,
14+
setOriginalRuntimeClips,
15+
} from "@/utils/model-downgrade-runtime";
1216
import {
1317
withHistory,
1418
type FieldWatcher,
@@ -42,6 +46,9 @@ export type SerializableModel = Omit<
4246
>;
4347

4448
export type LoopType = THREE.AnimationActionLoopStyles;
49+
export type SerializableAnimationClip = ReturnType<
50+
typeof THREE.AnimationClip.toJSON
51+
>;
4552

4653
export interface ModelsState {
4754
models: Record<string, ModelComponent>;
@@ -59,6 +66,7 @@ export interface ModelsState {
5966
loops: Record<string, Record<string, LoopType>>;
6067
hiddenAnimations: Record<string, string[]>;
6168
animationRenames: Record<string, Record<string, string>>;
69+
importedClips: Record<string, Record<string, SerializableAnimationClip>>;
6270
currentTime: Record<string, number>;
6371
frameStep: Record<string, number>;
6472
freeze: Record<string, boolean>;
@@ -98,6 +106,7 @@ interface ModelsActions extends SnapshotEnabledStore<ModelsState> {
98106
action: THREE.AnimationAction;
99107
clip: THREE.AnimationClip;
100108
}[],
109+
options?: { applyPersistedImports?: boolean },
101110
) => void;
102111
setMixerRef: (uuid: string, mixer: THREE.AnimationMixer | null) => void;
103112
setAnimation: (uuid: string, animation: string) => void;
@@ -151,6 +160,7 @@ const initialState: ModelsState = {
151160
loops: {},
152161
hiddenAnimations: {},
153162
animationRenames: {},
163+
importedClips: {},
154164
currentTime: {},
155165
frameStep: {},
156166
freeze: {},
@@ -300,6 +310,124 @@ function applyAnimationRenamesToClips(
300310
return changed ? renamedClips : clips;
301311
}
302312

313+
function serializeAnimationClip(
314+
clip: THREE.AnimationClip,
315+
): SerializableAnimationClip {
316+
return THREE.AnimationClip.toJSON(clip) as SerializableAnimationClip;
317+
}
318+
319+
function deserializeAnimationClip(
320+
clipSnapshot: SerializableAnimationClip,
321+
): THREE.AnimationClip | null {
322+
try {
323+
return THREE.AnimationClip.parse(clipSnapshot);
324+
} catch {
325+
return null;
326+
}
327+
}
328+
329+
function getClipEntryMixer(
330+
uuid: string,
331+
state: ModelsStore,
332+
clips: ClipEntry[],
333+
) {
334+
const currentMixer = state.mixerRef[uuid] ?? mixerCache.get(uuid);
335+
if (currentMixer) return currentMixer;
336+
337+
const actionMixer = (
338+
clips[0]?.action as { getMixer?: () => THREE.AnimationMixer }
339+
)?.getMixer?.();
340+
if (actionMixer) return actionMixer;
341+
342+
const runtime = getRuntimeModel(uuid);
343+
return runtime?.object ? new THREE.AnimationMixer(runtime.object) : null;
344+
}
345+
346+
function mergePersistedImportedClips(
347+
uuid: string,
348+
clips: ClipEntry[],
349+
state: ModelsStore,
350+
) {
351+
const importedClips = state.importedClips[uuid] ?? {};
352+
if (Object.keys(importedClips).length === 0) {
353+
return {
354+
clips: applyAnimationRenamesToClips(uuid, clips, state.animationRenames),
355+
mixer: state.mixerRef[uuid] ?? mixerCache.get(uuid) ?? null,
356+
};
357+
}
358+
359+
const mixer = getClipEntryMixer(uuid, state, clips);
360+
const nextClips = [
361+
...applyAnimationRenamesToClips(uuid, clips, state.animationRenames),
362+
];
363+
const renames = state.animationRenames[uuid] ?? {};
364+
365+
for (const clipSnapshot of Object.values(importedClips)) {
366+
const importedClip = deserializeAnimationClip(clipSnapshot);
367+
if (!importedClip) continue;
368+
369+
importedClip.name = renames[importedClip.name] ?? importedClip.name;
370+
const action = mixer
371+
? mixer.clipAction(importedClip)
372+
: ({} as THREE.AnimationAction);
373+
const index = nextClips.findIndex(
374+
(entry) => entry.clip.name === importedClip.name,
375+
);
376+
377+
if (index >= 0) {
378+
nextClips[index] = { action, clip: importedClip };
379+
continue;
380+
}
381+
382+
nextClips.push({ action, clip: importedClip });
383+
}
384+
385+
return { clips: nextClips, mixer };
386+
}
387+
388+
function upsertImportedClipSnapshots(
389+
importedClips: ModelsState["importedClips"],
390+
uuid: string,
391+
clips: THREE.AnimationClip[],
392+
) {
393+
if (clips.length === 0) return importedClips;
394+
395+
return {
396+
...importedClips,
397+
[uuid]: {
398+
...(importedClips[uuid] ?? {}),
399+
...Object.fromEntries(
400+
clips.map((clip) => [clip.name, serializeAnimationClip(clip)]),
401+
),
402+
},
403+
};
404+
}
405+
406+
function renameImportedClipSnapshot(
407+
importedClips: ModelsState["importedClips"],
408+
uuid: string,
409+
fromName: string,
410+
toName: string,
411+
) {
412+
const current = importedClips[uuid];
413+
const snapshot = current?.[fromName];
414+
if (!current || !snapshot) return importedClips;
415+
416+
const clip = deserializeAnimationClip(snapshot);
417+
if (!clip) return importedClips;
418+
419+
clip.name = toName;
420+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
421+
const { [fromName]: _removed, ...rest } = current;
422+
return {
423+
...importedClips,
424+
[uuid]: {
425+
...rest,
426+
[toName]: serializeAnimationClip(clip),
427+
},
428+
};
429+
}
430+
303431
function renameClipEntry(
304432
entry: ClipEntry,
305433
mixer: THREE.AnimationMixer | null | undefined,
@@ -389,6 +517,12 @@ function renameAnimationInState(
389517
fromName,
390518
toName,
391519
),
520+
importedClips: renameImportedClipSnapshot(
521+
state.importedClips,
522+
uuid,
523+
fromName,
524+
toName,
525+
),
392526
};
393527
}
394528

@@ -540,11 +674,14 @@ export const useModelsStore = create<ModelsStore>()(
540674
const { [uuid]: _________, ...animationRenames } =
541675
state.animationRenames;
542676
// eslint-disable-next-line @typescript-eslint/no-unused-vars
543-
const { [uuid]: __________, ...currentTime } = state.currentTime;
677+
const { [uuid]: __________, ...importedClips } =
678+
state.importedClips;
544679
// eslint-disable-next-line @typescript-eslint/no-unused-vars
545-
const { [uuid]: ___________, ...frameStep } = state.frameStep;
680+
const { [uuid]: ___________, ...currentTime } = state.currentTime;
546681
// eslint-disable-next-line @typescript-eslint/no-unused-vars
547-
const { [uuid]: ____________, ...freeze } = state.freeze;
682+
const { [uuid]: ____________, ...frameStep } = state.frameStep;
683+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
684+
const { [uuid]: _____________, ...freeze } = state.freeze;
548685

549686
return {
550687
models,
@@ -556,22 +693,44 @@ export const useModelsStore = create<ModelsStore>()(
556693
loops,
557694
hiddenAnimations,
558695
animationRenames,
696+
importedClips,
559697
currentTime,
560698
frameStep,
561699
freeze,
562700
};
563701
});
564702
},
565703

566-
setClips: (uuid, clips) =>
704+
setClips: (uuid, clips, options = {}) =>
567705
set((state) => {
568-
const renamedClips = applyAnimationRenamesToClips(
569-
uuid,
570-
clips,
571-
state.animationRenames,
572-
);
573-
clipsCache.set(uuid, renamedClips);
574-
return { clips: { ...state.clips, [uuid]: renamedClips } };
706+
const shouldApplyImports = options.applyPersistedImports ?? true;
707+
const result = shouldApplyImports
708+
? mergePersistedImportedClips(uuid, clips, state)
709+
: {
710+
clips: applyAnimationRenamesToClips(
711+
uuid,
712+
clips,
713+
state.animationRenames,
714+
),
715+
mixer: state.mixerRef[uuid] ?? mixerCache.get(uuid) ?? null,
716+
};
717+
718+
if (result.mixer) {
719+
mixerCache.set(uuid, result.mixer);
720+
}
721+
clipsCache.set(uuid, result.clips);
722+
723+
return {
724+
clips: { ...state.clips, [uuid]: result.clips },
725+
...(result.mixer && state.mixerRef[uuid] !== result.mixer
726+
? {
727+
mixerRef: {
728+
...state.mixerRef,
729+
[uuid]: result.mixer,
730+
},
731+
}
732+
: {}),
733+
};
575734
}),
576735

577736
setMixerRef: (uuid, mixer) => {
@@ -697,8 +856,19 @@ export const useModelsStore = create<ModelsStore>()(
697856
const existing = get().clips[uuid] ?? [];
698857
const updated = [...existing, { action, clip }];
699858
clipsCache.set(uuid, updated);
700-
console.log("addClip", uuid, clip);
701-
get().setClips(uuid, updated);
859+
setOriginalRuntimeClips(uuid, updated, mixer);
860+
clearDowngradedRuntimeModel(uuid);
861+
set((state) => ({
862+
clips: {
863+
...state.clips,
864+
[uuid]: updated,
865+
},
866+
importedClips: upsertImportedClipSnapshots(
867+
state.importedClips,
868+
uuid,
869+
[clip],
870+
),
871+
}));
702872
},
703873

704874
importAnimationsFromSource: async (targetUuid, source) => {
@@ -876,6 +1046,8 @@ export const useModelsStore = create<ModelsStore>()(
8761046
}
8771047

8781048
clipsCache.set(targetUuid, nextClips);
1049+
setOriginalRuntimeClips(targetUuid, nextClips, targetMixer);
1050+
clearDowngradedRuntimeModel(targetUuid);
8791051

8801052
return {
8811053
mixerRef: {
@@ -898,6 +1070,11 @@ export const useModelsStore = create<ModelsStore>()(
8981070
...state.loops,
8991071
[targetUuid]: loopMap,
9001072
},
1073+
importedClips: upsertImportedClipSnapshots(
1074+
state.importedClips,
1075+
targetUuid,
1076+
preparedClips.map((entry) => entry.clip),
1077+
),
9011078
};
9021079
});
9031080

@@ -975,6 +1152,8 @@ export const useModelsStore = create<ModelsStore>()(
9751152
clip: nextClip,
9761153
};
9771154
clipsCache.set(targetUuid, nextClips);
1155+
setOriginalRuntimeClips(targetUuid, nextClips, targetMixer);
1156+
clearDowngradedRuntimeModel(targetUuid);
9781157

9791158
set((state) => ({
9801159
mixerRef: {
@@ -985,6 +1164,15 @@ export const useModelsStore = create<ModelsStore>()(
9851164
...state.clips,
9861165
[targetUuid]: nextClips,
9871166
},
1167+
...(state.importedClips[targetUuid]?.[clipName]
1168+
? {
1169+
importedClips: upsertImportedClipSnapshots(
1170+
state.importedClips,
1171+
targetUuid,
1172+
[nextClip],
1173+
),
1174+
}
1175+
: {}),
9881176
}));
9891177

9901178
return { name: nextClip.name };
@@ -1025,6 +1213,7 @@ export const useModelsStore = create<ModelsStore>()(
10251213
loops: get().loops,
10261214
hiddenAnimations: get().hiddenAnimations,
10271215
animationRenames: get().animationRenames,
1216+
importedClips: get().importedClips,
10281217
currentTime: get().currentTime,
10291218
frameStep: get().frameStep,
10301219
freeze: get().freeze,
@@ -1040,6 +1229,7 @@ export const useModelsStore = create<ModelsStore>()(
10401229
loops: snapshot.loops,
10411230
hiddenAnimations: snapshot.hiddenAnimations ?? {},
10421231
animationRenames: snapshot.animationRenames ?? {},
1232+
importedClips: snapshot.importedClips ?? {},
10431233
currentTime: snapshot.currentTime,
10441234
frameStep: snapshot.frameStep,
10451235
freeze: snapshot.freeze,

0 commit comments

Comments
 (0)