cozyclay 3.1.1 → 3.1.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 +39 -0
- package/bin/agent/agent-routes.mjs +4 -4
- package/bin/agent/main-agent.mjs +21 -7
- package/bin/agent/main-prompt.mjs +7 -6
- package/bin/agent/script-tool.mjs +2 -1
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase.mjs +5 -4
- package/bin/agent/studio-task-routes.mjs +1 -1
- package/bin/agent/studio-tasks.mjs +116 -25
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/cozyclay.mjs +4 -2
- package/bin/live-endpoint.mjs +2 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/TaskBoard-GR8xZm5c.js +1 -0
- package/dist/assets/{app-BlwOpest.css → app-Bhe0TpGz.css} +1 -1
- package/dist/assets/app-C4QTcDcb.js +5144 -0
- package/dist/assets/compile-worker-oy_P9HHh.js +6 -0
- package/dist/assets/motion3-compile-dgGhrvPK.js +1 -0
- package/dist/assets/scene-asset-sync-BEjjhNEo.css +1 -0
- package/dist/assets/scene-asset-sync-qoxcdrhj.js +13 -0
- package/dist/assets/task-board-BqReHzOi.js +15 -0
- package/dist/assets/workflow-C87p6E1E.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +5 -3
- package/mcp/tool-handlers.mjs +5 -5
- package/package.json +1 -1
- package/src/App.jsx +42 -64
- package/src/bus-task-scopes.js +59 -11
- package/src/bus-task-targets.js +35 -9
- package/src/bus-tasks.js +23 -10
- package/src/command-bus.js +7 -4
- package/src/commands/ai.js +1 -1
- package/src/commands/export.js +1 -1
- package/src/commands/motion3.js +27 -7
- package/src/export-workspace.js +244 -0
- package/src/keyframe-pack.js +5 -1
- package/src/live-control.js +14 -7
- package/src/motion3/compile-job.js +15 -3
- package/src/motion3/compiler/channels.js +74 -24
- package/src/motion3/compiler/compile.js +2 -1
- package/src/motion3/compiler/idle.js +2 -0
- package/src/motion3/compiler/overlap.js +180 -0
- package/src/motion3/compiler/pose.js +6 -4
- package/src/motion3/compiler/validate-common.js +1 -0
- package/src/motion3/compiler/validate-parts.js +5 -1
- package/src/motion3/compiler/validate.js +30 -10
- package/src/motion3/correct.js +236 -0
- package/src/motion3/docs.js +8 -6
- package/src/motion3/face-lean.js +31 -0
- package/src/motion3/fix/index.js +5 -1
- package/src/motion3/fix/reach-body.js +172 -0
- package/src/motion3/hint-check.js +44 -0
- package/src/motion3/json-patch.js +120 -12
- package/src/motion3/lenient.js +46 -4
- package/src/motion3/object-keys.js +17 -5
- package/src/motion3/pipeline.js +46 -9
- package/src/motion3/qa/arm-checks.js +96 -0
- package/src/motion3/qa/asked.js +113 -0
- package/src/motion3/qa/goals.js +37 -8
- package/src/motion3/qa/hints-goal.js +6 -5
- package/src/motion3/qa/index.js +5 -2
- package/src/motion3/qa/key-checks.js +4 -3
- package/src/motion3/qa/motion-life.js +243 -0
- package/src/motion3/qa/spec-checks.js +15 -9
- package/src/motion3/qa/verdict.js +50 -0
- package/src/motion3/refs.js +38 -14
- package/src/motion3/repair-watch.js +132 -0
- package/src/motion3/repair.js +52 -19
- package/src/motion3/report.js +50 -9
- package/src/motion3/schema/topics.js +6 -4
- package/src/motion3/schema/types.js +1 -1
- package/src/motion3/skills/basic.js +15 -8
- package/src/motion3/skills/index.js +52 -18
- package/src/motion3/skills/info.js +1 -1
- package/src/motion3/skills/stance.js +6 -0
- package/src/motion3/skills/walk-fit.js +170 -0
- package/src/panels/PoseImagePanel.jsx +3 -2
- package/src/panels/SculptPanel.jsx +2 -2
- package/src/result-modal.jsx +3 -3
- package/src/shell/ExportFolderPreference.jsx +103 -0
- package/src/shell/PreferencesDialog.jsx +3 -166
- package/src/shell/StatusBar.jsx +1 -1
- package/src/shell/preference-controls.jsx +114 -0
- package/src/shell/preference-pages.jsx +72 -0
- package/src/shell/preferences.css +46 -0
- package/src/shot-prompt.js +29 -9
- package/src/studio-actions.js +2 -2
- package/src/studio-agent-motion.js +19 -9
- package/src/studio-agent-protocol.js +1 -1
- package/src/studio-app-binding.js +3 -2
- package/src/workflow/AgentPanel.jsx +2 -2
- package/src/workflow/CozySceneNode.jsx +1 -1
- package/src/workflow/WorkflowBuilder.jsx +2 -2
- package/src/workflow/keyframe-pack-request.js +3 -10
- package/src/workflow/task-board.css +1 -1
- package/src/workflow/task-board.js +7 -5
- package/tools/loc-baseline.json +1 -2
- package/tools/run-tests.mjs +5 -5
- package/dist/assets/TaskBoard-ngdqiu7u.js +0 -1
- package/dist/assets/app-DmR7jBkz.js +0 -5144
- package/dist/assets/compile-worker-D0pKioa0.js +0 -5
- package/dist/assets/motion3-compile-BB3_1-EB.js +0 -1
- package/dist/assets/scene-asset-sync-CBx7XRti.css +0 -1
- package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +0 -13
- package/dist/assets/task-board-ErQvfoaz.js +0 -14
- package/dist/assets/workflow-zQM-EKW1.js +0 -179
package/src/motion3/refs.js
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
// anchor:<name> a spec anchor, resolved as the compiler resolves it
|
|
6
6
|
// landmark:mouth the measured face landmark
|
|
7
7
|
// person:<id>.<Bone> another character's joint where its installed take has it (people.js)
|
|
8
|
-
// object:<id>[.center|.top|.base]
|
|
9
|
-
// top-face centre or bottom-face centre of the AABB of its
|
|
10
|
-
// part boxes' corners (default center)
|
|
8
|
+
// object:<id>[.center|.top|.base|.rim] a body or a group (object-pose.js objectBoxesAt): the centre,
|
|
9
|
+
// top-face centre or bottom-face centre of the world AABB of its
|
|
10
|
+
// part boxes' corners (default center); .rim is its opening (a cup's,
|
|
11
|
+
// a can's): the top face of its highest part in the part's own axes,
|
|
12
|
+
// so it tilts with it, and measured against another point the edge of
|
|
13
|
+
// that face nearest the point (where a lip meets a cup). A tilted cup's
|
|
14
|
+
// .top is the box's highest corner height over its middle, not its rim
|
|
11
15
|
// Frames are absolute; a frame outside the compiled window (out.f0 ..) answers null.
|
|
12
16
|
import { REF_KINDS } from "./schema/fields.js";
|
|
13
17
|
import { resolveBone } from "./bones.js";
|
|
@@ -20,14 +24,14 @@ import { add, apply, sub, len } from "./compiler/math.js";
|
|
|
20
24
|
import { sdBox } from "./qa/geometry.js";
|
|
21
25
|
import { personJoint, peopleList } from "./people.js";
|
|
22
26
|
|
|
23
|
-
const SYNTAX = { joint: "joint:<Bone>", bone: "bone:<Bone>", prop: "prop:<prop>.<point>", anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base]", person: "person:<id>.<Bone>" };
|
|
27
|
+
const SYNTAX = { joint: "joint:<Bone>", bone: "bone:<Bone>", prop: "prop:<prop>.<point>", anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base|.rim]", person: "person:<id>.<Bone>" };
|
|
24
28
|
|
|
25
29
|
function parse(ref) {
|
|
26
30
|
const m = /^([a-z]+):(.+)$/s.exec(String(ref));
|
|
27
31
|
if (!m) return null;
|
|
28
32
|
const [, kind, rest] = m;
|
|
29
33
|
if (kind === "prop") { const [name, point] = rest.split("."); return { kind, name, point: point ?? "" }; }
|
|
30
|
-
if (kind === "object") { const o = /^(.+)\.(center|top|base)$/s.exec(rest); return { kind, name: o ? o[1] : rest, point: o ? o[2] : null }; }
|
|
34
|
+
if (kind === "object") { const o = /^(.+)\.(center|top|base|rim)$/s.exec(rest); return { kind, name: o ? o[1] : rest, point: o ? o[2] : null }; }
|
|
31
35
|
if (kind === "person") { const dot = rest.lastIndexOf("."); return { kind, name: dot > 0 ? rest.slice(0, dot) : rest, point: dot > 0 ? rest.slice(dot + 1) : "" }; }
|
|
32
36
|
return { kind, name: rest, point: null };
|
|
33
37
|
}
|
|
@@ -42,6 +46,18 @@ function boxesPoint(boxes, which) {
|
|
|
42
46
|
return which === "top" ? [c[0], hi[1], c[2]] : which === "base" ? [c[0], lo[1], c[2]] : c;
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
/** The opening of a body or group's boxes: { o, n, r }, the top face of the highest part in its own axes (centre, axis, the smaller half width). */
|
|
50
|
+
export function openingOf(boxes) {
|
|
51
|
+
const tops = (boxes ?? []).map(b => ({ o: add(b.pos, apply(b.R, [0, 2 * b.half[1], 0])), n: [b.R[0][1], b.R[1][1], b.R[2][1]], r: Math.min(b.half[0], b.half[2]) }));
|
|
52
|
+
return tops.length ? tops.reduce((p, q) => (q.o[1] > p.o[1] ? q : p)) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The point of an opening's edge nearest p (its centre when p is on the axis). */
|
|
56
|
+
function rimNear(op, p) {
|
|
57
|
+
const v = sub(p, op.o), along = v[0] * op.n[0] + v[1] * op.n[1] + v[2] * op.n[2], across = sub(v, op.n.map(x => x * along)), d = len(across);
|
|
58
|
+
return d > 1e-6 ? add(op.o, across.map(x => (x * op.r) / d)) : op.o;
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
/** World point [x, y, z] of `ref` at absolute frame f of compile result out, or null. */
|
|
46
62
|
export function refPoint(out, f, ref) {
|
|
47
63
|
const fr = out.frames?.[f - (out.f0 ?? 0)], r = parse(ref);
|
|
@@ -52,7 +68,7 @@ export function refPoint(out, f, ref) {
|
|
|
52
68
|
case "prop": { const p = diag.props?.[r.name]?.[r.point]; return Array.isArray(p) ? p.slice() : null; }
|
|
53
69
|
case "anchor": return out.ctx.spec.anchors?.[r.name] ? resolveAnchor(out.ctx, r.name, s, { ...diag, frame: f }, c) : null;
|
|
54
70
|
case "landmark": return sk.landmarks?.[r.name] ? bonePoint(s, sk.index.Head, sk.landmarks[r.name]) : null;
|
|
55
|
-
case "object": { const boxes = objectBoxesAt(out, r.name, f); return boxes ? boxesPoint(boxes, r.point) : null; }
|
|
71
|
+
case "object": { const boxes = objectBoxesAt(out, r.name, f); return boxes ? (r.point === "rim" ? openingOf(boxes).o.slice() : boxesPoint(boxes, r.point)) : null; }
|
|
56
72
|
case "person": return personJoint(out.ctx.scene, r.name, r.point, f);
|
|
57
73
|
default: return null;
|
|
58
74
|
}
|
|
@@ -71,25 +87,33 @@ export function refDistance(out, f, a, b) {
|
|
|
71
87
|
const p = refPoint(out, f, surf[0]), boxes = objectBoxesAt(out, surf[1].name, f);
|
|
72
88
|
return p && boxes ? Math.max(0, Math.min(...boxes.map(box => sdBox(p, box)))) : null;
|
|
73
89
|
}
|
|
90
|
+
// an opening against another point: the edge of it nearest that point
|
|
91
|
+
const rim = [a, b].map(ref => { const r = parse(ref); return r?.kind === "object" && r.point === "rim" ? r : null; });
|
|
92
|
+
if (rim[0] || rim[1]) {
|
|
93
|
+
const [own, other] = rim[0] ? [rim[0], b] : [rim[1], a], p = refPoint(out, f, other), op = openingOf(objectBoxesAt(out, own.name, f));
|
|
94
|
+
return p && op ? len(sub(p, rimNear(op, p))) : null;
|
|
95
|
+
}
|
|
74
96
|
const p = refPoint(out, f, a), q = refPoint(out, f, b);
|
|
75
97
|
return p && q ? len(sub(p, q)) : null;
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
function bareHint(spec, { sk, scene }, ref, kinds) {
|
|
83
|
-
if (typeof ref !== "string" || !ref) return "";
|
|
84
|
-
const id = ref.replace(/\.(center|top|base)$/, ""), [prop, point] = ref.split(".");
|
|
100
|
+
/** The prefixed refs a bare name could mean in this spec, rig and scene (lenient.js takes the one when there is one). */
|
|
101
|
+
export function bareRefs(spec, { sk, scene }, ref, kinds = REF_KINDS) {
|
|
102
|
+
if (typeof ref !== "string" || !ref) return [];
|
|
103
|
+
const id = ref.replace(/\.(center|top|base|rim)$/, ""), [prop, point] = ref.split(".");
|
|
85
104
|
const bone = sk && (kinds.includes("joint") || kinds.includes("bone")) ? resolveBone(sk, ref).bone : null;
|
|
86
|
-
|
|
105
|
+
return [
|
|
87
106
|
kinds.includes("object") && (scene?.objects?.[id] || partsOf(scene, id).length) && `object:${ref}`,
|
|
88
107
|
bone && `${kinds.includes("joint") ? "joint" : "bone"}:${bone}`,
|
|
89
108
|
kinds.includes("anchor") && spec.anchors?.[ref] && `anchor:${ref}`,
|
|
90
109
|
kinds.includes("prop") && spec.props?.[prop] && (point === "origin" || spec.props[prop].points?.[point]) && `prop:${ref}`,
|
|
91
110
|
kinds.includes("landmark") && ref === "mouth" && "landmark:mouth",
|
|
92
111
|
].filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `; did you mean "object:empty-2"?` for a bare name (bareRefs), or "". A field that takes more than one ref kind stays strict. */
|
|
115
|
+
function bareHint(spec, ctx, ref, kinds) {
|
|
116
|
+
const known = bareRefs(spec, ctx, ref, kinds);
|
|
93
117
|
return known.length ? `; did you mean ${known.map(k => `"${k}"`).join(" or ")}?` : "";
|
|
94
118
|
}
|
|
95
119
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// The stop rule of the compile-repair loop (motion.compile): a short note in the answer when more
|
|
2
|
+
// compiles stopped paying. It never refuses a compile; the agent reads output.stuck and the prompt
|
|
3
|
+
// tells it to obey. The agent's own logs set the numbers: a compile is ~0.3 s but a repair step is
|
|
4
|
+
// ~1,250 output tokens of model time, a turn compiles median 8 / p90 17 times, a failure that survives
|
|
5
|
+
// two retries unchanged survives a third about a third of the time, and about a fifth of the
|
|
6
|
+
// compiles after a take first passes QA failed QA again (re-tweaking what already passed).
|
|
7
|
+
// polish an earlier compile of this character in this turn passed QA: say so, stop unless asked
|
|
8
|
+
// cap REPAIR_CAP compiles after the first: report what still fails on the best take
|
|
9
|
+
// stuck the same failing groups (or more) for STUCK_AFTER compiles in a row, neither fewer nor
|
|
10
|
+
// DELTA_STEP less severe: stop patching them (or re-applying their hints), start over,
|
|
11
|
+
// take another approach, or report what the best take still fails
|
|
12
|
+
// History is per character and agent turn (context.turnId); without a turn (MCP, tests) per character,
|
|
13
|
+
// with the cap counted along the base chain (a whole spec starts it again). In a turn the cap counts agent
|
|
14
|
+
// steps: the compiles of one tool call (context.callId; a script's nested ones share the script's) are one
|
|
15
|
+
// repair step, as a script that compares variants is one try; a compile with no call id is a step of its own.
|
|
16
|
+
// The 60 bench sessions: compiles counted one by one read 10-23 at the agent's 6-12th compiling call. The stuck streak
|
|
17
|
+
// counts the same steps: of the runs of 3 script compiles whose problems and issues did not drop (the SSE frames), 21 of
|
|
18
|
+
// 37 merged (48 of 71 base) lay inside one script, which compared variants; each step's last compile carries it on. A whole spec sent after a
|
|
19
|
+
// stuck note is the fresh start the note asked for, so the streak starts again too.
|
|
20
|
+
// A note never leaves the take empty: the agent obeys it by reporting, and a turn whose compiles all ran
|
|
21
|
+
// install: "ifClean" (or false) unclean ended with no take (5 of 30 bench runs). So the line keeps its best
|
|
22
|
+
// compile per placement (`at`, the last PLACES of them: a take plays in the character's placement frame,
|
|
23
|
+
// take.js, so it only meets the scene where it was baked) with its take, and a note on a compile that did
|
|
24
|
+
// not install itself answers `install`: the best take here, when the character carries no take or one the
|
|
25
|
+
// watch put on that this one beats. The caller installs it (no compile: the take is the one baked then).
|
|
26
|
+
// A take the agent installed (install: true, or an ifClean that installed) is never replaced for the rest
|
|
27
|
+
// of the line, nor one the character had before it (replay of merged r3 S8: the agent's own take was
|
|
28
|
+
// swapped for an earlier draft, so its report no longer named the character's take).
|
|
29
|
+
import { groupKey } from "./qa/severity.js";
|
|
30
|
+
|
|
31
|
+
const STUCK_AFTER = 3;
|
|
32
|
+
const REPAIR_CAP = 10;
|
|
33
|
+
const DELTA_STEP = 0.05;
|
|
34
|
+
const KEPT = 32;
|
|
35
|
+
// lines whose best takes are kept (the newest): a take is ~0.25 MB a 240-frame Y Bot take (rotations, 65 bones), so
|
|
36
|
+
// with PLACES each at most about 3 MB here; an older line keeps its notes but no take (its turn is over)
|
|
37
|
+
const TAKES = 4;
|
|
38
|
+
// placements per line whose best take is kept (S8 r1 moved the body 0.78 -> 0.88 -> 0.94 -> 0.88 m)
|
|
39
|
+
const PLACES = 3;
|
|
40
|
+
const SAY_CODES = 2;
|
|
41
|
+
|
|
42
|
+
const severity = groups => groups.reduce((s, g) => s + (Number.isFinite(g.severity) ? g.severity : 0), 0);
|
|
43
|
+
const superset = (a, b) => [...b].every(k => a.has(k));
|
|
44
|
+
const better = (a, b) => (a.ok !== b.ok ? a.ok : a.n !== b.n ? a.n < b.n : a.sev < b.sev);
|
|
45
|
+
const problems = n => `${n} problem${n === 1 ? "" : "s"}`;
|
|
46
|
+
const cut = s => (s.length > 40 ? `${s.slice(0, 39)}~` : s);
|
|
47
|
+
const named = c => `${c.specId} (${c.ok ? "QA passes" : problems(c.n)})`;
|
|
48
|
+
// a take plays in the character's placement frame: one the watch put on says so (a move misaligns it)
|
|
49
|
+
const POS = "bound to this position (recompile after a move)";
|
|
50
|
+
|
|
51
|
+
export function createRepairWatch() {
|
|
52
|
+
const lines = new Map();
|
|
53
|
+
const lineOf = key => {
|
|
54
|
+
let line = lines.get(key);
|
|
55
|
+
if (line) { lines.delete(key); lines.set(key, line); return line; }
|
|
56
|
+
// picks: the best compile per placement; held: the take the watch put on; mine: the agent's own last install
|
|
57
|
+
line = { compiles: 0, steps: 0, call: null, last: null, before: null, passed: null, best: null, picks: new Map(), held: null, mine: null, noted: false, depth: new Map() };
|
|
58
|
+
lines.set(key, line);
|
|
59
|
+
if (lines.size > KEPT) lines.delete(lines.keys().next().value);
|
|
60
|
+
return line;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* One compile's note. qa is the full QA (every group); base the specId it patched, absent for a whole
|
|
64
|
+
* spec; at the placement key of the compile (character pose and scene), take its baked take (kept while
|
|
65
|
+
* it is the best at `at`), installed whether this compile put its own take on the character, current the
|
|
66
|
+
* specId of the take the character carries now (after this compile).
|
|
67
|
+
* -> { say?, install?: { specId, take } } : install is the best take the caller puts on the character.
|
|
68
|
+
* callId the agent's tool call the compile ran in, or null.
|
|
69
|
+
* @param {{ characterId: string, turnId?: string | null, callId?: string | null, specId: string, base?: string, qa: { ok: boolean, groups: object[] },
|
|
70
|
+
* at?: string, take?: unknown, installed?: boolean, current?: string | null }} c
|
|
71
|
+
*/
|
|
72
|
+
function note({ characterId, turnId = null, callId = null, specId, base, qa, at = "", take, installed = false, current = null }) {
|
|
73
|
+
const line = lineOf(`${characterId}\u0000${turnId ?? ""}`), groups = qa.groups ?? [];
|
|
74
|
+
const now = { specId, at, ok: Boolean(qa.ok), fails: new Set(groups.map(groupKey)), n: groups.length, sev: severity(groups), streak: 1 };
|
|
75
|
+
// core: the failures the whole streak shares (its first compile's; the later ones hold them all)
|
|
76
|
+
now.core = now.fails;
|
|
77
|
+
// a step: one agent call (its compiles share callId) or one compile; the streak compares with the last compile of the
|
|
78
|
+
// step before, so the variants one script compares are one try, not three
|
|
79
|
+
const step = !callId || callId !== line.call;
|
|
80
|
+
if (step) line.before = line.last;
|
|
81
|
+
const prev = line.before, fresh = base === undefined && line.noted;
|
|
82
|
+
if (prev && !fresh && now.fails.size && prev.fails.size && superset(now.fails, prev.fails) && now.n >= prev.n && now.sev > prev.sev * (1 - DELTA_STEP))
|
|
83
|
+
Object.assign(now, { streak: prev.streak + 1, core: prev.core });
|
|
84
|
+
line.compiles += 1;
|
|
85
|
+
if (step) line.steps += 1;
|
|
86
|
+
line.call = callId;
|
|
87
|
+
const depth = base === undefined ? 0 : (line.depth.get(base) ?? 0) + 1;
|
|
88
|
+
line.depth.set(specId, depth);
|
|
89
|
+
const repairs = turnId ? line.steps - 1 : depth, passed = line.passed, best = line.best;
|
|
90
|
+
line.last = now;
|
|
91
|
+
if (now.ok && !line.passed) line.passed = specId;
|
|
92
|
+
if (!best || better(now, best)) line.best = now;
|
|
93
|
+
// the best take at this placement (each of the last PLACES keeps its own), and the agent's own install
|
|
94
|
+
let pick = line.picks.get(at);
|
|
95
|
+
if (!pick || better(now, pick)) {
|
|
96
|
+
pick = { ...now, take };
|
|
97
|
+
let older = lines.size - TAKES;
|
|
98
|
+
for (const other of lines.values()) if (older-- > 0) for (const p of other.picks.values()) p.take = undefined;
|
|
99
|
+
}
|
|
100
|
+
line.picks.delete(at);
|
|
101
|
+
line.picks.set(at, pick);
|
|
102
|
+
if (line.picks.size > PLACES) line.picks.delete(line.picks.keys().next().value);
|
|
103
|
+
if (installed) line.mine = now;
|
|
104
|
+
const past = c => (best && best.specId !== c.specId && better(best, c) ? `; best so far ${named(best)}` : "");
|
|
105
|
+
const kind = passed && passed !== specId ? "polish" : repairs >= REPAIR_CAP && !now.ok ? "cap" : now.streak >= STUCK_AFTER ? "stuck" : null;
|
|
106
|
+
line.noted = Boolean(kind) && !passed;
|
|
107
|
+
if (!kind) return {};
|
|
108
|
+
// the take to leave on the character: this compile's, or the agent's own, when it carries it; else fill an empty
|
|
109
|
+
// take, or upgrade the one the watch put on, with the best here; a take from before this line stays
|
|
110
|
+
const held = line.held, mine = line.mine && line.mine.specId === current, watched = held && held.specId === current;
|
|
111
|
+
const keeps = !installed && !mine && watched && held.at === at && !better(pick, held);
|
|
112
|
+
const fills = !installed && !mine && !keeps && (current === null || watched) && pick.take !== undefined;
|
|
113
|
+
const install = fills ? { specId: pick.specId, take: pick.take } : undefined;
|
|
114
|
+
if (install) line.held = pick;
|
|
115
|
+
// what the character carries after the note, in one sentence; then what to do (a cap note at most ~180 characters)
|
|
116
|
+
const status = installed ? `This take is installed${past(now)}.` : install ? `Installed the best take here, ${named(pick)}, ${POS}.`
|
|
117
|
+
: mine ? `Your installed take ${named(line.mine)} stays${past(line.mine)}.` : keeps ? `${named(held)} stays installed, ${POS}.`
|
|
118
|
+
: current !== null && pick.take !== undefined ? `The earlier take stays; to replace it, install the best take here, ${named(pick)}.`
|
|
119
|
+
: `Install the best take here, ${named(pick)}.`;
|
|
120
|
+
const asked = "stop unless the user asked for a specific change";
|
|
121
|
+
let say;
|
|
122
|
+
if (kind === "polish") say = !install ? `${passed} already passed QA: ${asked}.` : install.specId === passed ? `${passed} passed QA and is now installed, ${POS}: ${asked}.`
|
|
123
|
+
: `${passed} passed QA at another position; the best take here, ${named(pick)}, is installed, ${POS}. Report its problems; ${asked}.`;
|
|
124
|
+
else if (kind === "cap") say = `${repairs} repair ${turnId && callId ? "calls" : "compiles"}. ${status} Report its problems; stop unless asked for more.`;
|
|
125
|
+
else {
|
|
126
|
+
const codes = [...now.core], names = codes.slice(0, SAY_CODES).map(cut).join(", ") + (codes.length > SAY_CODES ? ` +${codes.length - SAY_CODES}` : "");
|
|
127
|
+
say = `${names} unchanged over ${now.streak} ${turnId && callId ? "calls" : "compiles"}. Stop patching it or re-applying its hint: start afresh or try another approach. ${status} If neither helps, report its problems.`;
|
|
128
|
+
}
|
|
129
|
+
return { say, ...(install ? { install } : {}) };
|
|
130
|
+
}
|
|
131
|
+
return { note };
|
|
132
|
+
}
|
package/src/motion3/repair.js
CHANGED
|
@@ -3,22 +3,37 @@
|
|
|
3
3
|
// the patch the latest compile gives that group. When none is kept, the top 2 groups just past
|
|
4
4
|
// their limit with no patch, or a missed goal, get a small local search (repair-tune.js: the
|
|
5
5
|
// responsible skill's args, the key pair of a speed spike, or for a goal where the body stands
|
|
6
|
-
// and how it leans); the best candidate is the round's change. A change
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// the
|
|
6
|
+
// and how it leans); the best candidate is the round's change. A change is kept only when
|
|
7
|
+
// qa/verdict.js compareQa calls it better (the summed severity drops, no goal that held fails, no
|
|
8
|
+
// more problems, and what got worse costs at most half of what got better), else it is undone, so
|
|
9
|
+
// the result is never worse than the spec as sent. A failure local to an arm (its reach, wrist,
|
|
10
|
+
// elbow, a hand or forearm into something) is tried without moving the body first: a patch or a
|
|
11
|
+
// candidate that moves the root (a stance, a walk's length, the root keys) comes only when nothing
|
|
12
|
+
// else is kept, and the applied entry says so (movedBody). A patch tried and not kept is not tried
|
|
13
|
+
// again, so the groups after it get their turn. A round that keeps nothing ends the repair. The
|
|
14
|
+
// compiler is deterministic and the candidates come in a fixed order, so the repair is too; no new
|
|
15
|
+
// compile starts after 20 s.
|
|
10
16
|
import { applyPatch } from "./json-patch.js";
|
|
11
17
|
import { groupKey } from "./qa/severity.js";
|
|
12
18
|
import { tuneCandidates } from "./repair-tune.js";
|
|
19
|
+
import { compareQa } from "./qa/verdict.js";
|
|
13
20
|
|
|
14
21
|
const TRIES = 3, TUNE_GROUPS = 2, LIMIT_MS = 20000;
|
|
15
22
|
const groupsOf = r => r?.qa?.groups ?? [];
|
|
16
23
|
const totalOf = r => groupsOf(r).reduce((sum, g) => sum + (g.severity ?? 0), 0);
|
|
17
24
|
const failedOf = r => (r?.qa?.goals ?? []).filter(g => !g.ok).length;
|
|
18
25
|
const groupIn = (r, key) => groupsOf(r).find(g => groupKey(g) === key);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
/** qa/verdict.js: lower summed severity, no goal lost, no more problems, no trade of one failure for worse ones. */
|
|
27
|
+
const better = (r, than) => compareQa(r?.qa, than?.qa).better;
|
|
28
|
+
|
|
29
|
+
const ARM_PART = /^(hand|farm|uarm)_[lr]\b|^(Left|Right)(Arm|ForeArm|Hand)/;
|
|
30
|
+
/** A failure of one arm: its reach, wrist or elbow, or a hand, forearm or upper arm into something. */
|
|
31
|
+
const armLocal = g => (g.kind === "REACH" && /^arm_miss_[lr]$/.test(g.subject)) || ["WRIST", "ELBOW"].includes(g.kind)
|
|
32
|
+
|| (["BODY", "FACE", "COLLIDE", "POP", "ACC", "SPEED", "FLIP"].includes(g.kind) && ARM_PART.test(g.subject));
|
|
33
|
+
/** A patch that moves where the body stands: a stance, a walk's length or end, a root key. */
|
|
34
|
+
const movesBody = patch => patch.some(o => /^\/use\/\d+\/args(\/(at|step|firstStep|to))?$/.test(o.path) && (o.path.endsWith("/args") ? ["at", "step", "firstStep", "to"].some(k => o.value?.[k] !== undefined) : true)
|
|
35
|
+
|| /^\/keys\/\d+\/set\/root$/.test(o.path) || (o.path === "/keys/-" && o.value?.set?.root !== undefined));
|
|
36
|
+
const MOVED = "the body moved: no change that keeps it in place fixed this arm's failure";
|
|
22
37
|
|
|
23
38
|
/**
|
|
24
39
|
* repairSpec(spec, { rounds }, compile) -> { spec, result, repair }
|
|
@@ -26,12 +41,13 @@ const better = (r, than) => totalOf(r) < totalOf(than) - 1e-9 && failedOf(r) <=
|
|
|
26
41
|
* @param {{ rounds?: number }} opts 1..8 (default 4)
|
|
27
42
|
* @param {(spec: object) => object} compile spec -> pipeline.js compileSpec result (qa.groups[i].patch)
|
|
28
43
|
* @returns {{ spec: object, result: object, repair: { rounds: number, compiles: number,
|
|
29
|
-
* applied: { kind: string, subject: string, patch: object[], severity: number[] }[], rejected: number } }}
|
|
44
|
+
* applied: { kind: string, subject: string, patch: object[], severity: number[], movedBody?: string }[], rejected: number } }}
|
|
30
45
|
*/
|
|
31
46
|
export function repairSpec(spec, opts, compile) {
|
|
32
47
|
const want = Math.min(8, Math.max(1, Math.round(Number(opts?.rounds ?? 4)) || 4)), t0 = performance.now();
|
|
33
48
|
const late = () => performance.now() - t0 >= LIMIT_MS;
|
|
34
49
|
let current = spec, result = compile(spec), compiles = 1, rejected = 0, rounds = 0;
|
|
50
|
+
const refused = new Set();
|
|
35
51
|
const applied = [];
|
|
36
52
|
/** Compiles current + patch; null (counted as rejected) when the patch or the spec fails. */
|
|
37
53
|
const attempt = patch => {
|
|
@@ -45,38 +61,55 @@ export function repairSpec(spec, opts, compile) {
|
|
|
45
61
|
}
|
|
46
62
|
};
|
|
47
63
|
const keep = (g, key, patch, { next, r }) => {
|
|
48
|
-
|
|
64
|
+
const moved = armLocal(g) && movesBody(patch);
|
|
65
|
+
applied.push({ kind: g.kind, subject: g.subject, patch, severity: [g.severity, groupIn(r, key)?.severity ?? 0], ...(moved ? { movedBody: MOVED } : {}) });
|
|
49
66
|
current = next; result = r;
|
|
50
67
|
};
|
|
51
|
-
/** The best
|
|
52
|
-
const
|
|
53
|
-
let
|
|
54
|
-
for (const patch of
|
|
68
|
+
/** The best of the patches for group g; true when one is kept. */
|
|
69
|
+
const best = (g, patches) => {
|
|
70
|
+
let top = null;
|
|
71
|
+
for (const patch of patches) {
|
|
55
72
|
if (late()) break;
|
|
56
73
|
const t = attempt(patch);
|
|
57
74
|
if (!t) continue;
|
|
58
|
-
if (better(t.r,
|
|
59
|
-
if (
|
|
75
|
+
if (better(t.r, top?.r ?? result)) { if (top) rejected += 1; top = { ...t, patch }; } else rejected += 1;
|
|
76
|
+
if (top && totalOf(top.r) === 0 && !failedOf(top.r)) break;
|
|
60
77
|
}
|
|
61
|
-
if (
|
|
62
|
-
return Boolean(
|
|
78
|
+
if (top) keep(g, groupKey(g), top.patch, top);
|
|
79
|
+
return Boolean(top);
|
|
80
|
+
};
|
|
81
|
+
/** The tune candidates for group g (repair-tune.js); an arm's failure tries those that keep the body in place first. */
|
|
82
|
+
const tune = g => {
|
|
83
|
+
const all = tuneCandidates(g, current);
|
|
84
|
+
if (!armLocal(g)) return best(g, all);
|
|
85
|
+
return best(g, all.filter(p => !movesBody(p))) || best(g, all.filter(movesBody));
|
|
63
86
|
};
|
|
64
87
|
while (rounds < want && !late()) {
|
|
65
|
-
|
|
88
|
+
// a patch already tried and not kept is not tried again: the groups after it get their turn
|
|
89
|
+
const keys = groupsOf(result).filter(g => g.patch && !refused.has(JSON.stringify(g.patch))).slice(0, TRIES).map(groupKey);
|
|
66
90
|
const near = groupsOf(result).filter(g => tuneCandidates(g, current).length).slice(0, TUNE_GROUPS).map(groupKey);
|
|
67
91
|
if (!keys.length && !near.length) break;
|
|
68
92
|
rounds += 1;
|
|
69
93
|
let kept = 0;
|
|
94
|
+
const deferred = [];
|
|
70
95
|
for (const key of keys) {
|
|
71
96
|
const g = groupIn(result, key);
|
|
72
97
|
if (!g?.patch || late()) continue;
|
|
98
|
+
// an arm's failure whose patch moves the body waits until nothing else is kept this round
|
|
99
|
+
if (armLocal(g) && movesBody(g.patch)) { deferred.push(key); continue; }
|
|
73
100
|
const t = attempt(g.patch);
|
|
74
|
-
if (t && better(t.r, result)) { keep(g, key, g.patch, t); kept += 1; } else if (t) rejected += 1;
|
|
101
|
+
if (t && better(t.r, result)) { keep(g, key, g.patch, t); kept += 1; } else { refused.add(JSON.stringify(g.patch)); if (t) rejected += 1; }
|
|
75
102
|
}
|
|
76
103
|
for (const key of kept ? [] : near) {
|
|
77
104
|
const g = groupIn(result, key);
|
|
78
105
|
if (g && !late() && tune(g)) { kept += 1; break; }
|
|
79
106
|
}
|
|
107
|
+
for (const key of kept ? [] : deferred) {
|
|
108
|
+
const g = groupIn(result, key);
|
|
109
|
+
if (!g?.patch || late()) continue;
|
|
110
|
+
const t = attempt(g.patch);
|
|
111
|
+
if (t && better(t.r, result)) { keep(g, key, g.patch, t); kept += 1; break; } else { refused.add(JSON.stringify(g.patch)); if (t) rejected += 1; }
|
|
112
|
+
}
|
|
80
113
|
if (!kept) break;
|
|
81
114
|
}
|
|
82
115
|
return { spec: current, result, repair: { rounds, compiles, applied, rejected } };
|
package/src/motion3/report.js
CHANGED
|
@@ -2,30 +2,55 @@
|
|
|
2
2
|
// head and readback take about 1.5 KB), so fitOutput keeps the most useful parts within
|
|
3
3
|
// MOTION_OUTPUT_BUDGET bytes of JSON and says what it cut in `more`; the cut parts stay in the
|
|
4
4
|
// spec store (spec-store.js) and motion.report pages through them. Keep order (first = last cut):
|
|
5
|
-
// 1. frames, fps, installed, specId, qa.ok/total/goals, observations, delta, repair (always;
|
|
5
|
+
// 1. brief, frames, fps, installed, specId, qa.ok/total/goals, observations, delta, repair (always;
|
|
6
6
|
// observations are bounded: a fixed set of joints and limbs, about 1 KB)
|
|
7
7
|
// 2. the first MIN_FULL qa.groups in full
|
|
8
8
|
// 3. probes
|
|
9
9
|
// 4. timeline (the planted spans go first, then entries from the end)
|
|
10
10
|
// 5. the other groups as one-liners { kind, subject, count, severity, first, last }
|
|
11
|
-
// 6.
|
|
12
|
-
//
|
|
11
|
+
// 6. qa.warnings (qa/arm-checks.js: never failing, so after every group), as many lines as fit
|
|
12
|
+
// 7. fixed, search, skills, ms
|
|
13
|
+
// An answer that does not fit and has a delta (base + patch) says observations.units only by reference: the
|
|
14
|
+
// base's answer gave them. Last resort: observations.asked and the brief lose lines, then full groups become one-liners.
|
|
15
|
+
// Room left after that turns one-liners back into full groups, from the top. A head still too big (a
|
|
16
|
+
// repair that applied many patches) keeps the newest repair.applied entries; more.repair counts the rest.
|
|
17
|
+
// brief (briefOf) is the answer in a few fixed-shape lines, so a script comparing variants returns it as
|
|
18
|
+
// it is instead of mapping qa itself:
|
|
19
|
+
// "<specId> qa ok|<problems> problems (<total> issues)[, installed|not installed]"
|
|
20
|
+
// "goals <n ok>/<n>[; MISS <name> <value> f<at> (<= or >= need)]...[; <n> more]" (when the spec has goals; 3 misses)
|
|
21
|
+
// "<KIND> <subject> x<count> f<first>-<last>" (the first 2 groups past the GOAL ones, which MISS names; subject cut to 48)
|
|
13
22
|
|
|
14
23
|
export const MOTION_OUTPUT_BUDGET = 6000;
|
|
15
|
-
const MIN_FULL = 4;
|
|
24
|
+
const MIN_FULL = 4, BRIEF_GROUPS = 2, BRIEF_MISSES = 3, BRIEF_SUBJECT = 48;
|
|
25
|
+
const AT_MOST = new Set(["near", "still"]), UNITS_SEEN = "as in the base's answer";
|
|
16
26
|
const size = v => new TextEncoder().encode(JSON.stringify(v)).length;
|
|
17
27
|
const oneLine = g => ({ kind: g.kind, subject: g.subject, count: g.count, severity: g.severity, first: g.first, last: g.last });
|
|
18
28
|
|
|
29
|
+
/** The fixed-shape summary lines of an answer with qa (see the head comment), or null without qa. */
|
|
30
|
+
export function briefOf({ specId, installed, qa }) {
|
|
31
|
+
if (!qa) return null;
|
|
32
|
+
const head = `${specId ?? "spec"} qa ${qa.ok ? "ok" : `${qa.problems ?? qa.groups?.length ?? 0} problems (${qa.total} issues)`}${installed === undefined ? "" : installed ? ", installed" : ", not installed"}`;
|
|
33
|
+
const goals = qa.goals ?? [], failed = goals.filter(g => !g.ok), miss = failed.slice(0, BRIEF_MISSES)
|
|
34
|
+
.map(g => `; MISS ${g.name} ${g.value ?? "never"}${g.at === null ? "" : ` f${g.at}`} (${AT_MOST.has(g.kind) ? "<=" : ">="} ${g.need})`).join("");
|
|
35
|
+
const more = failed.length > BRIEF_MISSES ? `; ${failed.length - BRIEF_MISSES} more` : "";
|
|
36
|
+
const groups = (qa.groups ?? []).filter(g => g.kind !== "GOAL").slice(0, BRIEF_GROUPS).map(g => `${g.kind} ${g.subject.slice(0, BRIEF_SUBJECT)} x${g.count} f${g.first}-${g.last}`);
|
|
37
|
+
return [head, ...(goals.length ? [`goals ${goals.length - failed.length}/${goals.length}${miss}${more}`] : []), ...groups];
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
/**
|
|
20
|
-
* full output -> output within `budget` bytes of JSON. When something was cut it carries
|
|
41
|
+
* full output -> output within `budget` bytes of JSON, with brief (briefOf) first. When something was cut it carries
|
|
21
42
|
* more: { specId, groups, timeline, probes }, how many items of each part are missing or
|
|
22
43
|
* shortened; motion.report { specId, part, from } pages the whole part.
|
|
23
44
|
*/
|
|
24
|
-
export function fitOutput(
|
|
45
|
+
export function fitOutput(answer, budget = MOTION_OUTPUT_BUDGET) {
|
|
46
|
+
const brief = briefOf(answer), full = brief ? { brief, ...answer } : answer;
|
|
25
47
|
if (size(full) <= budget) return full;
|
|
26
|
-
const { qa, observations, probes, timeline, fixed, search, skills, ms, ...head } = full;
|
|
48
|
+
const { qa, observations: all, probes, timeline, fixed, search, skills, ms, ...head } = full;
|
|
49
|
+
// an answer to base + patch leaves out the units the base's answer gave
|
|
50
|
+
const observations = all?.units && full.delta ? { ...all, units: UNITS_SEEN } : all;
|
|
27
51
|
const groups = qa?.groups ?? [], more = { specId: full.specId, groups: 0, timeline: 0, probes: 0 };
|
|
28
|
-
const
|
|
52
|
+
const { warnings, ...qaHead } = qa ?? {};
|
|
53
|
+
const out = { ...head, ...(qa ? { qa: { ...qaHead, groups: [] } } : {}), ...(observations ? { observations } : {}) };
|
|
29
54
|
// the counts in `more` may still grow by a digit or two after a check
|
|
30
55
|
const fits = extra => size({ ...out, ...extra, more }) <= budget - 32;
|
|
31
56
|
const nFull = Math.min(MIN_FULL, groups.length);
|
|
@@ -43,10 +68,26 @@ export function fitOutput(full, budget = MOTION_OUTPUT_BUDGET) {
|
|
|
43
68
|
more.timeline = list === lean ? timeline.length : timeline.length - n;
|
|
44
69
|
}
|
|
45
70
|
for (let i = nFull; i < groups.length && fits({ qa: { ...out.qa, groups: [...out.qa.groups, oneLine(groups[i])] } }); i++) out.qa.groups.push(oneLine(groups[i]));
|
|
71
|
+
for (let n = warnings?.length ?? 0; n > 0; n--) if (fits({ qa: { ...out.qa, warnings: warnings.slice(0, n) } })) { out.qa.warnings = warnings.slice(0, n); break; }
|
|
46
72
|
for (const [k, v] of [["fixed", fixed], ["search", search], ["skills", skills], ["ms", ms]]) if (v !== undefined && fits({ [k]: v })) out[k] = v;
|
|
47
73
|
for (let i = nFull; i < out.qa?.groups.length && fits({ qa: { ...out.qa, groups: out.qa.groups.map((g, j) => (j === i ? groups[i] : g)) } }); i++) out.qa.groups[i] = groups[i];
|
|
48
|
-
// last resort (huge hints):
|
|
74
|
+
// last resort (huge hints): observations.asked loses its goal and contact lines from the end, the brief its group
|
|
75
|
+
// lines, then full groups become one-liners from the end, then asked loses its skill lines
|
|
76
|
+
const asked = out.observations?.asked, cut = keep => { while (out.observations.asked.length > keep && !fits({})) out.observations.asked.pop(); };
|
|
77
|
+
if (asked) { out.observations = { ...out.observations, asked: [...asked] }; cut(asked.filter(l => l.startsWith("use[")).length); }
|
|
78
|
+
// the brief's group lines repeat qa.groups
|
|
79
|
+
while (out.brief?.length > 1 && !out.brief[out.brief.length - 1].startsWith("goals ") && !fits({})) out.brief = out.brief.slice(0, -1);
|
|
49
80
|
for (let i = nFull - 1; i >= 0 && !fits({}); i--) out.qa.groups[i] = oneLine(groups[i]);
|
|
81
|
+
// a long repair (each applied patch in full) keeps its newest changes; the stored spec holds them all
|
|
82
|
+
const applied = out.repair?.applied;
|
|
83
|
+
if (Array.isArray(applied) && !fits({})) {
|
|
84
|
+
let n = applied.length;
|
|
85
|
+
while (n > 0 && !fits({ repair: { ...out.repair, applied: applied.slice(applied.length - n) } })) n--;
|
|
86
|
+
out.repair = { ...out.repair, applied: applied.slice(applied.length - n) };
|
|
87
|
+
more.repair = applied.length - n;
|
|
88
|
+
}
|
|
89
|
+
if (asked) cut(0);
|
|
90
|
+
if (asked && !out.observations.asked.length) delete out.observations.asked;
|
|
50
91
|
more.groups = groups.length - (out.qa?.groups ?? []).filter(g => "worst" in g).length;
|
|
51
92
|
return { ...out, more };
|
|
52
93
|
}
|
|
@@ -42,7 +42,7 @@ export const EXAMPLES = {
|
|
|
42
42
|
{ f: 30, set: { lid: 1 }, step: ["lid"] }] },
|
|
43
43
|
refs: { props: { cupGrip: GRIP }, anchors: { spot: SPOT }, goals: [
|
|
44
44
|
{ name: "fingers at the spot", near: ["prop:cupGrip.fingers", "anchor:spot"], at: 30 },
|
|
45
|
-
{ name: "cup at the mouth", near: ["object:cup.
|
|
45
|
+
{ name: "cup at the mouth", near: ["object:cup.rim", "landmark:mouth"], tol: 0.06, at: [40, 50] },
|
|
46
46
|
{ name: "hand above the hips", above: ["joint:RightHand", "bone:Hips"], at: 30 }] },
|
|
47
47
|
fix: { fix: { reach: true, slide: true, penetration: 0.02, self: true, retime: false } },
|
|
48
48
|
};
|
|
@@ -99,17 +99,19 @@ export const NOTES = {
|
|
|
99
99
|
"at is an absolute frame or [f0, f1]; mode all (every frame; the default) or any (one frame; speed's default). A failed goal is a GOAL group first in qa.groups and makes qa.ok false.",
|
|
100
100
|
"after / before: another goal's name; at is cut to the frames after (before) the first frame that goal holds, so cause and effect keep their order wherever the event lands (the can moves after the kick). If that goal never holds, this one fails with value null.",
|
|
101
101
|
"Intent lines as goals: a contact is near between the touching point (joint:RightHandIndex4, joint:RightToeBase) and the object, tol 0.02-0.03; a negative is still on the object before: \"<contact goal>\"; an effect is moved on the object after: \"<contact goal>\" (mode any); a strike is speed of the joint, by m/s, before: \"<contact goal>\"; an effect on something nobody holds (a can that drops, a machine that rocks) moves it by spec.objects keys or fall (topic: \"objects\"), not a hinge or a swapped copy.",
|
|
102
|
-
"A cup or can brought to the mouth:
|
|
102
|
+
"A cup or can brought to the mouth: an aim key with its top at the lips leans 45 by default so the elbow stays low (lean 0 holds aim exactly; observations.wrist raised counts the frames with the elbow over the shoulder). qa.warnings (ARM, never failing) name an elbow over the shoulder, over the head or flared out, its frames and the key or elbow_<side> pole that fixes it."],
|
|
103
103
|
probes: ["motion.compile { probes } (at most 16); spec.probes are measured too, not stored; answered whether or not it installs.",
|
|
104
104
|
"distance [a, b] -> { min, minAt, max, maxAt, mean, first, last } in m; a bare object:<id> measures to its surface. position -> { at: [{ f, p }] }, at most 16 rows."],
|
|
105
105
|
channels: ["key.set names channels; values ease between keys and hold after the last key. step: [channel] jumps at f instead.",
|
|
106
|
-
"Flow (spec.flow, on by default): the spine, head, arm swing/bend and wrist carry their speed through a key (a small overshoot) and trail the body (spine 1, head 2, arms 3 frames; lag: { spine, head, arms } or a number); feet, root, hand targets, weights, poles, fingers and objects land on their keys on time. flow: false turns it off; { smooth: false } or {
|
|
106
|
+
"Flow (spec.flow, on by default): the spine, head, arm swing/bend and wrist carry their speed through a key (a small overshoot) and trail the body (spine 1, head 2, arms 3 frames; lag: { spine, head, arms } or a number); and each change breaks along the body (overlap: the lower spine, chest, head, upper arm, forearm and wrist start and settle a few frames apart, one side trailing the other, the arm's swing settling past its stop; less for a short move, none on an arm's IK or a walk); feet, root, hand targets, weights, poles, fingers and objects land on their keys on time. flow: false turns it off; { smooth: false }, { lag: false } or { overlap: false } one part (overlap: a number 0..2 scales it).",
|
|
107
107
|
"w_<anchor>_<side> (0..1) weights a hand onto an anchor; the weights of one hand add up to at most 1.",
|
|
108
108
|
"Any other numeric channel must come from spec.channels, a hinge (objects.<id>.channel), a check's when or a visible switch; names ending _vis are visibility switches (shown above 0.5)."],
|
|
109
|
-
refs: ["A ref names a point: joint:/bone:<Bone>, prop:<prop>.<point>, anchor:<name>, landmark:mouth, object:<id>[.center|.top|.base], person:<character id>.<Bone> (another character's joint in its installed take).",
|
|
109
|
+
refs: ["A ref names a point: joint:/bone:<Bone>, prop:<prop>.<point>, anchor:<name>, landmark:mouth, object:<id>[.center|.top|.base|.rim], person:<character id>.<Bone> (another character's joint in its installed take).",
|
|
110
|
+
"object .center/.top/.base are points of its world bounding box (a tilted cup's .top is not its rim); .rim is its opening (the top face in its own axes, tilting with it), against another point the nearest edge: drink goals take .rim.",
|
|
110
111
|
"object:<id> takes a body or a group (the box around its parts); in a distance a bare object:<id> means its surface.",
|
|
111
112
|
"Goals and probes take every kind; spec.checks keep joint:, prop: and anchor:."],
|
|
112
113
|
fix: [`spec.fix keys: ${FIX_KEYS.join(", ")}; false turns all off. output.fixed reports what each one changed.`,
|
|
113
114
|
"reach, slide, self and stance are on by default; penetration and retime are opt-in.",
|
|
115
|
+
"reach also brings the body to a hand on a world or scene-object anchor the straight arm misses: a lean, a hip shift, lower hips, the feet kept (fixed.reachBody lists each move); farther, place the body. retime is kept only when QA is no worse with it.",
|
|
114
116
|
"self keeps the arms out of the body on its fitted skin (QA BODY measures the same skin): wrist targets inside it go onto it, elbows turn about the shoulder-wrist line, a hand a little inside is pushed out. A fit's target (the mouth) is never moved; what is left is BODY, with checks.allow { part, object: <body part>, depth } for a contact meant to touch (a hand on a knee)."],
|
|
115
117
|
};
|
|
@@ -148,6 +148,6 @@ export const FIX_TYPES = {
|
|
|
148
148
|
|
|
149
149
|
export const REF_DOCS = {
|
|
150
150
|
joint: "joint:<Bone>", bone: "bone:<Bone> (= joint:)", prop: "prop:<prop>.<point> (origin exists)",
|
|
151
|
-
anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base], a body or group; bare = its surface (distance)",
|
|
151
|
+
anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base|.rim], a body or group; bare = its surface (distance)",
|
|
152
152
|
person: "person:<id>.<Bone>",
|
|
153
153
|
};
|
|
@@ -4,6 +4,7 @@ import { add, apply, ry } from "../compiler/math.js";
|
|
|
4
4
|
import { walk } from "../compiler/walk.js";
|
|
5
5
|
import { keyBook, skillError } from "./book.js";
|
|
6
6
|
import { walkEnd } from "./info.js";
|
|
7
|
+
import { fitTo, walkEndAt, r2, xzText, REACH_TOL } from "./walk-fit.js";
|
|
7
8
|
import { GRASP_FINGERS, GRASP_POINTS } from "../compiler/vocab.js";
|
|
8
9
|
import { clearStance, reachStance } from "./stance.js";
|
|
9
10
|
import { CLOSE, GAZE, gazeAt, headKeyed, earlyTransport, holdsOpen, transportKeys } from "./reach-human.js";
|
|
@@ -40,30 +41,36 @@ export function standIdle(ctx) {
|
|
|
40
41
|
/**
|
|
41
42
|
* walkTo({ to: [x, z] } | { heading: h | [h0, h1] }, steps, step = 0.5, turnDistance, ...): the
|
|
42
43
|
* walk generator from the pose at f0. With `to` the heading turns toward the point and the step
|
|
43
|
-
* count covers the distance
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* count covers the distance; with steps as well the stride is fitted so they end there (fitTo). Ends
|
|
45
|
+
* 16 + steps * period + 18 frames after f0, feet together; a take that ends after the last step holds
|
|
46
|
+
* that pose to its end. A walk with `to` that ends more than 0.1 m from it says where in `placed`. The
|
|
47
|
+
* steps keep a natural rhythm (compiler/gait-rhythm.js); vary: 0 makes every step the same.
|
|
46
48
|
*/
|
|
47
49
|
export function walkTo(ctx) {
|
|
48
50
|
const { args, state } = ctx;
|
|
49
51
|
if (!state.foot_l || !state.foot_r || !state.root) skillError(ctx.tag, "needs a standing pose keyed before f0 (use standIdle or another skill first)");
|
|
50
|
-
|
|
52
|
+
let gen = { type: "walk", f0: ctx.f0, ...args }, adjust, c0, h1;
|
|
51
53
|
delete gen.to; delete gen.arrive;
|
|
52
54
|
const h0 = state.yaw ?? 0, step = args.step ?? 0.5, first = args.firstStep ?? 0.4;
|
|
53
55
|
if (args.to) {
|
|
54
|
-
|
|
56
|
+
c0 = [(state.foot_l[0] + state.foot_r[0]) / 2, (state.foot_l[2] + state.foot_r[2]) / 2];
|
|
55
57
|
const dx = args.to[0] - c0[0], dz = args.to[args.to.length - 1] - c0[1], d = Math.hypot(dx, dz);
|
|
56
|
-
|
|
58
|
+
h1 = Math.atan2(dx, dz) * 180 / Math.PI;
|
|
57
59
|
while (h1 - h0 > 180) h1 -= 360;
|
|
58
60
|
while (h1 - h0 < -180) h1 += 360;
|
|
59
61
|
gen.heading = args.heading ?? [h0, h1];
|
|
60
62
|
gen.steps = args.steps ?? Math.max(2, 2 + Math.round((d - first) / step));
|
|
63
|
+
if (typeof gen.heading === "number") gen.heading = [h0, gen.heading];
|
|
64
|
+
if (ctx.fit && d > REACH_TOL) ({ gen, adjust } = fitTo(ctx, gen, c0, d, h1));
|
|
61
65
|
}
|
|
62
66
|
if (typeof gen.heading === "number") gen.heading = [h0, gen.heading];
|
|
63
67
|
const keys = walk(gen, ctx.keys, ctx.sk, ctx.tag), end = walkEnd(gen), last = ctx.frames - 1;
|
|
68
|
+
const at = args.to && walkEndAt(keys), miss = at ? Math.hypot(at[0] - args.to[0], at[1] - args.to[args.to.length - 1]) : 0;
|
|
69
|
+
const placed = miss > REACH_TOL ? { placed: { at: at.map(r2), heading: Math.round(gen.heading?.[1] ?? h0), why: `ends ${r2(miss)} m from to ${xzText(args.to)}` } } : {};
|
|
70
|
+
const more = { ...placed, ...(adjust ? { adjust } : {}) };
|
|
64
71
|
// the settle after the last step only holds the pose: a take that ends in it ends the walk there
|
|
65
|
-
if (end > last && Math.max(...keys.filter(k => k.f < end).map(k => k.f)) <= last) return { keys: keys.filter(k => k.f <= last), end: last, cut: `settle cut at f${last} (hold to the end)
|
|
66
|
-
return { keys, end };
|
|
72
|
+
if (end > last && Math.max(...keys.filter(k => k.f < end).map(k => k.f)) <= last) return { keys: keys.filter(k => k.f <= last), end: last, cut: `settle cut at f${last} (hold to the end)`, ...more };
|
|
73
|
+
return { keys, end, ...more };
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/** base when `props` has no other prop of that name (the same one again is fine), else base_<side>, base_<side>2, ... */
|