three-usd-robot 0.4.0 → 0.6.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.
package/dist/index.js CHANGED
@@ -1,664 +1,304 @@
1
- import { AssetPath, identity4, multiply, computeLocalTransform, invert, interpolate, DefaultAssetResolver, CrateReader, openUsdz, crateToUsdaFile, composeFile, Stage, extractRobotDescription, buildKinematicTree, composeLayer } from './chunk-FYVZ7YPW.js';
2
- export { ARTICULATION_ROOT_API, AssetPath, Attribute, COLLISION_API, CrateReader, DEFAULT_METERS_PER_UNIT, DEG2RAD, DefaultAssetResolver, Layer, PACKAGE_NAME, ParseError, Prim, Quat, RAD2DEG, RIGID_BODY_API, Relationship, Stage, TokenizeError, UsdMatrix, VERSION, buildKinematicTree, channelFromSamples, composeFile, composeLayer, computeLocalTransform, crateToUsdaFile, createMemoryResolver, driveKindFor, extractRobotDescription, fromUsdMatrix, gatherMeshDescendants, getJointAxis, getJointBodies, getJointDrive, getJointLimits, getJointLocalFrame, getJointStatePosition, getJointType, getTranslation, hasArticulationRootAPI, hasCollisionAPI, hasRigidBodyAPI, identity4, interpolate, invert, isMesh, isScope, isXform, iterDescendants, joinPosix, jointValueToSI, makeEuler, makeRotationFromQuat, makeRotationX, makeRotationY, makeRotationZ, makeScale, makeTranslation, multiply, multiplyAll, normalizeJointLimits, openUsdz, parseOpType, parseUsda, refineJointType, tokenize } from './chunk-FYVZ7YPW.js';
3
- import * as THREE4 from 'three';
1
+ import { stageGeometryProvider, exportRobotUsda } from './chunk-JZMCABQO.js';
2
+ export { PACKAGE_NAME, VERSION, exportRobotUsda, stageGeometryProvider, validateRobotDescription, writeUsdz } from './chunk-JZMCABQO.js';
3
+ export { JointObject, LinkObject, ThreeUsdRobot, ThreeUsdRobotLoader, axisVector, bindRobotMeshes, bindSceneMeshes, buildMeshGeometry, buildMeshMaterial, createTextureProvider } from './chunk-YVWSO27W.js';
4
+ import { identity4, refineJointType, multiply, invert, buildKinematicTree } from './chunk-4MEZM763.js';
5
+ export { ARTICULATION_ROOT_API, AssetPath, Attribute, COLLISION_API, CrateReader, DEFAULT_METERS_PER_UNIT, DEG2RAD, DefaultAssetResolver, Layer, MASS_API, MESH_COLLISION_API, PHYSICS_MATERIAL_API, ParseError, Prim, Quat, RAD2DEG, RIGID_BODY_API, Relationship, Stage, TokenizeError, UsdMatrix, buildKinematicTree, channelFromSamples, composeFile, composeLayer, computeLocalTransform, computeWorldTransform, crateToUsdaFile, createMemoryResolver, decomposeRigid, driveKindFor, extractRobotDescription, fromUsdMatrix, gatherMeshDescendants, getJointAxis, getJointBodies, getJointDrive, getJointLimits, getJointLocalFrame, getJointStatePosition, getJointType, getMassProperties, getTranslation, hasArticulationRootAPI, hasCollisionAPI, hasRigidBodyAPI, identity4, interpolate, invert, isMesh, isScope, isXform, iterDescendants, joinPosix, jointValueFromSI, jointValueToSI, makeEuler, makeRotationFromQuat, makeRotationX, makeRotationY, makeRotationZ, makeScale, makeTranslation, multiply, multiplyAll, normalizeJointLimits, openUsdz, parseOpType, parseUsda, refineJointType, resolveBoundMaterial, serializeUsda, toUsdMatrix, tokenize } from './chunk-4MEZM763.js';
6
+ import * as THREE from 'three';
4
7
 
5
- function axisVector(axis) {
6
- switch (axis) {
7
- case "X":
8
- return new THREE4.Vector3(1, 0, 0);
9
- case "Y":
10
- return new THREE4.Vector3(0, 1, 0);
11
- case "Z":
12
- return new THREE4.Vector3(0, 0, 1);
13
- }
14
- }
15
- var JointObject = class extends THREE4.Object3D {
16
- isJointObject = true;
17
- jointName;
18
- jointType;
19
- axisToken;
20
- axis;
21
- lower;
22
- upper;
23
- _value = 0;
24
- constructor(joint) {
25
- super();
26
- this.name = joint.name;
27
- this.jointName = joint.name;
28
- this.jointType = joint.type;
29
- this.axisToken = joint.axis;
30
- this.axis = axisVector(joint.axis);
31
- this.lower = joint.lower;
32
- this.upper = joint.upper;
33
- }
34
- get value() {
35
- return this._value;
36
- }
37
- get articulated() {
38
- return this.jointType !== "fixed";
39
- }
40
- /**
41
- * Set the joint value (radians for revolute/continuous, length for prismatic).
42
- * Optionally clamps to authored limits. Returns the value actually applied.
43
- */
44
- setValue(value, clampToLimits = true) {
45
- if (!this.articulated) return this._value;
46
- let v = value;
47
- if (clampToLimits) {
48
- if (this.lower !== void 0 && v < this.lower) v = this.lower;
49
- if (this.upper !== void 0 && v > this.upper) v = this.upper;
50
- }
51
- this._value = v;
52
- if (this.jointType === "prismatic") {
53
- this.position.copy(this.axis).multiplyScalar(v);
54
- this.quaternion.identity();
55
- } else {
56
- this.quaternion.setFromAxisAngle(this.axis, v);
57
- this.position.set(0, 0, 0);
58
- }
59
- return v;
60
- }
61
- };
62
- var LinkObject = class extends THREE4.Object3D {
63
- isLinkObject = true;
64
- linkName;
65
- primPath;
66
- constructor(link) {
67
- super();
68
- this.name = link.name;
69
- this.linkName = link.name;
70
- this.primPath = link.primPath;
71
- this.matrixAutoUpdate = false;
72
- }
73
- };
74
-
75
- // src/three/MaterialBinding.ts
76
- var DIFFUSE_INPUTS = [
77
- "inputs:diffuseColor",
78
- // UsdPreviewSurface
79
- "inputs:diffuse_color_constant",
80
- // OmniPBR
81
- "inputs:diffuse_tint",
82
- "inputs:base_color",
83
- "inputs:baseColor"
84
- ];
85
- var OPACITY_INPUTS = ["inputs:opacity", "inputs:opacity_constant"];
86
- var METALLIC_INPUTS = ["inputs:metallic", "inputs:metallic_constant"];
87
- var ROUGHNESS_INPUTS = ["inputs:roughness", "inputs:reflection_roughness_constant"];
88
- var SURFACE_OUTPUTS = ["outputs:surface", "outputs:mdl:surface"];
89
- function resolveBoundMaterial(stage, prim) {
90
- const materialPath = findBinding(prim);
91
- if (!materialPath) return void 0;
92
- const material = stage.GetPrimAtPath(materialPath);
93
- if (!material) return void 0;
94
- const shader = findSurfaceShader(material);
95
- if (!shader) return void 0;
96
- const result = {};
97
- const color = firstColor(shader, DIFFUSE_INPUTS);
98
- if (color) result.color = color;
99
- const opacity = firstNumber(shader, OPACITY_INPUTS);
100
- if (opacity !== void 0) result.opacity = opacity;
101
- const metalness = firstNumber(shader, METALLIC_INPUTS);
102
- if (metalness !== void 0) result.metalness = metalness;
103
- const roughness = firstNumber(shader, ROUGHNESS_INPUTS);
104
- if (roughness !== void 0) result.roughness = roughness;
105
- const texture = findDiffuseTexture(shader);
106
- if (texture !== void 0) result.colorTexture = texture;
107
- return result;
108
- }
109
- var DIFFUSE_TEXTURE_INPUTS = ["inputs:diffuse_texture", "inputs:diffuse_color_texture"];
110
- function findDiffuseTexture(shader) {
111
- for (const name of DIFFUSE_TEXTURE_INPUTS) {
112
- const v = shader.GetAttribute(name).Get();
113
- if (v instanceof AssetPath && v.path) return v.path;
114
- }
115
- const conn = shader.GetAttribute("inputs:diffuseColor").GetConnections()[0];
116
- if (conn) {
117
- const texPrim = shader.GetStage().GetPrimAtPath(conn.split(".")[0]);
118
- const file = texPrim?.GetAttribute("inputs:file").Get();
119
- if (file instanceof AssetPath && file.path) return file.path;
120
- }
121
- return void 0;
122
- }
123
- function findBinding(prim) {
124
- let p = prim;
125
- while (p) {
126
- const targets = p.GetRelationship("material:binding").GetTargets();
127
- if (targets.length > 0) return targets[0];
128
- p = p.GetParent();
129
- }
130
- return void 0;
131
- }
132
- function findSurfaceShader(material) {
133
- for (const out of SURFACE_OUTPUTS) {
134
- const conn = material.GetAttribute(out).GetConnections()[0];
135
- if (conn) {
136
- const shaderPath = conn.split(".")[0];
137
- const shader = material.GetStage().GetPrimAtPath(shaderPath);
138
- if (shader) return shader;
139
- }
140
- }
141
- return material.GetChildren().find((c) => c.GetTypeName() === "Shader") ?? void 0;
142
- }
143
- function firstColor(shader, names) {
144
- for (const name of names) {
145
- const v = shader.GetAttribute(name).Get();
146
- if (Array.isArray(v) && v.length >= 3 && v.every((n) => typeof n === "number")) {
147
- return [v[0], v[1], v[2]];
8
+ function geometryToExportMesh(geometry, options) {
9
+ const position = geometry.getAttribute("position");
10
+ if (!position || position.count === 0) return null;
11
+ const points = [];
12
+ for (let i = 0; i < position.count; i++) {
13
+ points.push([position.getX(i), position.getY(i), position.getZ(i)]);
14
+ }
15
+ const index = geometry.getIndex();
16
+ let faceVertexIndices = index ? Array.from(index.array) : points.map((_, i) => i);
17
+ const triangles = Math.floor(faceVertexIndices.length / 3);
18
+ if (triangles === 0) return null;
19
+ faceVertexIndices = faceVertexIndices.slice(0, triangles * 3);
20
+ const mesh = {
21
+ name: options.name,
22
+ kind: options.kind ?? "visual",
23
+ points,
24
+ faceVertexCounts: new Array(triangles).fill(3),
25
+ faceVertexIndices
26
+ };
27
+ const normal = geometry.getAttribute("normal");
28
+ if (normal && normal.count === position.count) {
29
+ const normals = [];
30
+ for (let i = 0; i < normal.count; i++) {
31
+ normals.push([normal.getX(i), normal.getY(i), normal.getZ(i)]);
148
32
  }
149
- }
150
- return void 0;
33
+ mesh.normals = normals;
34
+ }
35
+ const uv = geometry.getAttribute("uv");
36
+ if (uv && uv.count === position.count) {
37
+ const st = [];
38
+ for (let i = 0; i < uv.count; i++) st.push([uv.getX(i), uv.getY(i)]);
39
+ mesh.st = st;
40
+ }
41
+ if (options.transform && !isIdentityMat4(options.transform)) mesh.transform = options.transform;
42
+ if (options.material) mesh.material = options.material;
43
+ if (options.doubleSided) mesh.doubleSided = true;
44
+ return mesh;
151
45
  }
152
- function firstNumber(shader, names) {
153
- for (const name of names) {
154
- const v = shader.GetAttribute(name).Get();
155
- if (typeof v === "number") return v;
156
- }
157
- return void 0;
158
- }
159
-
160
- // src/three/MeshBinding.ts
161
- var DEFAULT_COLOR = 10132122;
162
- function buildMeshGeometry(meshPrim) {
163
- const points = meshPrim.GetAttribute("points").Get();
164
- if (!isVec3Array(points) || points.length === 0) return null;
165
- const geometry = new THREE4.BufferGeometry();
166
- geometry.setAttribute("position", new THREE4.Float32BufferAttribute(flat3(points), 3));
167
- const counts = meshPrim.GetAttribute("faceVertexCounts").Get();
168
- const indices = meshPrim.GetAttribute("faceVertexIndices").Get();
169
- if (isNumberArray(counts) && isNumberArray(indices)) {
170
- geometry.setIndex(triangulate(counts, indices));
171
- } else if (isNumberArray(indices)) {
172
- geometry.setIndex(indices.slice());
173
- }
174
- const normals = meshPrim.GetAttribute("normals").Get();
175
- if (isVec3Array(normals) && normals.length === points.length) {
176
- geometry.setAttribute("normal", new THREE4.Float32BufferAttribute(flat3(normals), 3));
177
- } else {
178
- geometry.computeVertexNormals();
179
- }
180
- const st = meshPrim.GetAttribute("primvars:st").Get();
181
- if (isVec2Array(st) && st.length === points.length) {
182
- geometry.setAttribute("uv", new THREE4.Float32BufferAttribute(flat2(st), 2));
46
+ function materialToExportMaterial(material, name, onWarn) {
47
+ const std = material;
48
+ if (!std.isMeshStandardMaterial) {
49
+ onWarn?.(`material "${name}": only MeshStandardMaterial exports to UsdPreviewSurface; skipped`);
50
+ return void 0;
51
+ }
52
+ if (std.map || std.normalMap || std.roughnessMap || std.metalnessMap || std.aoMap || std.emissiveMap || std.alphaMap) {
53
+ onWarn?.(
54
+ `material "${name}": texture maps are not carried from Three.js materials (image extraction is app-side); constants only`
55
+ );
183
56
  }
184
- return geometry;
185
- }
186
- function buildMeshMaterial(meshPrim, stage, textures) {
187
- const color = new THREE4.Color(DEFAULT_COLOR);
188
- let metalness = 0.1;
189
- let roughness = 0.8;
190
- let opacity = 1;
191
- const bound = stage ? resolveBoundMaterial(stage, meshPrim) : void 0;
192
- if (bound?.color) {
193
- color.setRGB(bound.color[0], bound.color[1], bound.color[2]);
194
- } else {
195
- const displayColor = meshPrim.GetAttribute("primvars:displayColor").Get();
196
- if (isVec3Array(displayColor) && displayColor[0]) {
197
- const [r, g, b] = displayColor[0];
198
- color.setRGB(r, g, b);
199
- }
57
+ const out = {
58
+ name,
59
+ diffuseColor: [std.color.r, std.color.g, std.color.b],
60
+ metallic: std.metalness,
61
+ roughness: std.roughness
62
+ };
63
+ if (std.transparent && std.opacity < 1) out.opacity = std.opacity;
64
+ if (std.emissive.r > 0 || std.emissive.g > 0 || std.emissive.b > 0) {
65
+ out.emissiveColor = [std.emissive.r, std.emissive.g, std.emissive.b];
200
66
  }
201
- if (bound?.metalness !== void 0) metalness = bound.metalness;
202
- if (bound?.roughness !== void 0) roughness = bound.roughness;
203
- if (bound?.opacity !== void 0) opacity = bound.opacity;
204
- const map = bound?.colorTexture && textures ? textures(bound.colorTexture) : null;
205
- if (map) color.setRGB(1, 1, 1);
206
- const doubleSided = meshPrim.GetAttribute("doubleSided").Get() === true;
207
- return new THREE4.MeshStandardMaterial({
208
- color,
209
- metalness,
210
- roughness,
211
- transparent: opacity < 1,
212
- opacity,
213
- side: doubleSided ? THREE4.DoubleSide : THREE4.FrontSide,
214
- ...map ? { map } : {}
215
- });
67
+ return out;
216
68
  }
217
- function bindRobotMeshes(stage, robot3d, desc, options = {}) {
218
- const loadVisuals = options.loadVisuals ?? true;
219
- const loadCollisions = options.loadCollisions ?? false;
220
- const textures = options.textureProvider;
221
- for (const [key, link] of Object.entries(desc.links)) {
222
- const linkObj = robot3d.getLinkObject(key);
223
- const linkPrim = stage.GetPrimAtPath(link.primPath);
224
- if (!linkObj || !linkPrim) continue;
225
- const collisionSet = new Set(link.collisionPrims ?? []);
226
- if (loadVisuals) {
227
- for (const meshPath of link.visualPrims) {
228
- if (collisionSet.has(meshPath)) continue;
229
- attachMesh(stage, linkPrim, meshPath, linkObj, "visual", textures);
230
- }
69
+ function threeGeometryProvider(robot, onWarn) {
70
+ const materialNames = /* @__PURE__ */ new Map();
71
+ let autoId = 0;
72
+ const convertMaterial = (material) => {
73
+ if (!materialNames.has(material)) {
74
+ const name = material.name || `Material_${autoId++}`;
75
+ materialNames.set(material, materialToExportMaterial(material, name, onWarn));
231
76
  }
232
- if (loadCollisions) {
233
- for (const meshPath of link.collisionPrims ?? []) {
234
- attachMesh(stage, linkPrim, meshPath, linkObj, "collision", textures);
77
+ return materialNames.get(material);
78
+ };
79
+ return (linkKey) => {
80
+ const linkObj = robot.getLinkObject(linkKey);
81
+ if (!linkObj) return [];
82
+ const out = [];
83
+ for (const childObj of linkObj.children) {
84
+ const meshObj = childObj;
85
+ if (!meshObj.isMesh) continue;
86
+ const material = Array.isArray(meshObj.material) ? meshObj.material[0] : meshObj.material;
87
+ if (Array.isArray(meshObj.material) && meshObj.material.length > 1) {
88
+ onWarn?.(`mesh "${meshObj.name}": multi-material meshes export their first material only`);
235
89
  }
90
+ const exportMaterial = material ? convertMaterial(material) : void 0;
91
+ const mesh = geometryToExportMesh(meshObj.geometry, {
92
+ name: meshObj.name || "mesh",
93
+ kind: meshObj.userData.kind === "collision" ? "collision" : "visual",
94
+ transform: [...meshObj.matrix.elements],
95
+ ...exportMaterial ? { material: exportMaterial } : {},
96
+ doubleSided: material?.side === THREE.DoubleSide
97
+ });
98
+ if (mesh) out.push(mesh);
236
99
  }
237
- }
238
- }
239
- function attachMesh(stage, linkPrim, meshPath, parent, kind, textures) {
240
- const meshPrim = stage.GetPrimAtPath(meshPath);
241
- if (!meshPrim) return;
242
- const geometry = buildMeshGeometry(meshPrim);
243
- if (!geometry) return;
244
- const mesh = new THREE4.Mesh(geometry, buildMeshMaterial(meshPrim, stage, textures));
245
- mesh.name = meshPrim.GetName();
246
- mesh.userData.kind = kind;
247
- mesh.userData.primPath = meshPath;
248
- if (kind === "collision") mesh.visible = false;
249
- mesh.matrixAutoUpdate = false;
250
- mesh.matrix.fromArray(relativeTransform(linkPrim, meshPrim));
251
- mesh.matrixWorldNeedsUpdate = true;
252
- parent.add(mesh);
100
+ return out;
101
+ };
253
102
  }
254
- function relativeTransform(linkPrim, meshPrim) {
255
- const chain = [];
256
- let p = meshPrim;
257
- const stop = linkPrim.GetPath();
258
- while (p && p.GetPath() !== stop) {
259
- chain.push(p);
260
- p = p.GetParent();
261
- }
262
- chain.reverse();
263
- let m = identity4();
264
- for (const prim of chain) {
265
- m = multiply(m, computeLocalTransform(prim).matrix);
266
- }
267
- return m;
103
+ function exportThreeUsdRobot(robot, options = {}) {
104
+ const { stage, ...rest } = options;
105
+ const geometry = stage ? stageGeometryProvider(stage) : threeGeometryProvider(robot, options.onWarn);
106
+ return exportRobotUsda(robot.robot, { ...rest, geometry });
268
107
  }
269
- function triangulate(faceVertexCounts, faceVertexIndices) {
270
- const tris = [];
271
- let offset = 0;
272
- for (const count of faceVertexCounts) {
273
- for (let k = 2; k < count; k++) {
274
- tris.push(
275
- faceVertexIndices[offset],
276
- faceVertexIndices[offset + k - 1],
277
- faceVertexIndices[offset + k]
278
- );
279
- }
280
- offset += count;
108
+ function isIdentityMat4(m) {
109
+ for (let i = 0; i < 16; i++) {
110
+ if (m[i] !== (i % 5 === 0 ? 1 : 0)) return false;
281
111
  }
282
- return tris;
283
- }
284
- function flat3(v) {
285
- const out = new Array(v.length * 3);
286
- for (let i = 0; i < v.length; i++) {
287
- out[i * 3] = v[i][0];
288
- out[i * 3 + 1] = v[i][1];
289
- out[i * 3 + 2] = v[i][2];
290
- }
291
- return out;
112
+ return true;
292
113
  }
293
- function flat2(v) {
294
- const out = new Array(v.length * 2);
295
- for (let i = 0; i < v.length; i++) {
296
- out[i * 2] = v[i][0];
297
- out[i * 2 + 1] = v[i][1];
298
- }
299
- return out;
300
- }
301
- function isNumberArray(v) {
302
- return Array.isArray(v) && v.every((n) => typeof n === "number");
303
- }
304
- function isVec3Array(v) {
305
- return Array.isArray(v) && v.every((e) => Array.isArray(e) && e.length === 3);
306
- }
307
- function isVec2Array(v) {
308
- return Array.isArray(v) && v.every((e) => Array.isArray(e) && e.length === 2);
309
- }
310
- function createTextureProvider(resolver, baseUrl) {
311
- const loader = new THREE4.TextureLoader();
312
- const cache = /* @__PURE__ */ new Map();
313
- return (assetPath) => {
314
- let url;
315
- try {
316
- url = resolver.resolve(assetPath, baseUrl);
317
- } catch {
318
- return null;
319
- }
320
- const cached = cache.get(url);
321
- if (cached) return cached;
322
- const texture = loader.load(url);
323
- texture.colorSpace = THREE4.SRGBColorSpace;
324
- texture.wrapS = THREE4.RepeatWrapping;
325
- texture.wrapT = THREE4.RepeatWrapping;
326
- cache.set(url, texture);
327
- return texture;
328
- };
329
- }
330
- var ThreeUsdRobot = class extends THREE4.Object3D {
331
- isThreeUsdRobot = true;
332
- robot;
333
- tree;
334
- clampJointLimits;
335
- linkObjects = /* @__PURE__ */ new Map();
336
- jointObjects = /* @__PURE__ */ new Map();
337
- dirty = true;
338
- helperSize;
339
- _showVisual = true;
340
- _showCollision = false;
341
- _showJointAxes = false;
342
- _showLinkFrames = false;
343
- jointAxesHelpers = [];
344
- linkFrameHelpers = [];
345
- constructor(robot, tree, options = {}) {
346
- super();
347
- this.name = robot.name;
348
- this.robot = robot;
349
- this.tree = tree;
350
- this.clampJointLimits = options.clampJointLimits ?? true;
351
- this.helperSize = options.helperSize ?? 0.15;
352
- for (const [key, link] of Object.entries(robot.links)) {
353
- this.linkObjects.set(key, new LinkObject(link));
354
- }
355
- this.attachRoot();
356
- this.attachTreeEdges();
357
- this.attachIsolatedLinks();
358
- this.applyStageNormalization(robot, options);
359
- if (options.applyInitialPose ?? true) this.applyInitialPose(robot);
360
- }
361
- /** Orient (Z-up → Y-up) and scale (metersPerUnit × unitScale) the robot root. */
362
- applyStageNormalization(robot, options) {
363
- const scale = (robot.metersPerUnit || 1) * (options.unitScale ?? 1);
364
- if (scale !== 1) this.scale.setScalar(scale);
365
- const conv = options.upAxisConversion ?? "none";
366
- const toY = conv === "Z" || conv === "auto" && robot.upAxis === "Z";
367
- if (toY) this.quaternion.setFromAxisAngle(new THREE4.Vector3(1, 0, 0), -Math.PI / 2);
368
- }
369
- /** Apply each joint's authored initial value, if any. */
370
- applyInitialPose(robot) {
371
- for (const [key, joint] of Object.entries(robot.joints)) {
372
- if (joint.initialValue === void 0) continue;
373
- this.jointObjects.get(key)?.setValue(joint.initialValue, this.clampJointLimits);
374
- }
375
- this.dirty = true;
376
- }
377
- attachRoot() {
378
- const rootObj = this.linkObjects.get(this.tree.root);
379
- if (!rootObj) return;
380
- const rootJointKey = this.tree.rootJoint;
381
- if (rootJointKey) {
382
- const j = this.robot.joints[rootJointKey];
383
- if (j) setMatrix(rootObj, multiply(j.jointFrame0, invert(j.jointFrame1)));
384
- }
385
- this.add(rootObj);
386
- }
387
- attachTreeEdges() {
388
- for (const linkKey of this.tree.order) {
389
- const node = this.tree.nodes[linkKey];
390
- if (!node || node.parent === null || node.jointToParent === null) continue;
391
- const parentObj = this.linkObjects.get(node.parent);
392
- const childObj = this.linkObjects.get(linkKey);
393
- const joint = this.robot.joints[node.jointToParent];
394
- if (!parentObj || !childObj || !joint) continue;
395
- this.attachJointChain(parentObj, childObj, node.jointToParent, joint);
396
- }
397
- }
398
- /** Build `parent → frame0 → motion → frame1⁻¹ → child` for one joint. */
399
- attachJointChain(parent, child, jointKey, joint) {
400
- const frame0 = new THREE4.Group();
401
- frame0.name = `${joint.name}:frame0`;
402
- setMatrix(frame0, joint.jointFrame0);
403
- const motion = new JointObject(joint);
404
- const frame1Inv = new THREE4.Group();
405
- frame1Inv.name = `${joint.name}:frame1Inv`;
406
- setMatrix(frame1Inv, invert(joint.jointFrame1));
407
- parent.add(frame0);
408
- frame0.add(motion);
409
- motion.add(frame1Inv);
410
- frame1Inv.add(child);
411
- this.jointObjects.set(jointKey, motion);
412
- }
413
- attachIsolatedLinks() {
414
- for (const key of this.tree.isolatedLinks) {
415
- const obj = this.linkObjects.get(key);
416
- if (obj && !obj.parent) this.add(obj);
417
- }
418
- }
419
- // -- Joint control -------------------------------------------------------
420
- /** Set one joint value. Unknown joints are ignored. Returns whether it applied. */
421
- setJointValue(name, value) {
422
- const joint = this.jointObjects.get(name);
423
- if (!joint) return false;
424
- joint.setValue(value, this.clampJointLimits);
425
- this.dirty = true;
426
- return true;
114
+
115
+ // src/three/RobotBuilder.ts
116
+ var RobotBuilder = class {
117
+ robotName;
118
+ upAxis;
119
+ metersPerUnit;
120
+ articulationRoot;
121
+ warn;
122
+ links = /* @__PURE__ */ new Map();
123
+ joints = /* @__PURE__ */ new Map();
124
+ materialCache = /* @__PURE__ */ new Map();
125
+ materialAutoId = 0;
126
+ constructor(options) {
127
+ this.robotName = options.name;
128
+ this.upAxis = options.upAxis ?? "Z";
129
+ this.metersPerUnit = options.metersPerUnit ?? 1;
130
+ if (options.articulationRoot !== void 0) this.articulationRoot = options.articulationRoot;
131
+ this.warn = options.onWarn ?? (() => {
132
+ });
427
133
  }
428
- /** Set several joint values at once (matrix update is coalesced). */
429
- setJointValues(values) {
430
- for (const [name, value] of Object.entries(values)) {
431
- const joint = this.jointObjects.get(name);
432
- if (joint) {
433
- joint.setValue(value, this.clampJointLimits);
434
- this.dirty = true;
435
- }
134
+ addLink(options) {
135
+ if (this.links.has(options.name)) {
136
+ throw new Error(`RobotBuilder: link "${options.name}" is already defined`);
436
137
  }
138
+ const world = resolveFrame(options.frame) ?? identity4();
139
+ const collisionExtras = {
140
+ ...options.collisionApproximation !== void 0 ? { approximation: options.collisionApproximation } : {},
141
+ ...options.physicsMaterial ? { material: options.physicsMaterial } : {}
142
+ };
143
+ const meshes = [
144
+ ...this.gatherMeshes(options.visuals ?? [], world, "visual"),
145
+ ...this.gatherMeshes(options.collisions ?? [], world, "collision", collisionExtras)
146
+ ];
147
+ this.links.set(options.name, {
148
+ world,
149
+ meshes,
150
+ ...options.inertial ? { inertial: options.inertial } : {}
151
+ });
152
+ return this;
437
153
  }
438
- getJointValue(name) {
439
- return this.jointObjects.get(name)?.value;
440
- }
441
- /** Recompute world matrices. Called lazily by the getters; safe to call directly. */
442
- updateKinematics() {
443
- this.updateMatrixWorld(true);
444
- this.dirty = false;
445
- }
446
- ensureUpdated() {
447
- if (this.dirty) this.updateKinematics();
448
- }
449
- // -- Queries -------------------------------------------------------------
450
- getLinkWorldMatrix(name) {
451
- const obj = this.linkObjects.get(name);
452
- if (!obj) throw new Error(`unknown link "${name}"`);
453
- this.ensureUpdated();
454
- return obj.matrixWorld.clone();
455
- }
456
- getLinkWorldPosition(name) {
457
- return new THREE4.Vector3().setFromMatrixPosition(this.getLinkWorldMatrix(name));
458
- }
459
- getLinkObject(name) {
460
- return this.linkObjects.get(name);
461
- }
462
- getJointObject(name) {
463
- return this.jointObjects.get(name);
464
- }
465
- getJoints() {
466
- return Object.values(this.robot.joints);
467
- }
468
- getLinks() {
469
- return Object.values(this.robot.links);
470
- }
471
- /** Names of the articulated (controllable) joints. */
472
- getJointNames() {
473
- return [...this.jointObjects.keys()];
474
- }
475
- getLinkNames() {
476
- return [...this.linkObjects.keys()];
477
- }
478
- getKinematicTree() {
479
- return this.tree;
154
+ addFixedJoint(options) {
155
+ return this.addJoint("fixed", options);
480
156
  }
481
- // -- Animation playback --------------------------------------------------
482
- /** Playback rate in time codes per second (from the stage; default 24). */
483
- getTimeCodesPerSecond() {
484
- return this.robot.timeCodesPerSecond ?? 24;
157
+ addRevoluteJoint(options) {
158
+ return this.addJoint("revolute", options);
485
159
  }
486
- /** Whether any joint has a time-sampled trajectory. */
487
- hasAnimation() {
488
- return Object.values(this.robot.joints).some((j) => j.valueSamples !== void 0);
160
+ addPrismaticJoint(options) {
161
+ return this.addJoint("prismatic", options);
489
162
  }
490
- /**
491
- * Animation range in time codes: the union of authored joint sample ranges,
492
- * falling back to the stage `startTimeCode`/`endTimeCode`. `null` if neither.
493
- */
494
- getTimeRange() {
495
- let start = Number.POSITIVE_INFINITY;
496
- let end = Number.NEGATIVE_INFINITY;
497
- for (const joint of Object.values(this.robot.joints)) {
498
- const times = joint.valueSamples?.times;
499
- if (!times || times.length === 0) continue;
500
- start = Math.min(start, times[0]);
501
- end = Math.max(end, times[times.length - 1]);
502
- }
503
- if (start <= end) return { start, end };
504
- const { startTimeCode, endTimeCode } = this.robot;
505
- if (startTimeCode !== void 0 && endTimeCode !== void 0) {
506
- return { start: startTimeCode, end: endTimeCode };
163
+ addJoint(type, options) {
164
+ if (this.joints.has(options.name)) {
165
+ throw new Error(`RobotBuilder: joint "${options.name}" is already defined`);
507
166
  }
508
- return null;
509
- }
510
- /** Sample every animated joint at time code `t` and apply the values. */
511
- setTime(t) {
512
- for (const [key, joint] of Object.entries(this.robot.joints)) {
513
- if (joint.valueSamples) this.setJointValue(key, interpolate(joint.valueSamples, t));
167
+ const world = resolveFrame(options.frame);
168
+ this.joints.set(options.name, { type, options, ...world ? { world } : {} });
169
+ return this;
170
+ }
171
+ /** Assemble the IR + geometry provider (validates the kinematic graph). */
172
+ build() {
173
+ const links = {};
174
+ for (const [name, link] of this.links) {
175
+ links[name] = {
176
+ name,
177
+ primPath: `/${this.robotName}/${name}`,
178
+ visualPrims: [],
179
+ ...link.inertial ? { inertial: link.inertial } : {},
180
+ // Authored placement, so links no joint reaches keep their build pose.
181
+ ...isIdentityMat42(link.world) ? {} : { worldTransform: link.world }
182
+ };
514
183
  }
515
- }
516
- // -- Display toggles -----------------------------------------------------
517
- get showVisual() {
518
- return this._showVisual;
519
- }
520
- set showVisual(v) {
521
- this._showVisual = v;
522
- this.setKindVisibility("visual", v);
523
- }
524
- get showCollision() {
525
- return this._showCollision;
526
- }
527
- set showCollision(v) {
528
- this._showCollision = v;
529
- this.setKindVisibility("collision", v);
530
- }
531
- get showJointAxes() {
532
- return this._showJointAxes;
533
- }
534
- set showJointAxes(v) {
535
- this._showJointAxes = v;
536
- if (v && this.jointAxesHelpers.length === 0) {
537
- for (const joint of this.jointObjects.values()) {
538
- const h = new THREE4.AxesHelper(this.helperSize);
539
- h.name = `${joint.jointName}:axes`;
540
- joint.add(h);
541
- this.jointAxesHelpers.push(h);
184
+ const joints = {};
185
+ for (const [name, pending] of this.joints) {
186
+ const { options } = pending;
187
+ const parent = options.parent ?? "";
188
+ if (parent !== "" && !this.links.has(parent)) {
189
+ throw new Error(`RobotBuilder: joint "${name}" references unknown parent "${parent}"`);
542
190
  }
543
- }
544
- for (const h of this.jointAxesHelpers) h.visible = v;
545
- }
546
- get showLinkFrames() {
547
- return this._showLinkFrames;
548
- }
549
- set showLinkFrames(v) {
550
- this._showLinkFrames = v;
551
- if (v && this.linkFrameHelpers.length === 0) {
552
- for (const link of this.linkObjects.values()) {
553
- const h = new THREE4.AxesHelper(this.helperSize);
554
- h.name = `${link.linkName}:frame`;
555
- link.add(h);
556
- this.linkFrameHelpers.push(h);
191
+ const childLink = this.links.get(options.child);
192
+ if (!childLink) {
193
+ throw new Error(
194
+ `RobotBuilder: joint "${name}" references unknown child "${options.child}"`
195
+ );
557
196
  }
197
+ const jointWorld = pending.world ?? childLink.world;
198
+ const parentWorld = parent === "" ? identity4() : this.links.get(parent).world;
199
+ const type = refineJointType(pending.type, options.lower, options.upper);
200
+ joints[name] = {
201
+ name,
202
+ primPath: `/${this.robotName}/${name}`,
203
+ type,
204
+ parent,
205
+ child: options.child,
206
+ axis: options.axis ?? "X",
207
+ jointFrame0: multiply(invert(parentWorld), jointWorld),
208
+ jointFrame1: multiply(invert(childLink.world), jointWorld),
209
+ ...options.lower !== void 0 ? { lower: options.lower } : {},
210
+ ...options.upper !== void 0 ? { upper: options.upper } : {},
211
+ ...options.initialValue !== void 0 ? { initialValue: options.initialValue } : {},
212
+ ...options.drive ? { drive: options.drive } : {}
213
+ };
558
214
  }
559
- for (const h of this.linkFrameHelpers) h.visible = v;
560
- }
561
- setKindVisibility(kind, visible) {
562
- this.traverse((o) => {
563
- if (o.userData.kind === kind) o.visible = visible;
564
- });
565
- }
566
- };
567
- function setMatrix(obj, m) {
568
- obj.matrixAutoUpdate = false;
569
- obj.matrix.fromArray(m);
570
- obj.matrixWorldNeedsUpdate = true;
571
- }
572
-
573
- // src/three/ThreeUsdRobotLoader.ts
574
- var ThreeUsdRobotLoader = class {
575
- options;
576
- constructor(options = {}) {
577
- this.options = options;
578
- }
579
- get resolver() {
580
- return this.options.assetResolver ?? new DefaultAssetResolver();
581
- }
582
- /**
583
- * Fetch an asset by URL and build the robot. `.usdz` packages are unzipped and
584
- * composed from their entries; everything else is treated as USDA text.
585
- */
586
- async loadAsync(url) {
587
- if (/\.usdz$/i.test(url)) {
588
- return this.parseUsdz(await this.fetchRootBytes(url));
215
+ if (this.articulationRoot !== void 0 && !this.links.has(this.articulationRoot)) {
216
+ throw new Error(`RobotBuilder: articulationRoot "${this.articulationRoot}" is not a link`);
589
217
  }
590
- const bytes = await this.fetchRootBytes(url);
591
- if (CrateReader.isCrate(bytes)) return this.parseCrate(bytes, url);
592
- return this.parse(new TextDecoder().decode(bytes), url);
593
- }
594
- async fetchRootBytes(url) {
595
- const resolver = this.resolver;
596
- if (resolver.fetchBytes) return resolver.fetchBytes(url);
597
- return new TextEncoder().encode(await resolver.fetchText(url));
598
- }
599
- /** Build a robot (composed, with meshes) from USDA source text. */
600
- async parse(text, baseUrl = "") {
601
- return this.buildFromStage(
602
- await this.composeStage(text, baseUrl, this.resolver),
603
- baseUrl,
604
- this.resolver
605
- );
606
- }
607
- /** Build a robot from the bytes of a `.usdz` package. */
608
- async parseUsdz(bytes) {
609
- const pkg = openUsdz(bytes);
610
- const rootText = await pkg.resolver.fetchText(pkg.rootEntry);
611
- const stage = await this.composeStage(rootText, pkg.rootEntry, pkg.resolver);
612
- return this.buildFromStage(stage, pkg.rootEntry, pkg.resolver);
613
- }
614
- /** Build a robot from the bytes of a binary crate (`.usdc` / binary `.usd`). */
615
- async parseCrate(bytes, baseUrl = "") {
616
- const file = crateToUsdaFile(new CrateReader(bytes));
617
- const composeOptions = this.options.onWarn ? { onWarn: this.options.onWarn } : {};
618
- const composed = await composeFile(file, baseUrl, this.resolver, composeOptions);
619
- return this.buildFromStage(Stage.OpenFromFile(composed), baseUrl, this.resolver);
620
- }
621
- /** Parse + compose USDA source into the Three.js-independent robot IR. */
622
- async parseRobotDescription(text, baseUrl = "") {
623
- const stage = await this.composeStage(text, baseUrl, this.resolver);
624
- return extractRobotDescription(stage, this.extractOptions());
625
- }
626
- buildFromStage(stage, baseUrl, resolver) {
627
- const robot = extractRobotDescription(stage, this.extractOptions());
628
- const tree = buildKinematicTree(robot);
629
- const robot3d = new ThreeUsdRobot(robot, tree, this.robotOptions());
630
- const loadVisuals = this.options.loadVisuals ?? true;
631
- const loadCollisions = this.options.loadCollisions ?? false;
632
- if (loadVisuals || loadCollisions) {
633
- const textureProvider = this.options.loadTextures ?? true ? createTextureProvider(resolver, baseUrl) : void 0;
634
- bindRobotMeshes(stage, robot3d, robot, {
635
- loadVisuals,
636
- loadCollisions,
637
- ...textureProvider ? { textureProvider } : {}
218
+ const desc = {
219
+ name: this.robotName,
220
+ rootLink: "",
221
+ links,
222
+ joints,
223
+ upAxis: this.upAxis,
224
+ metersPerUnit: this.metersPerUnit,
225
+ ...this.articulationRoot !== void 0 ? { articulationRoots: [this.articulationRoot] } : {}
226
+ };
227
+ const tree = buildKinematicTree(desc, { onWarn: this.warn });
228
+ desc.rootLink = tree.root;
229
+ if (tree.loopJoints.length > 0) desc.loopJoints = tree.loopJoints;
230
+ if (tree.warnings.length > 0) desc.warnings = tree.warnings;
231
+ const byLink = this.links;
232
+ const geometry = (linkKey) => byLink.get(linkKey)?.meshes ?? [];
233
+ return { desc, geometry, tree };
234
+ }
235
+ /** Build and export in one step (pass the result to `serializeUsda`). */
236
+ toUsda(options = {}) {
237
+ const { desc, geometry } = this.build();
238
+ return exportRobotUsda(desc, { onWarn: this.warn, ...options, geometry });
239
+ }
240
+ /** Collect `THREE.Mesh` descendants of `roots` as link-relative export meshes. */
241
+ gatherMeshes(roots, linkWorld, kind, collisionExtras = {}) {
242
+ const linkInverse = invert(linkWorld);
243
+ const out = [];
244
+ for (const root of roots) {
245
+ root.updateWorldMatrix(true, true);
246
+ root.traverse((obj) => {
247
+ const meshObj = obj;
248
+ if (!meshObj.isMesh) return;
249
+ const material = Array.isArray(meshObj.material) ? meshObj.material[0] : meshObj.material;
250
+ if (Array.isArray(meshObj.material) && meshObj.material.length > 1) {
251
+ this.warn(
252
+ `mesh "${meshObj.name}": multi-material meshes export their first material only`
253
+ );
254
+ }
255
+ const exportMaterial = material ? this.convertMaterial(material) : void 0;
256
+ const mesh = geometryToExportMesh(meshObj.geometry, {
257
+ name: meshObj.name || "mesh",
258
+ kind,
259
+ transform: multiply(linkInverse, [...meshObj.matrixWorld.elements]),
260
+ ...exportMaterial ? { material: exportMaterial } : {},
261
+ doubleSided: material?.side === THREE.DoubleSide
262
+ });
263
+ if (mesh) {
264
+ if (kind === "collision") {
265
+ if (collisionExtras.approximation !== void 0) {
266
+ mesh.collisionApproximation = collisionExtras.approximation;
267
+ }
268
+ if (collisionExtras.material) mesh.physicsMaterial = collisionExtras.material;
269
+ }
270
+ out.push(mesh);
271
+ } else {
272
+ this.warn(`mesh "${meshObj.name}": no triangle geometry; skipped`);
273
+ }
638
274
  });
639
275
  }
640
- return robot3d;
641
- }
642
- async composeStage(text, baseUrl, resolver) {
643
- const composeOptions = this.options.onWarn ? { onWarn: this.options.onWarn } : {};
644
- return Stage.OpenFromFile(await composeLayer(text, baseUrl, resolver, composeOptions));
276
+ return out;
645
277
  }
646
- robotOptions() {
647
- return {
648
- upAxisConversion: this.options.upAxisConversion ?? "auto",
649
- ...this.options.clampJointLimits !== void 0 ? { clampJointLimits: this.options.clampJointLimits } : {},
650
- ...this.options.unitScale !== void 0 ? { unitScale: this.options.unitScale } : {},
651
- ...this.options.applyDriveTargetsAsInitialPose !== void 0 ? { applyInitialPose: this.options.applyDriveTargetsAsInitialPose } : {}
652
- };
653
- }
654
- extractOptions() {
655
- return {
656
- ...this.options.robotName !== void 0 ? { robotName: this.options.robotName } : {},
657
- ...this.options.onWarn !== void 0 ? { onWarn: this.options.onWarn } : {}
658
- };
278
+ convertMaterial(material) {
279
+ if (!this.materialCache.has(material)) {
280
+ const name = material.name || `Material_${this.materialAutoId++}`;
281
+ this.materialCache.set(material, materialToExportMaterial(material, name, this.warn));
282
+ }
283
+ return this.materialCache.get(material);
659
284
  }
660
285
  };
286
+ function isIdentityMat42(m) {
287
+ for (let i = 0; i < 16; i++) {
288
+ if (m[i] !== (i % 5 === 0 ? 1 : 0)) return false;
289
+ }
290
+ return true;
291
+ }
292
+ function resolveFrame(frame) {
293
+ if (!frame) return void 0;
294
+ if (frame.isObject3D) {
295
+ const obj = frame;
296
+ obj.updateWorldMatrix(true, false);
297
+ return [...obj.matrixWorld.elements];
298
+ }
299
+ return [...frame.elements];
300
+ }
661
301
 
662
- export { JointObject, LinkObject, ThreeUsdRobot, ThreeUsdRobotLoader, axisVector, bindRobotMeshes, buildMeshGeometry, buildMeshMaterial, createTextureProvider, resolveBoundMaterial };
302
+ export { RobotBuilder, exportThreeUsdRobot, geometryToExportMesh, materialToExportMaterial, threeGeometryProvider };
663
303
  //# sourceMappingURL=index.js.map
664
304
  //# sourceMappingURL=index.js.map