urdf-loader 0.10.3 → 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.
@@ -1,551 +1,616 @@
1
- import * as THREE from 'three';
2
- import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
3
- import URDFLoader from './URDFLoader.js';
4
-
5
- const tempVec2 = new THREE.Vector2();
6
-
7
- // urdf-viewer element
8
- // Loads and displays a 3D view of a URDF-formatted robot
9
-
10
- // Events
11
- // urdf-change: Fires when the URDF has finished loading and getting processed
12
- // urdf-processed: Fires when the URDF has finished loading and getting processed
13
- // geometry-loaded: Fires when all the geometry has been fully loaded
14
- // ignore-limits-change: Fires when the 'ignore-limits' attribute changes
15
- // angle-change: Fires when an angle changes
16
- export default
17
- class URDFViewer extends HTMLElement {
18
-
19
- static get observedAttributes() {
20
-
21
- return ['package', 'urdf', 'up', 'display-shadow', 'ambient-color', 'ignore-limits'];
22
-
23
- }
24
-
25
- get package() { return this.getAttribute('package') || ''; }
26
- set package(val) { this.setAttribute('package', val); }
27
-
28
- get urdf() { return this.getAttribute('urdf') || ''; }
29
- set urdf(val) { this.setAttribute('urdf', val); }
30
-
31
- get ignoreLimits() { return this.hasAttribute('ignore-limits') || false; }
32
- set ignoreLimits(val) { val ? this.setAttribute('ignore-limits', val) : this.removeAttribute('ignore-limits'); }
33
-
34
- get up() { return this.getAttribute('up') || '+Z'; }
35
- set up(val) { this.setAttribute('up', val); }
36
-
37
- get displayShadow() { return this.hasAttribute('display-shadow') || false; }
38
- set displayShadow(val) { val ? this.setAttribute('display-shadow', '') : this.removeAttribute('display-shadow'); }
39
-
40
- get ambientColor() { return this.getAttribute('ambient-color') || '#455A64'; }
41
- set ambientColor(val) { val ? this.setAttribute('ambient-color', val) : this.removeAttribute('ambient-color'); }
42
-
43
- get autoRedraw() { return this.hasAttribute('auto-redraw') || false; }
44
- set autoRedraw(val) { val ? this.setAttribute('auto-redraw', true) : this.removeAttribute('auto-redraw'); }
45
-
46
- get noAutoRecenter() { return this.hasAttribute('no-auto-recenter') || false; }
47
- set noAutoRecenter(val) { val ? this.setAttribute('no-auto-recenter', true) : this.removeAttribute('no-auto-recenter'); }
48
-
49
- get jointValues() {
50
-
51
- const values = {};
52
- if (this.robot) {
53
-
54
- for (const name in this.robot.joints) {
55
-
56
- const joint = this.robot.joints[name];
57
- values[name] = joint.jointValue.length === 1 ? joint.angle : [...joint.jointValue];
58
-
59
- }
60
-
61
- }
62
-
63
- return values;
64
-
65
- }
66
- set jointValues(val) { this.setJointValues(val); }
67
-
68
- get angles() {
69
-
70
- return this.jointValues;
71
-
72
- }
73
- set angles(v) {
74
-
75
- this.jointValues = v;
76
-
77
- }
78
-
79
- /* Lifecycle Functions */
80
- constructor() {
81
-
82
- super();
83
-
84
- this._requestId = 0;
85
- this._dirty = false;
86
- this._loadScheduled = false;
87
- this.robot = null;
88
- this.loadMeshFunc = null;
89
- this.urlModifierFunc = null;
90
-
91
- // Scene setup
92
- const scene = new THREE.Scene();
93
-
94
- const ambientLight = new THREE.HemisphereLight(this.ambientColor, '#000');
95
- ambientLight.groundColor.lerp(ambientLight.color, 0.5);
96
- ambientLight.intensity = 0.5;
97
- ambientLight.position.set(0, 1, 0);
98
- scene.add(ambientLight);
99
-
100
- // Light setup
101
- const dirLight = new THREE.DirectionalLight(0xffffff);
102
- dirLight.position.set(4, 10, 1);
103
- dirLight.shadow.mapSize.width = 2048;
104
- dirLight.shadow.mapSize.height = 2048;
105
- dirLight.shadow.normalBias = 0.001;
106
- dirLight.castShadow = true;
107
- scene.add(dirLight);
108
- scene.add(dirLight.target);
109
-
110
- // Renderer setup
111
- const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
112
- renderer.setClearColor(0xffffff);
113
- renderer.setClearAlpha(0);
114
- renderer.shadowMap.enabled = true;
115
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
116
- renderer.outputEncoding = THREE.sRGBEncoding;
117
-
118
- // Camera setup
119
- const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
120
- camera.position.z = -10;
121
-
122
- // World setup
123
- const world = new THREE.Object3D();
124
- scene.add(world);
125
-
126
- const plane = new THREE.Mesh(
127
- new THREE.PlaneBufferGeometry(40, 40),
128
- new THREE.ShadowMaterial({ side: THREE.DoubleSide, transparent: true, opacity: 0.5 }),
129
- );
130
- plane.rotation.x = -Math.PI / 2;
131
- plane.position.y = -0.5;
132
- plane.receiveShadow = true;
133
- plane.scale.set(10, 10, 10);
134
- scene.add(plane);
135
-
136
- // Controls setup
137
- const controls = new OrbitControls(camera, renderer.domElement);
138
- controls.rotateSpeed = 2.0;
139
- controls.zoomSpeed = 5;
140
- controls.panSpeed = 2;
141
- controls.enableZoom = true;
142
- controls.enableDamping = false;
143
- controls.maxDistance = 50;
144
- controls.minDistance = 0.25;
145
- controls.addEventListener('change', () => this.recenter());
146
-
147
- this.scene = scene;
148
- this.world = world;
149
- this.renderer = renderer;
150
- this.camera = camera;
151
- this.controls = controls;
152
- this.plane = plane;
153
- this.directionalLight = dirLight;
154
- this.ambientLight = ambientLight;
155
-
156
- this._setUp(this.up);
157
-
158
- const _renderLoop = () => {
159
-
160
- if (this.parentNode) {
161
-
162
- this.updateSize();
163
-
164
- if (this._dirty || this.autoRedraw) {
165
-
166
- if (!this.noAutoRecenter) {
167
-
168
- this._updateEnvironment();
169
- }
170
-
171
- this.renderer.render(scene, camera);
172
- this._dirty = false;
173
-
174
- }
175
-
176
- // update controls after the environment in
177
- // case the controls are retargeted
178
- this.controls.update();
179
-
180
- }
181
- this._renderLoopId = requestAnimationFrame(_renderLoop);
182
-
183
- };
184
- _renderLoop();
185
-
186
- }
187
-
188
- connectedCallback() {
189
-
190
- // Add our initialize styles for the element if they haven't
191
- // been added yet
192
- if (!this.constructor._styletag) {
193
-
194
- const styletag = document.createElement('style');
195
- styletag.innerHTML =
196
- `
197
- ${ this.tagName } { display: block; }
198
- ${ this.tagName } canvas {
199
- width: 100%;
200
- height: 100%;
201
- }
202
- `;
203
- document.head.appendChild(styletag);
204
- this.constructor._styletag = styletag;
205
-
206
- }
207
-
208
- // add the renderer
209
- if (this.childElementCount === 0) {
210
-
211
- this.appendChild(this.renderer.domElement);
212
-
213
- }
214
-
215
- this.updateSize();
216
- requestAnimationFrame(() => this.updateSize());
217
-
218
- }
219
-
220
- disconnectedCallback() {
221
-
222
- cancelAnimationFrame(this._renderLoopId);
223
-
224
- }
225
-
226
- attributeChangedCallback(attr, oldval, newval) {
227
-
228
- this.recenter();
229
-
230
- switch (attr) {
231
-
232
- case 'package':
233
- case 'urdf': {
234
-
235
- this._scheduleLoad();
236
- break;
237
-
238
- }
239
-
240
- case 'up': {
241
-
242
- this._setUp(this.up);
243
- break;
244
-
245
- }
246
-
247
- case 'ambient-color': {
248
-
249
- this.ambientLight.color.set(this.ambientColor);
250
- this.ambientLight.groundColor.set('#000').lerp(this.ambientLight.color, 0.5);
251
- break;
252
-
253
- }
254
-
255
- case 'ignore-limits': {
256
-
257
- this._setIgnoreLimits(this.ignoreLimits, true);
258
- break;
259
-
260
- }
261
-
262
- }
263
-
264
- }
265
-
266
- /* Public API */
267
- updateSize() {
268
-
269
- const r = this.renderer;
270
- const w = this.clientWidth;
271
- const h = this.clientHeight;
272
- const currsize = r.getSize(tempVec2);
273
-
274
- if (currsize.width !== w || currsize.height !== h) {
275
-
276
- this.recenter();
277
-
278
- }
279
-
280
- r.setPixelRatio(window.devicePixelRatio);
281
- r.setSize(w, h, false);
282
-
283
- this.camera.aspect = w / h;
284
- this.camera.updateProjectionMatrix();
285
-
286
- }
287
-
288
- redraw() {
289
-
290
- this._dirty = true;
291
- }
292
-
293
- recenter() {
294
-
295
- this._updateEnvironment();
296
- this.redraw();
297
-
298
- }
299
-
300
- // Set the joint with jointName to
301
- // angle in degrees
302
- setJointValue(jointName, ...values) {
303
-
304
- if (!this.robot) return;
305
- if (!this.robot.joints[jointName]) return;
306
-
307
- if (this.robot.joints[jointName].setJointValue(...values)) {
308
-
309
- this.redraw();
310
- this.dispatchEvent(new CustomEvent('angle-change', { bubbles: true, cancelable: true, detail: jointName }));
311
-
312
- }
313
-
314
- }
315
-
316
- setJointValues(values) {
317
-
318
- for (const name in values) this.setJointValue(name, values[name]);
319
-
320
- }
321
-
322
- /* Private Functions */
323
- // Updates the position of the plane to be at the
324
- // lowest point below the robot and focuses the
325
- // camera on the center of the scene
326
- _updateEnvironment() {
327
-
328
- if (!this.robot) return;
329
-
330
- this.world.updateMatrixWorld();
331
-
332
- const bbox = new THREE.Box3();
333
- bbox.setFromObject(this.robot);
334
-
335
- const center = bbox.getCenter(new THREE.Vector3());
336
- this.controls.target.y = center.y;
337
- this.plane.position.y = bbox.min.y - 1e-3;
338
-
339
- const dirLight = this.directionalLight;
340
- dirLight.castShadow = this.displayShadow;
341
-
342
- if (this.displayShadow) {
343
-
344
- // Update the shadow camera rendering bounds to encapsulate the
345
- // model. We use the bounding sphere of the bounding box for
346
- // simplicity -- this could be a tighter fit.
347
- const sphere = bbox.getBoundingSphere(new THREE.Sphere());
348
- const minmax = sphere.radius;
349
- const cam = dirLight.shadow.camera;
350
- cam.left = cam.bottom = -minmax;
351
- cam.right = cam.top = minmax;
352
-
353
- // Update the camera to focus on the center of the model so the
354
- // shadow can encapsulate it
355
- const offset = dirLight.position.clone().sub(dirLight.target.position);
356
- dirLight.target.position.copy(center);
357
- dirLight.position.copy(center).add(offset);
358
-
359
- cam.updateProjectionMatrix();
360
-
361
- }
362
-
363
- }
364
-
365
- _scheduleLoad() {
366
-
367
- // if our current model is already what's being requested
368
- // or has been loaded then early out
369
- if (this._prevload === `${ this.package }|${ this.urdf }`) return;
370
- this._prevload = `${ this.package }|${ this.urdf }`;
371
-
372
- // if we're already waiting on a load then early out
373
- if (this._loadScheduled) return;
374
- this._loadScheduled = true;
375
-
376
- if (this.robot) {
377
-
378
- this.robot.traverse(c => c.dispose && c.dispose());
379
- this.robot.parent.remove(this.robot);
380
- this.robot = null;
381
-
382
- }
383
-
384
- requestAnimationFrame(() => {
385
-
386
- this._loadUrdf(this.package, this.urdf);
387
- this._loadScheduled = false;
388
-
389
- });
390
-
391
- }
392
-
393
- // Watch the package and urdf field and load the robot model.
394
- // This should _only_ be called from _scheduleLoad because that
395
- // ensures the that current robot has been removed
396
- _loadUrdf(pkg, urdf) {
397
-
398
- this.dispatchEvent(new CustomEvent('urdf-change', { bubbles: true, cancelable: true, composed: true }));
399
-
400
- if (urdf) {
401
-
402
- // Keep track of this request and make
403
- // sure it doesn't get overwritten by
404
- // a subsequent one
405
- this._requestId++;
406
- const requestId = this._requestId;
407
-
408
- const updateMaterials = mesh => {
409
-
410
- mesh.traverse(c => {
411
-
412
- if (c.isMesh) {
413
-
414
- c.castShadow = true;
415
- c.receiveShadow = true;
416
-
417
- if (c.material) {
418
-
419
- const mats =
420
- (Array.isArray(c.material) ? c.material : [c.material])
421
- .map(m => {
422
-
423
- if (m instanceof THREE.MeshBasicMaterial) {
424
-
425
- m = new THREE.MeshPhongMaterial();
426
-
427
- }
428
-
429
- if (m.map) {
430
-
431
- m.map.encoding = THREE.GammaEncoding;
432
-
433
- }
434
-
435
- return m;
436
-
437
- });
438
- c.material = mats.length === 1 ? mats[0] : mats;
439
-
440
- }
441
-
442
- }
443
-
444
- });
445
-
446
- };
447
-
448
- if (pkg.includes(':') && (pkg.split(':')[1].substring(0, 2)) !== '//') {
449
- // E.g. pkg = "pkg_name: path/to/pkg_name, pk2: path2/to/pk2"}
450
-
451
- // Convert pkg(s) into a map. E.g.
452
- // { "pkg_name": "path/to/pkg_name",
453
- // "pk2": "path2/to/pk2" }
454
-
455
- pkg = pkg.split(',').reduce((map, value) => {
456
-
457
- const split = value.split(/:/).filter(x => !!x);
458
- const pkgName = split.shift().trim();
459
- const pkgPath = split.join(':').trim();
460
- map[pkgName] = pkgPath;
461
-
462
- return map;
463
-
464
- }, {});
465
- }
466
-
467
- let robot = null;
468
- const manager = new THREE.LoadingManager();
469
- manager.onLoad = () => {
470
-
471
- // If another request has come in to load a new
472
- // robot, then ignore this one
473
- if (this._requestId !== requestId) {
474
-
475
- robot.traverse(c => c.dispose && c.dispose());
476
- return;
477
-
478
- }
479
-
480
- this.robot = robot;
481
- this.world.add(robot);
482
- updateMaterials(robot);
483
-
484
- this._setIgnoreLimits(this.ignoreLimits);
485
-
486
- this.dispatchEvent(new CustomEvent('urdf-processed', { bubbles: true, cancelable: true, composed: true }));
487
- this.dispatchEvent(new CustomEvent('geometry-loaded', { bubbles: true, cancelable: true, composed: true }));
488
-
489
- this.recenter();
490
-
491
- };
492
-
493
- if (this.urlModifierFunc) {
494
-
495
- manager.setURLModifier(this.urlModifierFunc);
496
-
497
- }
498
-
499
- const loader = new URDFLoader(manager);
500
- loader.packages = pkg;
501
- loader.loadMeshCb = this.loadMeshFunc;
502
- loader.fetchOptions = { mode: 'cors', credentials: 'same-origin' };
503
- loader.load(urdf, model => robot = model);
504
-
505
- }
506
-
507
- }
508
-
509
- // Watch the coordinate frame and update the
510
- // rotation of the scene to match
511
- _setUp(up) {
512
-
513
- if (!up) up = '+Z';
514
- up = up.toUpperCase();
515
- const sign = up.replace(/[^-+]/g, '')[0] || '+';
516
- const axis = up.replace(/[^XYZ]/gi, '')[0] || 'Z';
517
-
518
- const PI = Math.PI;
519
- const HALFPI = PI / 2;
520
- if (axis === 'X') this.world.rotation.set(0, 0, sign === '+' ? HALFPI : -HALFPI);
521
- if (axis === 'Z') this.world.rotation.set(sign === '+' ? -HALFPI : HALFPI, 0, 0);
522
- if (axis === 'Y') this.world.rotation.set(sign === '+' ? 0 : PI, 0, 0);
523
-
524
- }
525
-
526
- // Updates the current robot's angles to ignore
527
- // joint limits or not
528
- _setIgnoreLimits(ignore, dispatch = false) {
529
-
530
- if (this.robot) {
531
-
532
- Object
533
- .values(this.robot.joints)
534
- .forEach(joint => {
535
-
536
- joint.ignoreLimits = ignore;
537
- joint.setJointValue(...joint.jointValue);
538
-
539
- });
540
-
541
- }
542
-
543
- if (dispatch) {
544
-
545
- this.dispatchEvent(new CustomEvent('ignore-limits-change', { bubbles: true, cancelable: true, composed: true }));
546
-
547
- }
548
-
549
- }
550
-
551
- };
1
+ import * as THREE from 'three';
2
+ import { MeshPhongMaterial } from 'three';
3
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4
+ import URDFLoader from './URDFLoader.js';
5
+
6
+ const tempVec2 = new THREE.Vector2();
7
+ const emptyRaycast = () => {};
8
+
9
+ // urdf-viewer element
10
+ // Loads and displays a 3D view of a URDF-formatted robot
11
+
12
+ // Events
13
+ // urdf-change: Fires when the URDF has finished loading and getting processed
14
+ // urdf-processed: Fires when the URDF has finished loading and getting processed
15
+ // geometry-loaded: Fires when all the geometry has been fully loaded
16
+ // ignore-limits-change: Fires when the 'ignore-limits' attribute changes
17
+ // angle-change: Fires when an angle changes
18
+ export default
19
+ class URDFViewer extends HTMLElement {
20
+
21
+ static get observedAttributes() {
22
+
23
+ return ['package', 'urdf', 'up', 'display-shadow', 'ambient-color', 'ignore-limits', 'show-collision'];
24
+
25
+ }
26
+
27
+ get package() { return this.getAttribute('package') || ''; }
28
+ set package(val) { this.setAttribute('package', val); }
29
+
30
+ get urdf() { return this.getAttribute('urdf') || ''; }
31
+ set urdf(val) { this.setAttribute('urdf', val); }
32
+
33
+ get ignoreLimits() { return this.hasAttribute('ignore-limits') || false; }
34
+ set ignoreLimits(val) { val ? this.setAttribute('ignore-limits', val) : this.removeAttribute('ignore-limits'); }
35
+
36
+ get up() { return this.getAttribute('up') || '+Z'; }
37
+ set up(val) { this.setAttribute('up', val); }
38
+
39
+ get displayShadow() { return this.hasAttribute('display-shadow') || false; }
40
+ set displayShadow(val) { val ? this.setAttribute('display-shadow', '') : this.removeAttribute('display-shadow'); }
41
+
42
+ get ambientColor() { return this.getAttribute('ambient-color') || '#455A64'; }
43
+ set ambientColor(val) { val ? this.setAttribute('ambient-color', val) : this.removeAttribute('ambient-color'); }
44
+
45
+ get autoRedraw() { return this.hasAttribute('auto-redraw') || false; }
46
+ set autoRedraw(val) { val ? this.setAttribute('auto-redraw', true) : this.removeAttribute('auto-redraw'); }
47
+
48
+ get noAutoRecenter() { return this.hasAttribute('no-auto-recenter') || false; }
49
+ set noAutoRecenter(val) { val ? this.setAttribute('no-auto-recenter', true) : this.removeAttribute('no-auto-recenter'); }
50
+
51
+ get showCollision() { return this.hasAttribute('show-collision') || false; }
52
+ set showCollision(val) { val ? this.setAttribute('show-collision', true) : this.removeAttribute('show-collision'); }
53
+
54
+ get jointValues() {
55
+
56
+ const values = {};
57
+ if (this.robot) {
58
+
59
+ for (const name in this.robot.joints) {
60
+
61
+ const joint = this.robot.joints[name];
62
+ values[name] = joint.jointValue.length === 1 ? joint.angle : [...joint.jointValue];
63
+
64
+ }
65
+
66
+ }
67
+
68
+ return values;
69
+
70
+ }
71
+ set jointValues(val) { this.setJointValues(val); }
72
+
73
+ get angles() {
74
+
75
+ return this.jointValues;
76
+
77
+ }
78
+ set angles(v) {
79
+
80
+ this.jointValues = v;
81
+
82
+ }
83
+
84
+ /* Lifecycle Functions */
85
+ constructor() {
86
+
87
+ super();
88
+
89
+ this._requestId = 0;
90
+ this._dirty = false;
91
+ this._loadScheduled = false;
92
+ this.robot = null;
93
+ this.loadMeshFunc = null;
94
+ this.urlModifierFunc = null;
95
+
96
+ // Scene setup
97
+ const scene = new THREE.Scene();
98
+
99
+ const ambientLight = new THREE.HemisphereLight(this.ambientColor, '#000');
100
+ ambientLight.groundColor.lerp(ambientLight.color, 0.5);
101
+ ambientLight.intensity = 0.5;
102
+ ambientLight.position.set(0, 1, 0);
103
+ scene.add(ambientLight);
104
+
105
+ // Light setup
106
+ const dirLight = new THREE.DirectionalLight(0xffffff);
107
+ dirLight.position.set(4, 10, 1);
108
+ dirLight.shadow.mapSize.width = 2048;
109
+ dirLight.shadow.mapSize.height = 2048;
110
+ dirLight.shadow.normalBias = 0.001;
111
+ dirLight.castShadow = true;
112
+ scene.add(dirLight);
113
+ scene.add(dirLight.target);
114
+
115
+ // Renderer setup
116
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
117
+ renderer.setClearColor(0xffffff);
118
+ renderer.setClearAlpha(0);
119
+ renderer.shadowMap.enabled = true;
120
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
121
+ renderer.outputEncoding = THREE.sRGBEncoding;
122
+
123
+ // Camera setup
124
+ const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
125
+ camera.position.z = -10;
126
+
127
+ // World setup
128
+ const world = new THREE.Object3D();
129
+ scene.add(world);
130
+
131
+ const plane = new THREE.Mesh(
132
+ new THREE.PlaneBufferGeometry(40, 40),
133
+ new THREE.ShadowMaterial({ side: THREE.DoubleSide, transparent: true, opacity: 0.5 }),
134
+ );
135
+ plane.rotation.x = -Math.PI / 2;
136
+ plane.position.y = -0.5;
137
+ plane.receiveShadow = true;
138
+ plane.scale.set(10, 10, 10);
139
+ scene.add(plane);
140
+
141
+ // Controls setup
142
+ const controls = new OrbitControls(camera, renderer.domElement);
143
+ controls.rotateSpeed = 2.0;
144
+ controls.zoomSpeed = 5;
145
+ controls.panSpeed = 2;
146
+ controls.enableZoom = true;
147
+ controls.enableDamping = false;
148
+ controls.maxDistance = 50;
149
+ controls.minDistance = 0.25;
150
+ controls.addEventListener('change', () => this.recenter());
151
+
152
+ this.scene = scene;
153
+ this.world = world;
154
+ this.renderer = renderer;
155
+ this.camera = camera;
156
+ this.controls = controls;
157
+ this.plane = plane;
158
+ this.directionalLight = dirLight;
159
+ this.ambientLight = ambientLight;
160
+
161
+ this._setUp(this.up);
162
+
163
+ this._collisionMaterial = new MeshPhongMaterial({
164
+ transparent: true,
165
+ opacity: 0.35,
166
+ shininess: 2.5,
167
+ premultipliedAlpha: true,
168
+ color: 0xffbe38,
169
+ polygonOffset: true,
170
+ polygonOffsetFactor: -1,
171
+ polygonOffsetUnits: -1,
172
+ });
173
+
174
+ const _renderLoop = () => {
175
+
176
+ if (this.parentNode) {
177
+
178
+ this.updateSize();
179
+
180
+ if (this._dirty || this.autoRedraw) {
181
+
182
+ if (!this.noAutoRecenter) {
183
+
184
+ this._updateEnvironment();
185
+ }
186
+
187
+ this.renderer.render(scene, camera);
188
+ this._dirty = false;
189
+
190
+ }
191
+
192
+ // update controls after the environment in
193
+ // case the controls are retargeted
194
+ this.controls.update();
195
+
196
+ }
197
+ this._renderLoopId = requestAnimationFrame(_renderLoop);
198
+
199
+ };
200
+ _renderLoop();
201
+
202
+ }
203
+
204
+ connectedCallback() {
205
+
206
+ // Add our initialize styles for the element if they haven't
207
+ // been added yet
208
+ if (!this.constructor._styletag) {
209
+
210
+ const styletag = document.createElement('style');
211
+ styletag.innerHTML =
212
+ `
213
+ ${ this.tagName } { display: block; }
214
+ ${ this.tagName } canvas {
215
+ width: 100%;
216
+ height: 100%;
217
+ }
218
+ `;
219
+ document.head.appendChild(styletag);
220
+ this.constructor._styletag = styletag;
221
+
222
+ }
223
+
224
+ // add the renderer
225
+ if (this.childElementCount === 0) {
226
+
227
+ this.appendChild(this.renderer.domElement);
228
+
229
+ }
230
+
231
+ this.updateSize();
232
+ requestAnimationFrame(() => this.updateSize());
233
+
234
+ }
235
+
236
+ disconnectedCallback() {
237
+
238
+ cancelAnimationFrame(this._renderLoopId);
239
+
240
+ }
241
+
242
+ attributeChangedCallback(attr, oldval, newval) {
243
+
244
+ this._updateCollisionVisibility();
245
+ if (!this.noAutoRecenter) {
246
+ this.recenter();
247
+ }
248
+
249
+ switch (attr) {
250
+
251
+ case 'package':
252
+ case 'urdf': {
253
+
254
+ this._scheduleLoad();
255
+ break;
256
+
257
+ }
258
+
259
+ case 'up': {
260
+
261
+ this._setUp(this.up);
262
+ break;
263
+
264
+ }
265
+
266
+ case 'ambient-color': {
267
+
268
+ this.ambientLight.color.set(this.ambientColor);
269
+ this.ambientLight.groundColor.set('#000').lerp(this.ambientLight.color, 0.5);
270
+ break;
271
+
272
+ }
273
+
274
+ case 'ignore-limits': {
275
+
276
+ this._setIgnoreLimits(this.ignoreLimits, true);
277
+ break;
278
+
279
+ }
280
+
281
+ }
282
+
283
+ }
284
+
285
+ /* Public API */
286
+ updateSize() {
287
+
288
+ const r = this.renderer;
289
+ const w = this.clientWidth;
290
+ const h = this.clientHeight;
291
+ const currSize = r.getSize(tempVec2);
292
+
293
+ if (currSize.width !== w || currSize.height !== h) {
294
+
295
+ this.recenter();
296
+
297
+ }
298
+
299
+ r.setPixelRatio(window.devicePixelRatio);
300
+ r.setSize(w, h, false);
301
+
302
+ this.camera.aspect = w / h;
303
+ this.camera.updateProjectionMatrix();
304
+
305
+ }
306
+
307
+ redraw() {
308
+
309
+ this._dirty = true;
310
+ }
311
+
312
+ recenter() {
313
+
314
+ this._updateEnvironment();
315
+ this.redraw();
316
+
317
+ }
318
+
319
+ // Set the joint with jointName to
320
+ // angle in degrees
321
+ setJointValue(jointName, ...values) {
322
+
323
+ if (!this.robot) return;
324
+ if (!this.robot.joints[jointName]) return;
325
+
326
+ if (this.robot.joints[jointName].setJointValue(...values)) {
327
+
328
+ this.redraw();
329
+ this.dispatchEvent(new CustomEvent('angle-change', { bubbles: true, cancelable: true, detail: jointName }));
330
+
331
+ }
332
+
333
+ }
334
+
335
+ setJointValues(values) {
336
+
337
+ for (const name in values) this.setJointValue(name, values[name]);
338
+
339
+ }
340
+
341
+ /* Private Functions */
342
+ // Updates the position of the plane to be at the
343
+ // lowest point below the robot and focuses the
344
+ // camera on the center of the scene
345
+ _updateEnvironment() {
346
+
347
+ const robot = this.robot;
348
+ if (!robot) return;
349
+
350
+ this.world.updateMatrixWorld();
351
+
352
+ const bbox = new THREE.Box3();
353
+ bbox.makeEmpty();
354
+ robot.traverse(c => {
355
+ if (c.isURDFVisual) {
356
+ bbox.expandByObject(c);
357
+ }
358
+ });
359
+
360
+ const center = bbox.getCenter(new THREE.Vector3());
361
+ this.controls.target.y = center.y;
362
+ this.plane.position.y = bbox.min.y - 1e-3;
363
+
364
+ const dirLight = this.directionalLight;
365
+ dirLight.castShadow = this.displayShadow;
366
+
367
+ if (this.displayShadow) {
368
+
369
+ // Update the shadow camera rendering bounds to encapsulate the
370
+ // model. We use the bounding sphere of the bounding box for
371
+ // simplicity -- this could be a tighter fit.
372
+ const sphere = bbox.getBoundingSphere(new THREE.Sphere());
373
+ const minmax = sphere.radius;
374
+ const cam = dirLight.shadow.camera;
375
+ cam.left = cam.bottom = -minmax;
376
+ cam.right = cam.top = minmax;
377
+
378
+ // Update the camera to focus on the center of the model so the
379
+ // shadow can encapsulate it
380
+ const offset = dirLight.position.clone().sub(dirLight.target.position);
381
+ dirLight.target.position.copy(center);
382
+ dirLight.position.copy(center).add(offset);
383
+
384
+ cam.updateProjectionMatrix();
385
+
386
+ }
387
+
388
+ }
389
+
390
+ _scheduleLoad() {
391
+
392
+ // if our current model is already what's being requested
393
+ // or has been loaded then early out
394
+ if (this._prevload === `${ this.package }|${ this.urdf }`) return;
395
+ this._prevload = `${ this.package }|${ this.urdf }`;
396
+
397
+ // if we're already waiting on a load then early out
398
+ if (this._loadScheduled) return;
399
+ this._loadScheduled = true;
400
+
401
+ if (this.robot) {
402
+
403
+ this.robot.traverse(c => c.dispose && c.dispose());
404
+ this.robot.parent.remove(this.robot);
405
+ this.robot = null;
406
+
407
+ }
408
+
409
+ requestAnimationFrame(() => {
410
+
411
+ this._loadUrdf(this.package, this.urdf);
412
+ this._loadScheduled = false;
413
+
414
+ });
415
+
416
+ }
417
+
418
+ // Watch the package and urdf field and load the robot model.
419
+ // This should _only_ be called from _scheduleLoad because that
420
+ // ensures the that current robot has been removed
421
+ _loadUrdf(pkg, urdf) {
422
+
423
+ this.dispatchEvent(new CustomEvent('urdf-change', { bubbles: true, cancelable: true, composed: true }));
424
+
425
+ if (urdf) {
426
+
427
+ // Keep track of this request and make
428
+ // sure it doesn't get overwritten by
429
+ // a subsequent one
430
+ this._requestId++;
431
+ const requestId = this._requestId;
432
+
433
+ const updateMaterials = mesh => {
434
+
435
+ mesh.traverse(c => {
436
+
437
+ if (c.isMesh) {
438
+
439
+ c.castShadow = true;
440
+ c.receiveShadow = true;
441
+
442
+ if (c.material) {
443
+
444
+ const mats =
445
+ (Array.isArray(c.material) ? c.material : [c.material])
446
+ .map(m => {
447
+
448
+ if (m instanceof THREE.MeshBasicMaterial) {
449
+
450
+ m = new THREE.MeshPhongMaterial();
451
+
452
+ }
453
+
454
+ if (m.map) {
455
+
456
+ m.map.encoding = THREE.GammaEncoding;
457
+
458
+ }
459
+
460
+ return m;
461
+
462
+ });
463
+ c.material = mats.length === 1 ? mats[0] : mats;
464
+
465
+ }
466
+
467
+ }
468
+
469
+ });
470
+
471
+ };
472
+
473
+ if (pkg.includes(':') && (pkg.split(':')[1].substring(0, 2)) !== '//') {
474
+ // E.g. pkg = "pkg_name: path/to/pkg_name, pk2: path2/to/pk2"}
475
+
476
+ // Convert pkg(s) into a map. E.g.
477
+ // { "pkg_name": "path/to/pkg_name",
478
+ // "pk2": "path2/to/pk2" }
479
+
480
+ pkg = pkg.split(',').reduce((map, value) => {
481
+
482
+ const split = value.split(/:/).filter(x => !!x);
483
+ const pkgName = split.shift().trim();
484
+ const pkgPath = split.join(':').trim();
485
+ map[pkgName] = pkgPath;
486
+
487
+ return map;
488
+
489
+ }, {});
490
+ }
491
+
492
+ let robot = null;
493
+ const manager = new THREE.LoadingManager();
494
+ manager.onLoad = () => {
495
+
496
+ // If another request has come in to load a new
497
+ // robot, then ignore this one
498
+ if (this._requestId !== requestId) {
499
+
500
+ robot.traverse(c => c.dispose && c.dispose());
501
+ return;
502
+
503
+ }
504
+
505
+ this.robot = robot;
506
+ this.world.add(robot);
507
+ updateMaterials(robot);
508
+
509
+ this._setIgnoreLimits(this.ignoreLimits);
510
+ this._updateCollisionVisibility();
511
+
512
+ this.dispatchEvent(new CustomEvent('urdf-processed', { bubbles: true, cancelable: true, composed: true }));
513
+ this.dispatchEvent(new CustomEvent('geometry-loaded', { bubbles: true, cancelable: true, composed: true }));
514
+
515
+ this.recenter();
516
+
517
+ };
518
+
519
+ if (this.urlModifierFunc) {
520
+
521
+ manager.setURLModifier(this.urlModifierFunc);
522
+
523
+ }
524
+
525
+ const loader = new URDFLoader(manager);
526
+ loader.packages = pkg;
527
+ loader.loadMeshCb = this.loadMeshFunc;
528
+ loader.fetchOptions = { mode: 'cors', credentials: 'same-origin' };
529
+ loader.parseCollision = true;
530
+ loader.load(urdf, model => robot = model);
531
+
532
+ }
533
+
534
+ }
535
+
536
+ _updateCollisionVisibility() {
537
+
538
+ const showCollision = this.showCollision;
539
+ const collisionMaterial = this._collisionMaterial;
540
+ const robot = this.robot;
541
+
542
+ if (robot === null) return;
543
+
544
+ const colliders = [];
545
+ robot.traverse(c => {
546
+
547
+ if (c.isURDFCollider) {
548
+
549
+ c.visible = showCollision;
550
+ colliders.push(c);
551
+
552
+ }
553
+
554
+ });
555
+
556
+ colliders.forEach(coll => {
557
+
558
+ coll.traverse(c => {
559
+
560
+ if (c.isMesh) {
561
+
562
+ c.raycast = emptyRaycast;
563
+ c.material = collisionMaterial;
564
+ c.castShadow = false;
565
+
566
+ }
567
+
568
+ });
569
+
570
+ });
571
+
572
+ }
573
+
574
+ // Watch the coordinate frame and update the
575
+ // rotation of the scene to match
576
+ _setUp(up) {
577
+
578
+ if (!up) up = '+Z';
579
+ up = up.toUpperCase();
580
+ const sign = up.replace(/[^-+]/g, '')[0] || '+';
581
+ const axis = up.replace(/[^XYZ]/gi, '')[0] || 'Z';
582
+
583
+ const PI = Math.PI;
584
+ const HALFPI = PI / 2;
585
+ if (axis === 'X') this.world.rotation.set(0, 0, sign === '+' ? HALFPI : -HALFPI);
586
+ if (axis === 'Z') this.world.rotation.set(sign === '+' ? -HALFPI : HALFPI, 0, 0);
587
+ if (axis === 'Y') this.world.rotation.set(sign === '+' ? 0 : PI, 0, 0);
588
+
589
+ }
590
+
591
+ // Updates the current robot's angles to ignore
592
+ // joint limits or not
593
+ _setIgnoreLimits(ignore, dispatch = false) {
594
+
595
+ if (this.robot) {
596
+
597
+ Object
598
+ .values(this.robot.joints)
599
+ .forEach(joint => {
600
+
601
+ joint.ignoreLimits = ignore;
602
+ joint.setJointValue(...joint.jointValue);
603
+
604
+ });
605
+
606
+ }
607
+
608
+ if (dispatch) {
609
+
610
+ this.dispatchEvent(new CustomEvent('ignore-limits-change', { bubbles: true, cancelable: true, composed: true }));
611
+
612
+ }
613
+
614
+ }
615
+
616
+ };