urdf-loader 0.10.4 → 0.10.5

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/src/URDFLoader.js CHANGED
@@ -1,664 +1,664 @@
1
- import * as THREE from 'three';
2
- import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
3
- import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
4
- import { URDFRobot, URDFJoint, URDFLink, URDFCollider, URDFVisual, URDFMimicJoint } from './URDFClasses.js';
5
-
6
- /*
7
- Reference coordinate frames for THREE.js and ROS.
8
- Both coordinate systems are right handed so the URDF is instantiated without
9
- frame transforms. The resulting model can be rotated to rectify the proper up,
10
- right, and forward directions
11
-
12
- THREE.js
13
- Y
14
- |
15
- |
16
- .-----X
17
-
18
- Z
19
-
20
- ROS URDf
21
- Z
22
- | X
23
- | /
24
- Y-----.
25
-
26
- */
27
-
28
- const tempQuaternion = new THREE.Quaternion();
29
- const tempEuler = new THREE.Euler();
30
-
31
- // take a vector "x y z" and process it into
32
- // an array [x, y, z]
33
- function processTuple(val) {
34
-
35
- if (!val) return [0, 0, 0];
36
- return val.trim().split(/\s+/g).map(num => parseFloat(num));
37
-
38
- }
39
-
40
- // applies a rotation a threejs object in URDF order
41
- function applyRotation(obj, rpy, additive = false) {
42
-
43
- // if additive is true the rotation is applied in
44
- // addition to the existing rotation
45
- if (!additive) obj.rotation.set(0, 0, 0);
46
-
47
- tempEuler.set(rpy[0], rpy[1], rpy[2], 'ZYX');
48
- tempQuaternion.setFromEuler(tempEuler);
49
- tempQuaternion.multiply(obj.quaternion);
50
- obj.quaternion.copy(tempQuaternion);
51
-
52
- }
53
-
54
- /* URDFLoader Class */
55
- // Loads and reads a URDF file into a THREEjs Object3D format
56
- export default
57
- class URDFLoader {
58
-
59
- constructor(manager) {
60
-
61
- this.manager = manager || THREE.DefaultLoadingManager;
62
- this.loadMeshCb = this.defaultMeshLoader.bind(this);
63
- this.parseVisual = true;
64
- this.parseCollision = false;
65
- this.packages = '';
66
- this.workingPath = '';
67
- this.fetchOptions = {};
68
-
69
- }
70
-
71
- /* Public API */
72
- loadAsync(urdf) {
73
-
74
- return new Promise((resolve, reject) => {
75
-
76
- this.load(urdf, resolve, null, reject);
77
-
78
- });
79
-
80
- }
81
-
82
- // urdf: The path to the URDF within the package OR absolute
83
- // onComplete: Callback that is passed the model once loaded
84
- load(urdf, onComplete, onProgress, onError) {
85
-
86
- // Check if a full URI is specified before
87
- // prepending the package info
88
- const manager = this.manager;
89
- const workingPath = THREE.LoaderUtils.extractUrlBase(urdf);
90
- const urdfPath = this.manager.resolveURL(urdf);
91
-
92
- manager.itemStart(urdfPath);
93
-
94
- fetch(urdfPath, this.fetchOptions)
95
- .then(res => {
96
-
97
- if (res.ok) {
98
-
99
- if (onProgress) {
100
-
101
- onProgress(null);
102
-
103
- }
104
- return res.text();
105
-
106
- } else {
107
-
108
- throw new Error(`URDFLoader: Failed to load url '${ urdfPath }' with error code ${ res.status } : ${ res.statusText }.`);
109
-
110
- }
111
-
112
- })
113
- .then(data => {
114
-
115
- if (this.workingPath === '') {
116
-
117
- this.workingPath = workingPath;
118
-
119
- }
120
-
121
- const model = this.parse(data);
122
- onComplete(model);
123
- manager.itemEnd(urdfPath);
124
-
125
- })
126
- .catch(e => {
127
-
128
- if (onError) {
129
-
130
- onError(e);
131
-
132
- } else {
133
-
134
- console.error('URDFLoader: Error loading file.', e);
135
-
136
- }
137
- manager.itemError(urdfPath);
138
- manager.itemEnd(urdfPath);
139
-
140
- });
141
-
142
- }
143
-
144
- parse(content) {
145
-
146
- const packages = this.packages;
147
- const loadMeshCb = this.loadMeshCb;
148
- const parseVisual = this.parseVisual;
149
- const parseCollision = this.parseCollision;
150
- const workingPath = this.workingPath;
151
- const manager = this.manager;
152
- const linkMap = {};
153
- const jointMap = {};
154
- const materialMap = {};
155
-
156
- // Resolves the path of mesh files
157
- function resolvePath(path) {
158
-
159
- if (!/^package:\/\//.test(path)) {
160
-
161
- return workingPath ? workingPath + path : path;
162
-
163
- }
164
-
165
- // Remove "package://" keyword and split meshPath at the first slash
166
- const [targetPkg, relPath] = path.replace(/^package:\/\//, '').split(/\/(.+)/);
167
-
168
- if (typeof packages === 'string') {
169
-
170
- // "pkg" is one single package
171
- if (packages.endsWith(targetPkg)) {
172
-
173
- // "pkg" is the target package
174
- return packages + '/' + relPath;
175
-
176
- } else {
177
-
178
- // Assume "pkg" is the target package's parent directory
179
- return packages + '/' + targetPkg + '/' + relPath;
180
-
181
- }
182
-
183
- } else if (packages instanceof Function) {
184
-
185
- return packages(targetPkg) + '/' + relPath;
186
-
187
- } else if (typeof packages === 'object') {
188
-
189
- // "pkg" is a map of packages
190
- if (targetPkg in packages) {
191
-
192
- return packages[targetPkg] + '/' + relPath;
193
-
194
- } else {
195
-
196
- console.error(`URDFLoader : ${ targetPkg } not found in provided package list.`);
197
- return null;
198
-
199
- }
200
-
201
- }
202
-
203
- }
204
-
205
- // Process the URDF text format
206
- function processUrdf(data) {
207
-
208
- let children;
209
- if (data instanceof Document) {
210
-
211
- children = [ ...data.children ];
212
-
213
- } else if (data instanceof Element) {
214
-
215
- children = [ data ];
216
-
217
- } else {
218
-
219
- const parser = new DOMParser();
220
- const urdf = parser.parseFromString(data, 'text/xml');
221
- children = [ ...urdf.children ];
222
-
223
- }
224
-
225
- const robotNode = children.filter(c => c.nodeName === 'robot').pop();
226
- return processRobot(robotNode);
227
-
228
- }
229
-
230
- // Process the <robot> node
231
- function processRobot(robot) {
232
-
233
- const robotNodes = [ ...robot.children ];
234
- const links = robotNodes.filter(c => c.nodeName.toLowerCase() === 'link');
235
- const joints = robotNodes.filter(c => c.nodeName.toLowerCase() === 'joint');
236
- const materials = robotNodes.filter(c => c.nodeName.toLowerCase() === 'material');
237
- const obj = new URDFRobot();
238
-
239
- obj.robotName = robot.getAttribute('name');
240
- obj.urdfRobotNode = robot;
241
-
242
- // Create the <material> map
243
- materials.forEach(m => {
244
-
245
- const name = m.getAttribute('name');
246
- materialMap[name] = processMaterial(m);
247
-
248
- });
249
-
250
- // Create the <link> map
251
- const visualMap = {};
252
- const colliderMap = {};
253
- links.forEach(l => {
254
-
255
- const name = l.getAttribute('name');
256
- const isRoot = robot.querySelector(`child[link="${ name }"]`) === null;
257
- linkMap[name] = processLink(l, visualMap, colliderMap, isRoot ? obj : null);
258
-
259
- });
260
-
261
- // Create the <joint> map
262
- joints.forEach(j => {
263
-
264
- const name = j.getAttribute('name');
265
- jointMap[name] = processJoint(j);
266
-
267
- });
268
-
269
- obj.joints = jointMap;
270
- obj.links = linkMap;
271
- obj.colliders = colliderMap;
272
- obj.visual = visualMap;
273
-
274
- // Link up mimic joints
275
- const jointList = Object.values(jointMap);
276
- jointList.forEach(j => {
277
-
278
- if (j instanceof URDFMimicJoint) {
279
-
280
- jointMap[j.mimicJoint].mimicJoints.push(j);
281
-
282
- }
283
-
284
- });
285
-
286
- // Detect infinite loops of mimic joints
287
- jointList.forEach(j => {
288
-
289
- const uniqueJoints = new Set();
290
- const iterFunction = joint => {
291
-
292
- if (uniqueJoints.has(joint)) {
293
-
294
- throw new Error('URDFLoader: Detected an infinite loop of mimic joints.');
295
-
296
- }
297
-
298
- uniqueJoints.add(joint);
299
- joint.mimicJoints.forEach(j => {
300
-
301
- iterFunction(j);
302
-
303
- });
304
-
305
- };
306
-
307
- iterFunction(j);
308
- });
309
-
310
- obj.frames = {
311
- ...colliderMap,
312
- ...visualMap,
313
- ...linkMap,
314
- ...jointMap,
315
- };
316
-
317
- return obj;
318
-
319
- }
320
-
321
- // Process joint nodes and parent them
322
- function processJoint(joint) {
323
-
324
- const children = [ ...joint.children ];
325
- const jointType = joint.getAttribute('type');
326
-
327
- let obj;
328
-
329
- const mimicTag = children.find(n => n.nodeName.toLowerCase() === 'mimic');
330
- if (mimicTag) {
331
-
332
- obj = new URDFMimicJoint();
333
- obj.mimicJoint = mimicTag.getAttribute('joint');
334
- obj.multiplier = parseFloat(mimicTag.getAttribute('multiplier') || 1.0);
335
- obj.offset = parseFloat(mimicTag.getAttribute('offset') || 0.0);
336
-
337
- } else {
338
-
339
- obj = new URDFJoint();
340
-
341
- }
342
-
343
- obj.urdfNode = joint;
344
- obj.name = joint.getAttribute('name');
345
- obj.urdfName = obj.name;
346
- obj.jointType = jointType;
347
-
348
- let parent = null;
349
- let child = null;
350
- let xyz = [0, 0, 0];
351
- let rpy = [0, 0, 0];
352
-
353
- // Extract the attributes
354
- children.forEach(n => {
355
-
356
- const type = n.nodeName.toLowerCase();
357
- if (type === 'origin') {
358
-
359
- xyz = processTuple(n.getAttribute('xyz'));
360
- rpy = processTuple(n.getAttribute('rpy'));
361
-
362
- } else if (type === 'child') {
363
-
364
- child = linkMap[n.getAttribute('link')];
365
-
366
- } else if (type === 'parent') {
367
-
368
- parent = linkMap[n.getAttribute('link')];
369
-
370
- } else if (type === 'limit') {
371
-
372
- obj.limit.lower = parseFloat(n.getAttribute('lower') || obj.limit.lower);
373
- obj.limit.upper = parseFloat(n.getAttribute('upper') || obj.limit.upper);
374
-
375
- }
376
- });
377
-
378
- // Join the links
379
- parent.add(obj);
380
- obj.add(child);
381
- applyRotation(obj, rpy);
382
- obj.position.set(xyz[0], xyz[1], xyz[2]);
383
-
384
- // Set up the rotate function
385
- const axisNode = children.filter(n => n.nodeName.toLowerCase() === 'axis')[0];
386
-
387
- if (axisNode) {
388
-
389
- const axisXYZ = axisNode.getAttribute('xyz').split(/\s+/g).map(num => parseFloat(num));
390
- obj.axis = new THREE.Vector3(axisXYZ[0], axisXYZ[1], axisXYZ[2]);
391
- obj.axis.normalize();
392
-
393
- }
394
-
395
- return obj;
396
-
397
- }
398
-
399
- // Process the <link> nodes
400
- function processLink(link, visualMap, colliderMap, target = null) {
401
-
402
- if (target === null) {
403
-
404
- target = new URDFLink();
405
-
406
- }
407
-
408
- const children = [ ...link.children ];
409
- target.name = link.getAttribute('name');
410
- target.urdfName = target.name;
411
- target.urdfNode = link;
412
-
413
- if (parseVisual) {
414
-
415
- const visualNodes = children.filter(n => n.nodeName.toLowerCase() === 'visual');
416
- visualNodes.forEach(vn => {
417
-
418
- const v = processLinkElement(vn, materialMap);
419
- target.add(v);
420
-
421
- if (vn.hasAttribute('name')) {
422
-
423
- const name = vn.getAttribute('name');
424
- v.name = name;
425
- v.urdfName = name;
426
- visualMap[name] = v;
427
-
428
- }
429
-
430
- });
431
-
432
- }
433
-
434
- if (parseCollision) {
435
-
436
- const collisionNodes = children.filter(n => n.nodeName.toLowerCase() === 'collision');
437
- collisionNodes.forEach(cn => {
438
-
439
- const c = processLinkElement(cn);
440
- target.add(c);
441
-
442
- if (cn.hasAttribute('name')) {
443
-
444
- const name = cn.getAttribute('name');
445
- c.name = name;
446
- c.urdfName = name;
447
- colliderMap[name] = c;
448
-
449
- }
450
-
451
- });
452
-
453
- }
454
-
455
- return target;
456
-
457
- }
458
-
459
- function processMaterial(node) {
460
-
461
- const matNodes = [ ...node.children ];
462
- const material = new THREE.MeshPhongMaterial();
463
-
464
- material.name = node.getAttribute('name') || '';
465
- matNodes.forEach(n => {
466
-
467
- const type = n.nodeName.toLowerCase();
468
- if (type === 'color') {
469
-
470
- const rgba =
471
- n
472
- .getAttribute('rgba')
473
- .split(/\s/g)
474
- .map(v => parseFloat(v));
475
-
476
- material.color.setRGB(rgba[0], rgba[1], rgba[2]).convertSRGBToLinear();
477
- material.opacity = rgba[3];
478
- material.transparent = rgba[3] < 1;
479
- material.depthWrite = !material.transparent;
480
-
481
- } else if (type === 'texture') {
482
-
483
- // The URDF spec does not require that the <texture/> tag include
484
- // a filename attribute so skip loading the texture if not provided.
485
- const filename = n.getAttribute('filename');
486
- if (filename) {
487
-
488
- const loader = new THREE.TextureLoader(manager);
489
- const filePath = resolvePath(filename);
490
- material.map = loader.load(filePath);
491
- material.map.encoding = THREE.sRGBEncoding;
492
-
493
- }
494
-
495
- }
496
- });
497
-
498
- return material;
499
-
500
- }
501
-
502
- // Process the visual and collision nodes into meshes
503
- function processLinkElement(vn, materialMap = {}) {
504
-
505
- const isCollisionNode = vn.nodeName.toLowerCase() === 'collision';
506
- const children = [ ...vn.children ];
507
- let material = null;
508
-
509
- // get the material first
510
- const materialNode = children.filter(n => n.nodeName.toLowerCase() === 'material')[0];
511
- if (materialNode) {
512
-
513
- const name = materialNode.getAttribute('name');
514
- if (name && name in materialMap) {
515
-
516
- material = materialMap[name];
517
-
518
- } else {
519
-
520
- material = processMaterial(materialNode);
521
-
522
- }
523
-
524
- } else {
525
-
526
- material = new THREE.MeshPhongMaterial();
527
-
528
- }
529
-
530
- const group = isCollisionNode ? new URDFCollider() : new URDFVisual();
531
- group.urdfNode = vn;
532
-
533
- children.forEach(n => {
534
-
535
- const type = n.nodeName.toLowerCase();
536
- if (type === 'geometry') {
537
-
538
- const geoType = n.children[0].nodeName.toLowerCase();
539
- if (geoType === 'mesh') {
540
-
541
- const filename = n.children[0].getAttribute('filename');
542
- const filePath = resolvePath(filename);
543
-
544
- // file path is null if a package directory is not provided.
545
- if (filePath !== null) {
546
-
547
- const scaleAttr = n.children[0].getAttribute('scale');
548
- if (scaleAttr) {
549
-
550
- const scale = processTuple(scaleAttr);
551
- group.scale.set(scale[0], scale[1], scale[2]);
552
-
553
- }
554
-
555
- loadMeshCb(filePath, manager, (obj, err) => {
556
-
557
- if (err) {
558
-
559
- console.error('URDFLoader: Error loading mesh.', err);
560
-
561
- } else if (obj) {
562
-
563
- if (obj instanceof THREE.Mesh) {
564
-
565
- obj.material = material;
566
-
567
- }
568
-
569
- // We don't expect non identity rotations or positions. In the case of
570
- // COLLADA files the model might come in with a custom scale for unit
571
- // conversion.
572
- obj.position.set(0, 0, 0);
573
- obj.quaternion.identity();
574
- group.add(obj);
575
-
576
- }
577
-
578
- });
579
-
580
- }
581
-
582
- } else if (geoType === 'box') {
583
-
584
- const primitiveModel = new THREE.Mesh();
585
- primitiveModel.geometry = new THREE.BoxBufferGeometry(1, 1, 1);
586
- primitiveModel.material = material;
587
-
588
- const size = processTuple(n.children[0].getAttribute('size'));
589
- primitiveModel.scale.set(size[0], size[1], size[2]);
590
-
591
- group.add(primitiveModel);
592
-
593
- } else if (geoType === 'sphere') {
594
-
595
- const primitiveModel = new THREE.Mesh();
596
- primitiveModel.geometry = new THREE.SphereBufferGeometry(1, 30, 30);
597
- primitiveModel.material = material;
598
-
599
- const radius = parseFloat(n.children[0].getAttribute('radius')) || 0;
600
- primitiveModel.scale.set(radius, radius, radius);
601
-
602
- group.add(primitiveModel);
603
-
604
- } else if (geoType === 'cylinder') {
605
-
606
- const primitiveModel = new THREE.Mesh();
607
- primitiveModel.geometry = new THREE.CylinderBufferGeometry(1, 1, 1, 30);
608
- primitiveModel.material = material;
609
-
610
- const radius = parseFloat(n.children[0].getAttribute('radius')) || 0;
611
- const length = parseFloat(n.children[0].getAttribute('length')) || 0;
612
- primitiveModel.scale.set(radius, length, radius);
613
- primitiveModel.rotation.set(Math.PI / 2, 0, 0);
614
-
615
- group.add(primitiveModel);
616
-
617
- }
618
-
619
- } else if (type === 'origin') {
620
-
621
- const xyz = processTuple(n.getAttribute('xyz'));
622
- const rpy = processTuple(n.getAttribute('rpy'));
623
-
624
- group.position.set(xyz[0], xyz[1], xyz[2]);
625
- group.rotation.set(0, 0, 0);
626
- applyRotation(group, rpy);
627
-
628
- }
629
-
630
- });
631
-
632
- return group;
633
-
634
- }
635
-
636
- return processUrdf(content);
637
-
638
- }
639
-
640
- // Default mesh loading function
641
- defaultMeshLoader(path, manager, done) {
642
-
643
- if (/\.stl$/i.test(path)) {
644
-
645
- const loader = new STLLoader(manager);
646
- loader.load(path, geom => {
647
- const mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial());
648
- done(mesh);
649
- });
650
-
651
- } else if (/\.dae$/i.test(path)) {
652
-
653
- const loader = new ColladaLoader(manager);
654
- loader.load(path, dae => done(dae.scene));
655
-
656
- } else {
657
-
658
- console.warn(`URDFLoader: Could not load model at ${ path }.\nNo loader available`);
659
-
660
- }
661
-
662
- }
663
-
664
- };
1
+ import * as THREE from 'three';
2
+ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
3
+ import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
4
+ import { URDFRobot, URDFJoint, URDFLink, URDFCollider, URDFVisual, URDFMimicJoint } from './URDFClasses.js';
5
+
6
+ /*
7
+ Reference coordinate frames for THREE.js and ROS.
8
+ Both coordinate systems are right handed so the URDF is instantiated without
9
+ frame transforms. The resulting model can be rotated to rectify the proper up,
10
+ right, and forward directions
11
+
12
+ THREE.js
13
+ Y
14
+ |
15
+ |
16
+ .-----X
17
+
18
+ Z
19
+
20
+ ROS URDf
21
+ Z
22
+ | X
23
+ | /
24
+ Y-----.
25
+
26
+ */
27
+
28
+ const tempQuaternion = new THREE.Quaternion();
29
+ const tempEuler = new THREE.Euler();
30
+
31
+ // take a vector "x y z" and process it into
32
+ // an array [x, y, z]
33
+ function processTuple(val) {
34
+
35
+ if (!val) return [0, 0, 0];
36
+ return val.trim().split(/\s+/g).map(num => parseFloat(num));
37
+
38
+ }
39
+
40
+ // applies a rotation a threejs object in URDF order
41
+ function applyRotation(obj, rpy, additive = false) {
42
+
43
+ // if additive is true the rotation is applied in
44
+ // addition to the existing rotation
45
+ if (!additive) obj.rotation.set(0, 0, 0);
46
+
47
+ tempEuler.set(rpy[0], rpy[1], rpy[2], 'ZYX');
48
+ tempQuaternion.setFromEuler(tempEuler);
49
+ tempQuaternion.multiply(obj.quaternion);
50
+ obj.quaternion.copy(tempQuaternion);
51
+
52
+ }
53
+
54
+ /* URDFLoader Class */
55
+ // Loads and reads a URDF file into a THREEjs Object3D format
56
+ export default
57
+ class URDFLoader {
58
+
59
+ constructor(manager) {
60
+
61
+ this.manager = manager || THREE.DefaultLoadingManager;
62
+ this.loadMeshCb = this.defaultMeshLoader.bind(this);
63
+ this.parseVisual = true;
64
+ this.parseCollision = false;
65
+ this.packages = '';
66
+ this.workingPath = '';
67
+ this.fetchOptions = {};
68
+
69
+ }
70
+
71
+ /* Public API */
72
+ loadAsync(urdf) {
73
+
74
+ return new Promise((resolve, reject) => {
75
+
76
+ this.load(urdf, resolve, null, reject);
77
+
78
+ });
79
+
80
+ }
81
+
82
+ // urdf: The path to the URDF within the package OR absolute
83
+ // onComplete: Callback that is passed the model once loaded
84
+ load(urdf, onComplete, onProgress, onError) {
85
+
86
+ // Check if a full URI is specified before
87
+ // prepending the package info
88
+ const manager = this.manager;
89
+ const workingPath = THREE.LoaderUtils.extractUrlBase(urdf);
90
+ const urdfPath = this.manager.resolveURL(urdf);
91
+
92
+ manager.itemStart(urdfPath);
93
+
94
+ fetch(urdfPath, this.fetchOptions)
95
+ .then(res => {
96
+
97
+ if (res.ok) {
98
+
99
+ if (onProgress) {
100
+
101
+ onProgress(null);
102
+
103
+ }
104
+ return res.text();
105
+
106
+ } else {
107
+
108
+ throw new Error(`URDFLoader: Failed to load url '${ urdfPath }' with error code ${ res.status } : ${ res.statusText }.`);
109
+
110
+ }
111
+
112
+ })
113
+ .then(data => {
114
+
115
+ if (this.workingPath === '') {
116
+
117
+ this.workingPath = workingPath;
118
+
119
+ }
120
+
121
+ const model = this.parse(data);
122
+ onComplete(model);
123
+ manager.itemEnd(urdfPath);
124
+
125
+ })
126
+ .catch(e => {
127
+
128
+ if (onError) {
129
+
130
+ onError(e);
131
+
132
+ } else {
133
+
134
+ console.error('URDFLoader: Error loading file.', e);
135
+
136
+ }
137
+ manager.itemError(urdfPath);
138
+ manager.itemEnd(urdfPath);
139
+
140
+ });
141
+
142
+ }
143
+
144
+ parse(content) {
145
+
146
+ const packages = this.packages;
147
+ const loadMeshCb = this.loadMeshCb;
148
+ const parseVisual = this.parseVisual;
149
+ const parseCollision = this.parseCollision;
150
+ const workingPath = this.workingPath;
151
+ const manager = this.manager;
152
+ const linkMap = {};
153
+ const jointMap = {};
154
+ const materialMap = {};
155
+
156
+ // Resolves the path of mesh files
157
+ function resolvePath(path) {
158
+
159
+ if (!/^package:\/\//.test(path)) {
160
+
161
+ return workingPath ? workingPath + path : path;
162
+
163
+ }
164
+
165
+ // Remove "package://" keyword and split meshPath at the first slash
166
+ const [targetPkg, relPath] = path.replace(/^package:\/\//, '').split(/\/(.+)/);
167
+
168
+ if (typeof packages === 'string') {
169
+
170
+ // "pkg" is one single package
171
+ if (packages.endsWith(targetPkg)) {
172
+
173
+ // "pkg" is the target package
174
+ return packages + '/' + relPath;
175
+
176
+ } else {
177
+
178
+ // Assume "pkg" is the target package's parent directory
179
+ return packages + '/' + targetPkg + '/' + relPath;
180
+
181
+ }
182
+
183
+ } else if (packages instanceof Function) {
184
+
185
+ return packages(targetPkg) + '/' + relPath;
186
+
187
+ } else if (typeof packages === 'object') {
188
+
189
+ // "pkg" is a map of packages
190
+ if (targetPkg in packages) {
191
+
192
+ return packages[targetPkg] + '/' + relPath;
193
+
194
+ } else {
195
+
196
+ console.error(`URDFLoader : ${ targetPkg } not found in provided package list.`);
197
+ return null;
198
+
199
+ }
200
+
201
+ }
202
+
203
+ }
204
+
205
+ // Process the URDF text format
206
+ function processUrdf(data) {
207
+
208
+ let children;
209
+ if (data instanceof Document) {
210
+
211
+ children = [ ...data.children ];
212
+
213
+ } else if (data instanceof Element) {
214
+
215
+ children = [ data ];
216
+
217
+ } else {
218
+
219
+ const parser = new DOMParser();
220
+ const urdf = parser.parseFromString(data, 'text/xml');
221
+ children = [ ...urdf.children ];
222
+
223
+ }
224
+
225
+ const robotNode = children.filter(c => c.nodeName === 'robot').pop();
226
+ return processRobot(robotNode);
227
+
228
+ }
229
+
230
+ // Process the <robot> node
231
+ function processRobot(robot) {
232
+
233
+ const robotNodes = [ ...robot.children ];
234
+ const links = robotNodes.filter(c => c.nodeName.toLowerCase() === 'link');
235
+ const joints = robotNodes.filter(c => c.nodeName.toLowerCase() === 'joint');
236
+ const materials = robotNodes.filter(c => c.nodeName.toLowerCase() === 'material');
237
+ const obj = new URDFRobot();
238
+
239
+ obj.robotName = robot.getAttribute('name');
240
+ obj.urdfRobotNode = robot;
241
+
242
+ // Create the <material> map
243
+ materials.forEach(m => {
244
+
245
+ const name = m.getAttribute('name');
246
+ materialMap[name] = processMaterial(m);
247
+
248
+ });
249
+
250
+ // Create the <link> map
251
+ const visualMap = {};
252
+ const colliderMap = {};
253
+ links.forEach(l => {
254
+
255
+ const name = l.getAttribute('name');
256
+ const isRoot = robot.querySelector(`child[link="${ name }"]`) === null;
257
+ linkMap[name] = processLink(l, visualMap, colliderMap, isRoot ? obj : null);
258
+
259
+ });
260
+
261
+ // Create the <joint> map
262
+ joints.forEach(j => {
263
+
264
+ const name = j.getAttribute('name');
265
+ jointMap[name] = processJoint(j);
266
+
267
+ });
268
+
269
+ obj.joints = jointMap;
270
+ obj.links = linkMap;
271
+ obj.colliders = colliderMap;
272
+ obj.visual = visualMap;
273
+
274
+ // Link up mimic joints
275
+ const jointList = Object.values(jointMap);
276
+ jointList.forEach(j => {
277
+
278
+ if (j instanceof URDFMimicJoint) {
279
+
280
+ jointMap[j.mimicJoint].mimicJoints.push(j);
281
+
282
+ }
283
+
284
+ });
285
+
286
+ // Detect infinite loops of mimic joints
287
+ jointList.forEach(j => {
288
+
289
+ const uniqueJoints = new Set();
290
+ const iterFunction = joint => {
291
+
292
+ if (uniqueJoints.has(joint)) {
293
+
294
+ throw new Error('URDFLoader: Detected an infinite loop of mimic joints.');
295
+
296
+ }
297
+
298
+ uniqueJoints.add(joint);
299
+ joint.mimicJoints.forEach(j => {
300
+
301
+ iterFunction(j);
302
+
303
+ });
304
+
305
+ };
306
+
307
+ iterFunction(j);
308
+ });
309
+
310
+ obj.frames = {
311
+ ...colliderMap,
312
+ ...visualMap,
313
+ ...linkMap,
314
+ ...jointMap,
315
+ };
316
+
317
+ return obj;
318
+
319
+ }
320
+
321
+ // Process joint nodes and parent them
322
+ function processJoint(joint) {
323
+
324
+ const children = [ ...joint.children ];
325
+ const jointType = joint.getAttribute('type');
326
+
327
+ let obj;
328
+
329
+ const mimicTag = children.find(n => n.nodeName.toLowerCase() === 'mimic');
330
+ if (mimicTag) {
331
+
332
+ obj = new URDFMimicJoint();
333
+ obj.mimicJoint = mimicTag.getAttribute('joint');
334
+ obj.multiplier = parseFloat(mimicTag.getAttribute('multiplier') || 1.0);
335
+ obj.offset = parseFloat(mimicTag.getAttribute('offset') || 0.0);
336
+
337
+ } else {
338
+
339
+ obj = new URDFJoint();
340
+
341
+ }
342
+
343
+ obj.urdfNode = joint;
344
+ obj.name = joint.getAttribute('name');
345
+ obj.urdfName = obj.name;
346
+ obj.jointType = jointType;
347
+
348
+ let parent = null;
349
+ let child = null;
350
+ let xyz = [0, 0, 0];
351
+ let rpy = [0, 0, 0];
352
+
353
+ // Extract the attributes
354
+ children.forEach(n => {
355
+
356
+ const type = n.nodeName.toLowerCase();
357
+ if (type === 'origin') {
358
+
359
+ xyz = processTuple(n.getAttribute('xyz'));
360
+ rpy = processTuple(n.getAttribute('rpy'));
361
+
362
+ } else if (type === 'child') {
363
+
364
+ child = linkMap[n.getAttribute('link')];
365
+
366
+ } else if (type === 'parent') {
367
+
368
+ parent = linkMap[n.getAttribute('link')];
369
+
370
+ } else if (type === 'limit') {
371
+
372
+ obj.limit.lower = parseFloat(n.getAttribute('lower') || obj.limit.lower);
373
+ obj.limit.upper = parseFloat(n.getAttribute('upper') || obj.limit.upper);
374
+
375
+ }
376
+ });
377
+
378
+ // Join the links
379
+ parent.add(obj);
380
+ obj.add(child);
381
+ applyRotation(obj, rpy);
382
+ obj.position.set(xyz[0], xyz[1], xyz[2]);
383
+
384
+ // Set up the rotate function
385
+ const axisNode = children.filter(n => n.nodeName.toLowerCase() === 'axis')[0];
386
+
387
+ if (axisNode) {
388
+
389
+ const axisXYZ = axisNode.getAttribute('xyz').split(/\s+/g).map(num => parseFloat(num));
390
+ obj.axis = new THREE.Vector3(axisXYZ[0], axisXYZ[1], axisXYZ[2]);
391
+ obj.axis.normalize();
392
+
393
+ }
394
+
395
+ return obj;
396
+
397
+ }
398
+
399
+ // Process the <link> nodes
400
+ function processLink(link, visualMap, colliderMap, target = null) {
401
+
402
+ if (target === null) {
403
+
404
+ target = new URDFLink();
405
+
406
+ }
407
+
408
+ const children = [ ...link.children ];
409
+ target.name = link.getAttribute('name');
410
+ target.urdfName = target.name;
411
+ target.urdfNode = link;
412
+
413
+ if (parseVisual) {
414
+
415
+ const visualNodes = children.filter(n => n.nodeName.toLowerCase() === 'visual');
416
+ visualNodes.forEach(vn => {
417
+
418
+ const v = processLinkElement(vn, materialMap);
419
+ target.add(v);
420
+
421
+ if (vn.hasAttribute('name')) {
422
+
423
+ const name = vn.getAttribute('name');
424
+ v.name = name;
425
+ v.urdfName = name;
426
+ visualMap[name] = v;
427
+
428
+ }
429
+
430
+ });
431
+
432
+ }
433
+
434
+ if (parseCollision) {
435
+
436
+ const collisionNodes = children.filter(n => n.nodeName.toLowerCase() === 'collision');
437
+ collisionNodes.forEach(cn => {
438
+
439
+ const c = processLinkElement(cn);
440
+ target.add(c);
441
+
442
+ if (cn.hasAttribute('name')) {
443
+
444
+ const name = cn.getAttribute('name');
445
+ c.name = name;
446
+ c.urdfName = name;
447
+ colliderMap[name] = c;
448
+
449
+ }
450
+
451
+ });
452
+
453
+ }
454
+
455
+ return target;
456
+
457
+ }
458
+
459
+ function processMaterial(node) {
460
+
461
+ const matNodes = [ ...node.children ];
462
+ const material = new THREE.MeshPhongMaterial();
463
+
464
+ material.name = node.getAttribute('name') || '';
465
+ matNodes.forEach(n => {
466
+
467
+ const type = n.nodeName.toLowerCase();
468
+ if (type === 'color') {
469
+
470
+ const rgba =
471
+ n
472
+ .getAttribute('rgba')
473
+ .split(/\s/g)
474
+ .map(v => parseFloat(v));
475
+
476
+ material.color.setRGB(rgba[0], rgba[1], rgba[2]).convertSRGBToLinear();
477
+ material.opacity = rgba[3];
478
+ material.transparent = rgba[3] < 1;
479
+ material.depthWrite = !material.transparent;
480
+
481
+ } else if (type === 'texture') {
482
+
483
+ // The URDF spec does not require that the <texture/> tag include
484
+ // a filename attribute so skip loading the texture if not provided.
485
+ const filename = n.getAttribute('filename');
486
+ if (filename) {
487
+
488
+ const loader = new THREE.TextureLoader(manager);
489
+ const filePath = resolvePath(filename);
490
+ material.map = loader.load(filePath);
491
+ material.map.encoding = THREE.sRGBEncoding;
492
+
493
+ }
494
+
495
+ }
496
+ });
497
+
498
+ return material;
499
+
500
+ }
501
+
502
+ // Process the visual and collision nodes into meshes
503
+ function processLinkElement(vn, materialMap = {}) {
504
+
505
+ const isCollisionNode = vn.nodeName.toLowerCase() === 'collision';
506
+ const children = [ ...vn.children ];
507
+ let material = null;
508
+
509
+ // get the material first
510
+ const materialNode = children.filter(n => n.nodeName.toLowerCase() === 'material')[0];
511
+ if (materialNode) {
512
+
513
+ const name = materialNode.getAttribute('name');
514
+ if (name && name in materialMap) {
515
+
516
+ material = materialMap[name];
517
+
518
+ } else {
519
+
520
+ material = processMaterial(materialNode);
521
+
522
+ }
523
+
524
+ } else {
525
+
526
+ material = new THREE.MeshPhongMaterial();
527
+
528
+ }
529
+
530
+ const group = isCollisionNode ? new URDFCollider() : new URDFVisual();
531
+ group.urdfNode = vn;
532
+
533
+ children.forEach(n => {
534
+
535
+ const type = n.nodeName.toLowerCase();
536
+ if (type === 'geometry') {
537
+
538
+ const geoType = n.children[0].nodeName.toLowerCase();
539
+ if (geoType === 'mesh') {
540
+
541
+ const filename = n.children[0].getAttribute('filename');
542
+ const filePath = resolvePath(filename);
543
+
544
+ // file path is null if a package directory is not provided.
545
+ if (filePath !== null) {
546
+
547
+ const scaleAttr = n.children[0].getAttribute('scale');
548
+ if (scaleAttr) {
549
+
550
+ const scale = processTuple(scaleAttr);
551
+ group.scale.set(scale[0], scale[1], scale[2]);
552
+
553
+ }
554
+
555
+ loadMeshCb(filePath, manager, (obj, err) => {
556
+
557
+ if (err) {
558
+
559
+ console.error('URDFLoader: Error loading mesh.', err);
560
+
561
+ } else if (obj) {
562
+
563
+ if (obj instanceof THREE.Mesh) {
564
+
565
+ obj.material = material;
566
+
567
+ }
568
+
569
+ // We don't expect non identity rotations or positions. In the case of
570
+ // COLLADA files the model might come in with a custom scale for unit
571
+ // conversion.
572
+ obj.position.set(0, 0, 0);
573
+ obj.quaternion.identity();
574
+ group.add(obj);
575
+
576
+ }
577
+
578
+ });
579
+
580
+ }
581
+
582
+ } else if (geoType === 'box') {
583
+
584
+ const primitiveModel = new THREE.Mesh();
585
+ primitiveModel.geometry = new THREE.BoxBufferGeometry(1, 1, 1);
586
+ primitiveModel.material = material;
587
+
588
+ const size = processTuple(n.children[0].getAttribute('size'));
589
+ primitiveModel.scale.set(size[0], size[1], size[2]);
590
+
591
+ group.add(primitiveModel);
592
+
593
+ } else if (geoType === 'sphere') {
594
+
595
+ const primitiveModel = new THREE.Mesh();
596
+ primitiveModel.geometry = new THREE.SphereBufferGeometry(1, 30, 30);
597
+ primitiveModel.material = material;
598
+
599
+ const radius = parseFloat(n.children[0].getAttribute('radius')) || 0;
600
+ primitiveModel.scale.set(radius, radius, radius);
601
+
602
+ group.add(primitiveModel);
603
+
604
+ } else if (geoType === 'cylinder') {
605
+
606
+ const primitiveModel = new THREE.Mesh();
607
+ primitiveModel.geometry = new THREE.CylinderBufferGeometry(1, 1, 1, 30);
608
+ primitiveModel.material = material;
609
+
610
+ const radius = parseFloat(n.children[0].getAttribute('radius')) || 0;
611
+ const length = parseFloat(n.children[0].getAttribute('length')) || 0;
612
+ primitiveModel.scale.set(radius, length, radius);
613
+ primitiveModel.rotation.set(Math.PI / 2, 0, 0);
614
+
615
+ group.add(primitiveModel);
616
+
617
+ }
618
+
619
+ } else if (type === 'origin') {
620
+
621
+ const xyz = processTuple(n.getAttribute('xyz'));
622
+ const rpy = processTuple(n.getAttribute('rpy'));
623
+
624
+ group.position.set(xyz[0], xyz[1], xyz[2]);
625
+ group.rotation.set(0, 0, 0);
626
+ applyRotation(group, rpy);
627
+
628
+ }
629
+
630
+ });
631
+
632
+ return group;
633
+
634
+ }
635
+
636
+ return processUrdf(content);
637
+
638
+ }
639
+
640
+ // Default mesh loading function
641
+ defaultMeshLoader(path, manager, done) {
642
+
643
+ if (/\.stl$/i.test(path)) {
644
+
645
+ const loader = new STLLoader(manager);
646
+ loader.load(path, geom => {
647
+ const mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial());
648
+ done(mesh);
649
+ });
650
+
651
+ } else if (/\.dae$/i.test(path)) {
652
+
653
+ const loader = new ColladaLoader(manager);
654
+ loader.load(path, dae => done(dae.scene));
655
+
656
+ } else {
657
+
658
+ console.warn(`URDFLoader: Could not load model at ${ path }.\nNo loader available`);
659
+
660
+ }
661
+
662
+ }
663
+
664
+ };