cozyclay 3.0.0 → 3.0.1
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 +28 -0
- package/bin/agent/agent-routes.mjs +3 -3
- package/bin/agent/agent-runner.mjs +5 -5
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +93 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/dist/app/index.html +4 -4
- package/dist/assets/analytics-BWCqdgWA.js +1 -0
- package/dist/assets/app-DfTCxaKk.js +5087 -0
- package/dist/assets/compile-worker-BxKXh07s.js +5 -0
- package/dist/assets/{first-shot-handoff-CC4xe4HM.js → first-shot-handoff-DNO8kv_N.js} +1 -1
- package/dist/assets/{landing-DjHD8u7k.js → landing-yfZaJkWc.js} +1 -1
- package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
- package/dist/assets/workflow-D78eiKTa.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/app-context.js +7 -2
- package/src/command-bus.js +2 -2
- package/src/commands/motion3.js +12 -5
- 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 +8 -0
- package/src/motion3/compiler/limbs.js +35 -8
- package/src/motion3/compiler/pose.js +17 -2
- package/src/motion3/compiler/validate-objects.js +3 -2
- package/src/motion3/compiler/validate-parts.js +2 -0
- package/src/motion3/compiler/validate.js +3 -3
- package/src/motion3/compiler/vocab.js +17 -16
- package/src/motion3/describe.js +5 -2
- package/src/motion3/docs.js +1 -1
- package/src/motion3/free-motion.js +42 -11
- package/src/motion3/object-pose.js +1 -1
- 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/schema/fields.js +4 -4
- package/src/motion3/schema/topics.js +6 -4
- package/src/motion3/schema/types.js +10 -8
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +2 -1
- 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 +15 -0
- package/src/qa-label.js +25 -0
- package/src/studio-actions.js +4 -0
- package/src/studio-agent-protocol.js +8 -8
- package/src/studio-app-binding.js +2 -2
- package/src/studio-error-codes.js +8 -1
- 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.1\",\"repository\":\"NomaDamas/CozyClay\",\"content_sha256\":\"bedf955b7367c9d4e883baa9c0b6fd14a205f6d39a3bf447f628488cd2893564\"}","signature":"+2TEXB+bXFUPN34/ls1YvimKkHfSdSCs0IiIyjIhsHqvz3j+XcKkEkqx1zjXn8MQlybgStGOS290YH40Nw1hBQ=="}
|
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-yfZaJkWc.js"></script>
|
|
305
|
+
<link rel="modulepreload" crossorigin href="/assets/analytics-BWCqdgWA.js">
|
|
306
|
+
<link rel="modulepreload" crossorigin href="/assets/first-shot-handoff-DNO8kv_N.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-D78eiKTa.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-BWCqdgWA.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-U3kS2rvK.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.1",
|
|
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/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,7 +2,7 @@
|
|
|
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';
|
|
5
|
+
import { StudioProtocolError, StudioSchemas, validateStudioSchema, validateReceipt, validateStudioIdentity, refusalCode } from './studio-agent-protocol.js';
|
|
6
6
|
import { agentTurnControls, markAgentTurn } from './agent-turn-mark.js';
|
|
7
7
|
|
|
8
8
|
// A batching/retention adapter over the existing object history owner. The
|
|
@@ -203,7 +203,7 @@ export function createCommandBus({ registry, ports }) {
|
|
|
203
203
|
function refusal(request, before, error, mutated) {
|
|
204
204
|
const changed = mutated ?? (ports.read().revision !== before.revision);
|
|
205
205
|
return validateReceipt({ ok: false, commandId: request.commandId, host: before.host,
|
|
206
|
-
code: error
|
|
206
|
+
code: refusalCode(error), phase: changed ? 'commit' : 'admission', affectedIds: [], expectedTargets: [], currentTargets: [],
|
|
207
207
|
mutated: changed, preserved: { authoredState: changed ? 'changed' : 'unchanged' }, recovery: { action: 'inspect', retryAllowed: false },
|
|
208
208
|
message: [...String(error.message || error)].slice(0, 500).join('') });
|
|
209
209
|
}
|
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 = {}) => {
|
|
@@ -153,12 +160,12 @@ export function register(registry, ports) {
|
|
|
153
160
|
const output = report(r, { frames: r.frameCount, fps: TIMELINE_FPS, installed: wanted, ...adjustedOf(notes) }, { ...delta, ...(r.repair ? { repair: r.repair } : {}), ...(timing ? { timing } : {}) });
|
|
154
161
|
// agent-progress.js: viewport ghost, timeline beats, agent panel
|
|
155
162
|
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
|
|
163
|
+
return { affectedIds: installs ? [characterId] : [], summary: `Compiled ${r.frameCount} frames; QA ${qaSay(qa)}${how}${adjustedSay(notes)}.`, output, ...workerWarning() };
|
|
157
164
|
});
|
|
158
165
|
} });
|
|
159
166
|
registry.register({ ...declarations[1], available: mounted, run({ characterId, checks }) {
|
|
160
167
|
const character = characterOf(ports, characterId), take = installed(characterId);
|
|
161
|
-
return after(compile(character, take.spec, { checks }), r => ({ affectedIds: [], summary: `QA ${r.qa
|
|
168
|
+
return after(compile(character, take.spec, { checks }), r => ({ affectedIds: [], summary: `QA ${qaSay(r.qa)}${adjustedSay(r.notes ?? [])}.`,
|
|
162
169
|
output: report(r, adjustedOf(r.notes ?? [])), ...workerWarning() }));
|
|
163
170
|
} });
|
|
164
171
|
registry.register({ ...declarations[2], available: mounted, run({ characterId, frames, bones = DEFAULT_BONES }) {
|
|
@@ -196,11 +203,11 @@ export function register(registry, ports) {
|
|
|
196
203
|
// motion.keyObject (C14): one key of a held object (or a hinge angle) in the installed take's spec, compiled
|
|
197
204
|
// and installed whatever QA says (a user edit), as one undo entry
|
|
198
205
|
registry.register({ ...declarations[5], available: mounted, target: args => args.characterId, run({ characterId, ...args }, context) {
|
|
199
|
-
const character = characterOf(ports, characterId), take = installed(characterId), scene =
|
|
206
|
+
const character = characterOf(ports, characterId), take = installed(characterId), scene = sceneFor(character);
|
|
200
207
|
const { spec, keyed } = asArgument(() => keyObject(take.spec, scene, args, expandUse(take.spec, { sk: rigOf(character), scene, lenient: true }).spec.objects));
|
|
201
208
|
return after(compile(character, spec, { lenient: true }, { prompt: take.prompt }), r => {
|
|
202
209
|
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
|
|
210
|
+
return { affectedIds: [characterId], summary: `${keyed.removed ? "Removed the key of" : "Keyed"} ${keyed.objectId} at f${keyed.f}; QA ${qaSay(r.qa)}; installed.`,
|
|
204
211
|
output: report(r, { frames: r.frameCount, fps: TIMELINE_FPS, installed: true, keyed }, timing ? { timing } : {}), ...workerWarning() };
|
|
205
212
|
});
|
|
206
213
|
} });
|
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 } : {}) };
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// that frame (hinged, carried, following; object-pose.js objectPoseAt), in its own axes
|
|
11
11
|
// { kind: "landmark", name: "mouth" } measured face landmark on the Head bone
|
|
12
12
|
// { kind: "prop", prop: "cig", point: "tip", offset: [x,y,z] } a point on a prop held by the other hand
|
|
13
|
+
// { kind: "person", person: "char-b", bone: "LeftArm", offset: [x,y,z] } world offset from another character's joint
|
|
14
|
+
// where its installed take has it at that frame (people.js)
|
|
13
15
|
// spec.props = { name: { hand: "r", origin: ["RightHandIndex2", "RightHandMiddle2"],
|
|
14
16
|
// points: { name: [d, l, n] } } } offsets along the palm axes (m)
|
|
15
17
|
import { add, sub, scale, len, apply, ry } from "./math.js";
|
|
@@ -18,6 +20,7 @@ import { palmAxes } from "./limbs.js";
|
|
|
18
20
|
import { SIDES } from "./pose.js";
|
|
19
21
|
import { HAND_CLEAR, keepOut } from "../body.js";
|
|
20
22
|
import { groupsOf } from "../scene.js";
|
|
23
|
+
import { personJoint, peopleList } from "../people.js";
|
|
21
24
|
|
|
22
25
|
function armReach(sk, k) {
|
|
23
26
|
const side = SIDES[k], P = sk.restWorldPos, I = sk.index;
|
|
@@ -85,6 +88,11 @@ function anchorPoint(ctx, a, name, s, diag, c, side) {
|
|
|
85
88
|
if (!p) throw new Error(`MOTION_SPEC_INVALID: anchor "${name}" needs prop ${a.prop}.${a.point}, which is held by an arm solved later`);
|
|
86
89
|
return add(p, a.offset ?? [0, 0, 0]);
|
|
87
90
|
}
|
|
91
|
+
case "person": {
|
|
92
|
+
const p = personJoint(ctx.scene, a.person, a.bone, diag?.frame ?? 0);
|
|
93
|
+
if (!p) throw new Error(`MOTION_SPEC_INVALID: anchor "${name}" names person "${a.person}" bone "${a.bone}", which the scene does not have (people: ${peopleList(ctx.scene)})`);
|
|
94
|
+
return add(p, a.offset ?? [0, 0, 0]);
|
|
95
|
+
}
|
|
88
96
|
default: throw new Error(`MOTION_SPEC_INVALID: anchor "${name}" has unknown kind "${a.kind}"`);
|
|
89
97
|
}
|
|
90
98
|
}
|
|
@@ -103,16 +103,21 @@ export function twoBone(sk, s, root, mid, end, target, pole, restBend, stop = nu
|
|
|
103
103
|
return inside || d - dc;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Aim a foot: Foot->ToeBase along yaw/pitch of its rest direction, sole kept level. With `tilt` (pose.js tiltFrame: a body
|
|
108
|
+
* turned well past upright) the yaw and pitch are read in that frame and the sole levels to its up, so a foot in a flip
|
|
109
|
+
* points with the shin instead of staying flat to the floor; no toe-off then (the toes are off the ground).
|
|
110
|
+
*/
|
|
111
|
+
export function orientFoot(sk, s, side, yawDeg, pitchDeg, tilt = null) {
|
|
108
112
|
const foot = sk.index[`${side}Foot`], toe = sk.index[`${side}ToeBase`];
|
|
109
113
|
const rest = sub(sk.restWorldPos[toe], sk.restWorldPos[foot]);
|
|
110
|
-
const want = apply(ry(yawDeg), apply(rx(pitchDeg), rest));
|
|
114
|
+
const want = tilt ? apply(tilt.R, apply(ry(yawDeg - tilt.yaw), apply(rx(pitchDeg), rest))) : apply(ry(yawDeg), apply(rx(pitchDeg), rest));
|
|
115
|
+
const up = tilt ? apply(tilt.R, [0, 1, 0]) : [0, 1, 0];
|
|
111
116
|
// level the sole: the foot's own carried up vector goes to world up (no roll from the leg IK)
|
|
112
|
-
rotateSubtree(sk, s, foot, s.P[foot].slice(), align(sub(s.P[toe], s.P[foot]), carried(sk, s, foot, [0, 1, 0]), want,
|
|
117
|
+
rotateSubtree(sk, s, foot, s.P[foot].slice(), align(sub(s.P[toe], s.P[foot]), carried(sk, s, foot, [0, 1, 0]), want, up));
|
|
113
118
|
// Toe-off: bend the toe joint so the toes stay flat on the ground while the heel rises.
|
|
114
119
|
const tip = sk.index[`${side}Toe_End`];
|
|
115
|
-
if (pitchDeg > 0 && tip !== undefined) {
|
|
120
|
+
if (!tilt && pitchDeg > 0 && tip !== undefined) {
|
|
116
121
|
const seg = sub(s.P[tip], s.P[toe]), flat = [seg[0], 0, seg[2]];
|
|
117
122
|
if (len(flat) > 1e-6) rotateSubtree(sk, s, toe, s.P[toe].slice(), align(seg, [0, 1, 0], flat, [0, 1, 0]));
|
|
118
123
|
}
|
|
@@ -144,11 +149,13 @@ export function wrist(sk, s, side, twistDeg, flexDeg) {
|
|
|
144
149
|
rotateSubtree(sk, s, h, s.P[h].slice(), axisRot(axT, clamp(twistDeg, -85, 85)));
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
// Finger curl presets, degrees per phalanx (1, 2, 3); thumb is [opposition, curl, curl].
|
|
152
|
+
// Finger curl presets, degrees per phalanx (1, 2, 3); thumb is [opposition, curl, curl]. A fist and a grip keep the
|
|
153
|
+
// relaxed thumb here and close it with THUMB_WRAP: these thumb curls turn about the knuckle line and the hand's long axis,
|
|
154
|
+
// which only swings a thumb out to the side (a fist read as a thumbs-up).
|
|
148
155
|
const FINGER_PRESETS = {
|
|
149
156
|
relaxed: { Index: [12, 18, 10], Middle: [15, 20, 12], Ring: [18, 22, 12], Pinky: [20, 24, 14], Thumb: [15, 10, 8] },
|
|
150
|
-
fist: { Index: [80, 95, 60], Middle: [85, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [
|
|
151
|
-
grip: { Index: [55, 60, 35], Middle: [60, 62, 35], Ring: [62, 62, 35], Pinky: [65, 62, 35], Thumb: [
|
|
157
|
+
fist: { Index: [80, 95, 60], Middle: [85, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [15, 10, 8] },
|
|
158
|
+
grip: { Index: [55, 60, 35], Middle: [60, 62, 35], Ring: [62, 62, 35], Pinky: [65, 62, 35], Thumb: [15, 10, 8] },
|
|
152
159
|
cigarette: { Index: [8, 10, 6], Middle: [10, 12, 8], Ring: [60, 75, 45], Pinky: [65, 80, 45], Thumb: [35, 25, 20] },
|
|
153
160
|
point: { Index: [0, 0, 0], Middle: [80, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [40, 30, 30] },
|
|
154
161
|
};
|
|
@@ -166,5 +173,25 @@ export function fingers(sk, s, side, weights) {
|
|
|
166
173
|
rotateSubtree(sk, s, j, s.P[j].slice(), axisRot(ax, sign * curl(f, k)));
|
|
167
174
|
}
|
|
168
175
|
}
|
|
176
|
+
const wrap = [0, 1, 2, 3].map(i => Object.entries(weights ?? {}).reduce((acc, [p, w]) => acc + (w || 0) * (THUMB_WRAP[p]?.[i] ?? 0), 0));
|
|
177
|
+
if (wrap.some(Boolean)) wrapThumb(sk, s, side, wrap);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// A closing hand's thumb, by preset weight after the relaxed one, degrees: turned in about the hand's long axis toward the
|
|
181
|
+
// palm and across it, then flexed at its two joints about their own axes (bone x palm normal). The fist's thumb lies over
|
|
182
|
+
// the index and middle fingers' middle phalanges; the grip's closes around a handle against the index finger. Searched on
|
|
183
|
+
// both rigs within the joints' ranges (thumb tip to the index's middle phalanx: fist 8.4 -> 3.7 cm Y Bot, 6.9 -> 2.4 X Bot).
|
|
184
|
+
const THUMB_WRAP = { fist: [-70, -5, 5, 80], grip: [-30, -30, 30, 10] };
|
|
185
|
+
function wrapThumb(sk, s, side, [oppose, across, flex2, flex3]) {
|
|
186
|
+
const sign = side === "Left" ? -1 : 1, J = k => sk.index[`${side}HandThumb${k}`];
|
|
187
|
+
if (J(1) === undefined) return;
|
|
188
|
+
const { d, n } = palmAxes(sk, s, side);
|
|
189
|
+
rotateSubtree(sk, s, J(1), s.P[J(1)].slice(), axisRot(d, sign * oppose));
|
|
190
|
+
// n = d x l flips with the mirror, so the turn across the palm keeps its sign on both hands
|
|
191
|
+
rotateSubtree(sk, s, J(1), s.P[J(1)].slice(), axisRot(n, across));
|
|
192
|
+
for (const [k, deg] of [[2, flex2], [3, flex3]]) {
|
|
193
|
+
const ax = norm(cross(norm(sub(s.P[J(k + 1)], s.P[J(k)])), n));
|
|
194
|
+
rotateSubtree(sk, s, J(k), s.P[J(k)].slice(), axisRot(ax, sign * deg));
|
|
195
|
+
}
|
|
169
196
|
}
|
|
170
197
|
|
|
@@ -82,6 +82,21 @@ function hangOf(sk, down) {
|
|
|
82
82
|
return hang;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The frame the knee and elbow poles and the feet are read in once the whole body turns well past upright (a flip, a
|
|
87
|
+
* cartwheel): the root's yaw, pitch and roll, the tilt eased in from TILT_FROM to TILT_FULL degrees (the larger of the
|
|
88
|
+
* wrapped pitch and roll). Under TILT_FROM it is null and the poles turn with the yaw only, as they always have, so an
|
|
89
|
+
* upright take is unchanged.
|
|
90
|
+
*/
|
|
91
|
+
const TILT_FROM = 60, TILT_FULL = 120;
|
|
92
|
+
const wrap180 = a => a - 360 * Math.round(a / 360);
|
|
93
|
+
export function tiltFrame(yaw, pitch, roll) {
|
|
94
|
+
const p = wrap180(pitch), r = wrap180(roll), u = (Math.max(Math.abs(p), Math.abs(r)) - TILT_FROM) / (TILT_FULL - TILT_FROM);
|
|
95
|
+
if (u <= 0) return null;
|
|
96
|
+
const w = u >= 1 ? 1 : u * u * (3 - 2 * u);
|
|
97
|
+
return { yaw, R: mul(ry(yaw), mul(rx(p * w), rz(r * w))) };
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
export function pose(sk, c, hooks = {}) {
|
|
86
101
|
const s = restState(sk);
|
|
87
102
|
const diag = {};
|
|
@@ -107,7 +122,7 @@ export function pose(sk, c, hooks = {}) {
|
|
|
107
122
|
rotateSubtree(sk, s, hips, s.P[hips].slice(), mul(ry(yaw), mul(rx(num(c.pitch)), rz(num(c.roll)))));
|
|
108
123
|
const shift = sub(v3(c.root, s.P[hips]), s.P[hips]);
|
|
109
124
|
for (let j = 0; j < s.P.length; j++) s.P[j] = add(s.P[j], shift);
|
|
110
|
-
const body = ry(yaw);
|
|
125
|
+
const tilt = tiltFrame(yaw, num(c.pitch), num(c.roll)), body = tilt?.R ?? ry(yaw);
|
|
111
126
|
// Legs: two-bone IK to the ankle targets, the knee no further folded than its stop (limbs.js KNEE_STOP), then aim the feet.
|
|
112
127
|
for (const [k, side] of Object.entries(SIDES)) {
|
|
113
128
|
if (!c[`foot_${k}`]) continue;
|
|
@@ -117,7 +132,7 @@ export function pose(sk, c, hooks = {}) {
|
|
|
117
132
|
// a target nearer the hip than the knee folds leaves a residual (leg_miss); leg_fold says what it asked
|
|
118
133
|
const fold = askedFold(sk, s, side, target, LEG);
|
|
119
134
|
if (fold > KNEE_FOLD) diag[`leg_fold_${k}`] = fold;
|
|
120
|
-
orientFoot(sk, s, side, num(c[`fyaw_${k}`], yaw), num(c[`fpitch_${k}`]));
|
|
135
|
+
orientFoot(sk, s, side, num(c[`fyaw_${k}`], yaw), num(c[`fpitch_${k}`]), tilt);
|
|
121
136
|
}
|
|
122
137
|
// Arms: blend the FK hand toward the hook's target, poles blended the same way.
|
|
123
138
|
for (const k of hooks.armOrder ?? ["r", "l"]) {
|
|
@@ -15,7 +15,7 @@ import { isFree } from "../free-motion.js";
|
|
|
15
15
|
import { checkRef } from "../refs.js";
|
|
16
16
|
import { partsOf } from "../object-pose.js";
|
|
17
17
|
|
|
18
|
-
const KEY = "{ f, pos: [x, y, z], rot: [x, y, z, w] } or { f, pos, aim: [x, y, z], roll? }", FALL = "{ at, onto?, bounce? }";
|
|
18
|
+
const KEY = "{ f, pos: [x, y, z], rot: [x, y, z, w] } or { f, pos, aim: [x, y, z], roll? }", FALL = "{ at, onto?, bounce?, tumble? }";
|
|
19
19
|
const isQuat = q => Array.isArray(q) && q.length === 4 && q.every(Number.isFinite) && Math.abs(Math.hypot(...q) - 1) <= 0.01;
|
|
20
20
|
const isDir = v => isVec3(v) && Math.hypot(...v) > 1e-9;
|
|
21
21
|
|
|
@@ -77,7 +77,7 @@ function frameOk(err, at, v, frames) {
|
|
|
77
77
|
return false;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/** objects.<id>.fall (C17): { at, onto?, bounce? } or a list, each on its own frame; a carry falls only out of hand. */
|
|
80
|
+
/** objects.<id>.fall (C17): { at, onto?, bounce?, tumble? } or a list, each on its own frame; a carry falls only out of hand. */
|
|
81
81
|
function validateFalls(err, unknown, at, o, { frames, scene, carry }) {
|
|
82
82
|
const list = Array.isArray(o.fall) ? o.fall : [o.fall], seen = new Set();
|
|
83
83
|
if (!list.length) err(`${at}.fall is an empty list; give ${FALL} or a list of them`);
|
|
@@ -94,6 +94,7 @@ function validateFalls(err, unknown, at, o, { frames, scene, carry }) {
|
|
|
94
94
|
}
|
|
95
95
|
if (fl.onto !== undefined) objectOk(err, scene, `${fat}.onto`, fl.onto, { groups: true });
|
|
96
96
|
if (fl.bounce !== undefined && !(Number.isFinite(fl.bounce) && fl.bounce >= 0 && fl.bounce <= 0.6)) err(`${fat}.bounce must be 0..0.6 (the share of its landing speed it bounces back with; 0 lands dead), got ${fmt(fl.bounce)}`);
|
|
97
|
+
if (fl.tumble !== undefined && typeof fl.tumble !== "boolean") err(`${fat}.tumble must be true (it rolls to rest on a stable face, the default) or false (it drops straight down as it was), got ${fmt(fl.tumble)}`);
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -8,6 +8,7 @@ import { ANCHOR_FIELDS, PROP_FIELDS, FIT_FIELDS, WALK_FIELDS, CHECK_FIELDS, CHEC
|
|
|
8
8
|
import { boneOk, objectOk, refOk } from "./validate-common.js";
|
|
9
9
|
import { validateObjects } from "./validate-objects.js";
|
|
10
10
|
import { validateGoals } from "./validate-goals.js";
|
|
11
|
+
import { personJoint, peopleList } from "../people.js";
|
|
11
12
|
|
|
12
13
|
const SIDE = new Set(["l", "r"]);
|
|
13
14
|
const isObject = v => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
@@ -29,6 +30,7 @@ function validateAnchors(spec, err, { sk, scene, unknown = err }) {
|
|
|
29
30
|
if (a.kind === "bone") boneOk(err, sk, at, a.bone);
|
|
30
31
|
if (a.kind === "segment") { boneOk(err, sk, `${at}.from`, a.from); boneOk(err, sk, `${at}.to`, a.to); if (a.reach !== undefined && !SIDE.has(a.reach)) err(`${at}.reach must be "l" or "r"`); }
|
|
31
32
|
if (a.kind === "object") objectOk(err, scene, at, a.object, { groups: true });
|
|
33
|
+
if (a.kind === "person" && !personJoint(scene, a.person, a.bone ?? "", 0)) err(`${at} (person) needs person: the id of another character with an installed take (people: ${peopleList(scene)}) and bone: one of its joints, got ${fmt(a.person)} ${fmt(a.bone)}`);
|
|
32
34
|
if (a.kind === "landmark" && a.name !== "mouth") err(`${at}: the only landmark is "mouth", got ${fmt(a.name)}`);
|
|
33
35
|
if (a.kind === "prop" && !spec.props?.[a.prop]?.points?.[a.point] && a.point !== "origin") err(`${at} needs prop point ${a.prop}.${a.point}, which spec.props does not define`);
|
|
34
36
|
}
|
|
@@ -23,10 +23,10 @@ function checkValue(err, at, ch, v) {
|
|
|
23
23
|
if (!isVec3(v)) return err(`${at} ${ch} must be [x, y, z] (3 finite numbers), got ${fmt(v)}`);
|
|
24
24
|
if (POLE_CHANNELS.has(ch)) {
|
|
25
25
|
const l = Math.hypot(...v);
|
|
26
|
-
if (l < 1e-6) return err(`${at} ${ch} = ${fmt(v)} has no direction; a pole is ${CONVENTIONS.pole}`);
|
|
27
|
-
if (ch.startsWith("knee") && v[2] < -0.7 * l) err(`${at} ${ch} = ${fmt(v)} points backward; knees bend toward +z. A pole is ${CONVENTIONS.pole}`);
|
|
26
|
+
if (l < 1e-6) return err(`${at} ${ch} = ${fmt(v)} has no direction; a pole is a ${CONVENTIONS.pole}`);
|
|
27
|
+
if (ch.startsWith("knee") && v[2] < -0.7 * l) err(`${at} ${ch} = ${fmt(v)} points backward; knees bend toward +z. A pole is a ${CONVENTIONS.pole}`);
|
|
28
28
|
}
|
|
29
|
-
if (ch.startsWith("foot") && v[1] < 0.04) err(`${at} ${ch} y = ${v[1]} puts the ankle into the floor; foot_* is ${CONVENTIONS.foot}`);
|
|
29
|
+
if (ch.startsWith("foot") && v[1] < 0.04) err(`${at} ${ch} y = ${v[1]} puts the ankle into the floor; foot_* is the ${CONVENTIONS.foot}`);
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
if (MAP_CHANNELS.has(ch)) {
|
|
@@ -14,33 +14,34 @@ export const FINGER_PRESETS = ["relaxed", "fist", "grip", "cigarette", "point"];
|
|
|
14
14
|
|
|
15
15
|
// [min, max, convention] for numeric channels whose values have a hard range.
|
|
16
16
|
const RANGES = [
|
|
17
|
-
[/^(armw)_[lr]$/, 0, 1, "0
|
|
17
|
+
[/^(armw)_[lr]$/, 0, 1, "0 FK, 1 IK to the hand target"],
|
|
18
18
|
[/^arm_hang$/, 0, 1, "1 = a free FK forearm hangs plumb"],
|
|
19
|
-
[/^w_.+_[lr]$/, 0, 1, "
|
|
20
|
-
[/^wtwist_[lr]$/, -85, 85, "
|
|
21
|
-
[/^wflex_[lr]$/, -70, 70, "
|
|
22
|
-
[/^fpitch_[lr]$/, -60, 90, "toe-down pitch
|
|
23
|
-
|
|
24
|
-
[/^(
|
|
19
|
+
[/^w_.+_[lr]$/, 0, 1, "hand-target weight; a hand's sum <= 1"],
|
|
20
|
+
[/^wtwist_[lr]$/, -85, 85, "twist about the forearm, deg"],
|
|
21
|
+
[/^wflex_[lr]$/, -70, 70, "flex about the knuckle line, deg"],
|
|
22
|
+
[/^fpitch_[lr]$/, -60, 90, "toe-down pitch, deg: + lifts the heel (toes flat), - lifts the toes"],
|
|
23
|
+
// the whole-body pitch and roll have none: they turn in full (flips, cartwheels; pose.js tiltFrame)
|
|
24
|
+
[/^(spine_pitch|head_pitch)$/, -120, 120, "degrees, + bends forward"],
|
|
25
|
+
[/^(spine_roll|head_roll)$/, -90, 90, "degrees"],
|
|
25
26
|
];
|
|
26
27
|
export const rangeOf = ch => RANGES.find(([re]) => re.test(ch)) ?? null;
|
|
27
28
|
|
|
28
29
|
export const CONVENTIONS = {
|
|
29
30
|
yaw: "yaw 0 faces +Z and + turns toward +X",
|
|
30
|
-
fyaw: "foot heading
|
|
31
|
-
pole: "
|
|
32
|
-
foot: "
|
|
33
|
-
hand: "
|
|
34
|
-
root: "
|
|
31
|
+
fyaw: "foot heading, as yaw",
|
|
32
|
+
pole: "body-frame direction (root yaw): +z forward, +x its left, +y up",
|
|
33
|
+
foot: "ANKLE target, world m (standing 0.087 X Bot, 0.105 Y Bot)",
|
|
34
|
+
hand: "WRIST target, world m",
|
|
35
|
+
root: "hips joint, world m (standing 1.03 X Bot, 0.99 Y Bot)",
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
/** What each channel family means (a family drops the _l/_r side), for motion.describe "channels". */
|
|
38
39
|
export const CHANNEL_DOCS = {
|
|
39
|
-
root: CONVENTIONS.root, yaw: `
|
|
40
|
-
spine_yaw: "degrees", spine_pitch: "
|
|
41
|
-
arm_rest: "FK arms down
|
|
40
|
+
root: CONVENTIONS.root, yaw: `deg; ${CONVENTIONS.yaw}`, pitch: "degrees", roll: "degrees",
|
|
41
|
+
spine_yaw: "degrees", spine_pitch: "deg, + bends forward", spine_roll: "degrees", head_yaw: "degrees", head_pitch: "degrees, + looks down", head_roll: "degrees",
|
|
42
|
+
arm_rest: "FK arms down, deg, default 72", arm_hang: "1 = a free FK forearm hangs plumb (standIdle and walks key it)", foot: CONVENTIONS.foot, fyaw: CONVENTIONS.fyaw, fpitch: "toe-down pitch, degrees, + heel up",
|
|
42
43
|
knee: `knee pole, ${CONVENTIONS.pole}`, armw: "0 = FK arm, 1 = IK to hand_* / the anchors", hand: CONVENTIONS.hand, elbow: `elbow pole, ${CONVENTIONS.pole}`,
|
|
43
|
-
wtwist: "wrist twist, degrees", wflex: "wrist flex, degrees", aswing: "FK arm swing,
|
|
44
|
+
wtwist: "wrist twist, degrees", wflex: "wrist flex, degrees", aswing: "FK arm swing, deg, + forward", abend: "FK elbow bend, deg",
|
|
44
45
|
fingers: "{ preset: weight 0..1 }",
|
|
45
46
|
};
|
|
46
47
|
export const channelFamily = ch => ch.replace(/_[lr]$/, "");
|
package/src/motion3/describe.js
CHANGED
|
@@ -81,7 +81,7 @@ const SCHEMAS = {
|
|
|
81
81
|
probes: () => listOf({ ...objectOf(PROBE_FIELDS, PROBE_TYPES, "probes", ["name"]), ...oneOfRequired(["distance", "position"]) }, { maxItems: 16 }),
|
|
82
82
|
channels: () => ({ type: "object", description: "key.set: channel -> value",
|
|
83
83
|
properties: Object.fromEntries([...VEC_CHANNELS, ...NUM_CHANNELS, ...MAP_CHANNELS].filter(ch => !SKILL_CHANNELS.has(ch)).map(ch => [ch, channelSchema(ch)])),
|
|
84
|
-
patternProperties: { "^w_.+_[lr]$": { type: "number", minimum: 0, maximum: 1, description: rangeOf("w_a_l")[3] }, "_vis$": { type: "number", description: "shown
|
|
84
|
+
patternProperties: { "^w_.+_[lr]$": { type: "number", minimum: 0, maximum: 1, description: rangeOf("w_a_l")[3] }, "_vis$": { type: "number", description: "shown > 0.5" } },
|
|
85
85
|
additionalProperties: { type: "number", description: "spec.channels, an object's or a check's when" } }),
|
|
86
86
|
refs: () => ({ type: "string", oneOf: REF_KINDS.map(k => ({ pattern: `^${k}:`, description: typeOf(REF_DOCS, k, "refs") })) }),
|
|
87
87
|
fix: () => ({ oneOf: [{ const: false }, objectOf(FIX_KEYS, FIX_TYPES, "fix")] }),
|
|
@@ -103,6 +103,8 @@ function describeSkill(name) {
|
|
|
103
103
|
const MEANT = { topics: ["objects", "channels"], skills: ["reachGrip"] };
|
|
104
104
|
/** Topics no round-2 session needed for a first spec: essentials names their parts, the full topic has the fields. */
|
|
105
105
|
const SHALLOW = ["checks"];
|
|
106
|
+
/** Skills essentials leaves to topic skills: a flip or cartwheel is no first spec's need, and essentials has no room. */
|
|
107
|
+
const FIRST_SPEC_SKIP = ["flip"];
|
|
106
108
|
|
|
107
109
|
/** A shared shape whose fields are mostly a skill's args (walk, walkTo): its own fields and `...<skill> args`. */
|
|
108
110
|
function shapeLine(d) {
|
|
@@ -134,7 +136,8 @@ function essentials() {
|
|
|
134
136
|
}
|
|
135
137
|
// use.skill without the enum: the skills list below names them
|
|
136
138
|
if (shared.use) shared.use = objectOf(USE_FIELDS, USE_TYPES, "use", ["skill", "f0"]);
|
|
137
|
-
|
|
139
|
+
// a skill no first spec needs (FIRST_SPEC_SKIP) is in topic skills only
|
|
140
|
+
const skills = SKILL_NAMES.filter(n => !FIRST_SPEC_SKIP.includes(n)).map(n => {
|
|
138
141
|
const { args, required = [], needs, frames, adds } = SKILL_INFO[n] ?? bad(`describe: skill ${n} has no entry in skills/info.js`);
|
|
139
142
|
return { name: n, args: briefType({ type: "object", properties: args, required }, { means: MEANT.skills.includes(n) }), needs, frames, adds };
|
|
140
143
|
});
|
package/src/motion3/docs.js
CHANGED
|
@@ -6,7 +6,7 @@ export const MOTION_ACTION_HELP = {
|
|
|
6
6
|
+ "Send spec, or base (an answered specId) + optional patch (JSON Patch); base alone or patch: [] re-measures (new probes). "
|
|
7
7
|
+ "spec = { schema: \"cozyclay.motion-spec.v1\", fps: 24, frames (number or \"auto\"), ... } (parts: motion.describe); any number may be { range: [lo, hi] } (searched). "
|
|
8
8
|
+ "probes (at most 16, request level; spec.probes: measured, not stored): { name, distance: [ref, ref] | position: ref, frames, every? }. "
|
|
9
|
-
+ "Refs need a prefix (joint: prop: anchor: landmark: object:; goals[].moved: a bare id too) and take loose bone names (any case); spec.checks need exact ones. "
|
|
9
|
+
+ "Refs need a prefix (joint: prop: anchor: landmark: object: person:; goals[].moved: a bare id too) and take loose bone names (any case); spec.checks need exact ones. "
|
|
10
10
|
+ "repair: { rounds } (1-8, default 4) applies group patches while severity drops. "
|
|
11
11
|
+ "qa judges only what is broken; speed, acceleration, balance, knee bend are observations unless checks.limits names them. "
|
|
12
12
|
+ "Answers frames, installed, specId, qa { ok, total, goals, groups (GOAL first, then severity; cause, hint, patch?) }, observations, delta (with base), repair, probes, timeline, fixed, search, skills. output.more: motion.report pages the rest.",
|