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.
- package/CHANGELOG.md +57 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/bin/agent/agent-routes.mjs +3 -3
- package/bin/agent/agent-runner.mjs +5 -5
- package/bin/agent/pi-tools.mjs +12 -1
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +99 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +1 -1
- package/dist/app/index.html +4 -4
- package/dist/assets/analytics-DbKG0NgD.js +6 -0
- package/dist/assets/app-34i-iUy1.js +5088 -0
- package/dist/assets/compile-worker-DYpa2nvO.js +5 -0
- package/dist/assets/{first-shot-handoff-CC4xe4HM.js → first-shot-handoff-CrGZ3n_O.js} +1 -1
- package/dist/assets/helvetiker-bold.typeface-BUlyMz0-.js +13 -0
- package/dist/assets/{landing-DjHD8u7k.js → landing-mq5bnqFE.js} +1 -1
- package/dist/assets/scene-asset-sync-7J-w5oNh.js +20 -0
- package/dist/assets/workflow-dUhurYUc.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/workflow/index.html +3 -3
- package/package.json +1 -1
- package/src/App.jsx +1 -1
- package/src/agent-progress.js +3 -2
- package/src/agent-turn-mark.js +33 -0
- package/src/app-context.js +7 -2
- package/src/command-bus.js +10 -11
- package/src/commands/elements/character.js +2 -1
- package/src/commands/elements/motion.js +7 -1
- package/src/commands/elements/scene.js +3 -0
- package/src/commands/elements/stage.js +2 -1
- package/src/commands/elements/take-read.js +31 -0
- package/src/commands/elements.js +16 -2
- package/src/commands/motion3.js +15 -6
- package/src/commands/objects.js +2 -2
- package/src/commands/shot.js +6 -2
- package/src/domains/motion/take-source.js +4 -2
- package/src/draft-ghost.js +3 -2
- package/src/motion/beat-board-model.js +1 -1
- package/src/motion/beat-board.jsx +2 -1
- package/src/motion3/compile-job.js +4 -0
- package/src/motion3/compiler/anchors.js +68 -4
- package/src/motion3/compiler/compile.js +5 -1
- package/src/motion3/compiler/grip.js +8 -2
- package/src/motion3/compiler/limbs.js +35 -8
- package/src/motion3/compiler/pose.js +17 -2
- package/src/motion3/compiler/socket-grip.js +107 -0
- package/src/motion3/compiler/validate-objects.js +3 -2
- package/src/motion3/compiler/validate-parts.js +9 -2
- package/src/motion3/compiler/validate.js +3 -3
- package/src/motion3/compiler/vocab.js +23 -16
- package/src/motion3/describe.js +5 -2
- package/src/motion3/docs.js +1 -1
- package/src/motion3/free-motion.js +54 -15
- package/src/motion3/object-keys.js +36 -5
- package/src/motion3/object-pose.js +39 -6
- package/src/motion3/people.js +82 -0
- package/src/motion3/qa/checks.js +6 -2
- package/src/motion3/qa/hints-contact.js +17 -1
- package/src/motion3/qa/index.js +6 -4
- package/src/motion3/qa/people-checks.js +37 -0
- package/src/motion3/qa/style.js +14 -4
- package/src/motion3/refs.js +10 -1
- package/src/motion3/scene.js +4 -3
- package/src/motion3/schema/fields.js +4 -4
- package/src/motion3/schema/topics.js +9 -6
- package/src/motion3/schema/types.js +11 -9
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +10 -3
- package/src/motion3/skills/flip.js +200 -0
- package/src/motion3/skills/impact.js +109 -0
- package/src/motion3/skills/index.js +3 -1
- package/src/motion3/skills/info.js +19 -4
- package/src/motion3/sockets.js +104 -0
- package/src/motion3/take-edit.js +1 -1
- package/src/motion3/take.js +6 -4
- package/src/object-catalog.jsx +3 -1
- package/src/object-grasp.js +38 -0
- package/src/object-look.js +2 -0
- package/src/object-travel.js +6 -3
- package/src/panels/ObjectLookPanel.jsx +24 -3
- package/src/props.jsx +5 -5
- package/src/qa-label.js +25 -0
- package/src/scene-objects.js +19 -11
- package/src/shell/details-selection.js +2 -1
- package/src/studio-actions.js +4 -0
- package/src/studio-agent-protocol.js +11 -12
- package/src/studio-app-binding.js +3 -3
- package/src/studio-elements.js +3 -1
- package/src/studio-error-codes.js +8 -1
- package/src/text/fonts/helvetiker-LICENSE.txt +13 -0
- package/src/text/fonts/helvetiker-bold.typeface.json +1 -0
- package/src/text/text-object.jsx +106 -0
- package/src/text/text-record.js +122 -0
- package/src/workflow/AgentProgressCard.jsx +6 -4
- package/tools/run-tests.mjs +2 -2
- package/dist/assets/analytics-C9sUnndd.js +0 -1
- package/dist/assets/app-CCh--1Hc.js +0 -5087
- package/dist/assets/compile-worker-CkxiD6lH.js +0 -5
- package/dist/assets/scene-asset-sync-Hx_FzdOH.js +0 -20
- package/dist/assets/workflow-C4yKH1p4.js +0 -179
|
@@ -1 +1 @@
|
|
|
1
|
-
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.0.
|
|
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-
|
|
305
|
-
<link rel="modulepreload" crossorigin href="/assets/analytics-
|
|
306
|
-
<link rel="modulepreload" crossorigin href="/assets/first-shot-handoff-
|
|
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">
|
package/dist/workflow/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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.
|
|
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; },
|
package/src/agent-progress.js
CHANGED
|
@@ -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 ?? [],
|
package/src/agent-turn-mark.js
CHANGED
|
@@ -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
|
+
}
|
package/src/app-context.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/command-bus.js
CHANGED
|
@@ -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
|
-
//
|
|
107
|
-
//
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/commands/elements.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/src/commands/motion3.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
} });
|
package/src/commands/objects.js
CHANGED
|
@@ -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
|
package/src/commands/shot.js
CHANGED
|
@@ -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
|
}
|
package/src/draft-ghost.js
CHANGED
|
@@ -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 ·
|
|
32
|
-
export const draftLabel = draft => `${ko("Draft", "초안")} ${draft?.n ?? 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
|
-
{
|
|
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 } : {}) };
|