cozyclay 3.1.0 → 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 +65 -0
- package/bin/agent/agent-routes.mjs +12 -11
- package/bin/agent/main-agent.mjs +21 -7
- package/bin/agent/main-prompt.mjs +7 -6
- package/bin/agent/plan-tool.mjs +114 -0
- package/bin/agent/script-tool.mjs +4 -1
- package/bin/agent/sculpt-sketch.mjs +190 -0
- package/bin/agent/session-store.mjs +2 -1
- package/bin/agent/sketch-code.mjs +151 -0
- package/bin/agent/sketch-sandbox.mjs +102 -0
- 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/agent/turn-frames.mjs +4 -0
- package/bin/cozyclay.mjs +4 -2
- package/bin/live-endpoint.mjs +2 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/{TaskBoard-DdDXMgZm.js → TaskBoard-GR8xZm5c.js} +1 -1
- 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 +7 -7
- package/package.json +3 -1
- package/src/App.jsx +44 -66
- 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/elements.js +4 -3
- package/src/commands/export.js +1 -1
- package/src/commands/motion3.js +27 -7
- package/src/commands/objects.js +1 -1
- package/src/export-workspace.js +244 -0
- package/src/keyframe-pack.js +5 -1
- package/src/live-control.js +14 -7
- package/src/material-look.js +189 -0
- package/src/material-presets.js +78 -0
- package/src/material-textures.js +157 -0
- 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/object-look.js +9 -5
- package/src/panels/PoseImagePanel.jsx +3 -2
- package/src/panels/SculptPanel.jsx +2 -2
- package/src/primitive-look.js +38 -0
- package/src/props.jsx +20 -28
- package/src/render-passes.js +7 -1
- package/src/result-modal.jsx +3 -3
- package/src/scene-objects.js +2 -2
- package/src/sculpt-colours.js +65 -0
- package/src/sculpt-compare.js +13 -50
- package/src/sculpt-csg.js +112 -0
- package/src/sculpt-curve.js +123 -0
- package/src/sculpt-cutters.js +43 -0
- package/src/sculpt-detail.js +207 -0
- package/src/sculpt-geometry-loft-convex.js +100 -0
- package/src/sculpt-geometry-shapes.js +250 -0
- package/src/sculpt-geometry.js +33 -15
- package/src/sculpt-loft-rings.js +225 -0
- package/src/sculpt-noise.js +65 -0
- package/src/sculpt-part-fields.js +27 -0
- package/src/sculpt-recipe-read.js +65 -0
- package/src/sculpt-recipe.js +33 -75
- package/src/sculpt-run.js +1 -1
- package/src/sculpt-scatter-items.js +183 -0
- package/src/sculpt-scatter-shape.js +57 -0
- package/src/sculpt-scatter.js +124 -0
- package/src/sculpt-shapes-curved.js +211 -0
- package/src/sculpt-shapes-loft-convex.js +218 -0
- package/src/sculpt-shapes.js +138 -0
- package/src/sculpt-sketch-state.js +76 -0
- package/src/sculpt-sketch.jsx +124 -0
- package/src/sculpt-surface-fields.js +65 -0
- 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/studio-elements.js +5 -1
- package/src/workflow/AgentPanel.jsx +6 -6
- package/src/workflow/AgentPlanCard.jsx +41 -0
- package/src/workflow/CozySceneNode.jsx +1 -1
- package/src/workflow/WorkflowBuilder.jsx +2 -2
- package/src/workflow/agent-client.js +14 -11
- package/src/workflow/agent-plan.css +93 -0
- package/src/workflow/agent-plan.js +59 -0
- 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 +8 -8
- package/dist/assets/app-B2BpfCl2.js +0 -5088
- package/dist/assets/compile-worker-D0pKioa0.js +0 -5
- package/dist/assets/motion3-compile-B887n77J.js +0 -1
- package/dist/assets/scene-asset-sync-COGOlF5T.css +0 -1
- package/dist/assets/scene-asset-sync-CQzEdsqu.js +0 -13
- package/dist/assets/task-board-BKaM7d-9.js +0 -14
- package/dist/assets/workflow-B8Dhzexj.js +0 -179
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, ... */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// f0. The agent's own keys come after the skill keys (a key of the agent wins on the same
|
|
5
5
|
// frame), and the agent's own anchors/props/fits replace a skill's of the same name.
|
|
6
6
|
import { stateAt } from "../compiler/walk.js";
|
|
7
|
-
import { didYouMean, suggestField } from "../compiler/vocab.js";
|
|
7
|
+
import { didYouMean, suggestField, fmt } from "../compiler/vocab.js";
|
|
8
8
|
import { USE_FIELDS } from "../schema/fields.js";
|
|
9
9
|
import { SKILL_INFO } from "./info.js";
|
|
10
10
|
import { fixOptions } from "../fix/index.js";
|
|
@@ -38,32 +38,66 @@ function mergeChecks(into, part) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const isOpen = v => Boolean(v) && typeof v === "object" && !Array.isArray(v) && (v.range ?? v.search) !== undefined;
|
|
42
|
+
const said = t => (t.description ? ` (${t.description})` : "");
|
|
43
|
+
|
|
44
|
+
/** What is wrong with one skill arg against its schema fragment (skills/info.js): a value of the wrong kind, an enum miss; null when fine or not checked. */
|
|
45
|
+
function argProblem(at, v, t) {
|
|
46
|
+
if (!t || v === undefined || isOpen(v)) return null;
|
|
47
|
+
if (t.enum && !t.enum.includes(v)) return `${at} must be one of ${t.enum.map(e => JSON.stringify(e)).join(", ")}${said(t)}, got ${fmt(v)}${didYouMean(String(v), t.enum.map(String))}`;
|
|
48
|
+
if ((t.type === "number" || t.type === "integer") && !Number.isFinite(v)) return `${at} must be a number${said(t)}, got ${fmt(v)}`;
|
|
49
|
+
if (t.type === "string" && typeof v !== "string") return `${at} must be a string${said(t)}, got ${fmt(v)}`;
|
|
50
|
+
if (t.type === "boolean" && typeof v !== "boolean") return `${at} must be true or false${said(t)}, got ${fmt(v)}`;
|
|
51
|
+
if (t.type === "array" && t.items?.type === "number" && !(Array.isArray(v) && v.length >= (t.minItems ?? 0) && v.every(x => Number.isFinite(x) || isOpen(x))))
|
|
52
|
+
return `${at} must be ${t.minItems === 2 ? "[x, z]" : t.minItems === 3 ? "[x, y, z]" : "an array of numbers"}${said(t)}, got ${fmt(v)}`;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Every problem of spec.use that needs no expansion, all at once: the entry's shape, the skill name, unknown fields
|
|
58
|
+
* and args, args of the wrong kind, required args, f0. lenient (a saved take) checks only what expansion cannot do without.
|
|
59
|
+
*/
|
|
60
|
+
function usageProblems(spec, lenient = false) {
|
|
61
|
+
if (!Array.isArray(spec.use)) return ["spec.use must be an array of { skill, args, f0 }"];
|
|
62
|
+
const out = [];
|
|
63
|
+
spec.use.forEach((u, i) => {
|
|
64
|
+
if (!u || typeof u !== "object" || Array.isArray(u)) return out.push(`use[${i}] must be { skill, args, f0 }, got ${fmt(u)}`);
|
|
65
|
+
if (!SKILLS[u.skill]) return out.push(`use[${i}].skill "${u.skill}" is not a skill${didYouMean(String(u.skill), SKILL_NAMES)} (skills: ${SKILL_NAMES.join(", ")})`);
|
|
66
|
+
if (!lenient) for (const f of Object.keys(u)) if (!USE_FIELDS.includes(f)) out.push(`use[${i}] has unknown field "${f}"${suggestField(f, USE_FIELDS)}; an entry is { skill, args, f0 }`);
|
|
67
|
+
if (u.args !== undefined && (!u.args || typeof u.args !== "object" || Array.isArray(u.args))) out.push(`use[${i}].args must be an object, got ${JSON.stringify(u.args)}`);
|
|
68
|
+
else if (!lenient) {
|
|
69
|
+
const info = SKILL_INFO[u.skill], known = Object.keys(info.args);
|
|
70
|
+
for (const [a, v] of Object.entries(u.args ?? {})) {
|
|
71
|
+
if (!known.includes(a)) out.push(`use[${i}].args.${a} is not an arg of ${u.skill}${suggestField(a, known)} (args: ${known.join(", ")})`);
|
|
72
|
+
else { const p = argProblem(`use[${i}].args.${a}`, v, info.args[a]); if (p) out.push(p); }
|
|
73
|
+
}
|
|
74
|
+
for (const a of info.required ?? []) if (u.args?.[a] === undefined) out.push(`use[${i}] ${u.skill} needs args.${a}${said(info.args[a])}`);
|
|
75
|
+
}
|
|
76
|
+
// past the last frame is said in order below, beside what an earlier skill needs (set frames >= ...)
|
|
77
|
+
if (!Number.isInteger(u.f0) || u.f0 < 0) out.push(`use[${i}].f0 must be an integer frame within 0..${spec.frames - 1}, got ${JSON.stringify(u.f0)}`);
|
|
78
|
+
});
|
|
79
|
+
return out;
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
/**
|
|
49
|
-
* spec with `use` -> { spec: plain spec (no use), used: [{ skill, f0, end, paced?, cut? }] };
|
|
50
|
-
*
|
|
51
|
-
*
|
|
83
|
+
* spec with `use` -> { spec: plain spec (no use), used: [{ skill, f0, end, paced?, cut?, placed?, adjust? }] }; every
|
|
84
|
+
* problem of the use entries themselves is refused at once (usageProblems); lenient ignores unknown use[i] fields and
|
|
85
|
+
* args. fit ({ refusals: [] }, correct.js fitWalks) lets a skill fit its own args to what they ask for (walkTo: the
|
|
86
|
+
* stride to reach to), as adjust: { args, note }, or refuse into fit.refusals. auto (frames-auto.js) lets a skill
|
|
87
|
+
* that holds to the last frame end where its own motion ends. A skill whose release or settle the last frame cuts ends
|
|
52
88
|
* there holding (cut says so); one that needs frames past the last for its own motion is refused.
|
|
53
89
|
*/
|
|
54
|
-
export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
|
|
90
|
+
export function expandUse(spec, { sk, scene, lenient = false, auto = false, fit }) {
|
|
55
91
|
if (!spec.use?.length) return { spec, used: [] };
|
|
56
|
-
|
|
92
|
+
const problems = usageProblems(spec, lenient);
|
|
93
|
+
if (problems.length) bad(problems.join("\n"));
|
|
57
94
|
const own = (spec.keys ?? []).map((k, i) => ({ ...k, src: `keys[${i}]` }));
|
|
58
95
|
const merged = { anchors: {}, props: {}, objects: {}, fits: [], checks: {}, channels: [], fills: {}, goals: [] }, skillKeys = [], used = [];
|
|
59
96
|
spec.use.forEach((u, i) => {
|
|
60
|
-
const tag = `use[${i}] ${u
|
|
61
|
-
if (
|
|
62
|
-
for (const f of Object.keys(u)) if (!USE_FIELDS.includes(f) && !lenient) bad(`use[${i}] has unknown field "${f}"${suggestField(f, USE_FIELDS)}; an entry is { skill, args, f0 }`);
|
|
63
|
-
argsOk(u, i, lenient);
|
|
64
|
-
if (!Number.isInteger(u.f0) || u.f0 < 0 || u.f0 > spec.frames - 1) bad(`use[${i}].f0 must be an integer frame within 0..${spec.frames - 1}, got ${JSON.stringify(u.f0)}`);
|
|
97
|
+
const tag = `use[${i}] ${u.skill}`;
|
|
98
|
+
if (u.f0 > spec.frames - 1) bad(`use[${i}].f0 must be an integer frame within 0..${spec.frames - 1}, got ${JSON.stringify(u.f0)}`);
|
|
65
99
|
const keys = [...skillKeys, ...own];
|
|
66
|
-
const ctx = { sk, scene, frames: spec.frames, fps: spec.fps ?? 24, auto, props: { ...merged.props, ...spec.props }, self: spec.fix !== false && spec.fix?.self !== false, f0: u.f0, args: structuredClone(u.args ?? {}), tag, keys,
|
|
100
|
+
const ctx = { sk, scene, frames: spec.frames, fps: spec.fps ?? 24, auto, fit, props: { ...merged.props, ...spec.props }, self: spec.fix !== false && spec.fix?.self !== false, f0: u.f0, args: structuredClone(u.args ?? {}), tag, keys,
|
|
67
101
|
use: spec.use, index: i, place: fixOptions(spec.fix).stance, state: structuredClone(stateAt(keys, u.f0)), anchors: { ...merged.anchors, ...spec.anchors }, objects: { ...merged.objects, ...spec.objects } };
|
|
68
102
|
const part = SKILLS[u.skill](ctx), last = Math.max(part.end ?? u.f0, ...part.keys.map(k => k.f));
|
|
69
103
|
if (last > spec.frames - 1) bad(`${tag} needs ${last - u.f0} frames from f${u.f0} (to f${last}), past the last frame f${spec.frames - 1}; set frames >= ${last + 1} or frames: "auto"`);
|
|
@@ -77,7 +111,7 @@ export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
|
|
|
77
111
|
// fields a skill fills into an entry when it leaves them out (throwObject: a carry's release and fall)
|
|
78
112
|
for (const [id, f] of Object.entries(part.fills ?? {})) merged.fills[id] = { ...merged.fills[id], ...f };
|
|
79
113
|
merged.goals.push(...(part.goals ?? []));
|
|
80
|
-
used.push({ skill: u.skill, f0: u.f0, end: part.end, ...(part.paced ? { paced: part.paced } : {}), ...(part.cut ? { cut: part.cut } : {}), ...(part.placed ? { placed: part.placed } : {}) });
|
|
114
|
+
used.push({ skill: u.skill, f0: u.f0, end: part.end, ...(part.paced ? { paced: part.paced } : {}), ...(part.cut ? { cut: part.cut } : {}), ...(part.placed ? { placed: part.placed } : {}), ...(part.adjust ? { adjust: part.adjust } : {}) });
|
|
81
115
|
});
|
|
82
116
|
const out = { ...spec, keys: [...skillKeys, ...own], anchors: { ...merged.anchors, ...spec.anchors }, props: { ...merged.props, ...spec.props },
|
|
83
117
|
objects: { ...merged.objects, ...spec.objects } };
|
|
@@ -25,7 +25,7 @@ export const SKILL_INFO = {
|
|
|
25
25
|
place: { at: 0.03 },
|
|
26
26
|
},
|
|
27
27
|
walkTo: {
|
|
28
|
-
args: { to: xz("[x, z] world m: the heading turns toward it and the steps cover the distance"), ...walkArgs,
|
|
28
|
+
args: { to: xz("[x, z] world m: the heading turns toward it and the steps cover the distance (given steps, the stride or the step count and cadence is fitted to it in a natural gait, or the walk is refused)"), ...walkArgs,
|
|
29
29
|
heading: { oneOf: [num(), WALK_TYPES.heading], description: "degrees: h1, or [h0, h1]" },
|
|
30
30
|
arrive: int("with to and no steps/period/step/swing: the frame to end by (feet together); the engine picks the cadence (observations.planned)") },
|
|
31
31
|
needs: "a standing pose keyed before f0", frames: "16 + steps * period + 18",
|
|
@@ -51,6 +51,12 @@ export function clearStance(sk, scene, c, h) {
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
|
|
55
|
+
/** The standing body at c ([x, z]) facing h: its least distance to the bodies (ids not in skip; m, negative inside). */
|
|
56
|
+
export function standClearance(sk, scene, c, h, skip = new Set()) {
|
|
57
|
+
const boxes = obstacles({ objects: Object.fromEntries(Object.entries(scene?.objects ?? {}).filter(([id]) => !skip.has(id))) });
|
|
58
|
+
return boxes.length ? clearance(sk, boxes, c, h) : Infinity;
|
|
59
|
+
}
|
|
54
60
|
/**
|
|
55
61
|
* The world point an anchor definition names on the still scene, as anchors.js resolves it: world p; an object's (or a
|
|
56
62
|
* group's) `local` point, its base pivot without one; with `grasp` and no `local`, the centre of its parts (where a
|