three-usd-robot 0.9.0 → 0.11.0

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.
@@ -0,0 +1,701 @@
1
+ import { identity4, multiply, invert, nearestAngleBranch, multiplyAll, decomposeJointRelative, interpolate } from './chunk-LDO5FKQS.js';
2
+ import * as THREE4 from 'three';
3
+
4
+ function axisVector(axis) {
5
+ switch (axis) {
6
+ case "X":
7
+ return new THREE4.Vector3(1, 0, 0);
8
+ case "Y":
9
+ return new THREE4.Vector3(0, 1, 0);
10
+ case "Z":
11
+ return new THREE4.Vector3(0, 0, 1);
12
+ }
13
+ }
14
+ var JointObject = class extends THREE4.Object3D {
15
+ isJointObject = true;
16
+ jointName;
17
+ /** Full USD prim path of the joint — the collision-proof address. */
18
+ primPath;
19
+ jointType;
20
+ axisToken;
21
+ axis;
22
+ lower;
23
+ upper;
24
+ _value = 0;
25
+ constructor(joint) {
26
+ super();
27
+ this.name = joint.name;
28
+ this.jointName = joint.name;
29
+ this.primPath = joint.primPath;
30
+ this.jointType = joint.type;
31
+ this.axisToken = joint.axis;
32
+ this.axis = axisVector(joint.axis);
33
+ this.lower = joint.lower;
34
+ this.upper = joint.upper;
35
+ }
36
+ get value() {
37
+ return this._value;
38
+ }
39
+ get articulated() {
40
+ return this.jointType !== "fixed";
41
+ }
42
+ /**
43
+ * Set the joint value (radians for revolute/continuous, length for prismatic).
44
+ * Optionally clamps to authored limits. Returns the value actually applied.
45
+ */
46
+ setValue(value, clampToLimits = true) {
47
+ if (!this.articulated) return this._value;
48
+ let v = value;
49
+ if (clampToLimits) {
50
+ if (this.lower !== void 0 && v < this.lower) v = this.lower;
51
+ if (this.upper !== void 0 && v > this.upper) v = this.upper;
52
+ }
53
+ this._value = v;
54
+ if (this.jointType === "prismatic") {
55
+ this.position.copy(this.axis).multiplyScalar(v);
56
+ this.quaternion.identity();
57
+ } else {
58
+ this.quaternion.setFromAxisAngle(this.axis, v);
59
+ this.position.set(0, 0, 0);
60
+ }
61
+ return v;
62
+ }
63
+ };
64
+ var LinkObject = class extends THREE4.Object3D {
65
+ isLinkObject = true;
66
+ linkName;
67
+ primPath;
68
+ constructor(link) {
69
+ super();
70
+ this.name = link.name;
71
+ this.linkName = link.name;
72
+ this.primPath = link.primPath;
73
+ this.matrixAutoUpdate = false;
74
+ }
75
+ };
76
+ var ThreeUsdRobot = class extends THREE4.Object3D {
77
+ isThreeUsdRobot = true;
78
+ robot;
79
+ tree;
80
+ clampJointLimits;
81
+ /**
82
+ * The composed USD stage this robot was built from — the full prim tree, for
83
+ * inspection tooling (structure panels, attribute browsers). Attached by
84
+ * {@link ThreeUsdRobotLoader}; `undefined` for programmatically-built robots.
85
+ */
86
+ stage;
87
+ linkObjects = /* @__PURE__ */ new Map();
88
+ jointObjects = /* @__PURE__ */ new Map();
89
+ linkKeyByPath = /* @__PURE__ */ new Map();
90
+ jointKeyByPath = /* @__PURE__ */ new Map();
91
+ dirty = true;
92
+ /** Constructed (fk rest) local matrix of every link, for baked→fk restore. */
93
+ restLocal = /* @__PURE__ */ new Map();
94
+ _displayMode = "fk";
95
+ /** Stage-space link worlds while baked (`null` in fk mode). */
96
+ bakedStageWorld = null;
97
+ /** Frozen `frame0 · motion · frame1⁻¹` per tree joint while baked. */
98
+ bakedChainRel = null;
99
+ debugBaked;
100
+ bakedDebugWarned = false;
101
+ warnedPoseKeys = /* @__PURE__ */ new Set();
102
+ warnedNonUniformScale = false;
103
+ helperSize;
104
+ _showVisual = true;
105
+ _showCollision = false;
106
+ _showJointAxes = false;
107
+ _showLinkFrames = false;
108
+ jointAxesHelpers = [];
109
+ linkFrameHelpers = [];
110
+ constructor(robot, tree, options = {}) {
111
+ super();
112
+ this.name = robot.name;
113
+ this.robot = robot;
114
+ this.tree = tree;
115
+ this.clampJointLimits = options.clampJointLimits ?? true;
116
+ this.helperSize = options.helperSize ?? 0.15;
117
+ for (const [key, link] of Object.entries(robot.links)) {
118
+ this.linkObjects.set(key, new LinkObject(link));
119
+ this.linkKeyByPath.set(link.primPath, key);
120
+ }
121
+ for (const [key, joint] of Object.entries(robot.joints)) {
122
+ this.jointKeyByPath.set(joint.primPath, key);
123
+ }
124
+ this.attachRoot();
125
+ this.attachTreeEdges();
126
+ this.attachIsolatedLinks();
127
+ this.applyStageNormalization(robot, options);
128
+ if (options.applyInitialPose ?? true) this.applyInitialPose(robot);
129
+ for (const [key, obj] of this.linkObjects) this.restLocal.set(key, obj.matrix.toArray());
130
+ const debug = options.debugBakedTransforms;
131
+ this.debugBaked = debug ? {
132
+ anchorTolerance: (typeof debug === "object" ? debug.anchorTolerance : void 0) ?? 1e-3,
133
+ axisTolerance: (typeof debug === "object" ? debug.axisTolerance : void 0) ?? 0.01
134
+ } : null;
135
+ }
136
+ /** Orient (authored upAxis → target world up) and scale (metersPerUnit × unitScale) the root. */
137
+ applyStageNormalization(robot, options) {
138
+ const scale = (robot.metersPerUnit || 1) * (options.unitScale ?? 1);
139
+ if (scale !== 1) this.scale.setScalar(scale);
140
+ const rotateX = (angle) => this.quaternion.setFromAxisAngle(new THREE4.Vector3(1, 0, 0), angle);
141
+ if (options.worldUp) {
142
+ if (options.worldUp === "Y" && robot.upAxis === "Z") rotateX(-Math.PI / 2);
143
+ else if (options.worldUp === "Z" && robot.upAxis === "Y") rotateX(Math.PI / 2);
144
+ return;
145
+ }
146
+ const conv = options.upAxisConversion ?? "none";
147
+ if (conv === "Z" || conv === "auto" && robot.upAxis === "Z") rotateX(-Math.PI / 2);
148
+ }
149
+ /** Apply each joint's authored initial value, if any. */
150
+ applyInitialPose(robot) {
151
+ for (const [key, joint] of Object.entries(robot.joints)) {
152
+ if (joint.initialValue === void 0) continue;
153
+ this.jointObjects.get(key)?.setValue(joint.initialValue, this.clampJointLimits);
154
+ }
155
+ this.dirty = true;
156
+ }
157
+ attachRoot() {
158
+ const rootObj = this.linkObjects.get(this.tree.root);
159
+ if (!rootObj) return;
160
+ const rootJointKey = this.tree.rootJoint;
161
+ const rootLink = this.robot.links[this.tree.root];
162
+ if (rootJointKey) {
163
+ const j = this.robot.joints[rootJointKey];
164
+ if (j) setMatrix(rootObj, multiply(j.jointFrame0, invert(j.jointFrame1)));
165
+ } else if (rootLink?.worldTransform) {
166
+ setMatrix(rootObj, rootLink.worldTransform);
167
+ }
168
+ this.add(rootObj);
169
+ }
170
+ attachTreeEdges() {
171
+ for (const linkKey of this.tree.order) {
172
+ const node = this.tree.nodes[linkKey];
173
+ if (!node || node.parent === null || node.jointToParent === null) continue;
174
+ const parentObj = this.linkObjects.get(node.parent);
175
+ const childObj = this.linkObjects.get(linkKey);
176
+ const joint = this.robot.joints[node.jointToParent];
177
+ if (!parentObj || !childObj || !joint) continue;
178
+ this.attachJointChain(parentObj, childObj, node.jointToParent, joint);
179
+ }
180
+ }
181
+ /** Build `parent → frame0 → motion → frame1⁻¹ → child` for one joint. */
182
+ attachJointChain(parent, child, jointKey, joint) {
183
+ const frame0 = new THREE4.Group();
184
+ frame0.name = `${joint.name}:frame0`;
185
+ setMatrix(frame0, joint.jointFrame0);
186
+ const motion = new JointObject(joint);
187
+ const frame1Inv = new THREE4.Group();
188
+ frame1Inv.name = `${joint.name}:frame1Inv`;
189
+ setMatrix(frame1Inv, invert(joint.jointFrame1));
190
+ parent.add(frame0);
191
+ frame0.add(motion);
192
+ motion.add(frame1Inv);
193
+ frame1Inv.add(child);
194
+ this.jointObjects.set(jointKey, motion);
195
+ }
196
+ attachIsolatedLinks() {
197
+ for (const key of this.tree.isolatedLinks) {
198
+ const obj = this.linkObjects.get(key);
199
+ if (!obj || obj.parent) continue;
200
+ const worldTransform = this.robot.links[key]?.worldTransform;
201
+ if (worldTransform) setMatrix(obj, worldTransform);
202
+ this.add(obj);
203
+ }
204
+ }
205
+ // -- Naming --------------------------------------------------------------
206
+ /** Resolve a link reference — key or full prim path — to the extractor key. */
207
+ linkKey(ref) {
208
+ if (this.linkObjects.has(ref)) return ref;
209
+ return this.linkKeyByPath.get(ref) ?? ref;
210
+ }
211
+ /** Resolve a joint reference — key or full prim path — to the extractor key. */
212
+ jointKey(ref) {
213
+ if (this.jointObjects.has(ref)) return ref;
214
+ return this.jointKeyByPath.get(ref) ?? ref;
215
+ }
216
+ // -- Joint control -------------------------------------------------------
217
+ /**
218
+ * Set one joint value, addressed by key or full prim path. Unknown joints
219
+ * are ignored. Returns whether it applied. Always restores `"fk"` display
220
+ * mode first (see {@link setLinkTransforms}).
221
+ */
222
+ setJointValue(name, value) {
223
+ this.exitBakedMode();
224
+ const joint = this.jointObjects.get(this.jointKey(name));
225
+ if (!joint) return false;
226
+ joint.setValue(value, this.clampJointLimits);
227
+ this.dirty = true;
228
+ return true;
229
+ }
230
+ /**
231
+ * Set several joint values at once (matrix update is coalesced). Always
232
+ * restores `"fk"` display mode first, recomputing every link purely from
233
+ * joint values — even an empty batch returns from baked playback (see
234
+ * {@link setLinkTransforms}).
235
+ */
236
+ setJointValues(values) {
237
+ this.exitBakedMode();
238
+ for (const [name, value] of Object.entries(values)) {
239
+ const joint = this.jointObjects.get(this.jointKey(name));
240
+ if (joint) {
241
+ joint.setValue(value, this.clampJointLimits);
242
+ this.dirty = true;
243
+ }
244
+ }
245
+ }
246
+ getJointValue(name) {
247
+ return this.jointObjects.get(this.jointKey(name))?.value;
248
+ }
249
+ /** Recompute world matrices. Called lazily by the getters; safe to call directly. */
250
+ updateKinematics() {
251
+ this.updateMatrixWorld(true);
252
+ this.dirty = false;
253
+ }
254
+ ensureUpdated() {
255
+ if (this.dirty) this.updateKinematics();
256
+ }
257
+ // -- Baked link transforms (M23) -----------------------------------------
258
+ /**
259
+ * `"fk"` (default): link placements derive from joint values. `"baked"`:
260
+ * {@link setLinkTransforms} wrote link poses directly and the joints no
261
+ * longer constrain the display; any `setJointValue`-family call restores fk.
262
+ */
263
+ get displayMode() {
264
+ return this._displayMode;
265
+ }
266
+ /**
267
+ * Drive link world poses directly — the display path for baked recordings
268
+ * (Isaac Sim body-transform time samples, maximal-coordinate playback).
269
+ * usdview-like semantics: constraint deviations are shown, never corrected —
270
+ * {@link validateLinkTransforms} measures them,
271
+ * {@link jointValuesFromLinkTransforms} projects onto the joints instead.
272
+ *
273
+ * Enters `"baked"` display mode; joint values stay untouched. Return to fk
274
+ * with {@link setJointValues} (any batch, even `{}`), which recomputes
275
+ * every link purely from joint values.
276
+ *
277
+ * Keys are link keys or full prim paths; unknown keys warn once and are
278
+ * skipped. Unspecified links KEEP their current world pose — a track that
279
+ * omits a link means "it did not move". Poses are rigid, `quaternion` in
280
+ * `[x, y, z, w]` order, interpreted per `opts.space` (default `"world"`:
281
+ * the Three.js scene world after `worldUp` normalization — pair a Z-up
282
+ * meter track with `worldUp: "Z"`). Matrix updates are coalesced into the
283
+ * next render / world query. Returns the number of poses applied.
284
+ */
285
+ setLinkTransforms(poses, opts = {}) {
286
+ const targets = this.resolvePoseTargets(poses, opts.space ?? "world");
287
+ if (this._displayMode !== "baked") this.enterBakedMode();
288
+ const current = this.bakedStageWorld ?? /* @__PURE__ */ new Map();
289
+ const rels = this.bakedChainRel ?? /* @__PURE__ */ new Map();
290
+ const next = /* @__PURE__ */ new Map();
291
+ for (const key of this.tree.order) {
292
+ const node = this.tree.nodes[key];
293
+ const obj = this.linkObjects.get(key);
294
+ if (!node || !obj) continue;
295
+ const world = targets.get(key) ?? current.get(key);
296
+ if (!world) continue;
297
+ if (node.parent === null || node.jointToParent === null) {
298
+ next.set(key, world);
299
+ setMatrix(obj, world);
300
+ continue;
301
+ }
302
+ const parentWorld = next.get(node.parent);
303
+ const chainRel = rels.get(node.jointToParent);
304
+ if (!parentWorld || !chainRel) continue;
305
+ next.set(key, world);
306
+ setMatrix(obj, multiply(invert(multiply(parentWorld, chainRel)), world));
307
+ }
308
+ for (const key of this.tree.isolatedLinks) {
309
+ const obj = this.linkObjects.get(key);
310
+ const world = targets.get(key) ?? current.get(key);
311
+ if (!obj || !world) continue;
312
+ next.set(key, world);
313
+ setMatrix(obj, world);
314
+ }
315
+ this.bakedStageWorld = next;
316
+ this.dirty = true;
317
+ this.warnBakedDeviationOnce(next);
318
+ return targets.size;
319
+ }
320
+ /**
321
+ * Measure how far a link-pose batch deviates from the joint constraints,
322
+ * without touching the display. Unspecified links resolve to their current
323
+ * displayed pose, so the report predicts exactly what
324
+ * {@link setLinkTransforms} with the same batch would show.
325
+ *
326
+ * Keyed by joint prim path; covers every joint — fixed joints (`q` = 0
327
+ * check), loop joints dropped from the fk tree (closure error) and the
328
+ * world-fixed root attachment (a moved base against a fixed-base model).
329
+ * Typical signatures: a constant `anchorError` offset on every joint —
330
+ * recording/model mismatch (wrong version or scale); growth over time —
331
+ * maximal-coordinate solver drift; large uniform `axisError` — a
332
+ * coordinate-convention bug (Y/Z-up or quaternion order).
333
+ */
334
+ validateLinkTransforms(poses, opts = {}) {
335
+ const out = {};
336
+ for (const { joint, rel } of this.decomposeJoints(poses, opts.space ?? "world")) {
337
+ out[joint.primPath] = this.buildResidual(joint, rel.q, rel);
338
+ }
339
+ return out;
340
+ }
341
+ /**
342
+ * Project a link-pose batch onto the joint manifold: the closed-form 1-DOF
343
+ * joint values that best reproduce it, plus the same residuals as
344
+ * {@link validateLinkTransforms}. `values` covers the articulated tree
345
+ * joints, keyed by joint prim path, and feeds {@link setJointValues}
346
+ * directly — the constraint-respecting playback of the same track:
347
+ *
348
+ * ```ts
349
+ * robot.setJointValues(robot.jointValuesFromLinkTransforms(poses, { previous }).values);
350
+ * ```
351
+ *
352
+ * Residual `q` / `limitExceeded` always report the unclamped projection,
353
+ * also when `clampLimits` clamps `values`.
354
+ */
355
+ jointValuesFromLinkTransforms(poses, opts = {}) {
356
+ const values = {};
357
+ const residuals = {};
358
+ for (const { key, joint, rel } of this.decomposeJoints(poses, opts.space ?? "world")) {
359
+ let q = rel.q;
360
+ if (joint.type === "revolute" || joint.type === "continuous") {
361
+ const previous = opts.previous?.[joint.primPath] ?? opts.previous?.[key];
362
+ if (previous !== void 0) q = nearestAngleBranch(q, previous);
363
+ }
364
+ residuals[joint.primPath] = this.buildResidual(joint, q, rel);
365
+ if (!this.jointObjects.get(key)?.articulated) continue;
366
+ if (opts.clampLimits) {
367
+ if (joint.lower !== void 0 && q < joint.lower) q = joint.lower;
368
+ if (joint.upper !== void 0 && q > joint.upper) q = joint.upper;
369
+ }
370
+ values[joint.primPath] = q;
371
+ }
372
+ return { values, residuals };
373
+ }
374
+ /** Restore the constructed fk link placements (no-op when already `"fk"`). */
375
+ exitBakedMode() {
376
+ if (this._displayMode === "fk") return;
377
+ for (const [key, local] of this.restLocal) {
378
+ const obj = this.linkObjects.get(key);
379
+ if (obj) setMatrix(obj, local);
380
+ }
381
+ this.bakedStageWorld = null;
382
+ this.bakedChainRel = null;
383
+ this.bakedDebugWarned = false;
384
+ this._displayMode = "fk";
385
+ this.dirty = true;
386
+ }
387
+ /** Freeze the fk state a baked session builds on (joints cannot move while baked). */
388
+ enterBakedMode() {
389
+ const rels = this.computeChainRels();
390
+ this.bakedChainRel = rels;
391
+ this.bakedStageWorld = this.computeStageWorlds(rels);
392
+ this._displayMode = "baked";
393
+ }
394
+ /** `frame0 · motion(q) · frame1⁻¹` of every tree joint, from live joint values. */
395
+ computeChainRels() {
396
+ const rels = /* @__PURE__ */ new Map();
397
+ for (const [key, motion] of this.jointObjects) {
398
+ const joint = this.robot.joints[key];
399
+ if (!joint) continue;
400
+ motion.updateMatrix();
401
+ rels.set(
402
+ key,
403
+ multiplyAll([joint.jointFrame0, motion.matrix.toArray(), invert(joint.jointFrame1)])
404
+ );
405
+ }
406
+ return rels;
407
+ }
408
+ /** Stage-space world transform of every link under the current display state. */
409
+ computeStageWorlds(rels) {
410
+ const worlds = /* @__PURE__ */ new Map();
411
+ for (const key of this.tree.order) {
412
+ const node = this.tree.nodes[key];
413
+ const obj = this.linkObjects.get(key);
414
+ if (!node || !obj) continue;
415
+ const local = obj.matrix.toArray();
416
+ if (node.parent === null || node.jointToParent === null) {
417
+ worlds.set(key, local);
418
+ continue;
419
+ }
420
+ const parentWorld = worlds.get(node.parent);
421
+ const chainRel = rels.get(node.jointToParent);
422
+ if (!parentWorld || !chainRel) continue;
423
+ worlds.set(key, multiplyAll([parentWorld, chainRel, local]));
424
+ }
425
+ for (const key of this.tree.isolatedLinks) {
426
+ const obj = this.linkObjects.get(key);
427
+ if (obj) worlds.set(key, obj.matrix.toArray());
428
+ }
429
+ return worlds;
430
+ }
431
+ currentStageWorlds() {
432
+ return this.bakedStageWorld ?? this.computeStageWorlds(this.computeChainRels());
433
+ }
434
+ /** Resolve pose keys to link keys and convert each pose to a rigid stage-space matrix. */
435
+ resolvePoseTargets(poses, space) {
436
+ const targets = /* @__PURE__ */ new Map();
437
+ const entries = Object.entries(poses);
438
+ if (entries.length === 0) return targets;
439
+ const toStage = space === "world" ? this.sceneToStageConverter() : null;
440
+ const position = new THREE4.Vector3();
441
+ const quaternion = new THREE4.Quaternion();
442
+ const matrix = new THREE4.Matrix4();
443
+ for (const [ref, pose] of entries) {
444
+ const key = this.linkKey(ref);
445
+ if (!this.linkObjects.has(key)) {
446
+ this.warnUnknownPoseKey(ref);
447
+ continue;
448
+ }
449
+ position.fromArray(pose.position);
450
+ quaternion.fromArray(pose.quaternion).normalize();
451
+ toStage?.(position, quaternion);
452
+ targets.set(key, matrix.compose(position, quaternion, UNIT_SCALE).toArray());
453
+ }
454
+ return targets;
455
+ }
456
+ /**
457
+ * Scene world → stage space, undoing the robot's own world transform
458
+ * (up-axis rotation, unit scale, any user placement) as a similarity — so
459
+ * link locals stay rigid and the root keeps carrying the scale.
460
+ */
461
+ sceneToStageConverter() {
462
+ this.updateWorldMatrix(true, false);
463
+ const rootPosition = new THREE4.Vector3();
464
+ const rootQuaternion = new THREE4.Quaternion();
465
+ const rootScale = new THREE4.Vector3();
466
+ this.matrixWorld.decompose(rootPosition, rootQuaternion, rootScale);
467
+ const s = rootScale.x;
468
+ if (!this.warnedNonUniformScale && (Math.abs(rootScale.y - s) > 1e-6 * Math.abs(s) || Math.abs(rootScale.z - s) > 1e-6 * Math.abs(s))) {
469
+ this.warnedNonUniformScale = true;
470
+ console.warn(
471
+ `three-usd-robot: non-uniform world scale on "${this.name}"; space:"world" poses are off`
472
+ );
473
+ }
474
+ const invQuaternion = rootQuaternion.clone().invert();
475
+ const invScale = s !== 0 ? 1 / s : 1;
476
+ return (position, quaternion) => {
477
+ position.sub(rootPosition).applyQuaternion(invQuaternion).multiplyScalar(invScale);
478
+ quaternion.premultiply(invQuaternion);
479
+ };
480
+ }
481
+ /**
482
+ * Decompose every joint's parent→child transform under a pose batch
483
+ * (unspecified links resolve to their current displayed pose, mirroring
484
+ * {@link setLinkTransforms}). Pure — the display is untouched.
485
+ */
486
+ decomposeJoints(poses, space) {
487
+ const worlds = new Map(this.currentStageWorlds());
488
+ for (const [key, world] of this.resolvePoseTargets(poses, space)) worlds.set(key, world);
489
+ const out = [];
490
+ for (const [key, joint] of Object.entries(this.robot.joints)) {
491
+ const childWorld = worlds.get(joint.child);
492
+ const parentWorld = joint.parent === "" ? IDENTITY4 : worlds.get(joint.parent);
493
+ if (!childWorld || !parentWorld) continue;
494
+ out.push({ key, joint, rel: decomposeJointRelative(joint, parentWorld, childWorld) });
495
+ }
496
+ return out;
497
+ }
498
+ buildResidual(joint, q, rel) {
499
+ return {
500
+ anchorError: rel.anchorError * this.robot.metersPerUnit,
501
+ axisError: rel.axisError,
502
+ q,
503
+ limitExceeded: joint.lower !== void 0 && q < joint.lower - LIMIT_EPS || joint.upper !== void 0 && q > joint.upper + LIMIT_EPS
504
+ };
505
+ }
506
+ warnUnknownPoseKey(ref) {
507
+ if (this.warnedPoseKeys.has(ref)) return;
508
+ this.warnedPoseKeys.add(ref);
509
+ console.warn(`three-usd-robot: unknown link "${ref}" in a pose batch; ignoring`);
510
+ }
511
+ /** `debugBakedTransforms`: warn once per baked session when poses break the constraints. */
512
+ warnBakedDeviationOnce(worlds) {
513
+ if (!this.debugBaked || this.bakedDebugWarned) return;
514
+ const { anchorTolerance, axisTolerance } = this.debugBaked;
515
+ let count = 0;
516
+ let worst = null;
517
+ for (const joint of Object.values(this.robot.joints)) {
518
+ const childWorld = worlds.get(joint.child);
519
+ const parentWorld = joint.parent === "" ? IDENTITY4 : worlds.get(joint.parent);
520
+ if (!childWorld || !parentWorld) continue;
521
+ const rel = decomposeJointRelative(joint, parentWorld, childWorld);
522
+ const anchor = rel.anchorError * this.robot.metersPerUnit;
523
+ if (anchor <= anchorTolerance && rel.axisError <= axisTolerance) continue;
524
+ count++;
525
+ const score = anchor / anchorTolerance + rel.axisError / axisTolerance;
526
+ if (!worst || score > worst.score) {
527
+ worst = { path: joint.primPath, anchor, axis: rel.axisError, score };
528
+ }
529
+ }
530
+ if (!worst) return;
531
+ this.bakedDebugWarned = true;
532
+ console.warn(
533
+ `three-usd-robot: baked poses deviate from ${count} joint constraint(s) \u2014 worst ${worst.path}: anchor ${(worst.anchor * 1e3).toFixed(3)} mm, axis ${worst.axis.toFixed(4)} rad (recording/model mismatch? warned once per baked session)`
534
+ );
535
+ }
536
+ // -- Queries -------------------------------------------------------------
537
+ /** World matrix of a link, addressed by key or full prim path. */
538
+ getLinkWorldMatrix(name) {
539
+ const obj = this.linkObjects.get(this.linkKey(name));
540
+ if (!obj) throw new Error(`unknown link "${name}"`);
541
+ this.ensureUpdated();
542
+ return obj.matrixWorld.clone();
543
+ }
544
+ getLinkWorldPosition(name) {
545
+ return new THREE4.Vector3().setFromMatrixPosition(this.getLinkWorldMatrix(name));
546
+ }
547
+ /** Link object by key or full prim path. */
548
+ getLinkObject(name) {
549
+ return this.linkObjects.get(this.linkKey(name));
550
+ }
551
+ /** Joint object by key or full prim path. */
552
+ getJointObject(name) {
553
+ return this.jointObjects.get(this.jointKey(name));
554
+ }
555
+ /**
556
+ * Table of link prim path → {@link LinkObject}. Prim paths are the
557
+ * collision-proof way to pin a link (e.g. to attach tools or gizmos).
558
+ */
559
+ getLinkObjectsByPath() {
560
+ const out = /* @__PURE__ */ new Map();
561
+ for (const [path, key] of this.linkKeyByPath) {
562
+ const obj = this.linkObjects.get(key);
563
+ if (obj) out.set(path, obj);
564
+ }
565
+ return out;
566
+ }
567
+ /**
568
+ * Table of joint prim path → {@link JointObject}, covering the joints
569
+ * realized in the kinematic tree (loop joints have no motion node).
570
+ */
571
+ getJointObjectsByPath() {
572
+ const out = /* @__PURE__ */ new Map();
573
+ for (const [path, key] of this.jointKeyByPath) {
574
+ const obj = this.jointObjects.get(key);
575
+ if (obj) out.set(path, obj);
576
+ }
577
+ return out;
578
+ }
579
+ getJoints() {
580
+ return Object.values(this.robot.joints);
581
+ }
582
+ getLinks() {
583
+ return Object.values(this.robot.links);
584
+ }
585
+ /** Names of the articulated (controllable) joints. */
586
+ getJointNames() {
587
+ return [...this.jointObjects.keys()];
588
+ }
589
+ getLinkNames() {
590
+ return [...this.linkObjects.keys()];
591
+ }
592
+ getKinematicTree() {
593
+ return this.tree;
594
+ }
595
+ /** Authored stage up-axis (`"Y"` or `"Z"`) — unaffected by `worldUp` normalization. */
596
+ get upAxis() {
597
+ return this.robot.upAxis;
598
+ }
599
+ /** Authored stage scale in meters per unit (already applied to the root). */
600
+ get metersPerUnit() {
601
+ return this.robot.metersPerUnit;
602
+ }
603
+ // -- Animation playback --------------------------------------------------
604
+ /** Playback rate in time codes per second (from the stage; default 24). */
605
+ getTimeCodesPerSecond() {
606
+ return this.robot.timeCodesPerSecond ?? 24;
607
+ }
608
+ /** Whether any joint has a time-sampled trajectory. */
609
+ hasAnimation() {
610
+ return Object.values(this.robot.joints).some((j) => j.valueSamples !== void 0);
611
+ }
612
+ /**
613
+ * Animation range in time codes: the union of authored joint sample ranges,
614
+ * falling back to the stage `startTimeCode`/`endTimeCode`. `null` if neither.
615
+ */
616
+ getTimeRange() {
617
+ let start = Number.POSITIVE_INFINITY;
618
+ let end = Number.NEGATIVE_INFINITY;
619
+ for (const joint of Object.values(this.robot.joints)) {
620
+ const times = joint.valueSamples?.times;
621
+ if (!times || times.length === 0) continue;
622
+ start = Math.min(start, times[0]);
623
+ end = Math.max(end, times[times.length - 1]);
624
+ }
625
+ if (start <= end) return { start, end };
626
+ const { startTimeCode, endTimeCode } = this.robot;
627
+ if (startTimeCode !== void 0 && endTimeCode !== void 0) {
628
+ return { start: startTimeCode, end: endTimeCode };
629
+ }
630
+ return null;
631
+ }
632
+ /** Sample every animated joint at time code `t` and apply the values (an fk drive — leaves baked mode). */
633
+ setTime(t) {
634
+ this.exitBakedMode();
635
+ for (const [key, joint] of Object.entries(this.robot.joints)) {
636
+ if (joint.valueSamples) this.setJointValue(key, interpolate(joint.valueSamples, t));
637
+ }
638
+ }
639
+ // -- Display toggles -----------------------------------------------------
640
+ get showVisual() {
641
+ return this._showVisual;
642
+ }
643
+ set showVisual(v) {
644
+ this._showVisual = v;
645
+ this.setKindVisibility("visual", v);
646
+ }
647
+ get showCollision() {
648
+ return this._showCollision;
649
+ }
650
+ set showCollision(v) {
651
+ this._showCollision = v;
652
+ this.setKindVisibility("collision", v);
653
+ }
654
+ get showJointAxes() {
655
+ return this._showJointAxes;
656
+ }
657
+ set showJointAxes(v) {
658
+ this._showJointAxes = v;
659
+ if (v && this.jointAxesHelpers.length === 0) {
660
+ for (const joint of this.jointObjects.values()) {
661
+ const h = new THREE4.AxesHelper(this.helperSize);
662
+ h.name = `${joint.jointName}:axes`;
663
+ joint.add(h);
664
+ this.jointAxesHelpers.push(h);
665
+ }
666
+ }
667
+ for (const h of this.jointAxesHelpers) h.visible = v;
668
+ }
669
+ get showLinkFrames() {
670
+ return this._showLinkFrames;
671
+ }
672
+ set showLinkFrames(v) {
673
+ this._showLinkFrames = v;
674
+ if (v && this.linkFrameHelpers.length === 0) {
675
+ for (const link of this.linkObjects.values()) {
676
+ const h = new THREE4.AxesHelper(this.helperSize);
677
+ h.name = `${link.linkName}:frame`;
678
+ link.add(h);
679
+ this.linkFrameHelpers.push(h);
680
+ }
681
+ }
682
+ for (const h of this.linkFrameHelpers) h.visible = v;
683
+ }
684
+ setKindVisibility(kind, visible) {
685
+ this.traverse((o) => {
686
+ if (o.userData.kind === kind) o.visible = visible;
687
+ });
688
+ }
689
+ };
690
+ var UNIT_SCALE = new THREE4.Vector3(1, 1, 1);
691
+ var IDENTITY4 = identity4();
692
+ var LIMIT_EPS = 1e-9;
693
+ function setMatrix(obj, m) {
694
+ obj.matrixAutoUpdate = false;
695
+ obj.matrix.fromArray(m);
696
+ obj.matrixWorldNeedsUpdate = true;
697
+ }
698
+
699
+ export { JointObject, LinkObject, ThreeUsdRobot, axisVector };
700
+ //# sourceMappingURL=chunk-ENWFOYHU.js.map
701
+ //# sourceMappingURL=chunk-ENWFOYHU.js.map