@helix3/helix-cli 0.1.7-helix3.43 → 0.1.7-helix3.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/ability.d.ts +2 -2
  2. package/dist/ability.js +6 -5
  3. package/dist/ability.js.map +1 -1
  4. package/dist/character/stages/conform.js +12 -3
  5. package/dist/character/stages/conform.js.map +1 -1
  6. package/dist/gesture/axisReference.d.ts +22 -0
  7. package/dist/gesture/axisReference.js +32 -0
  8. package/dist/gesture/axisReference.js.map +1 -0
  9. package/dist/gesture/capture.d.ts +75 -0
  10. package/dist/gesture/capture.js +274 -0
  11. package/dist/gesture/capture.js.map +1 -0
  12. package/dist/gesture/harnessPage.d.ts +17 -0
  13. package/dist/gesture/harnessPage.js +147 -0
  14. package/dist/gesture/harnessPage.js.map +1 -0
  15. package/dist/gesture/inspect.d.ts +104 -0
  16. package/dist/gesture/inspect.js +357 -0
  17. package/dist/gesture/inspect.js.map +1 -0
  18. package/dist/gesture/rig.d.ts +12 -0
  19. package/dist/gesture/rig.js +111 -0
  20. package/dist/gesture/rig.js.map +1 -0
  21. package/dist/gesture/worldRuntime.d.ts +27 -0
  22. package/dist/gesture/worldRuntime.js +73 -0
  23. package/dist/gesture/worldRuntime.js.map +1 -0
  24. package/dist/index.js +123 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/install.d.ts +2 -2
  27. package/dist/install.js +5 -9
  28. package/dist/install.js.map +1 -1
  29. package/dist/lib.d.ts +15 -0
  30. package/dist/lib.js +31 -0
  31. package/dist/lib.js.map +1 -1
  32. package/dist/paths.d.ts +4 -0
  33. package/dist/paths.js +11 -0
  34. package/dist/paths.js.map +1 -0
  35. package/dist/tsconfig.build.tsbuildinfo +1 -1
  36. package/package.json +2 -2
  37. package/vendor/README.md +40 -0
  38. package/vendor/capture-body.glb +0 -0
  39. package/vendor/capture-face.glb +0 -0
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MAX_VIEWS = exports.VIEW_NAMES = void 0;
37
+ exports.viewAzimuth = viewAzimuth;
38
+ exports.viewLabel = viewLabel;
39
+ exports.captureGesture = captureGesture;
40
+ exports.arrangeGrid = arrangeGrid;
41
+ exports.parseMoment = parseMoment;
42
+ exports.clipSide = clipSide;
43
+ exports.resolveViews = resolveViews;
44
+ exports.sharesAxis = sharesAxis;
45
+ // Renders a gesture clip as a contact sheet — front and side, several times across the clip — so an agent
46
+ // with no browser can still LOOK at a pose before handing it to a human. Measurement (./inspect) says where
47
+ // the bones are; this says how it reads, which is the one thing numbers cannot.
48
+ //
49
+ // Always the DEFAULT character: a clip replicates by name and plays on whatever body each player has, so a
50
+ // pose is authored against the canonical body every custom character is conformed to. That is why the render
51
+ // assets are vendored (see vendor/README.md) and this needs no asset download.
52
+ const node_fs_1 = require("node:fs");
53
+ const node_os_1 = require("node:os");
54
+ const node_path_1 = require("node:path");
55
+ const captureScreenshots_1 = require("../screenshot/captureScreenshots");
56
+ const staticServe_1 = require("../screenshot/staticServe");
57
+ const harnessPage_1 = require("./harnessPage");
58
+ const worldRuntime_1 = require("./worldRuntime");
59
+ const paths_1 = require("../paths");
60
+ /** 0/25/50/75/100 % of the clip — enough to read a gesture's arc without a sheet per frame. */
61
+ const DEFAULT_FRAMES = 5;
62
+ exports.VIEW_NAMES = ['front', 'back', 'right', 'left', 'three-quarter'];
63
+ exports.MAX_VIEWS = 2;
64
+ /** Far enough off-axis to lift a limb clear of the torso silhouette, near enough to still read as a front view. */
65
+ const THREE_QUARTER_DEG = 40;
66
+ /**
67
+ * Camera azimuth around Y, 0 = front. left/right are the CHARACTER's own, matching the readback's
68
+ * character-left / character-right — a viewer-relative reading would invert them, which is the trap to avoid.
69
+ * `three-quarter` follows the clip's ACTIVE side: from the wrong side it occludes as badly as a lateral view.
70
+ */
71
+ function viewAzimuth(view, side) {
72
+ switch (view) {
73
+ case 'front': return 0;
74
+ case 'back': return 180;
75
+ case 'right': return -90;
76
+ case 'left': return 90;
77
+ case 'three-quarter': return side === 'left' ? THREE_QUARTER_DEG : -THREE_QUARTER_DEG;
78
+ }
79
+ }
80
+ function viewLabel(view, side) {
81
+ if (view === 'three-quarter')
82
+ return `3/4 ${side}`;
83
+ return view === 'left' || view === 'right' ? `${view} side` : view;
84
+ }
85
+ // Portrait, because a standing figure is tall and narrow — a square cell spends ~40% of its pixels on background.
86
+ const DEFAULT_CELL_H = 420;
87
+ const CELL_ASPECT = 0.52;
88
+ // A single moment gets the whole image budget instead of a fifth of it: with only two cells there is room to
89
+ // see a hand's orientation, which is what a "fix this specific thing" question actually turns on.
90
+ const MOMENT_CELL_H = 760;
91
+ const MOMENT_ASPECT = 0.62;
92
+ /** vendor/ ships beside dist/ in the published package and sits one level up from it in the repo. */
93
+ function vendorDir() {
94
+ return (0, node_path_1.resolve)(__dirname, '..', '..', 'vendor');
95
+ }
96
+ async function captureGesture(opts) {
97
+ const onProgress = opts.onProgress ?? (() => undefined);
98
+ const frames = Math.max(2, opts.frames ?? DEFAULT_FRAMES);
99
+ const moment = opts.at !== undefined;
100
+ const cellH = opts.cellHeight ?? (moment ? MOMENT_CELL_H : DEFAULT_CELL_H);
101
+ const cellW = Math.round(cellH * (moment ? MOMENT_ASPECT : CELL_ASPECT));
102
+ const timeoutMs = opts.timeoutMs ?? 60000;
103
+ const systemDir = (0, node_path_1.join)(opts.worldDir, 'public', paths_1.MODULES_DIR, worldRuntime_1.SYSTEM_SLUG);
104
+ const threeDir = (0, worldRuntime_1.threeRoot)(opts.worldDir); // the same walk inspect uses — hoisted layouts, friendly npm-install error
105
+ const stage = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'helix-gesture-shot-'));
106
+ let browser;
107
+ let server;
108
+ try {
109
+ // Linked, not copied: three's addons tree is large, and the static server's traversal guard compares
110
+ // path STRINGS against the root, so a link resolving outside it still serves.
111
+ (0, node_fs_1.symlinkSync)(threeDir, (0, node_path_1.join)(stage, 'three'), 'junction');
112
+ (0, node_fs_1.symlinkSync)((0, node_path_1.dirname)(systemDir), (0, node_path_1.join)(stage, paths_1.MODULES_DIR), 'junction');
113
+ (0, node_fs_1.copyFileSync)((0, node_path_1.join)(vendorDir(), 'capture-body.glb'), (0, node_path_1.join)(stage, 'capture-body.glb'));
114
+ (0, node_fs_1.copyFileSync)((0, node_path_1.join)(vendorDir(), 'capture-face.glb'), (0, node_path_1.join)(stage, 'capture-face.glb'));
115
+ (0, node_fs_1.copyFileSync)(opts.clip, (0, node_path_1.join)(stage, 'clip.json'));
116
+ // The page needs concrete seconds, not fractions, so the clip is parsed here rather than in-page.
117
+ const source = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(stage, 'clip.json'), 'utf8'));
118
+ const duration = readDuration(source, opts.clip);
119
+ const side = clipSide(source);
120
+ const chosen = resolveViews(opts.views, source, moment);
121
+ const times = moment
122
+ ? [parseMoment(opts.at, duration)]
123
+ : Array.from({ length: frames }, (_, i) => Math.round((i / (frames - 1)) * duration * 1000) / 1000);
124
+ const config = { ...arrangeGrid(times, chosen, moment, side), cellW, cellH };
125
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(stage, 'index.html'), (0, harnessPage_1.harnessPage)(config));
126
+ const playwright = await Promise.resolve().then(() => __importStar(require('playwright')));
127
+ server = await (0, staticServe_1.startStaticServer)(stage);
128
+ const attempts = [];
129
+ for (const candidate of (0, captureScreenshots_1.selectBrowserCandidates)(opts.browser)) {
130
+ try {
131
+ browser = await playwright.chromium.launch(candidate.launchOptions);
132
+ onProgress(`using ${candidate.label}`);
133
+ break;
134
+ }
135
+ catch (err) {
136
+ attempts.push({ key: candidate.key, label: candidate.label, message: err instanceof Error ? err.message : String(err) });
137
+ }
138
+ }
139
+ if (!browser)
140
+ throw new Error((0, captureScreenshots_1.composeLaunchFailure)(attempts));
141
+ const page = await browser.newPage({
142
+ viewport: { width: config.colLabels.length * cellW + 32, height: config.rowLabels.length * cellH + 32 },
143
+ deviceScaleFactor: 1,
144
+ });
145
+ const pageErrors = [];
146
+ page.on('pageerror', (err) => pageErrors.push(err.message));
147
+ page.on('console', (msg) => { if (msg.type() === 'error')
148
+ pageErrors.push(msg.text()); });
149
+ await page.goto(`http://127.0.0.1:${server.port}/`);
150
+ try {
151
+ await page.waitForFunction(`window.${harnessPage_1.READY_FLAG} !== undefined`, undefined, { timeout: timeoutMs });
152
+ }
153
+ catch {
154
+ throw new Error(`the capture page never finished after ${timeoutMs}ms.${pageErrors.length ? `\n page errors:\n ${pageErrors.join('\n ')}` : ''}`);
155
+ }
156
+ const state = await page.evaluate(`window.${harnessPage_1.READY_FLAG}`);
157
+ if (!state.ready)
158
+ throw new Error(`the capture page failed to render:\n ${state.error ?? 'unknown error'}`);
159
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(opts.out), { recursive: true });
160
+ await page.locator('#sheet').screenshot({ path: opts.out });
161
+ return {
162
+ file: opts.out,
163
+ clip: state.clip ?? 'gesture',
164
+ duration: state.duration ?? duration,
165
+ times,
166
+ views: chosen,
167
+ moment,
168
+ sharedAxis: sharesAxis(chosen),
169
+ warnings: state.warnings ?? [],
170
+ };
171
+ }
172
+ finally {
173
+ if (browser)
174
+ await browser.close();
175
+ if (server)
176
+ await server.close();
177
+ (0, node_fs_1.rmSync)(stage, { recursive: true, force: true });
178
+ }
179
+ }
180
+ const timeLabel = (t) => `t=${t.toFixed(2)}s`;
181
+ /**
182
+ * A sequence reads best with time across and views stacked; a single moment reads best with the views SIDE BY
183
+ * SIDE, so the two renders of the same instant sit next to each other for comparison.
184
+ */
185
+ function arrangeGrid(times, views, moment, side) {
186
+ const cells = [];
187
+ const az = (v) => viewAzimuth(v, side);
188
+ if (moment) {
189
+ views.forEach((v, col) => times.forEach((t, row) => cells.push({ t, azimuth: az(v), col, row })));
190
+ return { cells, colLabels: views.map((v) => viewLabel(v, side)), rowLabels: times.map(timeLabel) };
191
+ }
192
+ views.forEach((v, row) => times.forEach((t, col) => cells.push({ t, azimuth: az(v), col, row })));
193
+ return { cells, colLabels: times.map(timeLabel), rowLabels: views.map((v) => viewLabel(v, side)) };
194
+ }
195
+ /** `--at` takes seconds or a percentage of duration, matching `inspect --t`. */
196
+ function parseMoment(at, duration) {
197
+ const raw = typeof at === 'number' ? at : at.trim();
198
+ const seconds = typeof raw === 'number'
199
+ ? raw
200
+ : raw.endsWith('%') ? (parseFloat(raw) / 100) * duration : parseFloat(raw);
201
+ if (!Number.isFinite(seconds))
202
+ throw new Error(`--at: '${at}' is not a number of seconds or a percentage`);
203
+ if (seconds < 0 || seconds > duration)
204
+ throw new Error(`--at: ${seconds}s is outside the clip (0..${duration}s)`);
205
+ return Math.round(seconds * 1000) / 1000;
206
+ }
207
+ function readDuration(source, clipPath) {
208
+ const duration = typeof source.duration === 'number' ? source.duration : NaN;
209
+ if (!Number.isFinite(duration) || duration <= 0)
210
+ throw new Error(`${clipPath}: "duration" must be a positive number`);
211
+ return duration;
212
+ }
213
+ /**
214
+ * Which side of the body a clip actually works on, so a left-handed gesture is never rendered from the side
215
+ * its own torso hides. The clip already declares this, so it is derived rather than asked for.
216
+ *
217
+ * KEYED BONES ARE THE GROUND TRUTH — most masks say nothing about sidedness. Of the authorable set
218
+ * (upperBody, lowerBody, leftArm, rightArm, spine, armsHead) only two are one-sided; the rest are bilateral,
219
+ * so a left-handed clip masked `upperBody` is identified by its bones, not its mask. The mask is consulted
220
+ * only as a tiebreaker, and by PATTERN rather than an exact name, so a future one-sided mask works untouched.
221
+ */
222
+ function clipSide(source) {
223
+ const bones = new Set();
224
+ for (const key of Array.isArray(source.keys) ? source.keys : []) {
225
+ const at = key.bones;
226
+ if (at !== null && typeof at === 'object')
227
+ for (const bone of Object.keys(at))
228
+ bones.add(bone);
229
+ }
230
+ let left = 0, right = 0;
231
+ for (const bone of bones) {
232
+ if (bone.endsWith('_l'))
233
+ left++;
234
+ else if (bone.endsWith('_r'))
235
+ right++;
236
+ }
237
+ if (left !== right)
238
+ return left > right ? 'left' : 'right';
239
+ const masks = `${typeof source.mask === 'string' ? source.mask : ''} ${typeof source.overrideMask === 'string' ? source.overrideMask : ''}`;
240
+ if (/left/i.test(masks))
241
+ return 'left';
242
+ if (/right/i.test(masks))
243
+ return 'right';
244
+ // Genuinely two-sided or unsided (a bow, a nod): either flank shows it, so take the one the standard
245
+ // action set is built around.
246
+ return 'right';
247
+ }
248
+ /**
249
+ * Defaults differ by MODE, because the modes answer different questions. A sequence is compared over time, so
250
+ * it wants the ORTHOGONAL pair — front plus a lateral — which is what makes "moves side-to-side, not
251
+ * front-to-back" readable; a 3/4 there would conflate the two axes it exists to separate. A single moment is
252
+ * being disambiguated, so it wants the 3/4, which lifts a limb clear of the torso instead of hiding it behind
253
+ * it. Every view stays legal in both modes: a mode-dependent enum would be a footgun to document, and neither
254
+ * pairing is actually invalid.
255
+ */
256
+ function resolveViews(requested, source, moment = false) {
257
+ if (requested !== undefined && requested.length > 0) {
258
+ const unique = [...new Set(requested)];
259
+ if (unique.length > exports.MAX_VIEWS)
260
+ throw new Error(`--views takes at most ${exports.MAX_VIEWS} (got ${unique.length}) — more rows shrink every cell past useful`);
261
+ return unique;
262
+ }
263
+ return moment ? ['front', 'three-quarter'] : ['front', clipSide(source)];
264
+ }
265
+ /** front+back and left+right are each a single axis seen twice: both are frontal projections of the same plane. */
266
+ function sharesAxis(views) {
267
+ if (views.length < 2)
268
+ return false;
269
+ // A 3/4 mixes both axes, so it never leaves a pairing depth-blind.
270
+ const axis = (v) => v === 'three-quarter' ? 'mixed' : v === 'front' || v === 'back' ? 'depth' : 'lateral';
271
+ const axes = views.map(axis);
272
+ return !axes.includes('mixed') && new Set(axes).size === 1;
273
+ }
274
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/gesture/capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export interface HarnessCell {
2
+ /** Sample time in seconds. */
3
+ t: number;
4
+ /** Camera azimuth in degrees around Y; 0 = front. */
5
+ azimuth: number;
6
+ col: number;
7
+ row: number;
8
+ }
9
+ export interface HarnessConfig {
10
+ cells: HarnessCell[];
11
+ colLabels: string[];
12
+ rowLabels: string[];
13
+ cellW: number;
14
+ cellH: number;
15
+ }
16
+ export declare const READY_FLAG = "__helixGestureCapture";
17
+ export declare function harnessPage(config: HarnessConfig): string;
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ // The capture harness page. Assembled as a string because `tsc` does not copy assets into dist, and the
3
+ // page is written into a temp dir at run time anyway (alongside links to the world's three + system).
4
+ //
5
+ // The page renders a grid it is TOLD about — a flat cell list plus row/column labels — rather than deciding
6
+ // the arrangement itself. That is what lets one code path serve both the contact sheet (times across, views
7
+ // down) and the single-moment comparison (views across, one row, much larger cells).
8
+ //
9
+ // Why it poses TWO skeletons: the default character ships as body + face, each carrying its own skeleton
10
+ // with a different skin joint count (77-node full tree vs a 16-node root->spine->neck->head chain), so they
11
+ // cannot share one skin. Posing each by bone name is what keeps the head following spine and neck keys.
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.READY_FLAG = void 0;
14
+ exports.harnessPage = harnessPage;
15
+ exports.READY_FLAG = '__helixGestureCapture';
16
+ function harnessPage(config) {
17
+ return `<!doctype html>
18
+ <meta charset="utf-8">
19
+ <title>gesture capture</title>
20
+ <style>
21
+ :root { color-scheme: light }
22
+ body { margin: 0; background: #f4f4f6; font: 12px/1.3 ui-sans-serif, system-ui, sans-serif; color: #222 }
23
+ #sheet { position: relative; display: inline-block }
24
+ #sheet canvas { display: block }
25
+ .lbl { position: absolute; font-variant-numeric: tabular-nums; color: #444; pointer-events: none; font-weight: 600 }
26
+ .col { top: 5px; transform: translateX(-50%) }
27
+ .row { left: 5px; transform: translateY(-50%) rotate(-90deg); transform-origin: left center }
28
+ #err { padding: 12px; color: #a00; font-family: ui-monospace, monospace; white-space: pre-wrap }
29
+ </style>
30
+ <div id="sheet"></div>
31
+ <div id="err"></div>
32
+ <script type="importmap">
33
+ { "imports": { "three": "/three/build/three.module.js", "three/": "/three/" } }
34
+ </script>
35
+ <script type="module">
36
+ import * as THREE from 'three';
37
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
38
+ import { compileGesture, GesturePlayer } from '/helix_modules/humanoid-character/index.js';
39
+
40
+ const CONFIG = ${JSON.stringify(config)};
41
+ const FLAG = ${JSON.stringify(exports.READY_FLAG)};
42
+ const fail = (message) => { document.getElementById('err').textContent = message; window[FLAG] = { ready: false, error: message }; };
43
+
44
+ try {
45
+ const clip = await (await fetch('/clip.json')).json();
46
+ const compiled = compileGesture(clip);
47
+
48
+ const loader = new GLTFLoader();
49
+ const parts = [];
50
+ for (const file of ['/capture-body.glb', '/capture-face.glb']) {
51
+ const gltf = await loader.loadAsync(file);
52
+ const root = gltf.scene;
53
+ root.traverse((o) => { if (o.isSkinnedMesh) { o.frustumCulled = false; o.castShadow = true; } });
54
+ // The bind pose must be restored before every pose, or deltas accumulate across grid cells.
55
+ const bind = new Map();
56
+ root.traverse((n) => { if (n.isBone === true) bind.set(n, n.quaternion.clone()); });
57
+ parts.push({ root, bind, player: new GesturePlayer(root) });
58
+ }
59
+
60
+ const scene = new THREE.Scene();
61
+ scene.background = new THREE.Color(0xf4f4f6);
62
+ for (const p of parts) scene.add(p.root);
63
+
64
+ // Three-point rig: a key light high and camera-left, a softer fill opposite, a rim from behind to keep
65
+ // the silhouette off the background. Ambient alone flattens exactly the depth cues a pose is judged by.
66
+ scene.add(new THREE.HemisphereLight(0xffffff, 0x555560, 0.55));
67
+ const key = new THREE.DirectionalLight(0xffffff, 2.0); key.position.set(-2.2, 3.2, 3.0); scene.add(key);
68
+ const fill = new THREE.DirectionalLight(0xffffff, 0.7); fill.position.set(2.6, 1.0, 1.8); scene.add(fill);
69
+ const rim = new THREE.DirectionalLight(0xffffff, 0.9); rim.position.set(0.4, 2.0, -3.2); scene.add(rim);
70
+
71
+ const poseAll = (t) => {
72
+ for (const p of parts) {
73
+ for (const [bone, q] of p.bind) bone.quaternion.copy(q);
74
+ p.player.pose(compiled.gesture, t);
75
+ }
76
+ for (const p of parts) p.root.updateMatrixWorld(true);
77
+ };
78
+
79
+ const cellW = CONFIG.cellW, cellH = CONFIG.cellH;
80
+
81
+ // Frame from the UNION of every pose in the grid, not the bind pose: one camera for the whole image keeps
82
+ // the cells comparable (a per-cell refit would make the body appear to zoom), while the union guarantees an
83
+ // overhead reach cannot be clipped out of frame.
84
+ const box = new THREE.Box3();
85
+ for (const t of [...new Set(CONFIG.cells.map((c) => c.t))]) { poseAll(t); box.expandByObject(scene); }
86
+ const centre = box.getCenter(new THREE.Vector3());
87
+ // Horizontal extent is per-VIEW: a camera at azimuth t sees |dx*cos t| + |dz*sin t| across, so an off-axis
88
+ // 3/4 is wider than the front view and would clip if only world X were budgeted. One camera is shared, so
89
+ // take the widest view in the grid.
90
+ const dx = box.max.x - box.min.x, dz = box.max.z - box.min.z;
91
+ const widest = Math.max(...CONFIG.cells.map((c) => {
92
+ const a = (c.azimuth * Math.PI) / 180;
93
+ return Math.abs(dx * Math.cos(a)) + Math.abs(dz * Math.sin(a));
94
+ }), 0.1);
95
+ const span = Math.max(box.max.y - box.min.y, widest / (cellW / cellH), 0.1);
96
+ const fov = 32;
97
+ const radius = (span * 0.60) / Math.tan((fov / 2) * Math.PI / 180);
98
+
99
+ const width = CONFIG.colLabels.length * cellW, totalHeight = CONFIG.rowLabels.length * cellH;
100
+ const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
101
+ renderer.setPixelRatio(1);
102
+ renderer.setSize(width, totalHeight);
103
+ renderer.setScissorTest(true);
104
+ const sheet = document.getElementById('sheet');
105
+ sheet.appendChild(renderer.domElement);
106
+
107
+ const camera = new THREE.PerspectiveCamera(fov, cellW / cellH, 0.05, 100);
108
+
109
+ // Everything renders in ONE frame: with scissor per cell the drawing buffer ends up holding the whole
110
+ // image, so the screenshot needs no per-cell readback. Sorted by time so each pose is applied once per
111
+ // distinct time rather than once per cell.
112
+ for (const cell of [...CONFIG.cells].sort((a, b) => a.t - b.t)) {
113
+ poseAll(cell.t);
114
+ const angle = (cell.azimuth * Math.PI) / 180;
115
+ camera.position.set(centre.x + Math.sin(angle) * radius, centre.y, centre.z + Math.cos(angle) * radius);
116
+ camera.lookAt(centre);
117
+ camera.updateProjectionMatrix();
118
+ // WebGL's origin is bottom-left; row 0 must be the TOP of the image.
119
+ const y = totalHeight - (cell.row + 1) * cellH;
120
+ renderer.setViewport(cell.col * cellW, y, cellW, cellH);
121
+ renderer.setScissor(cell.col * cellW, y, cellW, cellH);
122
+ renderer.render(scene, camera);
123
+ }
124
+
125
+ CONFIG.colLabels.forEach((text, c) => {
126
+ const el = document.createElement('div');
127
+ el.className = 'lbl col';
128
+ el.style.left = (c * cellW + cellW / 2) + 'px';
129
+ el.textContent = text;
130
+ sheet.appendChild(el);
131
+ });
132
+ CONFIG.rowLabels.forEach((text, r) => {
133
+ const el = document.createElement('div');
134
+ el.className = 'lbl row';
135
+ el.style.top = (r * cellH + cellH / 2) + 'px';
136
+ el.textContent = text;
137
+ sheet.appendChild(el);
138
+ });
139
+
140
+ window[FLAG] = { ready: true, clip: compiled.gesture.name, duration: compiled.gesture.duration, warnings: compiled.warnings ?? [] };
141
+ } catch (err) {
142
+ fail(err && err.stack ? err.stack : String(err));
143
+ }
144
+ </script>
145
+ `;
146
+ }
147
+ //# sourceMappingURL=harnessPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harnessPage.js","sourceRoot":"","sources":["../../src/gesture/harnessPage.ts"],"names":[],"mappings":";AAAA,wGAAwG;AACxG,sGAAsG;AACtG,EAAE;AACF,4GAA4G;AAC5G,4GAA4G;AAC5G,qFAAqF;AACrF,EAAE;AACF,yGAAyG;AACzG,4GAA4G;AAC5G,wGAAwG;;;AAqBxG,kCAkIC;AApIY,QAAA,UAAU,GAAG,uBAAuB,CAAC;AAElD,SAAgB,WAAW,CAAC,MAAqB;IAC/C,OAAO;;;;;;;;;;;;;;;;;;;;;;;iBAuBQ,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;eACxB,IAAI,CAAC,SAAS,CAAC,kBAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwGxC,CAAC;AACF,CAAC"}
@@ -0,0 +1,104 @@
1
+ import { type BoneAxisReference } from './axisReference';
2
+ export declare const DEFAULT_BONES: string[];
3
+ export interface InspectOptions {
4
+ worldDir: string;
5
+ /** Clip JSON path, or '-' for stdin. */
6
+ clip: string;
7
+ /** Times in seconds, or '35%' of duration. Empty + no motion/axes ⇒ ['0']. */
8
+ times?: string[];
9
+ bones?: string[];
10
+ motion?: boolean;
11
+ axes?: boolean;
12
+ /** Isolate part of the clip for --motion; without it the rest→pose→rest travel dominates every number. */
13
+ motionWindow?: [number, number] | null;
14
+ /** Which bone --axes measures the effect on (default: the last requested bone that exists). */
15
+ probe?: string | null;
16
+ /** Custom character GLB; default is the embedded canonical skeleton. */
17
+ rig?: string | null;
18
+ }
19
+ export interface BoneReadback {
20
+ pos?: number[];
21
+ dir?: number[] | null;
22
+ reads: string;
23
+ facing?: FacingReadback | null;
24
+ error?: string;
25
+ }
26
+ export interface FacingReadback {
27
+ palm?: number[];
28
+ fingers?: number[];
29
+ reads?: string;
30
+ unavailable?: string;
31
+ }
32
+ export interface MotionReadback {
33
+ lateral?: number;
34
+ vertical?: number;
35
+ depth?: number;
36
+ pathLength?: number;
37
+ dominant?: string;
38
+ purity?: number;
39
+ reversals?: number;
40
+ reads: string;
41
+ error?: string;
42
+ }
43
+ export interface AxisRow {
44
+ axis: string;
45
+ magnitude: number;
46
+ delta?: number[];
47
+ reads: string;
48
+ }
49
+ /** A serializable axis-table row. `bones` is the group's pattern SOURCE, not a RegExp — the result of this
50
+ * module is JSON-emitted (`--json`, the MCP), and a RegExp stringifies to `{}`, losing the group identity. */
51
+ export interface AxisRangeRow {
52
+ bones: string;
53
+ axes: {
54
+ x: string;
55
+ y: string;
56
+ z: string;
57
+ };
58
+ natural: {
59
+ x: [number, number];
60
+ y: [number, number];
61
+ z: [number, number];
62
+ };
63
+ }
64
+ export interface InspectResult {
65
+ clip: string;
66
+ duration: number;
67
+ rig: string;
68
+ system: string;
69
+ warnings: string[];
70
+ frames: {
71
+ t: number;
72
+ bones: Record<string, BoneReadback>;
73
+ }[];
74
+ motion: Record<string, MotionReadback> | null;
75
+ axes: {
76
+ probe: string;
77
+ keys: {
78
+ t: number;
79
+ rows: AxisRow[];
80
+ }[];
81
+ } | null;
82
+ /** Axis-table rows for the bones this clip actually keys — the context a warning needs, not all 15 groups. */
83
+ keyedAxisRows: AxisRangeRow[];
84
+ }
85
+ /** Exported for tests: these two produce the human-facing readback, and are duck-typed on .x/.y/.z so
86
+ * they need no three. A sign error here silently misleads every measurement an agent takes. */
87
+ /** Coarse compass for a unit vector — never a precise angle, because conformed rigs differ by ~20° at the extremities. */
88
+ export declare function compass(v: any): string;
89
+ export interface Refs {
90
+ head: number | null;
91
+ shoulder: number;
92
+ hip: number;
93
+ centerX: number;
94
+ centerZ: number;
95
+ }
96
+ /** Plain-English readback so an agent with no eyes can still tell a good pose from a broken one. */
97
+ export declare function describePose(name: string, pos: any, dir: any, ref: Refs): string;
98
+ export declare function inspectGesture(opts: InspectOptions): Promise<InspectResult>;
99
+ /** Human-readable report. `--json` returns the InspectResult instead. */
100
+ export declare function formatInspect(r: InspectResult, opts?: {
101
+ motionWindow?: [number, number] | null;
102
+ }): string;
103
+ /** Natural-range context for the bones a clip actually keys — the table rows that matter, not all 15. */
104
+ export declare function relevantAxisRows(table: readonly BoneAxisReference[] | null, clipJson: any): AxisRangeRow[];