Skip to content

Commit 9b453a7

Browse files
committed
allow per animation override on workflow
1 parent 57a64c0 commit 9b453a7

8 files changed

Lines changed: 551 additions & 110 deletions

File tree

src/components/panels/top/workflows.tsx

Lines changed: 379 additions & 91 deletions
Large diffs are not rendered by default.

src/components/workflows/workflow-camera-preview.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
buildPlaybackClip,
2323
getAnimationClipFps,
2424
makeInPlaceClip,
25+
type InPlaceAxisModeInput,
2526
} from "@/utils/animation-clips";
2627
import type { ResolvedWorkflowCamera } from "@/utils/workflow-camera";
2728
import type {
@@ -40,6 +41,7 @@ type WorkflowCameraPreviewProps = {
4041
modelUuid?: string;
4142
animationName?: string;
4243
forceAnimationsInPlace?: boolean;
44+
forceAnimationsInPlaceMode?: InPlaceAxisModeInput;
4345
};
4446
onCameraChange: (camera: {
4547
distance: number;
@@ -66,6 +68,7 @@ function PreviewModel({
6668
modelUuid?: string;
6769
animationName?: string;
6870
forceAnimationsInPlace?: boolean;
71+
forceAnimationsInPlaceMode?: InPlaceAxisModeInput;
6972
};
7073
}) {
7174
const clips = useModelsStore((state) => state.clips);
@@ -144,7 +147,10 @@ function PreviewModel({
144147
if (!clipRef) return;
145148

146149
const playbackClip = previewAnimation.forceAnimationsInPlace
147-
? makeInPlaceClip(clipRef.clip)
150+
? makeInPlaceClip(
151+
clipRef.clip,
152+
previewAnimation.forceAnimationsInPlaceMode,
153+
)
148154
: clipRef.clip;
149155

150156
const [trimStart, trimEnd] = modelDurations[animationName] ?? [
@@ -182,6 +188,7 @@ function PreviewModel({
182188
previewAnimation?.animationName,
183189
previewAnimation?.modelUuid,
184190
previewAnimation?.forceAnimationsInPlace,
191+
previewAnimation?.forceAnimationsInPlaceMode,
185192
]);
186193

187194
if (!object) return null;

src/hooks/next/use-export.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,33 @@ const NORMAL_MAP_EXPORT_FORMATS = new Set<ExportFormat>([
4040
"unity",
4141
]);
4242

43+
function getCaptureTiming(
44+
payload: CaptureStartPayload | null | undefined,
45+
defaults: {
46+
intervalMs: number;
47+
frameCount: number;
48+
},
49+
) {
50+
return {
51+
intervalMs: Math.max(
52+
1,
53+
Math.round(
54+
Number.isFinite(payload?.frameIntervalMs)
55+
? (payload?.frameIntervalMs ?? defaults.intervalMs)
56+
: defaults.intervalMs,
57+
),
58+
),
59+
frameCount: Math.max(
60+
1,
61+
Math.round(
62+
Number.isFinite(payload?.frameCount)
63+
? (payload?.frameCount ?? defaults.frameCount)
64+
: defaults.frameCount,
65+
),
66+
),
67+
};
68+
}
69+
4370
export const useExport = () => {
4471
const images = useRef<{ name: string; dataURL: string }[]>([]);
4572
const normalImages = useRef<{ name: string; dataURL: string }[]>([]);
@@ -263,21 +290,25 @@ export const useExport = () => {
263290
}) ??
264291
`animation_${lastIndex.current + 1}`;
265292
const capturePayload = { ...(payload ?? {}), label: sequenceLabel };
293+
const captureTiming = getCaptureTiming(capturePayload, {
294+
intervalMs: intervals,
295+
frameCount: iterations,
296+
});
266297

267298
images.current = [];
268299
normalImages.current = [];
269300
activeCaptureRef.current = capturePayload;
270301

271302
intervalRef.current = scheduleInterval(
272303
captureScreenshotData,
273-
intervals,
274-
iterations,
304+
captureTiming.intervalMs,
305+
captureTiming.frameCount,
275306
() => {
276307
PubSub.emit(EventType.ASSETS_CREATION_PROGRESS, {
277308
label: capturePayload.label,
278309
workflowRunId: capturePayload.workflowRunId,
279310
capturedFrames: images.current.length,
280-
expectedFrames: iterations,
311+
expectedFrames: captureTiming.frameCount,
281312
});
282313
},
283314
async () => {
@@ -286,7 +317,7 @@ export const useExport = () => {
286317
label: capturePayload.label,
287318
workflowRunId: capturePayload.workflowRunId,
288319
capturedFrames: images.current.length,
289-
expectedFrames: iterations,
320+
expectedFrames: captureTiming.frameCount,
290321
status: "done",
291322
});
292323
activeCaptureRef.current = null;
@@ -300,7 +331,7 @@ export const useExport = () => {
300331
: undefined,
301332
exportWidth,
302333
exportHeight,
303-
Math.round(1000 / intervals),
334+
Math.round(1000 / captureTiming.intervalMs),
304335
capturePayload.rowMetadata,
305336
);
306337
lastIndex.current += 1;
@@ -328,18 +359,22 @@ export const useExport = () => {
328359
}
329360

330361
const payload = activeCaptureRef.current;
362+
const captureTiming = getCaptureTiming(payload, {
363+
intervalMs: intervals,
364+
frameCount: iterations,
365+
});
331366
PubSub.emit(EventType.STOP_ASSETS_CREATION, {
332367
label: payload?.label,
333368
workflowRunId: payload?.workflowRunId,
334369
capturedFrames: images.current.length,
335-
expectedFrames: iterations,
370+
expectedFrames: captureTiming.frameCount,
336371
status: "cancelled",
337372
});
338373

339374
activeCaptureRef.current = null;
340375
images.current = [];
341376
normalImages.current = [];
342-
}, [iterations]);
377+
}, [intervals, iterations]);
343378

344379
const addScreenshot = useCallback(() => {
345380
if (!gl) return;

src/hooks/next/use-workflow.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
import { useEntitiesStore } from "@/store/next/entities";
2020
import {
2121
buildWorkflowSteps,
22+
getWorkflowStepCaptureSettings,
2223
type BuildWorkflowStepsInput,
24+
type WorkflowCaptureSettings,
2325
type WorkflowStep,
2426
} from "@/utils/workflows";
2527
import {
@@ -198,7 +200,11 @@ async function setStepAnimation(
198200
if (!clip) return;
199201

200202
if (options?.forceAnimationsInPlace) {
201-
modelState.forceCurrentAnimationInPlace(step.modelUuid, step.animationName);
203+
modelState.forceCurrentAnimationInPlace(
204+
step.modelUuid,
205+
step.animationName,
206+
options.forceAnimationsInPlaceMode,
207+
);
202208
}
203209

204210
const currentAnimation = modelState.animations[step.modelUuid];
@@ -208,10 +214,6 @@ async function setStepAnimation(
208214
: Promise.resolve();
209215

210216
modelState.setAnimation(step.modelUuid, step.animationName);
211-
modelState.setDuration(step.modelUuid, step.animationName, [
212-
0,
213-
clip.clip.duration,
214-
]);
215217
modelState.mixerRef[step.modelUuid]?.setTime(0);
216218

217219
await waiter;
@@ -295,8 +297,10 @@ export const useWorkflow = () => {
295297

296298
const cameraDistance = useSettingsStore.getState().cameraDistance;
297299
const cameraAngle = useSettingsStore.getState().cameraAngle;
298-
const intervals = useImagesStore.getState().intervals;
299-
const iterations = useImagesStore.getState().iterations;
300+
const defaultCaptureSettings: WorkflowCaptureSettings = {
301+
frameIntervalMs: useImagesStore.getState().intervals,
302+
frameCount: useImagesStore.getState().iterations,
303+
};
300304
const cameraUUID = useCamerasStore.getState().mainCamera;
301305
const mainCameraType = cameraUUID
302306
? useCamerasStore.getState().cameras[cameraUUID]?.type
@@ -314,7 +318,7 @@ export const useWorkflow = () => {
314318
currentStep: 0,
315319
totalSteps: steps.length,
316320
currentFrame: 0,
317-
expectedFrames: iterations,
321+
expectedFrames: defaultCaptureSettings.frameCount,
318322
currentLabel: "",
319323
currentAnimation: "",
320324
currentDirection: "",
@@ -327,12 +331,17 @@ export const useWorkflow = () => {
327331

328332
const step = steps[index];
329333
const dir = getDirectionForStep(workflow, step);
334+
const captureSettings = getWorkflowStepCaptureSettings(
335+
step,
336+
options?.captureSettingsByAnimation,
337+
defaultCaptureSettings,
338+
);
330339

331340
setWorkflowState((prev) => ({
332341
...prev,
333342
currentStep: index + 1,
334343
currentFrame: 0,
335-
expectedFrames: iterations,
344+
expectedFrames: captureSettings.frameCount,
336345
currentLabel: step.rowLabel,
337346
currentAnimation: step.animationName,
338347
currentDirection: step.directionLabel,
@@ -377,14 +386,18 @@ export const useWorkflow = () => {
377386
const captureDone = waitForCaptureDone({
378387
label: step.rowLabel,
379388
workflowRunId,
380-
timeoutMs: intervals * iterations + CAPTURE_TIMEOUT_BUFFER_MS,
389+
timeoutMs:
390+
captureSettings.frameIntervalMs * captureSettings.frameCount +
391+
CAPTURE_TIMEOUT_BUFFER_MS,
381392
});
382393

383394
PubSub.emit(EventType.START_ASSETS_CREATION, {
384395
label: step.rowLabel,
385396
workflowRunId,
386397
stepIndex: index + 1,
387398
totalSteps: steps.length,
399+
frameIntervalMs: captureSettings.frameIntervalMs,
400+
frameCount: captureSettings.frameCount,
388401
rowMetadata: {
389402
workflow: {
390403
workflowId: workflow.id,

src/lib/events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export type CaptureStartPayload = {
8181
workflowRunId?: string;
8282
stepIndex?: number;
8383
totalSteps?: number;
84+
frameIntervalMs?: number;
85+
frameCount?: number;
8486
rowMetadata?: ExportRowMetadata;
8587
};
8688

src/utils/workflow-camera.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { computePosition, type WorkflowDirection } from "@/constants/workflows";
22
import type { CameraType } from "@/types/camera";
3+
import type { WorkflowCaptureSettingsByAnimation } from "./workflows";
4+
import type { InPlaceAxisModeInput } from "./animation-clips";
35
import * as THREE from "three";
46

57
export type WorkflowCameraTarget = [number, number, number];
@@ -19,9 +21,11 @@ export type WorkflowRunOptions = {
1921
target?: WorkflowCameraTarget;
2022
directionOverrides?: Record<string, WorkflowCameraDirectionOverride>;
2123
forceAnimationsInPlace?: boolean;
24+
forceAnimationsInPlaceMode?: InPlaceAxisModeInput;
2225
skipStepLabels?: string[];
2326
includeHiddenAnimations?: boolean;
2427
captureNormalMaps?: boolean;
28+
captureSettingsByAnimation?: WorkflowCaptureSettingsByAnimation;
2529
};
2630

2731
export type ResolvedWorkflowCamera = {

src/utils/workflows.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ export interface WorkflowStep {
77
rowLabel: string;
88
}
99

10+
export type WorkflowCaptureSettings = {
11+
frameIntervalMs: number;
12+
frameCount: number;
13+
};
14+
15+
export type WorkflowCaptureSettingsInput = Partial<WorkflowCaptureSettings>;
16+
17+
export type WorkflowCaptureSettingsByAnimation = Record<
18+
string,
19+
WorkflowCaptureSettingsInput
20+
>;
21+
1022
export type WorkflowClipEntry = {
1123
clip: {
1224
name: string;
@@ -127,13 +139,47 @@ export function getDisabledWorkflowAnimationGroupKeys(
127139
.map((group) => group.key);
128140
}
129141

142+
export function getWorkflowAnimationGroupKey(
143+
step: Pick<WorkflowStep, "animationName">,
144+
): string {
145+
return step.animationName;
146+
}
147+
148+
export function normalizeWorkflowCaptureSettings(
149+
settings: WorkflowCaptureSettingsInput | undefined,
150+
defaults: WorkflowCaptureSettings,
151+
): WorkflowCaptureSettings {
152+
const interval = Number.isFinite(settings?.frameIntervalMs)
153+
? settings?.frameIntervalMs
154+
: defaults.frameIntervalMs;
155+
const count = Number.isFinite(settings?.frameCount)
156+
? settings?.frameCount
157+
: defaults.frameCount;
158+
159+
return {
160+
frameIntervalMs: Math.max(1, Math.round(interval ?? 1)),
161+
frameCount: Math.max(1, Math.round(count ?? 1)),
162+
};
163+
}
164+
165+
export function getWorkflowStepCaptureSettings(
166+
step: WorkflowStep,
167+
settingsByAnimation: WorkflowCaptureSettingsByAnimation | undefined,
168+
defaults: WorkflowCaptureSettings,
169+
): WorkflowCaptureSettings {
170+
return normalizeWorkflowCaptureSettings(
171+
settingsByAnimation?.[getWorkflowAnimationGroupKey(step)],
172+
defaults,
173+
);
174+
}
175+
130176
export function groupWorkflowStepsByAnimation(
131177
steps: WorkflowStep[],
132178
): WorkflowStepGroup[] {
133179
const groups = new Map<string, WorkflowStepGroup>();
134180

135181
for (const step of steps) {
136-
const key = step.animationName;
182+
const key = getWorkflowAnimationGroupKey(step);
137183
const group = groups.get(key);
138184

139185
if (group) {

tests/unit/workflows.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
buildWorkflowSteps,
55
getDisabledWorkflowAnimationGroupKeys,
66
getHiddenWorkflowStepLabels,
7+
getWorkflowStepCaptureSettings,
78
groupWorkflowStepsByAnimation,
89
isWorkflowStepHidden,
910
type WorkflowClipEntry,
@@ -146,4 +147,49 @@ describe("workflow utilities", () => {
146147
expect(steps.map((step) => step.rowLabel)).toContain("model-a-_walk_N");
147148
expect(steps.map((step) => step.rowLabel)).toContain("model-b-_walk_N");
148149
});
150+
151+
it("resolves capture timing overrides by animation group", () => {
152+
const timingSteps = buildWorkflowSteps(workflow, {
153+
clips: { modelA: [clip("walk"), clip("idle")] },
154+
modelUuids: ["modelA"],
155+
});
156+
const walkStep = timingSteps.find((step) => step.animationName === "walk")!;
157+
const idleStep = timingSteps.find((step) => step.animationName === "idle")!;
158+
const defaults = { frameIntervalMs: 100, frameCount: 10 };
159+
160+
expect(
161+
getWorkflowStepCaptureSettings(
162+
walkStep,
163+
{ walk: { frameIntervalMs: 50, frameCount: 24 } },
164+
defaults,
165+
),
166+
).toEqual({ frameIntervalMs: 50, frameCount: 24 });
167+
expect(
168+
getWorkflowStepCaptureSettings(
169+
idleStep,
170+
{ walk: { frameIntervalMs: 50, frameCount: 24 } },
171+
defaults,
172+
),
173+
).toEqual(defaults);
174+
});
175+
176+
it("normalizes partial and invalid capture timing overrides", () => {
177+
const walkStep = buildWorkflowSteps(workflow, {
178+
clips: { modelA: [clip("walk")] },
179+
modelUuids: ["modelA"],
180+
})[0];
181+
182+
expect(
183+
getWorkflowStepCaptureSettings(
184+
walkStep,
185+
{
186+
walk: {
187+
frameIntervalMs: Number.NaN,
188+
frameCount: 4.6,
189+
},
190+
},
191+
{ frameIntervalMs: 100, frameCount: 10 },
192+
),
193+
).toEqual({ frameIntervalMs: 100, frameCount: 5 });
194+
});
149195
});

0 commit comments

Comments
 (0)