cozyclay 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/THIRD_PARTY_NOTICES.md +10 -0
  3. package/bin/agent/agent-routes.mjs +3 -3
  4. package/bin/agent/agent-runner.mjs +5 -5
  5. package/bin/agent/pi-tools.mjs +12 -1
  6. package/bin/agent/session-store.mjs +2 -2
  7. package/bin/agent/studio-context-budget.mjs +99 -0
  8. package/bin/agent/studio-prompt.mjs +1 -1
  9. package/bin/agent/studio-tools.mjs +1 -1
  10. package/dist/app/index.html +4 -4
  11. package/dist/assets/analytics-DbKG0NgD.js +6 -0
  12. package/dist/assets/app-34i-iUy1.js +5088 -0
  13. package/dist/assets/compile-worker-DYpa2nvO.js +5 -0
  14. package/dist/assets/{first-shot-handoff-CC4xe4HM.js → first-shot-handoff-CrGZ3n_O.js} +1 -1
  15. package/dist/assets/helvetiker-bold.typeface-BUlyMz0-.js +13 -0
  16. package/dist/assets/{landing-DjHD8u7k.js → landing-mq5bnqFE.js} +1 -1
  17. package/dist/assets/scene-asset-sync-7J-w5oNh.js +20 -0
  18. package/dist/assets/workflow-dUhurYUc.js +179 -0
  19. package/dist/cozyclay-package.json +1 -1
  20. package/dist/index.html +3 -3
  21. package/dist/workflow/index.html +3 -3
  22. package/package.json +1 -1
  23. package/src/App.jsx +1 -1
  24. package/src/agent-progress.js +3 -2
  25. package/src/agent-turn-mark.js +33 -0
  26. package/src/app-context.js +7 -2
  27. package/src/command-bus.js +10 -11
  28. package/src/commands/elements/character.js +2 -1
  29. package/src/commands/elements/motion.js +7 -1
  30. package/src/commands/elements/scene.js +3 -0
  31. package/src/commands/elements/stage.js +2 -1
  32. package/src/commands/elements/take-read.js +31 -0
  33. package/src/commands/elements.js +16 -2
  34. package/src/commands/motion3.js +15 -6
  35. package/src/commands/objects.js +2 -2
  36. package/src/commands/shot.js +6 -2
  37. package/src/domains/motion/take-source.js +4 -2
  38. package/src/draft-ghost.js +3 -2
  39. package/src/motion/beat-board-model.js +1 -1
  40. package/src/motion/beat-board.jsx +2 -1
  41. package/src/motion3/compile-job.js +4 -0
  42. package/src/motion3/compiler/anchors.js +68 -4
  43. package/src/motion3/compiler/compile.js +5 -1
  44. package/src/motion3/compiler/grip.js +8 -2
  45. package/src/motion3/compiler/limbs.js +35 -8
  46. package/src/motion3/compiler/pose.js +17 -2
  47. package/src/motion3/compiler/socket-grip.js +107 -0
  48. package/src/motion3/compiler/validate-objects.js +3 -2
  49. package/src/motion3/compiler/validate-parts.js +9 -2
  50. package/src/motion3/compiler/validate.js +3 -3
  51. package/src/motion3/compiler/vocab.js +23 -16
  52. package/src/motion3/describe.js +5 -2
  53. package/src/motion3/docs.js +1 -1
  54. package/src/motion3/free-motion.js +54 -15
  55. package/src/motion3/object-keys.js +36 -5
  56. package/src/motion3/object-pose.js +39 -6
  57. package/src/motion3/people.js +82 -0
  58. package/src/motion3/qa/checks.js +6 -2
  59. package/src/motion3/qa/hints-contact.js +17 -1
  60. package/src/motion3/qa/index.js +6 -4
  61. package/src/motion3/qa/people-checks.js +37 -0
  62. package/src/motion3/qa/style.js +14 -4
  63. package/src/motion3/refs.js +10 -1
  64. package/src/motion3/scene.js +4 -3
  65. package/src/motion3/schema/fields.js +4 -4
  66. package/src/motion3/schema/topics.js +9 -6
  67. package/src/motion3/schema/types.js +11 -9
  68. package/src/motion3/settle.js +136 -0
  69. package/src/motion3/skills/basic.js +10 -3
  70. package/src/motion3/skills/flip.js +200 -0
  71. package/src/motion3/skills/impact.js +109 -0
  72. package/src/motion3/skills/index.js +3 -1
  73. package/src/motion3/skills/info.js +19 -4
  74. package/src/motion3/sockets.js +104 -0
  75. package/src/motion3/take-edit.js +1 -1
  76. package/src/motion3/take.js +6 -4
  77. package/src/object-catalog.jsx +3 -1
  78. package/src/object-grasp.js +38 -0
  79. package/src/object-look.js +2 -0
  80. package/src/object-travel.js +6 -3
  81. package/src/panels/ObjectLookPanel.jsx +24 -3
  82. package/src/props.jsx +5 -5
  83. package/src/qa-label.js +25 -0
  84. package/src/scene-objects.js +19 -11
  85. package/src/shell/details-selection.js +2 -1
  86. package/src/studio-actions.js +4 -0
  87. package/src/studio-agent-protocol.js +11 -12
  88. package/src/studio-app-binding.js +3 -3
  89. package/src/studio-elements.js +3 -1
  90. package/src/studio-error-codes.js +8 -1
  91. package/src/text/fonts/helvetiker-LICENSE.txt +13 -0
  92. package/src/text/fonts/helvetiker-bold.typeface.json +1 -0
  93. package/src/text/text-object.jsx +106 -0
  94. package/src/text/text-record.js +122 -0
  95. package/src/workflow/AgentProgressCard.jsx +6 -4
  96. package/tools/run-tests.mjs +2 -2
  97. package/dist/assets/analytics-C9sUnndd.js +0 -1
  98. package/dist/assets/app-CCh--1Hc.js +0 -5087
  99. package/dist/assets/compile-worker-CkxiD6lH.js +0 -5
  100. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +0 -20
  101. package/dist/assets/workflow-C4yKH1p4.js +0 -179
@@ -1 +1 @@
1
- {"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.0.0\",\"repository\":\"NomaDamas/CozyClay\",\"content_sha256\":\"e6a50c77650f2894d8154267dec8faafb86cad41e859b2594f233d5a18348530\"}","signature":"rwtYOUdZUQo57FluVUioxvNg+gKzRgBC38gyKcd7GIrm5nhq3iff5eV90XV7YFAJfVHn466qu3iLBsnjKkpIDA=="}
1
+ {"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.0.2\",\"repository\":\"NomaDamas/CozyClay\",\"content_sha256\":\"7232c78719451c250c0be94e5f375e56c3c05984e94882724c50048b900c6c71\"}","signature":"+LLNPJc5HYZikPGbfrI57GjEjarn3TRJiaHrrsn4z1CuRMN958copspe2aRT+Y3j5wBo1vH0gYI2UsYn2J6sBA=="}
package/dist/index.html CHANGED
@@ -301,9 +301,9 @@
301
301
  .btn, a.card, .seg button, .tutorial-steps li { transition: none; }
302
302
  }
303
303
  </style>
304
- <script type="module" crossorigin src="/assets/landing-DjHD8u7k.js"></script>
305
- <link rel="modulepreload" crossorigin href="/assets/analytics-C9sUnndd.js">
306
- <link rel="modulepreload" crossorigin href="/assets/first-shot-handoff-CC4xe4HM.js">
304
+ <script type="module" crossorigin src="/assets/landing-mq5bnqFE.js"></script>
305
+ <link rel="modulepreload" crossorigin href="/assets/analytics-DbKG0NgD.js">
306
+ <link rel="modulepreload" crossorigin href="/assets/first-shot-handoff-CrGZ3n_O.js">
307
307
  </head>
308
308
  <body>
309
309
  <header class="site">
@@ -8,11 +8,11 @@
8
8
  <meta name="theme-color" content="#101116" />
9
9
  <meta name="description" content="CozyClay visual workflow builder" />
10
10
  <title>CozyClay Workflow</title>
11
- <script type="module" crossorigin src="/assets/workflow-C4yKH1p4.js"></script>
11
+ <script type="module" crossorigin src="/assets/workflow-dUhurYUc.js"></script>
12
12
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
13
- <link rel="modulepreload" crossorigin href="/assets/analytics-C9sUnndd.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/analytics-DbKG0NgD.js">
14
14
  <link rel="modulepreload" crossorigin href="/assets/fflate.module-KbGj5-H6.js">
15
- <link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-Hx_FzdOH.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-7J-w5oNh.js">
16
16
  <link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-_S8NQl5B.css">
17
17
  <link rel="stylesheet" crossorigin href="/assets/workflow-DRdwoDBT.css">
18
18
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozyclay",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "Open source previs software in the browser: block a scene, pose characters, author camera moves and cuts, then take the same shots to an AI video model.",
5
5
  "scripts": {
6
6
  "dev": "node tools/dev-full.mjs --host 127.0.0.1 --port 5180",
package/src/App.jsx CHANGED
@@ -1282,7 +1282,7 @@ export default function App() {
1282
1282
  const [inspectorActionsOpen, setInspectorActionsOpen] = useState(false);
1283
1283
 
1284
1284
  const objectsDomain = useObjects(appContext.forRender({
1285
- get animatedSceneObjects() { return animatedSceneObjects; },
1285
+ get animatedSceneObjects() { return animatedSceneObjects; }, get attachFrameRef() { return attachFrameRef; },
1286
1286
  get castMemberOf() { return castMemberOf; },
1287
1287
  get charIdFromHierarchyId() { return charIdFromHierarchyId; },
1288
1288
  get characters() { return characters; },
@@ -5,6 +5,7 @@
5
5
  // In-page only: nothing here is saved with the project, undone, or sent to the agent. Each character keeps its
6
6
  // last KEEP drafts; the images keep the last KEEP. A new agent turn starts clean (clearProgress).
7
7
 
8
+ import { qaProblems } from "./qa-label.js";
8
9
  const KEEP = 12;
9
10
  const drafts = new Map(), sheets = [], listeners = new Set();
10
11
  let version = 0, counter = 0;
@@ -21,7 +22,7 @@ const pick = (row, keys) => Object.fromEntries(keys.filter(k => row?.[k] !== und
21
22
  /**
22
23
  * publishDraft(characterId, { take, installed, specId, label, qa, timeline }) -> the stored draft:
23
24
  * { n (1, 2, ... per character), at (ms), characterId, take, frames, installed, specId, label,
24
- * qa: { ok, total, groups: [{ kind, subject, count, first, last, worst }], goals: [{ name, kind, ok, value, need, at }] },
25
+ * qa: { ok, total, problems, groups: [{ kind, subject, count, first, last, worst }], goals: [{ name, kind, ok, value, need, at }] },
25
26
  * timeline: the compile's beats (pipeline.js timelineOf) }.
26
27
  */
27
28
  export function publishDraft(characterId, { take, installed = false, specId = null, label = null, qa = null, timeline = [] }) {
@@ -29,7 +30,7 @@ export function publishDraft(characterId, { take, installed = false, specId = nu
29
30
  const draft = Object.freeze({
30
31
  n: (list.at(-1)?.n ?? 0) + 1, at: Date.now(), seq: ++counter, characterId, take, frames: take?.frameCount ?? 0,
31
32
  installed: !!installed, specId, label,
32
- qa: { ok: !!qa?.ok, total: qa?.total ?? 0,
33
+ qa: { ok: !!qa?.ok, total: qa?.total ?? 0, problems: qaProblems(qa),
33
34
  groups: (qa?.groups ?? []).map(g => pick(g, ["kind", "subject", "count", "first", "last", "worst"])),
34
35
  goals: (qa?.goals ?? []).map(g => pick(g, ["name", "kind", "ok", "value", "need", "at"])) },
35
36
  timeline: timeline ?? [],
@@ -23,8 +23,41 @@ export function markAgentTurn(id, turn) {
23
23
  const { mark } = turn;
24
24
  if (!mark) throw new StudioProtocolError('STALE_TARGET', 'The agent turn has no mark to roll back to.');
25
25
  turn.mark = null;
26
+ turn.edits = (turn.edits ?? []).filter(edit => edit.mark !== mark);
26
27
  // No session at the mark: everything the turn authored came after it.
27
28
  turn.session?.rollback(mark.point ?? undefined);
28
29
  turn.affectedIds = mark.affectedIds;
29
30
  return { txId: turn.txId, affectedIds: [...mark.since], toasts: [] };
30
31
  }
32
+
33
+ // The turn's own edits, newest last: each keeps the savepoint before it, so undo_edit on the
34
+ // newest one's receipt rolls back to there and the turn stays open (9240db36 was refused twice).
35
+ /** Where the turn stands before a command runs: the savepoint, the ids so far, the open mark. */
36
+ export const turnEditPoint = turn => turn && { point: turn.session?.savepoint() ?? null, affectedIds: new Set(turn.affectedIds), since: turn.mark && new Set(turn.mark.since), mark: turn.mark };
37
+
38
+ /** Files an authored receipt of the turn with the point before it; answers the receipt. */
39
+ export function recordTurnEdit(turn, at, recorded) {
40
+ if (turn && at && recorded?.ok && recorded.authored) (turn.edits ??= []).push({ ...at, receiptId: recorded.receiptId, ids: recorded.affectedIds });
41
+ return recorded;
42
+ }
43
+
44
+ /**
45
+ * edit.undo from inside the owning turn once it has authored. A receipt of the turn's newest edit
46
+ * rolls back to the point before it and answers the receipt (before: the bus's read at admission;
47
+ * read: a read after); anything else is refused with what the agent can do instead.
48
+ */
49
+ export function undoTurnEdit(turn, args, request, before, read) {
50
+ const edits = turn.edits ?? [], at = edits.findIndex(edit => edit.receiptId === args.receiptId);
51
+ if (at < 0) throw new StudioProtocolError('TARGET_BUSY', `Finish the agent turn before traversing history or opening another transaction. Inside the turn, undo_edit with the receiptId of this message's newest edit reverts it${edits.length ? ` (now ${edits.at(-1).receiptId})` : ''}; an earlier message's edit is undone after this one ends.`);
52
+ const edit = edits[at];
53
+ if (at < edits.length - 1) throw new StudioProtocolError('UNDO_CONFLICT', `Later edits of this message come after it; undo them first, newest first: ${edits.slice(at + 1).reverse().map(later => later.receiptId).join(', ')}.`);
54
+ turn.session.rollback(edit.point ?? undefined);
55
+ edits.pop();
56
+ turn.affectedIds = edit.affectedIds;
57
+ // Nothing came after a mark this edit predates, so the mark moves to now: a script's rollback still finds it.
58
+ if (turn.mark && edit.mark !== turn.mark) turn.mark = { point: turn.session.savepoint(), affectedIds: new Set(turn.affectedIds), since: new Set() };
59
+ else if (turn.mark) turn.mark.since = edit.since;
60
+ return { ok: true, status: 'completed', kind: 'transaction', action: 'edit.undo', txId: turn.txId, commandId: request.commandId, receiptId: crypto.randomUUID(), host: before.host,
61
+ summary: `Undid ${edit.receiptId} inside this message.`, authored: false, revision: { before: before.revision, after: read().revision }, affectedIds: edit.ids, delta: [],
62
+ checks: { coverage: 'agent-turn-savepoint' }, undo: null, warnings: [], output: { undoneReceiptId: edit.receiptId } };
63
+ }
@@ -1,8 +1,13 @@
1
- import { createContext, useContext } from 'react';
1
+ import { createContext, useContext, useMemo } from 'react';
2
2
  import { StudioProtocolError } from './studio-agent-protocol.js';
3
3
 
4
4
  export const AppContext = createContext(null);
5
- export function useBus() { return useContext(AppContext).bus; }
5
+ // Reading `bus` refreshes the studio binding (it serializes the whole scene), so a panel must not read it while it
6
+ // renders: playback re-renders panels every frame. The hook hands back a stable handle that opens the bus per call.
7
+ export function useBus() {
8
+ const app = useContext(AppContext);
9
+ return useMemo(() => ({ run: (...args) => app.bus.run(...args), subscribe: (...args) => app.bus.subscribe(...args) }), [app]);
10
+ }
6
11
 
7
12
  const ref = current => ({ current });
8
13
 
@@ -2,8 +2,8 @@
2
2
  // the editor ports own native state/history, and the existing journal owns
3
3
  // idempotency. Synchronous UI actions stay synchronous (including user gestures).
4
4
  import { HISTORY_LIMIT } from './history.js';
5
- import { StudioProtocolError, StudioSchemas, validateStudioSchema, validateReceipt, validateStudioIdentity } from './studio-agent-protocol.js';
6
- import { agentTurnControls, markAgentTurn } from './agent-turn-mark.js';
5
+ import { StudioProtocolError, StudioSchemas, validateStudioSchema, validateReceipt, validateStudioIdentity, refusalCode } from './studio-agent-protocol.js';
6
+ import { agentTurnControls, markAgentTurn, turnEditPoint, recordTurnEdit, undoTurnEdit } from './agent-turn-mark.js';
7
7
 
8
8
  // A batching/retention adapter over the existing object history owner. The
9
9
  // native store still owns every snapshot and transition; this tracks only the
@@ -102,12 +102,11 @@ export function createCommandBus({ registry, ports }) {
102
102
  finally { agentTurn = null; }
103
103
  return transactionReceipt(id, turn, request, turn.before);
104
104
  }
105
- // Every Studio turn opens a turn transaction (#715), so "undo the last thing" arrives
106
- // inside one. Until the turn has authored anything (no session yet) history traversal
107
- // is safe: the undo lands as its own entry and a later edit still groups into the turn.
108
- // After the first authored edit the turn owns the frontier and traversal waits (#735).
105
+ // Every Studio turn opens a turn transaction (#715), so "undo the last thing" arrives inside one. Until the
106
+ // turn has authored (no session yet) history traversal is safe: the undo lands as its own entry (#735).
107
+ // After that the turn owns the frontier: its own agent may undo its newest edit (agent-turn-mark.js).
109
108
  const traversal = id === 'edit.undo' || id === 'edit.redo';
110
- if (agentTurn && !id.startsWith('job.') && !(traversal && !agentTurn.session)) fail('TARGET_BUSY', 'Finish the agent turn before traversing history or opening another transaction.');
109
+ if (agentTurn && !id.startsWith('job.') && !(traversal && !agentTurn.session)) { if (id === 'edit.undo' && request.turnId === agentTurn.txId) return validateReceipt(undoTurnEdit(agentTurn, args, request, before, () => ports.read())); fail('TARGET_BUSY', 'Finish the agent turn before traversing history or opening another transaction.'); }
111
110
  if (traversal) {
112
111
  const redo = id === 'edit.redo', historyEntryId = ports.history?.(redo);
113
112
  const previous = args.receiptId ? ports.receipt(args.receiptId) : historyReceipts.get(historyEntryId)
@@ -203,7 +202,7 @@ export function createCommandBus({ registry, ports }) {
203
202
  function refusal(request, before, error, mutated) {
204
203
  const changed = mutated ?? (ports.read().revision !== before.revision);
205
204
  return validateReceipt({ ok: false, commandId: request.commandId, host: before.host,
206
- code: error.code ?? 'INVALID_ARGUMENT', phase: changed ? 'commit' : 'admission', affectedIds: [], expectedTargets: [], currentTargets: [],
205
+ code: refusalCode(error), phase: changed ? 'commit' : 'admission', affectedIds: [], expectedTargets: [], currentTargets: [],
207
206
  mutated: changed, preserved: { authoredState: changed ? 'changed' : 'unchanged' }, recovery: { action: 'inspect', retryAllowed: false },
208
207
  message: [...String(error.message || error)].slice(0, 500).join('') });
209
208
  }
@@ -358,13 +357,13 @@ export function createCommandBus({ registry, ports }) {
358
357
  const deadline = new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(controller.signal.reason), { once: true }));
359
358
  return Promise.race([value, deadline]);
360
359
  };
361
- const value = entry.kind === 'mutation' ? recordAction(entry.undoDomain, invoke, validated.characterId ?? null) : { result: invoke(), historyEntryId: null };
360
+ const point = turnEditPoint(turn), value = entry.kind === 'mutation' ? recordAction(entry.undoDomain, invoke, validated.characterId ?? null) : { result: invoke(), historyEntryId: null };
362
361
  const finish = ({ result, historyEntryId }) => mapResult(result, output => {
363
362
  clearTimer();
364
363
  releaseToasts?.(); releaseToasts = null;
365
- const refused = toastRefusal();
364
+ const refused = output?.output === undefined ? toastRefusal() : null; // a job that answered was not refused by a toast that showed meanwhile
366
365
  if (refused) throw refused;
367
- return remember(receipt(entry, request, before, { ...output, affectedIds: [...new Set([...output.affectedIds, ...nestedIds])] }, historyEntryId ?? committedHistoryId, toasts, turn));
366
+ return recordTurnEdit(turn, point, remember(receipt(entry, request, before, { ...output, affectedIds: [...new Set([...output.affectedIds, ...nestedIds])] }, historyEntryId ?? committedHistoryId, toasts, turn)));
368
367
  });
369
368
  const finished = value?.then ? value.then(finish) : finish(value);
370
369
  const answer = finished?.then ? finished.catch(rejected) : finished;
@@ -1,6 +1,7 @@
1
1
  import { createCharacterEntry } from '../../scenes.js';
2
2
  import { STUDIO_ELEMENTS } from '../../studio-elements.js';
3
3
  import { registerElementKind } from '../elements.js';
4
+ import { readCastTakes } from './take-read.js';
4
5
 
5
6
  const elements = STUDIO_ELEMENTS.filter(row => row.path.startsWith('character.')).map(row => {
6
7
  const { min, max, gizmo, ...element } = row;
@@ -9,5 +10,5 @@ const elements = STUDIO_ELEMENTS.filter(row => row.path.startsWith('character.')
9
10
  if (row.path === 'character.pose') return { ...element, readback: item => ({ text: item?.pose?.id ?? null }) };
10
11
  return element;
11
12
  });
12
- registerElementKind('character', { collection: true, documentKey: 'characters', elements, normalize: createCharacterEntry });
13
+ registerElementKind('character', { collection: true, documentKey: 'characters', elements, read: readCastTakes, normalize: createCharacterEntry });
13
14
  registerElementKind('poseLibrary', { collection: true, documentKey: 'customPoses', elements: [], normalize: value => value });
@@ -1,4 +1,5 @@
1
1
  import { registerElementKind } from '../elements.js';
2
+ import { readTake } from './take-read.js';
2
3
 
3
4
  const elements = [
4
5
  { path: 'motion.take.prompt', type: 'string' },
@@ -6,4 +7,9 @@ const elements = [
6
7
  { path: 'motion.takeRecipe', type: 'object', readOnly: true },
7
8
  { path: 'motion.takeVersions', type: 'array', readOnly: true },
8
9
  ];
9
- registerElementKind('motion', { collection: true, elements, normalize: row => ({ ...row, take: row.take && { ...row.take } }) });
10
+ // The full take is the take itself until an edit splits them: a read answers its source once.
11
+ const readMotion = (rows, named) => rows.map(row => {
12
+ const same = Boolean(row.fullTake?.resourceId) && row.fullTake.resourceId === row.take?.resourceId;
13
+ return { ...row, take: readTake(row.take, named(row.id)), fullTake: readTake(row.fullTake, !same && named(row.id)) };
14
+ });
15
+ registerElementKind('motion', { collection: true, elements, read: readMotion, normalize: row => ({ ...row, take: row.take && { ...row.take } }) });
@@ -1,4 +1,5 @@
1
1
  import { registerElementKind } from '../elements.js';
2
+ import { readCastTakes } from './take-read.js';
2
3
 
3
4
  // Array position is the authoritative order. The owner refreshes this persisted
4
5
  // field after every move, so generic set/read and the project file agree.
@@ -8,6 +9,8 @@ registerElementKind('scene', {
8
9
  { path: 'scene.name', type: 'string' },
9
10
  { path: 'scene.order', type: 'number' },
10
11
  ],
12
+ // a saved scene keeps its cast's takes: a read answers their spec ids (take-read.js)
13
+ read: (scenes, named) => scenes.map(scene => (scene?.stage?.characters ? { ...scene, stage: { ...scene.stage, characters: readCastTakes(scene.stage.characters, named) } } : scene)),
11
14
  normalize: scene => ({ ...scene, name: scene.name.trim(), order: Math.max(0, Math.round(scene.order)) }),
12
15
  });
13
16
  registerElementKind('project', {
@@ -1,6 +1,7 @@
1
1
  import { createSceneStage } from '../../scenes.js';
2
2
  import { STUDIO_ELEMENTS } from '../../studio-elements.js';
3
3
  import { registerElementKind } from '../elements.js';
4
+ import { readCastTakes } from './take-read.js';
4
5
 
5
6
  const elements = STUDIO_ELEMENTS.filter(row => row.path.startsWith('stage.'));
6
7
  export const STAGE_FIELDS = [...new Set(elements.map(element => (element.documentPath ?? element.path.slice(6)).split('.')[0]))];
@@ -8,4 +9,4 @@ export function normalizeStage(source) {
8
9
  const normalized = createSceneStage(source);
9
10
  return Object.fromEntries(STAGE_FIELDS.map(key => [key, normalized[key]]));
10
11
  }
11
- registerElementKind('stage', { elements, normalize: normalizeStage });
12
+ registerElementKind('stage', { elements, read: (stage, named) => (stage?.characters ? { ...stage, characters: readCastTakes(stage.characters, named) } : stage), normalize: normalizeStage });
@@ -0,0 +1,31 @@
1
+ import { specIdOf } from '../../motion3/spec-store.js';
2
+
3
+ // A take's source (its spec, placement and the scene boxes it was compiled against) runs to
4
+ // ~110k characters a character, and a document read carried it in full wherever a take sits:
5
+ // motion (as take and fullTake), the cast and stage (motionTake) and every saved scene. One read
6
+ // of motion alone answered 460k characters, ~300k tokens a model call for the rest of the turn.
7
+ // A read answers the take's specId instead (motion.compile { base: specId } recompiles it); a
8
+ // character named in ids also gets its spec and placement, never the scene boxes.
9
+ const LEFT_OUT = 'left out of this read: name this character in ids for its spec and placement, or compile { base: specId }';
10
+ const leftOutScene = scene => `left out (the ${Object.keys(scene?.objects ?? {}).length} scene objects it was compiled against)`;
11
+
12
+ /** A take source ({ schema, rig, spec, placement, scene, ... }) as a read answers it: { specId, ... }. */
13
+ function readSource(source, named) {
14
+ const specId = specIdOf(source.spec);
15
+ if (!named) return { specId, frames: source.spec?.frames, ...(source.takeId ? { takeId: source.takeId } : {}), ...(source.edit ? { edit: source.edit } : {}), spec: LEFT_OUT };
16
+ const { scene, ...kept } = source;
17
+ return { specId, ...kept, scene: leftOutScene(scene) };
18
+ }
19
+
20
+ /** A motion layer's take descriptor, its source read as above. */
21
+ export function readTake(take, named) {
22
+ if (!take?.source) return take;
23
+ const { source, ...descriptor } = take, specId = specIdOf(source.spec);
24
+ if (!named) return { ...descriptor, specId, source: LEFT_OUT };
25
+ const { scene, ...kept } = source;
26
+ return { ...descriptor, specId, source: { ...kept, scene: leftOutScene(scene) } };
27
+ }
28
+
29
+ /** Cast rows with their saved takes (motionTake) read as above. */
30
+ export const readCastTakes = (characters, named) => (Array.isArray(characters)
31
+ ? characters.map(row => (row?.motionTake?.spec ? { ...row, motionTake: readSource(row.motionTake, named(row.id)) } : row)) : characters);
@@ -42,15 +42,29 @@ export function elementSetSchema(kind) {
42
42
  export function elementTarget(kind, value, id, sceneId) {
43
43
  return kinds.get(projectionKind(kind)).collection ? value.find(row => row.id === id) : id === sceneId ? value : undefined;
44
44
  }
45
- export function readElementDocument(projection, { ids, select } = {}, sceneId) {
45
+ // A kind's `read` shapes what a document read answers (motion leaves out a take's bulky source
46
+ // unless its row id was named): `named` are the ids the caller asked for by name. A read answers
47
+ // at most DOCUMENT_READ_CHARS of kinds, the smallest first; the rest are named in `omitted` with
48
+ // their size, to select alone or by ids. One kind alone answers whole (ids narrow it).
49
+ const DOCUMENT_READ_CHARS = 90_000;
50
+ export function readElementDocument(projection, { ids, select, named = ids } = {}, sceneId) {
46
51
  const entries = Object.entries(projection).flatMap(([key, value]) => {
47
52
  const kind = projectionKind(key);
48
53
  if (select && !select.includes(key) && !select.includes(kind)) return [];
49
54
  if (!ids || ids.includes(key) || ids.includes(kind) || ids.includes(sceneId)) return [[key, value]];
50
55
  const selected = kinds.get(kind).collection ? value.filter(row => ids.includes(row.id)) : [];
51
56
  return selected.length ? [[key, selected]] : [];
57
+ }).map(([key, value]) => {
58
+ const read = kinds.get(projectionKind(key))?.read;
59
+ return [key, read ? read(value, id => Boolean(named?.includes(id))) : value];
52
60
  });
53
- return { document: structuredClone(Object.fromEntries(entries)), schema: Object.fromEntries(entries.map(([key]) => [key, elementSetSchema(projectionKind(key))])) };
61
+ const sized = entries.map(([key, value]) => { const schema = elementSetSchema(projectionKind(key)); return { key, value, schema, chars: JSON.stringify([value, schema]).length }; });
62
+ let room = DOCUMENT_READ_CHARS;
63
+ const fits = new Set(sized.length < 2 ? sized.map(row => row.key) : [...sized].sort((a, b) => a.chars - b.chars).filter(row => (room -= row.chars) >= 0).map(row => row.key));
64
+ const kept = sized.filter(row => fits.has(row.key)), omitted = sized.filter(row => !fits.has(row.key));
65
+ return { document: structuredClone(Object.fromEntries(kept.map(row => [row.key, row.value]))), schema: Object.fromEntries(kept.map(row => [row.key, row.schema])),
66
+ ...(omitted.length ? { omitted: Object.fromEntries(omitted.map(row => [row.key, row.chars])),
67
+ note: `Left out to keep this read under ${DOCUMENT_READ_CHARS} characters (sizes in omitted): select a kind alone, name ids, or page objects with scope "entities".` } : {}) };
54
68
  }
55
69
  export function readElement(document, path) {
56
70
  const element = kinds.get(path.slice(0, path.indexOf('.'))).elements.find(row => row.path === path);
@@ -18,9 +18,11 @@ import { fitOutput, reportPage } from "../motion3/report.js";
18
18
  import { keyObject } from "../motion3/key-edit.js";
19
19
  import { expandUse } from "../motion3/skills/index.js";
20
20
  import { publishDraft } from "../agent-progress.js";
21
+ import { qaSay } from "../qa-label.js";
21
22
  import { carryEdit } from "../motion3/edit-carry.js";
22
23
  import { normalizeCompileArgs } from "../motion3/lenient.js";
23
24
  import { createCompileRunner } from "./motion3-compile.js";
25
+ import { peopleOf } from "../motion3/people.js";
24
26
 
25
27
  const TIMELINE_FPS = 24;
26
28
  const id = { type: "string", minLength: 1 };
@@ -91,6 +93,11 @@ export function register(registry, ports) {
91
93
  if (Math.abs((character.scale ?? 1) - 1) > 1e-6) fail("TARGET_NOT_READY", "Set the character's scale back to 1 before compiling motion.");
92
94
  return sk;
93
95
  };
96
+ // the scene a compile sees: the objects, and the other characters' installed takes as moving bodies (people.js)
97
+ const sceneFor = character => {
98
+ const state = ports.state(), people = peopleOf(state.characters, character.id, id => owner().motionFor(id));
99
+ return { ...motionScene(state.objects), ...(Object.keys(people).length ? { people } : {}) };
100
+ };
94
101
  // one compile worker per editor in the browser, the calling thread elsewhere (motion3-compile.js)
95
102
  const runner = createCompileRunner();
96
103
  const workerWarning = () => { const note = runner.takeWarning(); return note ? { warnings: [note] } : {}; };
@@ -101,7 +108,7 @@ export function register(registry, ports) {
101
108
  const compile = (character, spec, opts = {}, bake) => {
102
109
  rigOf(character);
103
110
  const placed = bake && { position: [character.x ?? 0, character.y ?? 0, character.z ?? 0], yaw: character.rot ?? 0, ...bake };
104
- return runner.run({ model: character.model, scene: motionScene(ports.state().objects), spec, ...opts, ...(placed ? { bake: placed } : {}) });
111
+ return runner.run({ model: character.model, scene: sceneFor(character), spec, ...opts, ...(placed ? { bake: placed } : {}) });
105
112
  };
106
113
  /** The whole output, kept in the store (motion.report, delta) and cut to the receipt budget (report.js). */
107
114
  const report = (r, head, extra = {}) => {
@@ -134,7 +141,9 @@ export function register(registry, ports) {
134
141
  const character = characterOf(ports, characterId);
135
142
  if ((spec === undefined) === (base === undefined)) fail("INVALID_ARGUMENT", "Send exactly one of spec or base (the specId of a compiled spec, with patch).");
136
143
  if (patch !== undefined && base === undefined) fail("INVALID_ARGUMENT", "patch applies to base, the specId of a compiled spec; send base with it, or the whole spec.");
137
- const patched = [], source = spec ?? asArgument(() => applyPatch(store.get(base) ?? unknownSpec("base", base), patch ?? [], patched));
144
+ // a base the store forgot (a reload) may still be the installed take's spec: a document read names it by specId
145
+ const installedSpec = () => { const take = owner().motionFor(characterId); return take?.spec && specIdOf(take.spec) === base ? structuredClone(take.spec) : null; };
146
+ const patched = [], source = spec ?? asArgument(() => applyPatch(store.get(base) ?? installedSpec() ?? unknownSpec("base", base), patch ?? [], patched));
138
147
  // the take is always baked: the user watches every draft, installed or not
139
148
  return after(compile(character, source, { probes, repair }, {}), r => {
140
149
  const { qa, take } = r, notes = [...patched, ...(r.notes ?? [])];
@@ -153,12 +162,12 @@ export function register(registry, ports) {
153
162
  const output = report(r, { frames: r.frameCount, fps: TIMELINE_FPS, installed: wanted, ...adjustedOf(notes) }, { ...delta, ...(r.repair ? { repair: r.repair } : {}), ...(timing ? { timing } : {}) });
154
163
  // agent-progress.js: viewport ghost, timeline beats, agent panel
155
164
  publishDraft(characterId, { take, installed: installs || kept, specId: output.specId, label: r.label, qa, timeline: r.timeline });
156
- return { affectedIds: installs ? [characterId] : [], summary: `Compiled ${r.frameCount} frames; QA ${qa.ok ? "clean" : `${qa.total} issues`}${how}${adjustedSay(notes)}.`, output, ...workerWarning() };
165
+ return { affectedIds: installs ? [characterId] : [], summary: `Compiled ${r.frameCount} frames; QA ${qaSay(qa)}${how}${adjustedSay(notes)}.`, output, ...workerWarning() };
157
166
  });
158
167
  } });
159
168
  registry.register({ ...declarations[1], available: mounted, run({ characterId, checks }) {
160
169
  const character = characterOf(ports, characterId), take = installed(characterId);
161
- return after(compile(character, take.spec, { checks }), r => ({ affectedIds: [], summary: `QA ${r.qa.ok ? "clean" : `${r.qa.total} issues`}${adjustedSay(r.notes ?? [])}.`,
170
+ return after(compile(character, take.spec, { checks }), r => ({ affectedIds: [], summary: `QA ${qaSay(r.qa)}${adjustedSay(r.notes ?? [])}.`,
162
171
  output: report(r, adjustedOf(r.notes ?? [])), ...workerWarning() }));
163
172
  } });
164
173
  registry.register({ ...declarations[2], available: mounted, run({ characterId, frames, bones = DEFAULT_BONES }) {
@@ -196,11 +205,11 @@ export function register(registry, ports) {
196
205
  // motion.keyObject (C14): one key of a held object (or a hinge angle) in the installed take's spec, compiled
197
206
  // and installed whatever QA says (a user edit), as one undo entry
198
207
  registry.register({ ...declarations[5], available: mounted, target: args => args.characterId, run({ characterId, ...args }, context) {
199
- const character = characterOf(ports, characterId), take = installed(characterId), scene = motionScene(ports.state().objects);
208
+ const character = characterOf(ports, characterId), take = installed(characterId), scene = sceneFor(character);
200
209
  const { spec, keyed } = asArgument(() => keyObject(take.spec, scene, args, expandUse(take.spec, { sk: rigOf(character), scene, lenient: true }).spec.objects));
201
210
  return after(compile(character, spec, { lenient: true }, { prompt: take.prompt }), r => {
202
211
  const timing = put(character, r.take, context);
203
- return { affectedIds: [characterId], summary: `${keyed.removed ? "Removed the key of" : "Keyed"} ${keyed.objectId} at f${keyed.f}; QA ${r.qa.ok ? "clean" : `${r.qa.total} issues`}; installed.`,
212
+ return { affectedIds: [characterId], summary: `${keyed.removed ? "Removed the key of" : "Keyed"} ${keyed.objectId} at f${keyed.f}; QA ${qaSay(r.qa)}; installed.`,
204
213
  output: report(r, { frames: r.frameCount, fps: TIMELINE_FPS, installed: true, keyed }, timing ? { timing } : {}), ...workerWarning() };
205
214
  });
206
215
  } });
@@ -24,7 +24,7 @@ const PATCH_HINTS = { scale: 'use scaleX, scaleY, scaleZ', rotY: 'use rot for ya
24
24
  const mutation = (id, label, input) => ({ id, label, description: label, kind: 'mutation', undoDomain: 'objects', input });
25
25
  const semantic = [
26
26
  mutation('object.set', 'Set object fields', setInput),
27
- mutation('object.add', 'Add object', input({ kind: { type: 'string' }, placement, name: { type: 'string' }, parent: id, first: { type: 'boolean' } }, ['kind'])),
27
+ mutation('object.add', 'Add object', input({ kind: { type: 'string' }, placement, name: { type: 'string' }, parent: id, first: { type: 'boolean' }, text: setInput.properties.set.properties.text }, ['kind'])),
28
28
  // A clay prop from a recipe of soft parts (docs/sculpt-from-image.md). With `id` it re-sculpts that
29
29
  // sculpt in place (one undo); without, it creates one. The recipe is checked by sculpt-recipe.js.
30
30
  { ...mutation('object.sculpt', 'Sculpt a clay prop from a recipe', input({ recipe: { type: 'object', properties: {}, additionalProperties: true }, edits: { type: 'array', minItems: 1, maxItems: 48, items: { type: 'object', properties: {}, additionalProperties: true } }, id, name: { type: 'string', maxLength: 240 }, placement, parent: id }, [])),
@@ -75,7 +75,7 @@ export function register(registry, ports) {
75
75
  'object.add': args => {
76
76
  const before = owned().read();
77
77
  if (args.parent !== undefined) objectOf(args.parent);
78
- const object = createSceneObject(args.kind, before, args.placement);
78
+ const object = createSceneObject(args.kind, before, args.placement, args.text);
79
79
  if (!object) fail('INVALID_ARGUMENT', `Unknown object kind: ${args.kind}`);
80
80
  const placed = updateSceneObject([object], object.id, { ...(args.placement ?? {}), ...(args.name === undefined ? {} : { name: args.name }) })[0];
81
81
  // `first`: the Outliner lists Props in record order, so a UI-made object
@@ -22,10 +22,9 @@ const number = { type: 'number' };
22
22
  const frame = { type: 'integer', minimum: 0 };
23
23
  const input = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
24
24
  const mutation = (id, label, schema, description = label) => ({ id, label, description, kind: 'mutation', undoDomain: 'shot', input: schema });
25
- const existing = ['shot.create', 'shot.createStill', 'shot.setCaption', 'shot.setCastOverride', 'shot.setHold', 'shot.split', 'shot.duplicate', 'shot.remove', 'shot.setRange', 'shot.setCameraRail', 'shot.clearCameraRail', 'shot.reorder'].map(studioActionDeclaration);
25
+ const existing = ['shot.create', 'shot.createStill', 'shot.setCaption', 'shot.setCastOverride', 'shot.setHold', 'shot.split', 'shot.duplicate', 'shot.remove', 'shot.setRange', 'shot.setCameraRail', 'shot.clearCameraRail', 'shot.setCameraMode', 'shot.rename', 'shot.reorder'].map(studioActionDeclaration);
26
26
  const extra = [
27
27
  mutation('shot.set', 'Set shot fields', elementSetSchema('shot')),
28
- mutation('shot.rename', 'Rename shot', input({ shotId: id, name: { type: 'string', maxLength: 240 } })),
29
28
  mutation('shot.setCamera', 'Set camera block', input({ shotId: id, patch: { type: 'object', properties: {}, additionalProperties: true } })),
30
29
  mutation('shot.addKey', 'Add camera key', input({ shotId: id, frame })),
31
30
  mutation('shot.moveKey', 'Move camera key', input({ shotId: id, keyId: id, frame })),
@@ -159,6 +158,11 @@ export function register(registry, ports) {
159
158
  patchShot(shotId, shot => ({ ...shot, camera: removeCameraRail(shot.camera) }));
160
159
  },
161
160
  'shot.rename': ({ shotId, name }) => { shotOf(shotId); owner().write(current => renameShot(current, shotId, name)); },
161
+ 'shot.setCameraMode': ({ shotId, mode }) => {
162
+ const shot = shotOf(shotId);
163
+ if (mode === 'rail' && !createCameraBlock(shot.camera).cameraRail) fail('TARGET_NOT_READY', `${shot.name || shotId} has no camera rail; lay one with shot.setCameraRail.`);
164
+ patchShot(shotId, row => ({ ...row, camera: updateCameraBlock(row.camera, { mode }) }));
165
+ },
162
166
  'shot.setCamera': ({ shotId, patch }) => patchShot(shotId, shot => ({ ...shot, camera: updateCameraBlock(shot.camera, patch) })),
163
167
  'shot.addKey': ({ shotId, frame }) => {
164
168
  const shot = shotOf(shotId), target = Math.min(owner().state().frameCount - 1, frame);
@@ -22,7 +22,7 @@ const finite3 = value => Array.isArray(value) && value.length === 3 && value.eve
22
22
  export function takeSource(take, sceneObjects = []) {
23
23
  if (!isTakeV3(take) || !plain(take.spec)) return null;
24
24
  return structuredClone({ schema: TAKE_SCHEMA, rig: take.rig, spec: take.spec,
25
- placement: { position: take.placement?.position ?? [0, 0, 0], yaw: take.placement?.yaw ?? 0 }, scene: motionScene(sceneObjects) });
25
+ placement: { position: take.placement?.position ?? [0, 0, 0], yaw: take.placement?.yaw ?? 0 }, scene: { ...motionScene(sceneObjects), ...(plain(take.people) ? { people: take.people } : {}) } });
26
26
  }
27
27
 
28
28
  /** A saved source (with the Studio take id it carried), checked and owned by the caller; null when it is not one. */
@@ -31,8 +31,10 @@ export function normalizeTakeSource(value) {
31
31
  if (!plain(value.spec) || value.spec.schema !== SPEC_SCHEMA || !Number.isFinite(value.spec.frames)) return null;
32
32
  if (!plain(value.placement) || !finite3(value.placement.position) || !Number.isFinite(value.placement.yaw)) return null;
33
33
  const objects = plain(value.scene?.objects) ? value.scene.objects : {}, others = plain(value.scene?.others) ? { others: value.scene.others } : {};
34
+ // the joints a person anchor named when it was compiled (people.js keptPeople)
35
+ const people = plain(value.scene?.people) && Object.values(value.scene.people).every(p => plain(p) && Array.isArray(p.names) && Array.isArray(p.P) && p.P.length === p.frames * p.names.length * 3) ? { people: value.scene.people } : {};
34
36
  return structuredClone({ schema: TAKE_SCHEMA, rig: value.rig, spec: value.spec,
35
- placement: { position: value.placement.position, yaw: value.placement.yaw }, scene: { objects, ...others },
37
+ placement: { position: value.placement.position, yaw: value.placement.yaw }, scene: { objects, ...others, ...people },
36
38
  ...(typeof value.takeId === "string" && value.takeId ? { takeId: value.takeId } : {}),
37
39
  ...(normalizeTakeEdit(value.edit, value.spec.frames) ? { edit: normalizeTakeEdit(value.edit, value.spec.frames) } : {}) });
38
40
  }
@@ -6,6 +6,7 @@
6
6
  // GIZMO_LAYER, so captures, contact sheets and exports never see it.
7
7
  import { useSyncExternalStore } from "react";
8
8
  import { ko } from "./locale.js";
9
+ import { qaLabel } from "./qa-label.js";
9
10
 
10
11
  /** Teal: apart from the onion skin's blue (before) and orange (after). */
11
12
  export const DRAFT_GHOST_COLOR = "#2fc79a";
@@ -28,8 +29,8 @@ export function draftGhostState(draft, now) {
28
29
  return { shown: true, opacity: +opacity.toFixed(3), frame: Math.floor((age / 1000) * fps) % take.frameCount };
29
30
  }
30
31
 
31
- /** "Draft 3 · QA 31" ("초안 3 · QA 31"). */
32
- export const draftLabel = draft => `${ko("Draft", "초안")} ${draft?.n ?? 0} · QA ${draft?.qa?.total ?? 0}`;
32
+ /** "Draft 3 · 2 problems" ("초안 3 · 문제 2개"), "Draft 7 · QA clean": problems, not frame-level issues (qa-label.js). */
33
+ export const draftLabel = draft => `${ko("Draft", "초안")} ${draft?.n ?? 0} · ${qaLabel(draft?.qa)}`;
33
34
 
34
35
  let shown = true;
35
36
  const listeners = new Set();
@@ -63,7 +63,7 @@ export function beatBoardModel(draft, motion) {
63
63
  for (const block of blocks) block.rows = rows;
64
64
  const spans = groups.map(g => ({ f0: g.first ?? 0, f1: g.last ?? g.first ?? 0, text: `${g.kind} ${g.subject}` })).sort((a, b) => a.f0 - b.f0);
65
65
  const marks = goals.map(g => ({ f: g.at, ok: !!g.ok, text: goalText(g) })).sort((a, b) => a.f - b.f);
66
- return { mode, n: draft.n, total: draft.qa?.total ?? 0, ok: !!draft.qa?.ok, blocks, spans, marks };
66
+ return { mode, n: draft.n, total: draft.qa?.total ?? 0, ok: !!draft.qa?.ok, qa: draft.qa ?? null, blocks, spans, marks };
67
67
  }
68
68
 
69
69
  /** One goal as a line: "✓ name 1.2 cm (≤ 2 cm)"; a distance in metres reads in cm. */
@@ -8,6 +8,7 @@ import { useSyncExternalStore } from "react";
8
8
  import { ko } from "../locale.js";
9
9
  import { latestDraft, progressVersion, subscribeProgress } from "../agent-progress.js";
10
10
  import { beatBoardModel, blockTitle } from "./beat-board-model.js";
11
+ import { draftLabel } from "../draft-ghost.js";
11
12
  import "./beat-board.css";
12
13
 
13
14
  function stateChip(block) {
@@ -51,7 +52,7 @@ export function BeatBoard({ motion, frame, displayFrameCount, clipPct, onJump })
51
52
  {model.mode === "draft" && (
52
53
  <span className={"tl-beat-head" + (model.ok ? " ok" : "")} data-testid="beat-board-head"
53
54
  title={ko("The agent's newest draft, not installed yet", "에이전트가 만든 최신 초안 (아직 적용 전)")}>
54
- {ko(`Draft ${model.n} · QA ${model.total}`, `초안 ${model.n} · QA ${model.total}`)}
55
+ {draftLabel({ n: model.n, qa: model.qa })}
55
56
  </span>
56
57
  )}
57
58
  </div>
@@ -10,6 +10,7 @@ import { compileSpec, liftRequest } from "./pipeline.js";
10
10
  import { repairSpec } from "./repair.js";
11
11
  import { bakeTake } from "./take.js";
12
12
  import { skeletonFor } from "./rigs/index.js";
13
+ import { keptPeople } from "./people.js";
13
14
 
14
15
  const TIMELINE_FPS = 24;
15
16
  const refuse = (code, message) => { throw Object.assign(new Error(message), { code }); };
@@ -52,6 +53,9 @@ export function runCompileJob({ model, scene, spec: asked, probes: sentProbes, c
52
53
  // ms.repair: the whole repair's wall time, so a sweep that repairs every variant shows what it costs
53
54
  const r = fixed.result, label = fixed.spec?.label ?? null, ms = fixed.repair ? { ...r.ms, repair: Math.round(performance.now() - t0) } : r.ms;
54
55
  const take = bake ? bakeTake(sk, r.out, { position: bake.position, yaw: bake.yaw }, { spec: r.stored, prompt: ("prompt" in bake ? bake.prompt : label) ?? "" }) : undefined;
56
+ // a person anchor's joints ride with the take, so its saved source compiles the same without the others' takes (people.js)
57
+ const people = take && keptPeople(scene, [r.expanded, r.stored]);
58
+ if (people) take.people = people;
55
59
  return { stored: r.stored, qa: r.qa, observations: r.observations, used: r.used, fixed: r.fixed, search: r.search, timeline: r.timeline,
56
60
  ...(r.probes ? { probes: r.probes } : {}), ...(r.notes ? { notes: r.notes } : {}), ms, frameCount: r.out.frameCount, label,
57
61
  ...(fixed.repair ? { repair: fixed.repair } : {}), ...(take ? { take } : {}) };