three-usd-robot 0.4.0 → 0.5.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,841 @@
1
+ import { AssetPath, identity4, multiply, computeLocalTransform, invert, interpolate, DefaultAssetResolver, CrateReader, openUsdz, extractRobotDescription, buildKinematicTree, Stage, composeLayer, composeFile, crateToUsdaFile } from './chunk-36T6YOEX.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
+ jointType;
18
+ axisToken;
19
+ axis;
20
+ lower;
21
+ upper;
22
+ _value = 0;
23
+ constructor(joint) {
24
+ super();
25
+ this.name = joint.name;
26
+ this.jointName = joint.name;
27
+ this.jointType = joint.type;
28
+ this.axisToken = joint.axis;
29
+ this.axis = axisVector(joint.axis);
30
+ this.lower = joint.lower;
31
+ this.upper = joint.upper;
32
+ }
33
+ get value() {
34
+ return this._value;
35
+ }
36
+ get articulated() {
37
+ return this.jointType !== "fixed";
38
+ }
39
+ /**
40
+ * Set the joint value (radians for revolute/continuous, length for prismatic).
41
+ * Optionally clamps to authored limits. Returns the value actually applied.
42
+ */
43
+ setValue(value, clampToLimits = true) {
44
+ if (!this.articulated) return this._value;
45
+ let v = value;
46
+ if (clampToLimits) {
47
+ if (this.lower !== void 0 && v < this.lower) v = this.lower;
48
+ if (this.upper !== void 0 && v > this.upper) v = this.upper;
49
+ }
50
+ this._value = v;
51
+ if (this.jointType === "prismatic") {
52
+ this.position.copy(this.axis).multiplyScalar(v);
53
+ this.quaternion.identity();
54
+ } else {
55
+ this.quaternion.setFromAxisAngle(this.axis, v);
56
+ this.position.set(0, 0, 0);
57
+ }
58
+ return v;
59
+ }
60
+ };
61
+ var LinkObject = class extends THREE4.Object3D {
62
+ isLinkObject = true;
63
+ linkName;
64
+ primPath;
65
+ constructor(link) {
66
+ super();
67
+ this.name = link.name;
68
+ this.linkName = link.name;
69
+ this.primPath = link.primPath;
70
+ this.matrixAutoUpdate = false;
71
+ }
72
+ };
73
+
74
+ // src/three/MaterialBinding.ts
75
+ var DIFFUSE_INPUTS = [
76
+ "inputs:diffuseColor",
77
+ // UsdPreviewSurface
78
+ "inputs:diffuse_color_constant",
79
+ // OmniPBR
80
+ "inputs:diffuse_tint",
81
+ "inputs:base_color",
82
+ "inputs:baseColor"
83
+ ];
84
+ var OPACITY_INPUTS = ["inputs:opacity", "inputs:opacity_constant"];
85
+ var OPACITY_THRESHOLD_INPUTS = ["inputs:opacityThreshold", "inputs:opacity_threshold"];
86
+ var METALLIC_INPUTS = ["inputs:metallic", "inputs:metallic_constant"];
87
+ var ROUGHNESS_INPUTS = ["inputs:roughness", "inputs:reflection_roughness_constant"];
88
+ var EMISSIVE_INPUTS = ["inputs:emissiveColor", "inputs:emissive_color"];
89
+ var SURFACE_OUTPUTS = ["outputs:surface", "outputs:mdl:surface"];
90
+ var TEXTURE_LOOKUPS = {
91
+ color: {
92
+ surface: ["inputs:diffuseColor"],
93
+ direct: ["inputs:diffuse_texture", "inputs:diffuse_color_texture"]
94
+ },
95
+ opacity: {
96
+ surface: ["inputs:opacity"],
97
+ direct: ["inputs:opacity_texture", "inputs:opacity_color_texture"]
98
+ },
99
+ normal: {
100
+ surface: ["inputs:normal"],
101
+ direct: ["inputs:normalmap_texture", "inputs:normal_texture"]
102
+ },
103
+ roughness: {
104
+ surface: ["inputs:roughness"],
105
+ direct: ["inputs:reflectionroughness_texture", "inputs:roughness_texture"]
106
+ },
107
+ metalness: {
108
+ surface: ["inputs:metallic"],
109
+ direct: ["inputs:metallic_texture"]
110
+ },
111
+ occlusion: {
112
+ surface: ["inputs:occlusion"],
113
+ direct: ["inputs:ao_texture", "inputs:occlusion_texture"]
114
+ },
115
+ emissive: {
116
+ surface: ["inputs:emissiveColor"],
117
+ direct: ["inputs:emissive_color_texture", "inputs:emissive_mask_texture"]
118
+ }
119
+ };
120
+ function resolveBoundMaterial(stage, prim) {
121
+ const materialPath = findBinding(prim);
122
+ if (!materialPath) return void 0;
123
+ const material = stage.GetPrimAtPath(materialPath);
124
+ if (!material) return void 0;
125
+ const shader = findSurfaceShader(material);
126
+ if (!shader) return void 0;
127
+ const result = {};
128
+ const color = firstColor(shader, DIFFUSE_INPUTS);
129
+ if (color) result.color = color;
130
+ const opacity = firstNumber(shader, OPACITY_INPUTS);
131
+ if (opacity !== void 0) result.opacity = opacity;
132
+ const opacityThreshold = firstNumber(shader, OPACITY_THRESHOLD_INPUTS);
133
+ if (opacityThreshold !== void 0) result.opacityThreshold = opacityThreshold;
134
+ const metalness = firstNumber(shader, METALLIC_INPUTS);
135
+ if (metalness !== void 0) result.metalness = metalness;
136
+ const roughness = firstNumber(shader, ROUGHNESS_INPUTS);
137
+ if (roughness !== void 0) result.roughness = roughness;
138
+ const emissive = firstColor(shader, EMISSIVE_INPUTS);
139
+ if (emissive && shader.GetAttribute("inputs:enable_emission").Get() !== false) {
140
+ result.emissiveColor = emissive;
141
+ }
142
+ const colorTex = findTexture(shader, TEXTURE_LOOKUPS.color);
143
+ if (colorTex !== void 0) result.colorTexture = colorTex;
144
+ const opacityTex = findTexture(shader, TEXTURE_LOOKUPS.opacity);
145
+ if (opacityTex !== void 0) result.opacityTexture = opacityTex;
146
+ const normal = findTexture(shader, TEXTURE_LOOKUPS.normal);
147
+ if (normal !== void 0) result.normalTexture = normal;
148
+ const roughTex = findTexture(shader, TEXTURE_LOOKUPS.roughness);
149
+ if (roughTex !== void 0) result.roughnessTexture = roughTex;
150
+ const metalTex = findTexture(shader, TEXTURE_LOOKUPS.metalness);
151
+ if (metalTex !== void 0) result.metalnessTexture = metalTex;
152
+ const aoTex = findTexture(shader, TEXTURE_LOOKUPS.occlusion);
153
+ if (aoTex !== void 0) result.occlusionTexture = aoTex;
154
+ const emissiveTex = findTexture(shader, TEXTURE_LOOKUPS.emissive);
155
+ if (emissiveTex !== void 0) result.emissiveTexture = emissiveTex;
156
+ return result;
157
+ }
158
+ function findTexture(shader, lookup) {
159
+ for (const name of lookup.direct) {
160
+ const v = shader.GetAttribute(name).Get();
161
+ if (v instanceof AssetPath && v.path) return { path: v.path };
162
+ }
163
+ for (const name of lookup.surface) {
164
+ const conn = shader.GetAttribute(name).GetConnections()[0];
165
+ if (!conn) continue;
166
+ const texPrim = shader.GetStage().GetPrimAtPath(conn.split(".")[0]);
167
+ if (!texPrim) continue;
168
+ const file = texPrim.GetAttribute("inputs:file").Get();
169
+ if (file instanceof AssetPath && file.path) return readUvTexture(texPrim, file.path);
170
+ }
171
+ return void 0;
172
+ }
173
+ var WRAP_VALUES = /* @__PURE__ */ new Set(["repeat", "clamp", "mirror", "black"]);
174
+ function readUvTexture(texPrim, path) {
175
+ const tex = { path };
176
+ const wrapS = texPrim.GetAttribute("inputs:wrapS").Get();
177
+ if (typeof wrapS === "string" && WRAP_VALUES.has(wrapS)) tex.wrapS = wrapS;
178
+ const wrapT = texPrim.GetAttribute("inputs:wrapT").Get();
179
+ if (typeof wrapT === "string" && WRAP_VALUES.has(wrapT)) tex.wrapT = wrapT;
180
+ const scale = numArray(texPrim, "inputs:scale", 4);
181
+ if (scale) tex.scale = scale;
182
+ const bias = numArray(texPrim, "inputs:bias", 4);
183
+ if (bias) tex.bias = bias;
184
+ const transform = readTransform2d(texPrim);
185
+ if (transform) tex.transform = transform;
186
+ return tex;
187
+ }
188
+ function readTransform2d(texPrim) {
189
+ const conn = texPrim.GetAttribute("inputs:st").GetConnections()[0];
190
+ if (!conn) return void 0;
191
+ const node = texPrim.GetStage().GetPrimAtPath(conn.split(".")[0]);
192
+ if (!node || node.GetAttribute("info:id").Get() !== "UsdTransform2d") return void 0;
193
+ const transform = {};
194
+ const translation = numArray(node, "inputs:translation", 2);
195
+ if (translation) transform.translation = translation;
196
+ const scale = numArray(node, "inputs:scale", 2);
197
+ if (scale) transform.scale = scale;
198
+ const rotation = node.GetAttribute("inputs:rotation").Get();
199
+ if (typeof rotation === "number") transform.rotation = rotation;
200
+ return Object.keys(transform).length > 0 ? transform : void 0;
201
+ }
202
+ function numArray(prim, name, length) {
203
+ const v = prim.GetAttribute(name).Get();
204
+ if (Array.isArray(v) && v.length >= length && v.every((n) => typeof n === "number")) {
205
+ return v.slice(0, length);
206
+ }
207
+ return void 0;
208
+ }
209
+ function findBinding(prim) {
210
+ let p = prim;
211
+ while (p) {
212
+ const targets = p.GetRelationship("material:binding").GetTargets();
213
+ if (targets.length > 0) return targets[0];
214
+ p = p.GetParent();
215
+ }
216
+ return void 0;
217
+ }
218
+ function findSurfaceShader(material) {
219
+ for (const out of SURFACE_OUTPUTS) {
220
+ const conn = material.GetAttribute(out).GetConnections()[0];
221
+ if (conn) {
222
+ const shaderPath = conn.split(".")[0];
223
+ const shader = material.GetStage().GetPrimAtPath(shaderPath);
224
+ if (shader) return shader;
225
+ }
226
+ }
227
+ return material.GetChildren().find((c) => c.GetTypeName() === "Shader") ?? void 0;
228
+ }
229
+ function firstColor(shader, names) {
230
+ for (const name of names) {
231
+ const v = shader.GetAttribute(name).Get();
232
+ if (Array.isArray(v) && v.length >= 3 && v.every((n) => typeof n === "number")) {
233
+ return [v[0], v[1], v[2]];
234
+ }
235
+ }
236
+ return void 0;
237
+ }
238
+ function firstNumber(shader, names) {
239
+ for (const name of names) {
240
+ const v = shader.GetAttribute(name).Get();
241
+ if (typeof v === "number") return v;
242
+ }
243
+ return void 0;
244
+ }
245
+ var DEFAULT_COLOR = 10132122;
246
+ function buildMeshGeometry(meshPrim) {
247
+ const points = meshPrim.GetAttribute("points").Get();
248
+ if (!isVec3Array(points) || points.length === 0) return null;
249
+ const geometry = new THREE4.BufferGeometry();
250
+ geometry.setAttribute("position", new THREE4.Float32BufferAttribute(flat3(points), 3));
251
+ const counts = meshPrim.GetAttribute("faceVertexCounts").Get();
252
+ const indices = meshPrim.GetAttribute("faceVertexIndices").Get();
253
+ if (isNumberArray(counts) && isNumberArray(indices)) {
254
+ geometry.setIndex(triangulate(counts, indices));
255
+ } else if (isNumberArray(indices)) {
256
+ geometry.setIndex(indices.slice());
257
+ }
258
+ const normals = meshPrim.GetAttribute("normals").Get();
259
+ if (isVec3Array(normals) && normals.length === points.length) {
260
+ geometry.setAttribute("normal", new THREE4.Float32BufferAttribute(flat3(normals), 3));
261
+ } else {
262
+ geometry.computeVertexNormals();
263
+ }
264
+ const st = meshPrim.GetAttribute("primvars:st").Get();
265
+ if (isVec2Array(st) && st.length === points.length) {
266
+ geometry.setAttribute("uv", new THREE4.Float32BufferAttribute(flat2(st), 2));
267
+ }
268
+ return geometry;
269
+ }
270
+ function buildMeshMaterial(meshPrim, stage, textures) {
271
+ const color = new THREE4.Color(DEFAULT_COLOR);
272
+ let opacity = 1;
273
+ const bound = stage ? resolveBoundMaterial(stage, meshPrim) : void 0;
274
+ if (bound?.color) {
275
+ color.setRGB(bound.color[0], bound.color[1], bound.color[2]);
276
+ } else {
277
+ const displayColor = meshPrim.GetAttribute("primvars:displayColor").Get();
278
+ if (isVec3Array(displayColor) && displayColor[0]) {
279
+ const [r, g, b] = displayColor[0];
280
+ color.setRGB(r, g, b);
281
+ }
282
+ }
283
+ if (bound?.opacity !== void 0) opacity = bound.opacity;
284
+ const tex = (rt, cs) => rt && textures ? textures(rt.path, {
285
+ colorSpace: cs,
286
+ ...rt.wrapS ? { wrapS: rt.wrapS } : {},
287
+ ...rt.wrapT ? { wrapT: rt.wrapT } : {},
288
+ ...rt.transform ? { transform: rt.transform } : {}
289
+ }) : null;
290
+ const map = tex(bound?.colorTexture, "srgb");
291
+ if (map) {
292
+ const s = bound?.colorTexture?.scale;
293
+ if (s) color.setRGB(s[0], s[1], s[2]);
294
+ else color.setRGB(1, 1, 1);
295
+ }
296
+ const normalMap = tex(bound?.normalTexture, "linear");
297
+ const roughnessMap = tex(bound?.roughnessTexture, "linear");
298
+ const metalnessMap = tex(bound?.metalnessTexture, "linear");
299
+ const aoMap = tex(bound?.occlusionTexture, "linear");
300
+ const emissiveMap = tex(bound?.emissiveTexture, "srgb");
301
+ const metalness = bound?.metalness ?? bound?.metalnessTexture?.scale?.[0] ?? (metalnessMap ? 1 : 0.1);
302
+ const roughness = bound?.roughness ?? bound?.roughnessTexture?.scale?.[0] ?? (roughnessMap ? 1 : 0.8);
303
+ const emissive = new THREE4.Color(0);
304
+ if (bound?.emissiveColor) {
305
+ emissive.setRGB(bound.emissiveColor[0], bound.emissiveColor[1], bound.emissiveColor[2]);
306
+ } else if (emissiveMap) {
307
+ emissive.setRGB(1, 1, 1);
308
+ }
309
+ const opacityTex = bound?.opacityTexture;
310
+ const sharesColorMap = !!(opacityTex && opacityTex.path === bound?.colorTexture?.path);
311
+ const alphaMap = sharesColorMap ? null : tex(opacityTex, "linear");
312
+ const hasAlphaSource = opacity < 1 || sharesColorMap || !!alphaMap;
313
+ const threshold = bound?.opacityThreshold;
314
+ const alphaTest = threshold !== void 0 && threshold > 0 ? threshold : 0;
315
+ const transparent = alphaTest === 0 && hasAlphaSource;
316
+ const doubleSided = meshPrim.GetAttribute("doubleSided").Get() === true;
317
+ return new THREE4.MeshStandardMaterial({
318
+ color,
319
+ metalness,
320
+ roughness,
321
+ emissive,
322
+ transparent,
323
+ opacity,
324
+ ...alphaTest > 0 ? { alphaTest } : {},
325
+ side: doubleSided ? THREE4.DoubleSide : THREE4.FrontSide,
326
+ ...map ? { map } : {},
327
+ ...alphaMap ? { alphaMap } : {},
328
+ ...normalMap ? { normalMap } : {},
329
+ ...roughnessMap ? { roughnessMap } : {},
330
+ ...metalnessMap ? { metalnessMap } : {},
331
+ ...aoMap ? { aoMap } : {},
332
+ ...emissiveMap ? { emissiveMap } : {}
333
+ });
334
+ }
335
+ function bindRobotMeshes(stage, robot3d, desc, options = {}) {
336
+ const loadVisuals = options.loadVisuals ?? true;
337
+ const loadCollisions = options.loadCollisions ?? false;
338
+ const textures = options.textureProvider;
339
+ for (const [key, link] of Object.entries(desc.links)) {
340
+ const linkObj = robot3d.getLinkObject(key);
341
+ const linkPrim = stage.GetPrimAtPath(link.primPath);
342
+ if (!linkObj || !linkPrim) continue;
343
+ const collisionSet = new Set(link.collisionPrims ?? []);
344
+ if (loadVisuals) {
345
+ for (const meshPath of link.visualPrims) {
346
+ if (collisionSet.has(meshPath)) continue;
347
+ attachMesh(stage, linkPrim, meshPath, linkObj, "visual", textures);
348
+ }
349
+ }
350
+ if (loadCollisions) {
351
+ for (const meshPath of link.collisionPrims ?? []) {
352
+ attachMesh(stage, linkPrim, meshPath, linkObj, "collision", textures);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ function attachMesh(stage, linkPrim, meshPath, parent, kind, textures) {
358
+ const meshPrim = stage.GetPrimAtPath(meshPath);
359
+ if (!meshPrim) return;
360
+ const geometry = buildMeshGeometry(meshPrim);
361
+ if (!geometry) return;
362
+ const mesh = new THREE4.Mesh(geometry, buildMeshMaterial(meshPrim, stage, textures));
363
+ mesh.name = meshPrim.GetName();
364
+ mesh.userData.kind = kind;
365
+ mesh.userData.primPath = meshPath;
366
+ if (kind === "collision") mesh.visible = false;
367
+ mesh.matrixAutoUpdate = false;
368
+ mesh.matrix.fromArray(relativeTransform(linkPrim, meshPrim));
369
+ mesh.matrixWorldNeedsUpdate = true;
370
+ parent.add(mesh);
371
+ }
372
+ function relativeTransform(linkPrim, meshPrim) {
373
+ const chain = [];
374
+ let p = meshPrim;
375
+ const stop = linkPrim.GetPath();
376
+ while (p && p.GetPath() !== stop) {
377
+ chain.push(p);
378
+ p = p.GetParent();
379
+ }
380
+ chain.reverse();
381
+ let m = identity4();
382
+ for (const prim of chain) {
383
+ m = multiply(m, computeLocalTransform(prim).matrix);
384
+ }
385
+ return m;
386
+ }
387
+ function triangulate(faceVertexCounts, faceVertexIndices) {
388
+ const tris = [];
389
+ let offset = 0;
390
+ for (const count of faceVertexCounts) {
391
+ for (let k = 2; k < count; k++) {
392
+ tris.push(
393
+ faceVertexIndices[offset],
394
+ faceVertexIndices[offset + k - 1],
395
+ faceVertexIndices[offset + k]
396
+ );
397
+ }
398
+ offset += count;
399
+ }
400
+ return tris;
401
+ }
402
+ function flat3(v) {
403
+ const out = new Array(v.length * 3);
404
+ for (let i = 0; i < v.length; i++) {
405
+ out[i * 3] = v[i][0];
406
+ out[i * 3 + 1] = v[i][1];
407
+ out[i * 3 + 2] = v[i][2];
408
+ }
409
+ return out;
410
+ }
411
+ function flat2(v) {
412
+ const out = new Array(v.length * 2);
413
+ for (let i = 0; i < v.length; i++) {
414
+ out[i * 2] = v[i][0];
415
+ out[i * 2 + 1] = v[i][1];
416
+ }
417
+ return out;
418
+ }
419
+ function isNumberArray(v) {
420
+ return Array.isArray(v) && v.every((n) => typeof n === "number");
421
+ }
422
+ function isVec3Array(v) {
423
+ return Array.isArray(v) && v.every((e) => Array.isArray(e) && e.length === 3);
424
+ }
425
+ function isVec2Array(v) {
426
+ return Array.isArray(v) && v.every((e) => Array.isArray(e) && e.length === 2);
427
+ }
428
+ var DEG2RAD = Math.PI / 180;
429
+ function createTextureProvider(resolver, baseUrl) {
430
+ const images = /* @__PURE__ */ new Map();
431
+ const imageFor = (url) => {
432
+ let p = images.get(url);
433
+ if (!p) {
434
+ p = loadImage(resolver, url);
435
+ images.set(url, p);
436
+ }
437
+ return p;
438
+ };
439
+ return (assetPath, options = {}) => {
440
+ let url;
441
+ try {
442
+ url = resolver.resolve(assetPath, baseUrl);
443
+ } catch {
444
+ return null;
445
+ }
446
+ const texture = new THREE4.Texture();
447
+ texture.colorSpace = options.colorSpace === "linear" ? THREE4.NoColorSpace : THREE4.SRGBColorSpace;
448
+ texture.wrapS = toThreeWrap(options.wrapS);
449
+ texture.wrapT = toThreeWrap(options.wrapT);
450
+ applyTransform(texture, options.transform);
451
+ imageFor(url).then((image) => {
452
+ texture.image = image;
453
+ texture.needsUpdate = true;
454
+ }).catch(() => {
455
+ });
456
+ return texture;
457
+ };
458
+ }
459
+ async function loadImage(resolver, url) {
460
+ const bytes = resolver.fetchBytes ? await resolver.fetchBytes(url) : new TextEncoder().encode(await resolver.fetchText(url));
461
+ const blob = new Blob([bytes], { type: mimeOf(url) });
462
+ const objectUrl = URL.createObjectURL(blob);
463
+ try {
464
+ return await new Promise((resolve, reject) => {
465
+ const img = new Image();
466
+ img.onload = () => resolve(img);
467
+ img.onerror = () => reject(new Error(`failed to decode texture: ${url}`));
468
+ img.src = objectUrl;
469
+ });
470
+ } finally {
471
+ URL.revokeObjectURL(objectUrl);
472
+ }
473
+ }
474
+ function toThreeWrap(wrap) {
475
+ switch (wrap) {
476
+ case "clamp":
477
+ case "black":
478
+ return THREE4.ClampToEdgeWrapping;
479
+ case "mirror":
480
+ return THREE4.MirroredRepeatWrapping;
481
+ default:
482
+ return THREE4.RepeatWrapping;
483
+ }
484
+ }
485
+ function applyTransform(texture, t) {
486
+ if (!t) return;
487
+ if (t.scale) texture.repeat.set(t.scale[0], t.scale[1]);
488
+ if (t.translation) texture.offset.set(t.translation[0], t.translation[1]);
489
+ if (t.rotation !== void 0) {
490
+ texture.rotation = t.rotation * DEG2RAD;
491
+ texture.center.set(0, 0);
492
+ }
493
+ }
494
+ function mimeOf(url) {
495
+ if (/\.jpe?g$/i.test(url)) return "image/jpeg";
496
+ if (/\.webp$/i.test(url)) return "image/webp";
497
+ return "image/png";
498
+ }
499
+ var ThreeUsdRobot = class extends THREE4.Object3D {
500
+ isThreeUsdRobot = true;
501
+ robot;
502
+ tree;
503
+ clampJointLimits;
504
+ linkObjects = /* @__PURE__ */ new Map();
505
+ jointObjects = /* @__PURE__ */ new Map();
506
+ dirty = true;
507
+ helperSize;
508
+ _showVisual = true;
509
+ _showCollision = false;
510
+ _showJointAxes = false;
511
+ _showLinkFrames = false;
512
+ jointAxesHelpers = [];
513
+ linkFrameHelpers = [];
514
+ constructor(robot, tree, options = {}) {
515
+ super();
516
+ this.name = robot.name;
517
+ this.robot = robot;
518
+ this.tree = tree;
519
+ this.clampJointLimits = options.clampJointLimits ?? true;
520
+ this.helperSize = options.helperSize ?? 0.15;
521
+ for (const [key, link] of Object.entries(robot.links)) {
522
+ this.linkObjects.set(key, new LinkObject(link));
523
+ }
524
+ this.attachRoot();
525
+ this.attachTreeEdges();
526
+ this.attachIsolatedLinks();
527
+ this.applyStageNormalization(robot, options);
528
+ if (options.applyInitialPose ?? true) this.applyInitialPose(robot);
529
+ }
530
+ /** Orient (Z-up → Y-up) and scale (metersPerUnit × unitScale) the robot root. */
531
+ applyStageNormalization(robot, options) {
532
+ const scale = (robot.metersPerUnit || 1) * (options.unitScale ?? 1);
533
+ if (scale !== 1) this.scale.setScalar(scale);
534
+ const conv = options.upAxisConversion ?? "none";
535
+ const toY = conv === "Z" || conv === "auto" && robot.upAxis === "Z";
536
+ if (toY) this.quaternion.setFromAxisAngle(new THREE4.Vector3(1, 0, 0), -Math.PI / 2);
537
+ }
538
+ /** Apply each joint's authored initial value, if any. */
539
+ applyInitialPose(robot) {
540
+ for (const [key, joint] of Object.entries(robot.joints)) {
541
+ if (joint.initialValue === void 0) continue;
542
+ this.jointObjects.get(key)?.setValue(joint.initialValue, this.clampJointLimits);
543
+ }
544
+ this.dirty = true;
545
+ }
546
+ attachRoot() {
547
+ const rootObj = this.linkObjects.get(this.tree.root);
548
+ if (!rootObj) return;
549
+ const rootJointKey = this.tree.rootJoint;
550
+ if (rootJointKey) {
551
+ const j = this.robot.joints[rootJointKey];
552
+ if (j) setMatrix(rootObj, multiply(j.jointFrame0, invert(j.jointFrame1)));
553
+ }
554
+ this.add(rootObj);
555
+ }
556
+ attachTreeEdges() {
557
+ for (const linkKey of this.tree.order) {
558
+ const node = this.tree.nodes[linkKey];
559
+ if (!node || node.parent === null || node.jointToParent === null) continue;
560
+ const parentObj = this.linkObjects.get(node.parent);
561
+ const childObj = this.linkObjects.get(linkKey);
562
+ const joint = this.robot.joints[node.jointToParent];
563
+ if (!parentObj || !childObj || !joint) continue;
564
+ this.attachJointChain(parentObj, childObj, node.jointToParent, joint);
565
+ }
566
+ }
567
+ /** Build `parent → frame0 → motion → frame1⁻¹ → child` for one joint. */
568
+ attachJointChain(parent, child, jointKey, joint) {
569
+ const frame0 = new THREE4.Group();
570
+ frame0.name = `${joint.name}:frame0`;
571
+ setMatrix(frame0, joint.jointFrame0);
572
+ const motion = new JointObject(joint);
573
+ const frame1Inv = new THREE4.Group();
574
+ frame1Inv.name = `${joint.name}:frame1Inv`;
575
+ setMatrix(frame1Inv, invert(joint.jointFrame1));
576
+ parent.add(frame0);
577
+ frame0.add(motion);
578
+ motion.add(frame1Inv);
579
+ frame1Inv.add(child);
580
+ this.jointObjects.set(jointKey, motion);
581
+ }
582
+ attachIsolatedLinks() {
583
+ for (const key of this.tree.isolatedLinks) {
584
+ const obj = this.linkObjects.get(key);
585
+ if (obj && !obj.parent) this.add(obj);
586
+ }
587
+ }
588
+ // -- Joint control -------------------------------------------------------
589
+ /** Set one joint value. Unknown joints are ignored. Returns whether it applied. */
590
+ setJointValue(name, value) {
591
+ const joint = this.jointObjects.get(name);
592
+ if (!joint) return false;
593
+ joint.setValue(value, this.clampJointLimits);
594
+ this.dirty = true;
595
+ return true;
596
+ }
597
+ /** Set several joint values at once (matrix update is coalesced). */
598
+ setJointValues(values) {
599
+ for (const [name, value] of Object.entries(values)) {
600
+ const joint = this.jointObjects.get(name);
601
+ if (joint) {
602
+ joint.setValue(value, this.clampJointLimits);
603
+ this.dirty = true;
604
+ }
605
+ }
606
+ }
607
+ getJointValue(name) {
608
+ return this.jointObjects.get(name)?.value;
609
+ }
610
+ /** Recompute world matrices. Called lazily by the getters; safe to call directly. */
611
+ updateKinematics() {
612
+ this.updateMatrixWorld(true);
613
+ this.dirty = false;
614
+ }
615
+ ensureUpdated() {
616
+ if (this.dirty) this.updateKinematics();
617
+ }
618
+ // -- Queries -------------------------------------------------------------
619
+ getLinkWorldMatrix(name) {
620
+ const obj = this.linkObjects.get(name);
621
+ if (!obj) throw new Error(`unknown link "${name}"`);
622
+ this.ensureUpdated();
623
+ return obj.matrixWorld.clone();
624
+ }
625
+ getLinkWorldPosition(name) {
626
+ return new THREE4.Vector3().setFromMatrixPosition(this.getLinkWorldMatrix(name));
627
+ }
628
+ getLinkObject(name) {
629
+ return this.linkObjects.get(name);
630
+ }
631
+ getJointObject(name) {
632
+ return this.jointObjects.get(name);
633
+ }
634
+ getJoints() {
635
+ return Object.values(this.robot.joints);
636
+ }
637
+ getLinks() {
638
+ return Object.values(this.robot.links);
639
+ }
640
+ /** Names of the articulated (controllable) joints. */
641
+ getJointNames() {
642
+ return [...this.jointObjects.keys()];
643
+ }
644
+ getLinkNames() {
645
+ return [...this.linkObjects.keys()];
646
+ }
647
+ getKinematicTree() {
648
+ return this.tree;
649
+ }
650
+ // -- Animation playback --------------------------------------------------
651
+ /** Playback rate in time codes per second (from the stage; default 24). */
652
+ getTimeCodesPerSecond() {
653
+ return this.robot.timeCodesPerSecond ?? 24;
654
+ }
655
+ /** Whether any joint has a time-sampled trajectory. */
656
+ hasAnimation() {
657
+ return Object.values(this.robot.joints).some((j) => j.valueSamples !== void 0);
658
+ }
659
+ /**
660
+ * Animation range in time codes: the union of authored joint sample ranges,
661
+ * falling back to the stage `startTimeCode`/`endTimeCode`. `null` if neither.
662
+ */
663
+ getTimeRange() {
664
+ let start = Number.POSITIVE_INFINITY;
665
+ let end = Number.NEGATIVE_INFINITY;
666
+ for (const joint of Object.values(this.robot.joints)) {
667
+ const times = joint.valueSamples?.times;
668
+ if (!times || times.length === 0) continue;
669
+ start = Math.min(start, times[0]);
670
+ end = Math.max(end, times[times.length - 1]);
671
+ }
672
+ if (start <= end) return { start, end };
673
+ const { startTimeCode, endTimeCode } = this.robot;
674
+ if (startTimeCode !== void 0 && endTimeCode !== void 0) {
675
+ return { start: startTimeCode, end: endTimeCode };
676
+ }
677
+ return null;
678
+ }
679
+ /** Sample every animated joint at time code `t` and apply the values. */
680
+ setTime(t) {
681
+ for (const [key, joint] of Object.entries(this.robot.joints)) {
682
+ if (joint.valueSamples) this.setJointValue(key, interpolate(joint.valueSamples, t));
683
+ }
684
+ }
685
+ // -- Display toggles -----------------------------------------------------
686
+ get showVisual() {
687
+ return this._showVisual;
688
+ }
689
+ set showVisual(v) {
690
+ this._showVisual = v;
691
+ this.setKindVisibility("visual", v);
692
+ }
693
+ get showCollision() {
694
+ return this._showCollision;
695
+ }
696
+ set showCollision(v) {
697
+ this._showCollision = v;
698
+ this.setKindVisibility("collision", v);
699
+ }
700
+ get showJointAxes() {
701
+ return this._showJointAxes;
702
+ }
703
+ set showJointAxes(v) {
704
+ this._showJointAxes = v;
705
+ if (v && this.jointAxesHelpers.length === 0) {
706
+ for (const joint of this.jointObjects.values()) {
707
+ const h = new THREE4.AxesHelper(this.helperSize);
708
+ h.name = `${joint.jointName}:axes`;
709
+ joint.add(h);
710
+ this.jointAxesHelpers.push(h);
711
+ }
712
+ }
713
+ for (const h of this.jointAxesHelpers) h.visible = v;
714
+ }
715
+ get showLinkFrames() {
716
+ return this._showLinkFrames;
717
+ }
718
+ set showLinkFrames(v) {
719
+ this._showLinkFrames = v;
720
+ if (v && this.linkFrameHelpers.length === 0) {
721
+ for (const link of this.linkObjects.values()) {
722
+ const h = new THREE4.AxesHelper(this.helperSize);
723
+ h.name = `${link.linkName}:frame`;
724
+ link.add(h);
725
+ this.linkFrameHelpers.push(h);
726
+ }
727
+ }
728
+ for (const h of this.linkFrameHelpers) h.visible = v;
729
+ }
730
+ setKindVisibility(kind, visible) {
731
+ this.traverse((o) => {
732
+ if (o.userData.kind === kind) o.visible = visible;
733
+ });
734
+ }
735
+ };
736
+ function setMatrix(obj, m) {
737
+ obj.matrixAutoUpdate = false;
738
+ obj.matrix.fromArray(m);
739
+ obj.matrixWorldNeedsUpdate = true;
740
+ }
741
+
742
+ // src/three/ThreeUsdRobotLoader.ts
743
+ var ThreeUsdRobotLoader = class {
744
+ options;
745
+ constructor(options = {}) {
746
+ this.options = options;
747
+ }
748
+ get resolver() {
749
+ return this.options.assetResolver ?? new DefaultAssetResolver();
750
+ }
751
+ /**
752
+ * Fetch an asset by URL and build the robot. `.usdz` packages are unzipped and
753
+ * composed from their entries; everything else is sniffed for the crate magic
754
+ * and parsed as binary USDC or USDA text.
755
+ */
756
+ async loadAsync(url) {
757
+ const bytes = await this.fetchRootBytes(url);
758
+ if (/\.usdz$/i.test(url)) return this.parseUsdz(bytes);
759
+ if (CrateReader.isCrate(bytes)) return this.parseCrate(bytes, url);
760
+ return this.parse(new TextDecoder().decode(bytes), url);
761
+ }
762
+ async fetchRootBytes(url) {
763
+ const resolver = this.resolver;
764
+ if (resolver.fetchBytes) return resolver.fetchBytes(url);
765
+ return new TextEncoder().encode(await resolver.fetchText(url));
766
+ }
767
+ /** Build a robot (composed, with meshes) from USDA source text. */
768
+ async parse(text, baseUrl = "") {
769
+ return this.buildFromStage(
770
+ await this.composeStage(text, baseUrl, this.resolver),
771
+ baseUrl,
772
+ this.resolver
773
+ );
774
+ }
775
+ /** Build a robot from the bytes of a `.usdz` package. */
776
+ async parseUsdz(bytes) {
777
+ const pkg = openUsdz(bytes);
778
+ const rootBytes = await pkg.resolver.fetchBytes(pkg.rootEntry);
779
+ const stage = await this.composeStageFromBytes(rootBytes, pkg.rootEntry, pkg.resolver);
780
+ return this.buildFromStage(stage, pkg.rootEntry, pkg.resolver);
781
+ }
782
+ /** Build a robot from the bytes of a binary crate (`.usdc` / binary `.usd`). */
783
+ async parseCrate(bytes, baseUrl = "") {
784
+ const stage = await this.composeStageFromBytes(bytes, baseUrl, this.resolver);
785
+ return this.buildFromStage(stage, baseUrl, this.resolver);
786
+ }
787
+ /** Parse + compose USDA source into the Three.js-independent robot IR. */
788
+ async parseRobotDescription(text, baseUrl = "") {
789
+ const stage = await this.composeStage(text, baseUrl, this.resolver);
790
+ return extractRobotDescription(stage, this.extractOptions());
791
+ }
792
+ buildFromStage(stage, baseUrl, resolver) {
793
+ const robot = extractRobotDescription(stage, this.extractOptions());
794
+ const tree = buildKinematicTree(robot);
795
+ const robot3d = new ThreeUsdRobot(robot, tree, this.robotOptions());
796
+ const loadVisuals = this.options.loadVisuals ?? true;
797
+ const loadCollisions = this.options.loadCollisions ?? false;
798
+ if (loadVisuals || loadCollisions) {
799
+ const textureProvider = this.options.loadTextures ?? true ? createTextureProvider(resolver, baseUrl) : void 0;
800
+ bindRobotMeshes(stage, robot3d, robot, {
801
+ loadVisuals,
802
+ loadCollisions,
803
+ ...textureProvider ? { textureProvider } : {}
804
+ });
805
+ }
806
+ return robot3d;
807
+ }
808
+ async composeStage(text, baseUrl, resolver) {
809
+ const composeOptions = this.options.onWarn ? { onWarn: this.options.onWarn } : {};
810
+ return Stage.OpenFromFile(await composeLayer(text, baseUrl, resolver, composeOptions));
811
+ }
812
+ /** Compose a layer from raw bytes, sniffing binary crate vs USDA text. */
813
+ async composeStageFromBytes(bytes, baseUrl, resolver) {
814
+ const composeOptions = this.options.onWarn ? { onWarn: this.options.onWarn } : {};
815
+ const composed = CrateReader.isCrate(bytes) ? await composeFile(
816
+ crateToUsdaFile(new CrateReader(bytes)),
817
+ baseUrl,
818
+ resolver,
819
+ composeOptions
820
+ ) : await composeLayer(new TextDecoder().decode(bytes), baseUrl, resolver, composeOptions);
821
+ return Stage.OpenFromFile(composed);
822
+ }
823
+ robotOptions() {
824
+ return {
825
+ upAxisConversion: this.options.upAxisConversion ?? "auto",
826
+ ...this.options.clampJointLimits !== void 0 ? { clampJointLimits: this.options.clampJointLimits } : {},
827
+ ...this.options.unitScale !== void 0 ? { unitScale: this.options.unitScale } : {},
828
+ ...this.options.applyDriveTargetsAsInitialPose !== void 0 ? { applyInitialPose: this.options.applyDriveTargetsAsInitialPose } : {}
829
+ };
830
+ }
831
+ extractOptions() {
832
+ return {
833
+ ...this.options.robotName !== void 0 ? { robotName: this.options.robotName } : {},
834
+ ...this.options.onWarn !== void 0 ? { onWarn: this.options.onWarn } : {}
835
+ };
836
+ }
837
+ };
838
+
839
+ export { JointObject, LinkObject, ThreeUsdRobot, ThreeUsdRobotLoader, axisVector, bindRobotMeshes, buildMeshGeometry, buildMeshMaterial, createTextureProvider, resolveBoundMaterial };
840
+ //# sourceMappingURL=chunk-Y3NXBSX3.js.map
841
+ //# sourceMappingURL=chunk-Y3NXBSX3.js.map