urdf-loader
Advanced tools
Comparing version 0.2.4 to 0.2.5
{ | ||
"name": "urdf-loader", | ||
"version": "0.2.4", | ||
"version": "0.2.5", | ||
"description": "URDF Loader for THREE.js and webcomponent viewer", | ||
@@ -28,3 +28,3 @@ "scripts": { | ||
"peerDependencies": { | ||
"three": "^0.89.0" | ||
"three": "^0.94.0" | ||
}, | ||
@@ -35,3 +35,3 @@ "devDependencies": { | ||
"static-server": "^3.0.0", | ||
"three": "^0.91.0", | ||
"three": "^0.94.0", | ||
"threejs-model-loader": "0.0.1", | ||
@@ -38,0 +38,0 @@ "wc-loader": "^1.1.12", |
@@ -0,1 +1,2 @@ | ||
/* globals THREE URDFLoader */ | ||
// urdf-viewer element | ||
@@ -12,126 +13,199 @@ // Loads and displays a 3D view of a URDF-formatted robot | ||
static get observedAttributes() { | ||
return ['package', 'urdf', 'up', 'display-shadow', 'ambient-color', 'ignore-limits'] | ||
static get observedAttributes () { | ||
return ['package', 'urdf', 'up', 'display-shadow', 'ambient-color', 'ignore-limits']; | ||
} | ||
get package() { return this.getAttribute('package') || '' } | ||
set package(val) { this.setAttribute('package', val) } | ||
get package () { | ||
get urdf() { return this.getAttribute('urdf') || '' } | ||
set urdf(val) { this.setAttribute('urdf', val) } | ||
return this.getAttribute('package') || ''; | ||
get ignoreLimits() { return this.hasAttribute('ignore-limits') || false } | ||
set ignoreLimits(val) { | ||
val ? this.setAttribute('ignore-limits', val) : this.removeAttribute('ignore-limits') | ||
} | ||
set package (val) { | ||
get up() { return this.getAttribute('up') || '+Z' } | ||
set up(val) { this.setAttribute('up', val) } | ||
this.setAttribute('package', val); | ||
get displayShadow() { return this.hasAttribute('display-shadow') || false } | ||
set displayShadow(val) { | ||
val = !!val | ||
val ? this.setAttribute('display-shadow', '') : this.removeAttribute('display-shadow') | ||
} | ||
get ambientColor() { return this.getAttribute('ambient-color') || '#455A64' } | ||
set ambientColor(val) { | ||
val ? this.setAttribute('ambient-color', val) : this.removeAttribute('ambient-color') | ||
get urdf () { | ||
return this.getAttribute('urdf') || ''; | ||
} | ||
set urdf (val) { | ||
get autoRedraw() { return this.hasAttribute('auto-redraw') || false } | ||
set autoRedraw(val) { | ||
val ? this.setAttribute('auto-redraw', true) : this.removeAttribute('auto-redraw') | ||
this.setAttribute('urdf', val); | ||
} | ||
get angles() { | ||
const angles = {} | ||
get ignoreLimits () { | ||
return this.hasAttribute('ignore-limits') || false; | ||
} | ||
set ignoreLimits (val) { | ||
val ? this.setAttribute('ignore-limits', val) : this.removeAttribute('ignore-limits'); | ||
} | ||
get up () { | ||
return this.getAttribute('up') || '+Z'; | ||
} | ||
set up (val) { | ||
this.setAttribute('up', val); | ||
} | ||
get displayShadow () { | ||
return this.hasAttribute('display-shadow') || false; | ||
} | ||
set displayShadow (val) { | ||
val = !!val; | ||
val ? this.setAttribute('display-shadow', '') : this.removeAttribute('display-shadow'); | ||
} | ||
get ambientColor () { | ||
return this.getAttribute('ambient-color') || '#455A64'; | ||
} | ||
set ambientColor (val) { | ||
val ? this.setAttribute('ambient-color', val) : this.removeAttribute('ambient-color'); | ||
} | ||
get autoRedraw () { | ||
return this.hasAttribute('auto-redraw') || false; | ||
} | ||
set autoRedraw (val) { | ||
val ? this.setAttribute('auto-redraw', true) : this.removeAttribute('auto-redraw'); | ||
} | ||
get angles () { | ||
const angles = {}; | ||
if (this.robot) { | ||
for (let name in this.robot.urdf.joints) angles[name] = this.robot.urdf.joints[name].urdf.angle | ||
for (let name in this.robot.urdf.joints) angles[name] = this.robot.urdf.joints[name].urdf.angle; | ||
} | ||
return angles | ||
return angles; | ||
} | ||
set angles(val) { this._setAngles(val) } | ||
set angles (val) { | ||
get loadingManager() { | ||
this._setAngles(val); | ||
} | ||
get loadingManager () { | ||
return this._loadingManager = this._loadingManager || new THREE.LoadingManager(); | ||
} | ||
get urdfLoader() { | ||
return this._urdfLoader = this._urdfLoader || new URDFLoader(this.loadingManager) | ||
get urdfLoader () { | ||
return this._urdfLoader = this._urdfLoader || new URDFLoader(this.loadingManager); | ||
} | ||
/* Lifecycle Functions */ | ||
constructor() { | ||
super() | ||
constructor () { | ||
this._requestId = 0 | ||
this._dirty = false | ||
this.robot = null | ||
super(); | ||
this._requestId = 0; | ||
this._dirty = false; | ||
this.robot = null; | ||
// Scene setup | ||
const scene = new THREE.Scene() | ||
const scene = new THREE.Scene(); | ||
const ambientLight = new THREE.AmbientLight(this.ambientColor) | ||
scene.add(ambientLight) | ||
const ambientLight = new THREE.HemisphereLight(this.ambientColor, '#000'); | ||
ambientLight.groundColor.lerp(ambientLight.color, 0.5); | ||
ambientLight.intensity = 0.5; | ||
ambientLight.position.set(0, 1, 0); | ||
scene.add(ambientLight); | ||
// Light setup | ||
const dirLight = new THREE.DirectionalLight(0xffffff) | ||
dirLight.position.set(4, 10, 1) | ||
dirLight.shadow.mapSize.width = 2048 | ||
dirLight.shadow.mapSize.height = 2048 | ||
dirLight.castShadow = true | ||
const dirLight = new THREE.DirectionalLight(0xffffff); | ||
dirLight.position.set(4, 10, 1); | ||
dirLight.shadow.mapSize.width = 2048; | ||
dirLight.shadow.mapSize.height = 2048; | ||
dirLight.shadow.bias = -0.0001; | ||
dirLight.castShadow = true; | ||
scene.add(dirLight); | ||
scene.add(dirLight) | ||
// Renderer setup | ||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }) | ||
renderer.setClearColor(0xffffff) | ||
renderer.setClearAlpha(0) | ||
renderer.shadowMap.enabled = true | ||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | ||
renderer.setClearColor(0xffffff); | ||
renderer.setClearAlpha(0); | ||
renderer.shadowMap.enabled = true; | ||
renderer.gammaOutput = true; | ||
// Camera setup | ||
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000) | ||
camera.position.z = -10 | ||
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000); | ||
camera.position.z = -10; | ||
// World setup | ||
const world = new THREE.Object3D() | ||
scene.add(world) | ||
const world = new THREE.Object3D(); | ||
scene.add(world); | ||
const plane = new THREE.Mesh( | ||
new THREE.PlaneBufferGeometry( 40, 40 ), | ||
new THREE.ShadowMaterial( { side: THREE.DoubleSide, transparent: true, opacity: 0.25 } ) | ||
) | ||
plane.rotation.x = -Math.PI/2 | ||
plane.position.y = -0.5 | ||
plane.receiveShadow = true | ||
plane.scale.set(10, 10, 10) | ||
scene.add(plane) | ||
new THREE.PlaneBufferGeometry(40, 40), | ||
new THREE.ShadowMaterial({ side: THREE.DoubleSide, transparent: true, opacity: 0.5 }) | ||
); | ||
plane.rotation.x = -Math.PI / 2; | ||
plane.position.y = -0.5; | ||
plane.receiveShadow = true; | ||
plane.scale.set(10, 10, 10); | ||
scene.add(plane); | ||
// Controls setup | ||
const controls = new THREE.OrbitControls(camera, renderer.domElement) | ||
controls.rotateSpeed = 2.0 | ||
controls.zoomSpeed = 5 | ||
controls.panSpeed = 2 | ||
controls.enableZoom = true | ||
controls.enablePan = true | ||
controls.enableDamping = false | ||
controls.maxDistance = 50 | ||
controls.minDistance = 0.25 | ||
controls.addEventListener('change', () => this._dirty = true) | ||
const controls = new THREE.OrbitControls(camera, renderer.domElement); | ||
controls.rotateSpeed = 2.0; | ||
controls.zoomSpeed = 5; | ||
controls.panSpeed = 2; | ||
controls.enableZoom = true; | ||
controls.enablePan = false; | ||
controls.enableDamping = false; | ||
controls.maxDistance = 50; | ||
controls.minDistance = 0.25; | ||
controls.addEventListener('change', () => this._dirty = true); | ||
this.world = world | ||
this.renderer = renderer | ||
this.camera = camera | ||
this.controls = controls | ||
this.plane = plane | ||
this.ambientLight = ambientLight | ||
this.scene = scene; | ||
this.world = world; | ||
this.renderer = renderer; | ||
this.camera = camera; | ||
this.controls = controls; | ||
this.plane = plane; | ||
this.directionalLight = dirLight; | ||
this.ambientLight = ambientLight; | ||
const _renderLoop = () => { | ||
if(this.parentNode) { | ||
this.updateSize() | ||
if (this.parentNode) { | ||
this.updateSize(); | ||
if (this._dirty || this.autoRedraw) { | ||
this._updateEnvironment() | ||
this._updateEnvironment(); | ||
} | ||
@@ -141,30 +215,37 @@ | ||
// case the controls are retargeted | ||
this.controls.update() | ||
this.controls.update(); | ||
if (this._dirty || this.autoRedraw) { | ||
this.renderer.render(scene, camera) | ||
this._dirty = false | ||
this.renderer.render(scene, camera); | ||
this._dirty = false; | ||
} | ||
} | ||
this._renderLoopId = requestAnimationFrame(_renderLoop) | ||
} | ||
_renderLoop() | ||
this._renderLoopId = requestAnimationFrame(_renderLoop); | ||
}; | ||
_renderLoop(); | ||
} | ||
connectedCallback() { | ||
connectedCallback () { | ||
// Add our initialize styles for the element if they haven't | ||
// been added yet | ||
if (!this.constructor._styletag) { | ||
const styletag = document.createElement('style') | ||
const styletag = document.createElement('style'); | ||
styletag.innerHTML = | ||
` | ||
${this.tagName} { display: block; } | ||
${this.tagName} canvas { | ||
${ this.tagName } { display: block; } | ||
${ this.tagName } canvas { | ||
width: 100%; | ||
height: 100%; | ||
} | ||
` | ||
document.head.appendChild(styletag) | ||
this.constructor._styletag = styletag | ||
`; | ||
document.head.appendChild(styletag); | ||
this.constructor._styletag = styletag; | ||
} | ||
@@ -174,60 +255,84 @@ | ||
if (this.childElementCount === 0) { | ||
this.appendChild(this.renderer.domElement) | ||
this.appendChild(this.renderer.domElement); | ||
} | ||
this.updateSize() | ||
requestAnimationFrame(() => this.updateSize()) | ||
this.updateSize(); | ||
requestAnimationFrame(() => this.updateSize()); | ||
} | ||
disconnectedCallback() { | ||
cancelAnimationFrame(this._renderLoopId) | ||
disconnectedCallback () { | ||
cancelAnimationFrame(this._renderLoopId); | ||
} | ||
attributeChangedCallback(attr, oldval, newval) { | ||
this._dirty = true | ||
attributeChangedCallback (attr, oldval, newval) { | ||
switch(attr) { | ||
this._dirty = true; | ||
switch (attr) { | ||
case 'package': | ||
case 'urdf': { | ||
this._loadUrdf(this.package, this.urdf) | ||
break | ||
this._loadUrdf(this.package, this.urdf); | ||
break; | ||
} | ||
case 'up': { | ||
this._setUp(this.up) | ||
break | ||
this._setUp(this.up); | ||
break; | ||
} | ||
case 'ambient-color': { | ||
this.ambientLight.color.set(this.ambientColor) | ||
break | ||
this.ambientLight.color.set(this.ambientColor); | ||
this.ambientLight.groundColor.set('#000').lerp(this.ambientLight.color, 0.5); | ||
break; | ||
} | ||
case 'ignore-limits': { | ||
this._setIgnoreLimits(this.ignoreLimits, true) | ||
break | ||
this._setIgnoreLimits(this.ignoreLimits, true); | ||
break; | ||
} | ||
} | ||
} | ||
/* Public API */ | ||
updateSize() { | ||
const r = this.renderer | ||
const w = this.clientWidth | ||
const h = this.clientHeight | ||
const currsize = r.getSize() | ||
updateSize () { | ||
if (currsize.width != w || currsize.height != h) { | ||
this._dirty = true | ||
const r = this.renderer; | ||
const w = this.clientWidth; | ||
const h = this.clientHeight; | ||
const currsize = r.getSize(); | ||
if (currsize.width !== w || currsize.height !== h) { | ||
this._dirty = true; | ||
} | ||
r.setPixelRatio(window.devicePixelRatio) | ||
r.setSize(w, h, false) | ||
r.setPixelRatio(window.devicePixelRatio); | ||
r.setSize(w, h, false); | ||
this.camera.aspect = w / h | ||
this.camera.aspect = w / h; | ||
this.camera.updateProjectionMatrix(); | ||
} | ||
redraw() { | ||
this._dirty = true | ||
redraw () { | ||
this._dirty = true; | ||
} | ||
@@ -237,14 +342,20 @@ | ||
// angle in degrees | ||
setAngle(jointname, angle) { | ||
if (!this.robot) return | ||
setAngle (jointname, angle) { | ||
const joint = this.robot.urdf.joints[jointname] | ||
if (!this.robot) return; | ||
const joint = this.robot.urdf.joints[jointname]; | ||
if (joint && joint.urdf.angle !== angle) { | ||
joint.urdf.setAngle(angle) | ||
this._dirty = true | ||
joint.urdf.setAngle(angle); | ||
this._dirty = true; | ||
} | ||
} | ||
setAngles(angles) { | ||
for(name in angles) this.setAngle(name, angles[name]) | ||
setAngles (angles) { | ||
for (const name in angles) this.setAngle(name, angles[name]); | ||
} | ||
@@ -256,40 +367,54 @@ | ||
// camera on the center of the scene | ||
_updateEnvironment() { | ||
this.plane.visible = this.displayShadow | ||
if(this.robot && this.displayShadow) { | ||
this.world.updateMatrixWorld() | ||
_updateEnvironment () { | ||
const bbox = new THREE.Box3().setFromObject(this.robot) | ||
this.controls.target.y = bbox.getCenter(new THREE.Vector3()).y | ||
this.plane.position.y = bbox.min.y - 1e-3 | ||
this.plane.visible = this.displayShadow; | ||
if (this.robot && this.displayShadow) { | ||
this.world.updateMatrixWorld(); | ||
const bbox = new THREE.Box3().setFromObject(this.robot); | ||
this.controls.target.y = bbox.getCenter(new THREE.Vector3()).y; | ||
this.plane.position.y = bbox.min.y - 1e-3; | ||
const minmax = bbox.getBoundingSphere(new THREE.Sphere()).radius; | ||
const cam = this.directionalLight.shadow.camera; | ||
cam.left = cam.bottom = -minmax; | ||
cam.right = cam.top = minmax; | ||
cam.updateProjectionMatrix(); | ||
} | ||
} | ||
// Watch the package and urdf field and load the | ||
_loadUrdf(pkg, urdf) { | ||
_loadUrdf (pkg, urdf) { | ||
const _dispose = item => { | ||
if (!item) return | ||
if (item.parent) item.parent.remove(item) | ||
if (item.dispose) item.dispose() | ||
item.children.forEach(c => _dispose(c)) | ||
} | ||
if (this._prevload === `${pkg}|${urdf}`) return | ||
if (!item) return; | ||
if (item.parent) item.parent.remove(item); | ||
if (item.dispose) item.dispose(); | ||
item.children.forEach(c => _dispose(c)); | ||
_dispose(this.robot) | ||
this.robot = null | ||
}; | ||
this.dispatchEvent(new CustomEvent('urdf-change', { bubbles: true, cancelable: true, composed: true })) | ||
if (this._prevload === `${ pkg }|${ urdf }`) return; | ||
_dispose(this.robot); | ||
this.robot = null; | ||
this.dispatchEvent(new CustomEvent('urdf-change', { bubbles: true, cancelable: true, composed: true })); | ||
if (pkg && urdf) { | ||
this._prevload = `${pkg}|${urdf}` | ||
this._prevload = `${ pkg }|${ urdf }`; | ||
// Keep track of this request and make | ||
// sure it doesn't get overwritten by | ||
// a subsequent one | ||
this._requestId ++ | ||
const requestId = this._requestId | ||
this._requestId++; | ||
const requestId = this._requestId; | ||
let totalMeshes = 0 | ||
let meshesLoaded = 0 | ||
let totalMeshes = 0; | ||
let meshesLoaded = 0; | ||
this.urdfLoader.load( | ||
@@ -301,15 +426,19 @@ pkg, | ||
robot => { | ||
// If another request has come in to load a new | ||
// robot, then ignore this one | ||
if (this._requestId !== requestId) { | ||
_dispose(robot) | ||
return | ||
_dispose(robot); | ||
return; | ||
} | ||
this.robot = robot | ||
this.world.add(robot) | ||
this.robot = robot; | ||
this.world.add(robot); | ||
this._setIgnoreLimits(this.ignoreLimits) | ||
this._setIgnoreLimits(this.ignoreLimits); | ||
this.dispatchEvent(new CustomEvent('urdf-processed', { bubbles: true, cancelable: true, composed: true })) | ||
this.dispatchEvent(new CustomEvent('urdf-processed', { bubbles: true, cancelable: true, composed: true })); | ||
}, | ||
@@ -319,23 +448,60 @@ | ||
(path, ext, done) => { | ||
totalMeshes++ | ||
totalMeshes++; | ||
this.urdfLoader.defaultMeshLoader(path, ext, mesh => { | ||
const _enableShadows = o => { | ||
if (o instanceof THREE.Mesh) { | ||
o.castShadow = true | ||
mesh.traverse(c => { | ||
if (c.type === 'Mesh') { | ||
c.castShadow = true; | ||
c.receiveShadow = true; | ||
if (c.material) { | ||
const mats = | ||
(Array.isArray(c.material) ? c.material : [c.material]) | ||
.map(m => { | ||
if (m instanceof THREE.MeshBasicMaterial) { | ||
return new THREE.MeshPhongMaterial(); | ||
} | ||
if (m.map) { | ||
m.map.encoding = THREE.GammaEncoding; | ||
} | ||
return m; | ||
}); | ||
c.material = mats.length === 1 ? mats[0] : mats; | ||
} | ||
} | ||
o.children.forEach(c => _enableShadows(c)) | ||
} | ||
_enableShadows(mesh) | ||
done(mesh) | ||
meshesLoaded++ | ||
}); | ||
done(mesh); | ||
meshesLoaded++; | ||
if (meshesLoaded === totalMeshes && this._requestId === requestId) { | ||
this.dispatchEvent(new CustomEvent('geometry-loaded', { bubbles: true, cancelable: true, composed: true })) | ||
this.dispatchEvent(new CustomEvent('geometry-loaded', { bubbles: true, cancelable: true, composed: true })); | ||
} | ||
this._dirty = true | ||
}) | ||
}, | ||
{ mode: 'cors', credentials: 'same-origin' }) | ||
this._dirty = true; | ||
}); | ||
}, | ||
{ mode: 'cors', credentials: 'same-origin' }); | ||
} | ||
} | ||
@@ -345,13 +511,15 @@ | ||
// rotation of the scene to match | ||
_setUp(up) { | ||
if (!up) up = '+Z' | ||
up = up.toUpperCase() | ||
const sign = up.replace(/[^-+]/g, '')[0] || '+' | ||
const axis = up.replace(/[^XYZ]/gi, '')[0] || 'Z' | ||
_setUp (up) { | ||
const PI = Math.PI | ||
const HALFPI = PI / 2 | ||
if (axis === 'X') this.world.rotation.set(0, 0, sign === '+' ? HALFPI : -HALFPI) | ||
if (axis === 'Z') this.world.rotation.set(sign === '+' ? -HALFPI : HALFPI, 0, 0) | ||
if (axis === 'Y') this.world.rotation.set(sign === '+' ? 0 : PI, 0, 0) | ||
if (!up) up = '+Z'; | ||
up = up.toUpperCase(); | ||
const sign = up.replace(/[^-+]/g, '')[0] || '+'; | ||
const axis = up.replace(/[^XYZ]/gi, '')[0] || 'Z'; | ||
const PI = Math.PI; | ||
const HALFPI = PI / 2; | ||
if (axis === 'X') this.world.rotation.set(0, 0, sign === '+' ? HALFPI : -HALFPI); | ||
if (axis === 'Z') this.world.rotation.set(sign === '+' ? -HALFPI : HALFPI, 0, 0); | ||
if (axis === 'Y') this.world.rotation.set(sign === '+' ? 0 : PI, 0, 0); | ||
} | ||
@@ -361,17 +529,25 @@ | ||
// joint limits or not | ||
_setIgnoreLimits(ignore, dispatch = false) { | ||
_setIgnoreLimits (ignore, dispatch = false) { | ||
if (this.robot) { | ||
Object | ||
.values(this.robot.urdf.joints) | ||
.forEach(joint => { | ||
joint.urdf.ignoreLimits = ignore | ||
joint.urdf.setAngle(joint.urdf.angle) | ||
}) | ||
joint.urdf.ignoreLimits = ignore; | ||
joint.urdf.setAngle(joint.urdf.angle); | ||
}); | ||
} | ||
if (dispatch) { | ||
this.dispatchEvent(new CustomEvent('ignore-limits-change', { bubbles: true, cancelable: true, composed: true })) | ||
this.dispatchEvent(new CustomEvent('ignore-limits-change', { bubbles: true, cancelable: true, composed: true })); | ||
} | ||
} | ||
} | ||
}; |
@@ -0,1 +1,2 @@ | ||
/* globals THREE */ | ||
/* | ||
@@ -29,20 +30,26 @@ Reference coordinate frames for THREE.js and ROS. | ||
// Cached mesh loaders | ||
get STLLoader() { | ||
this._stlloader = this._stlloader || new THREE.STLLoader(this.manager) | ||
return this._stlloader | ||
get STLLoader () { | ||
this._stlloader = this._stlloader || new THREE.STLLoader(this.manager); | ||
return this._stlloader; | ||
} | ||
get DAELoader() { | ||
this._daeloader = this._daeloader || new THREE.ColladaLoader(this.manager) | ||
return this._daeloader | ||
get DAELoader () { | ||
this._daeloader = this._daeloader || new THREE.ColladaLoader(this.manager); | ||
return this._daeloader; | ||
} | ||
get TextureLoader() { | ||
this._textureloader = this._textureloader || new THREE.TextureLoader(this.manager) | ||
return this._textureloader | ||
get TextureLoader () { | ||
this._textureloader = this._textureloader || new THREE.TextureLoader(this.manager); | ||
return this._textureloader; | ||
} | ||
constructor(manager) { | ||
constructor (manager) { | ||
this.manager = manager || THREE.DefaultLoadingManager | ||
this.manager = manager || THREE.DefaultLoadingManager; | ||
@@ -54,17 +61,29 @@ } | ||
// HTMLCollection does not the by default | ||
forEach(coll, func) { return [].forEach.call(coll, func) } | ||
filter(coll, func) { return [].filter.call(coll, func) } | ||
forEach (coll, func) { | ||
return [].forEach.call(coll, func); | ||
} | ||
filter (coll, func) { | ||
return [].filter.call(coll, func); | ||
} | ||
// take a vector "x y z" and process it into | ||
// an array [x, y, z] | ||
_processTuple(val) { | ||
if (!val) return [0, 0, 0] | ||
return val.trim().split(/\s+/g).map(num => parseFloat(num)) | ||
_processTuple (val) { | ||
if (!val) return [0, 0, 0]; | ||
return val.trim().split(/\s+/g).map(num => parseFloat(num)); | ||
} | ||
// applies a rotation a threejs object in URDF order | ||
_applyRotation(obj, rpy) { | ||
obj.rotateOnAxis(new THREE.Vector3(0,0,1), rpy[2]) | ||
obj.rotateOnAxis(new THREE.Vector3(0,1,0), rpy[1]) | ||
obj.rotateOnAxis(new THREE.Vector3(1,0,0), rpy[0]) | ||
_applyRotation (obj, rpy) { | ||
obj.rotateOnAxis(new THREE.Vector3(0, 0, 1), rpy[2]); | ||
obj.rotateOnAxis(new THREE.Vector3(0, 1, 0), rpy[1]); | ||
obj.rotateOnAxis(new THREE.Vector3(1, 0, 0), rpy[0]); | ||
} | ||
@@ -76,6 +95,6 @@ | ||
// cb: Callback that is passed the model once loaded | ||
load(pkg, urdf, cb, loadMeshCb, fetchOptions) { | ||
load (pkg, urdf, cb, loadMeshCb, fetchOptions) { | ||
// normalize the path slashes | ||
let path = `${pkg}/${urdf}`.replace(/\\/g, '/').replace(/\/+/g, '/') | ||
let path = `${ pkg }/${ urdf }`.replace(/\\/g, '/').replace(/\/+/g, '/'); | ||
path = this.manager.resolveURL(path); | ||
@@ -85,22 +104,35 @@ | ||
.then(res => res.text()) | ||
.then(data => this.parse(pkg, data, cb, loadMeshCb)) | ||
.then(data => this.parse(pkg, data, cb, loadMeshCb)); | ||
} | ||
parse(pkg, content, cb, loadMeshCb) { | ||
cb(this._processUrdf(pkg, content, loadMeshCb || this.defaultMeshLoader.bind(this))) | ||
parse (pkg, content, cb, loadMeshCb) { | ||
cb(this._processUrdf(pkg, content, loadMeshCb || this.defaultMeshLoader.bind(this))); | ||
} | ||
// Default mesh loading function | ||
defaultMeshLoader(path, ext, done) { | ||
defaultMeshLoader (path, ext, done) { | ||
if (/\.stl$/i.test(path)) | ||
if (/\.stl$/i.test(path)) { | ||
this.STLLoader.load(path, geom => { | ||
const mesh = new THREE.Mesh() | ||
mesh.geometry = geom | ||
done(mesh) | ||
}) | ||
else if (/\.dae$/i.test(path)) | ||
this.DAELoader.load(path, dae => done(dae.scene)) | ||
else | ||
console.warn(`Could note load model at ${path}:\nNo loader available`) | ||
const mesh = new THREE.Mesh(); | ||
mesh.geometry = geom; | ||
done(mesh); | ||
}); | ||
} else if (/\.dae$/i.test(path)) { | ||
this.DAELoader.load(path, dae => done(dae.scene)); | ||
} else { | ||
console.warn(`Could note load model at ${ path }:\nNo loader available`); | ||
} | ||
} | ||
@@ -110,112 +142,150 @@ | ||
// Process the URDF text format | ||
_processUrdf(pkg, data, loadMeshCb) { | ||
const parser = new DOMParser() | ||
const urdf = parser.parseFromString(data, 'text/xml') | ||
_processUrdf (pkg, data, loadMeshCb) { | ||
const robottag = this.filter(urdf.children, c => c.nodeName === 'robot').pop() | ||
return this._processRobot(pkg, robottag, loadMeshCb) | ||
const parser = new DOMParser(); | ||
const urdf = parser.parseFromString(data, 'text/xml'); | ||
const robottag = this.filter(urdf.children, c => c.nodeName === 'robot').pop(); | ||
return this._processRobot(pkg, robottag, loadMeshCb); | ||
} | ||
// Process the <robot> node | ||
_processRobot(pkg, robot, loadMeshCb) { | ||
const links = [] | ||
const joints = [] | ||
const obj = new THREE.Object3D() | ||
obj.name = robot.getAttribute('name') | ||
obj.urdf = { node: robot } | ||
_processRobot (pkg, robot, loadMeshCb) { | ||
const links = []; | ||
const joints = []; | ||
const obj = new THREE.Object3D(); | ||
obj.name = robot.getAttribute('name'); | ||
obj.urdf = { node: robot }; | ||
// Process the <joint> and <link> nodes | ||
this.forEach(robot.children, n => { | ||
const type = n.nodeName.toLowerCase() | ||
if (type === 'link') links.push(n) | ||
else if (type === 'joint') joints.push(n) | ||
}) | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'link') links.push(n); | ||
else if (type === 'joint') joints.push(n); | ||
}); | ||
// Create the <link> map | ||
const linkMap = {} | ||
const linkMap = {}; | ||
this.forEach(links, l => { | ||
const name = l.getAttribute('name') | ||
linkMap[name] = this._processLink(pkg, l, loadMeshCb) | ||
}) | ||
const name = l.getAttribute('name'); | ||
linkMap[name] = this._processLink(pkg, l, loadMeshCb); | ||
}); | ||
// Create the <joint> map | ||
const jointMap = {} | ||
const jointMap = {}; | ||
this.forEach(joints, j => { | ||
const name = j.getAttribute('name') | ||
jointMap[name] = this._processJoint(j, linkMap) | ||
}) | ||
for (let key in linkMap) linkMap[key].parent ? null : obj.add(linkMap[key]) | ||
const name = j.getAttribute('name'); | ||
jointMap[name] = this._processJoint(j, linkMap); | ||
obj.urdf.joints = jointMap | ||
obj.urdf.links = linkMap | ||
}); | ||
return obj | ||
for (let key in linkMap) { | ||
if (linkMap[key].parent == null) { | ||
obj.add(linkMap[key]); | ||
} | ||
} | ||
obj.urdf.joints = jointMap; | ||
obj.urdf.links = linkMap; | ||
return obj; | ||
} | ||
// Process joint nodes and parent them | ||
_processJoint(joint, linkMap) { | ||
const jointType = joint.getAttribute('type') | ||
const obj = new THREE.Object3D() | ||
obj.name = joint.getAttribute('name') | ||
_processJoint (joint, linkMap) { | ||
const jointType = joint.getAttribute('type'); | ||
const obj = new THREE.Object3D(); | ||
obj.name = joint.getAttribute('name'); | ||
obj.urdf = { | ||
node: joint, type: jointType, angle: 0, axis: null, | ||
node: joint, | ||
type: jointType, | ||
angle: 0, | ||
axis: null, | ||
limits: { lower: 0, upper: 0 }, | ||
ignoreLimits: false, | ||
setAngle: () => {} | ||
} | ||
setAngle: () => {}, | ||
}; | ||
let parent = null | ||
let child = null | ||
let xyz = [0, 0, 0] | ||
let rpy = [0, 0, 0] | ||
let parent = null; | ||
let child = null; | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
// Extract the attributes | ||
this.forEach(joint.children, n => { | ||
const type = n.nodeName.toLowerCase() | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'origin') { | ||
xyz = this._processTuple(n.getAttribute('xyz')) | ||
rpy = this._processTuple(n.getAttribute('rpy')) | ||
} else if(type === 'child') { | ||
child = linkMap[n.getAttribute('link')] | ||
} else if(type === 'parent') { | ||
parent = linkMap[n.getAttribute('link')] | ||
} else if(type === 'limit') { | ||
obj.urdf.limits.lower = parseFloat(n.getAttribute('lower') || obj.urdf.limits.lower) | ||
obj.urdf.limits.upper = parseFloat(n.getAttribute('upper') || obj.urdf.limits.upper) | ||
xyz = this._processTuple(n.getAttribute('xyz')); | ||
rpy = this._processTuple(n.getAttribute('rpy')); | ||
} else if (type === 'child') { | ||
child = linkMap[n.getAttribute('link')]; | ||
} else if (type === 'parent') { | ||
parent = linkMap[n.getAttribute('link')]; | ||
} else if (type === 'limit') { | ||
obj.urdf.limits.lower = parseFloat(n.getAttribute('lower') || obj.urdf.limits.lower); | ||
obj.urdf.limits.upper = parseFloat(n.getAttribute('upper') || obj.urdf.limits.upper); | ||
} | ||
}) | ||
}); | ||
// Join the links | ||
parent.add(obj) | ||
obj.add(child) | ||
this._applyRotation(obj, rpy) | ||
obj.position.set(xyz[0], xyz[1], xyz[2]) | ||
parent.add(obj); | ||
obj.add(child); | ||
this._applyRotation(obj, rpy); | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
// Set up the rotate function | ||
const origRot = new THREE.Quaternion().copy(obj.quaternion) | ||
const origPos = new THREE.Vector3().copy(obj.position) | ||
const axisnode = this.filter(joint.children, n => n.nodeName.toLowerCase() === 'axis')[0] | ||
const origRot = new THREE.Quaternion().copy(obj.quaternion); | ||
const origPos = new THREE.Vector3().copy(obj.position); | ||
const axisnode = this.filter(joint.children, n => n.nodeName.toLowerCase() === 'axis')[0]; | ||
if (axisnode) { | ||
const axisxyz = axisnode.getAttribute('xyz').split(/\s+/g).map(num => parseFloat(num)) | ||
obj.urdf.axis = new THREE.Vector3(axisxyz[0], axisxyz[1], axisxyz[2]) | ||
obj.urdf.axis.normalize() | ||
const axisxyz = axisnode.getAttribute('xyz').split(/\s+/g).map(num => parseFloat(num)); | ||
obj.urdf.axis = new THREE.Vector3(axisxyz[0], axisxyz[1], axisxyz[2]); | ||
obj.urdf.axis.normalize(); | ||
} | ||
switch (jointType) { | ||
case 'fixed': break; | ||
case 'continuous': | ||
obj.urdf.limits.lower = -Infinity | ||
obj.urdf.limits.upper = Infinity | ||
obj.urdf.limits.lower = -Infinity; | ||
obj.urdf.limits.upper = Infinity; | ||
// fall through to revolute joint 'setAngle' function | ||
case 'revolute': | ||
obj.urdf.setAngle = function(angle = null) { | ||
if (!this.axis) return | ||
if (angle == null) return | ||
obj.urdf.setAngle = function (angle = null) { | ||
if (!this.axis) return; | ||
if (angle == null) return; | ||
if (!this.ignoreLimits) { | ||
angle = Math.min(this.limits.upper, angle) | ||
angle = Math.max(this.limits.lower, angle) | ||
angle = Math.min(this.limits.upper, angle); | ||
angle = Math.max(this.limits.lower, angle); | ||
} | ||
@@ -225,30 +295,36 @@ | ||
// expected angle for URDF, so negate it here | ||
const delta = new THREE.Quaternion().setFromAxisAngle(this.axis, angle) | ||
obj.quaternion.multiplyQuaternions(origRot, delta) | ||
const delta = new THREE.Quaternion().setFromAxisAngle(this.axis, angle); | ||
obj.quaternion.multiplyQuaternions(origRot, delta); | ||
this.angle = angle | ||
} | ||
break | ||
this.angle = angle; | ||
}; | ||
break; | ||
case 'prismatic': | ||
obj.urdf.setAngle = function(angle = null) { | ||
if (!this.axis) return | ||
if (angle == null) return | ||
obj.urdf.setAngle = function (angle = null) { | ||
if (!this.axis) return; | ||
if (angle == null) return; | ||
if (!this.ignoreLimits) { | ||
angle = Math.min(this.limits.upper, angle) | ||
angle = Math.max(this.limits.lower, angle) | ||
angle = Math.min(this.limits.upper, angle); | ||
angle = Math.max(this.limits.lower, angle); | ||
} | ||
obj.position.copy(origPos); | ||
obj.position.addScaledVector(this.axis, angle) | ||
obj.position.addScaledVector(this.axis, angle); | ||
this.angle = angle | ||
} | ||
break | ||
this.angle = angle; | ||
}; | ||
break; | ||
case 'floating': | ||
case 'planar': | ||
// TODO: Support these joint types | ||
console.warn(`'${ jointType }' joint not yet supported`) | ||
console.warn(`'${ jointType }' joint not yet supported`); | ||
} | ||
@@ -260,121 +336,158 @@ | ||
// TODO: Figure out how to handle setting and getting angles of other types | ||
obj.urdf.set = obj.urdf.setAngle | ||
obj.urdf.set = obj.urdf.setAngle; | ||
return obj | ||
return obj; | ||
} | ||
// Process the <link> nodes | ||
_processLink(pkg, link, loadMeshCb) { | ||
const visualNodes = this.filter(link.children, n => n.nodeName.toLowerCase() === 'visual') | ||
const obj = new THREE.Object3D() | ||
obj.name = link.getAttribute('name') | ||
obj.urdf = { node: link } | ||
_processLink (pkg, link, loadMeshCb) { | ||
this.forEach(visualNodes, vn => this._processVisualNode(pkg, vn, obj, loadMeshCb)) | ||
const visualNodes = this.filter(link.children, n => n.nodeName.toLowerCase() === 'visual'); | ||
const obj = new THREE.Object3D(); | ||
obj.name = link.getAttribute('name'); | ||
obj.urdf = { node: link }; | ||
return obj | ||
this.forEach(visualNodes, vn => this._processVisualNode(pkg, vn, obj, loadMeshCb)); | ||
return obj; | ||
} | ||
// Process the visual nodes into meshes | ||
_processVisualNode(pkg, vn, linkObj, loadMeshCb) { | ||
let xyz = [0, 0, 0] | ||
let rpy = [0, 0, 0] | ||
let scale = [1, 1, 1] | ||
let mesh = null | ||
_processVisualNode (pkg, vn, linkObj, loadMeshCb) { | ||
const material = new THREE.MeshLambertMaterial() | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
let scale = [1, 1, 1]; | ||
const material = new THREE.MeshPhongMaterial(); | ||
this.forEach(vn.children, n => { | ||
const type = n.nodeName.toLowerCase() | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'geometry') { | ||
const geoType = n.children[0].nodeName.toLowerCase() | ||
const geoType = n.children[0].nodeName.toLowerCase(); | ||
if (geoType === 'mesh') { | ||
const filename = n.children[0].getAttribute('filename').replace(/^((package:\/\/)|(model:\/\/))/, '') | ||
const path = pkg + '/' + filename | ||
const ext = path.match(/.*\.([A-Z0-9]+)$/i).pop() || '' | ||
let scale_exist = n.children[0].getAttribute('scale') | ||
if (scale_exist) scale = this._processTuple(scale_exist) | ||
const filename = n.children[0].getAttribute('filename').replace(/^((package:\/\/)|(model:\/\/))/, ''); | ||
const path = pkg + '/' + filename; | ||
const ext = path.match(/.*\.([A-Z0-9]+)$/i).pop() || ''; | ||
let scaleAttr = n.children[0].getAttribute('scale'); | ||
if (scaleAttr) scale = this._processTuple(scaleAttr); | ||
loadMeshCb(path, ext, obj => { | ||
if (obj) { | ||
if (obj instanceof THREE.Mesh) { | ||
obj.material = material | ||
obj.material.copy(material); | ||
} | ||
linkObj.add(obj) | ||
linkObj.add(obj); | ||
obj.position.set(xyz[0], xyz[1], xyz[2]) | ||
obj.rotation.set(0,0,0) | ||
obj.scale.set(scale[0], scale[1], scale[2]) | ||
this._applyRotation(obj, rpy) | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
obj.rotation.set(0, 0, 0); | ||
obj.scale.set(scale[0], scale[1], scale[2]); | ||
this._applyRotation(obj, rpy); | ||
} | ||
}) | ||
}); | ||
} else if (geoType === 'box') { | ||
requestAnimationFrame(() => { | ||
const mesh = new THREE.Mesh() | ||
mesh.geometry = new THREE.BoxGeometry(1, 1, 1) | ||
mesh.material = material | ||
const size = this._processTuple(n.children[0].getAttribute('size')) | ||
const mesh = new THREE.Mesh(); | ||
mesh.geometry = new THREE.BoxGeometry(1, 1, 1); | ||
mesh.material = material; | ||
linkObj.add(mesh) | ||
this._applyRotation(mesh, rpy) | ||
mesh.position.set(xyz[0], xyz[1], xyz[2]) | ||
mesh.scale.set(size[0], size[1], size[2]) | ||
}) | ||
const size = this._processTuple(n.children[0].getAttribute('size')); | ||
linkObj.add(mesh); | ||
this._applyRotation(mesh, rpy); | ||
mesh.position.set(xyz[0], xyz[1], xyz[2]); | ||
mesh.scale.set(size[0], size[1], size[2]); | ||
}); | ||
} else if (geoType === 'sphere') { | ||
requestAnimationFrame(() => { | ||
const mesh = new THREE.Mesh() | ||
mesh.geometry = new THREE.SphereGeometry(1, 20, 20) | ||
mesh.material = material | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0 | ||
mesh.position.set(xyz[0], xyz[1], xyz[2]) | ||
mesh.scale.set(radius, radius, radius) | ||
}) | ||
const mesh = new THREE.Mesh(); | ||
mesh.geometry = new THREE.SphereGeometry(1, 20, 20); | ||
mesh.material = material; | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
mesh.position.set(xyz[0], xyz[1], xyz[2]); | ||
mesh.scale.set(radius, radius, radius); | ||
}); | ||
} else if (geoType === 'cylinder') { | ||
requestAnimationFrame(() => { | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0 | ||
const length = parseFloat(n.children[0].getAttribute('length')) || 0 | ||
const mesh = new THREE.Mesh() | ||
mesh.geometry = new THREE.CylinderBufferGeometry(1, 1, 1, 25) | ||
mesh.material = material | ||
mesh.scale.set(radius, length, radius) | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
const length = parseFloat(n.children[0].getAttribute('length')) || 0; | ||
const obj = new THREE.Object3D() | ||
obj.add(mesh) | ||
mesh.rotation.set(Math.PI, 0, 0) | ||
const mesh = new THREE.Mesh(); | ||
mesh.geometry = new THREE.CylinderBufferGeometry(1, 1, 1, 25); | ||
mesh.material = material; | ||
mesh.scale.set(radius, length, radius); | ||
linkObj.add(obj) | ||
this._applyRotation(obj, rpy) | ||
obj.position.set(xyz[0], xyz[1], xyz[2]) | ||
}) | ||
const obj = new THREE.Object3D(); | ||
obj.add(mesh); | ||
mesh.rotation.set(Math.PI, 0, 0); | ||
linkObj.add(obj); | ||
this._applyRotation(obj, rpy); | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
}); | ||
} | ||
} else if(type === 'origin') { | ||
xyz = this._processTuple(n.getAttribute('xyz')) | ||
rpy = this._processTuple(n.getAttribute('rpy')) | ||
} else if(type === 'material') { | ||
} else if (type === 'origin') { | ||
xyz = this._processTuple(n.getAttribute('xyz')); | ||
rpy = this._processTuple(n.getAttribute('rpy')); | ||
} else if (type === 'material') { | ||
this.forEach(n.children, c => { | ||
if (c.nodeName.toLowerCase() === 'color') { | ||
let rgba = c.getAttribute('rgba') | ||
.split(/\s/g) | ||
.map(v => parseFloat(v)) | ||
.map(v => parseFloat(v)); | ||
material.color.r = rgba[0] | ||
material.color.g = rgba[1] | ||
material.color.b = rgba[2] | ||
material.opacity = rgba[3] | ||
material.color.r = rgba[0]; | ||
material.color.g = rgba[1]; | ||
material.color.b = rgba[2]; | ||
material.opacity = rgba[3]; | ||
if (material.opacity < 1) material.transparent = true | ||
if (material.opacity < 1) material.transparent = true; | ||
} else if (c.nodeName.toLowerCase() === 'texture') { | ||
const filename = c.getAttribute('filename').replace(/^(package:\/\/)/, '') | ||
const path = pkg + '/' + filename | ||
material.map = this._textureloader.load(path) | ||
const filename = c.getAttribute('filename').replace(/^(package:\/\/)/, ''); | ||
const path = pkg + '/' + filename; | ||
material.map = this._textureloader.load(path); | ||
} | ||
}) | ||
}); | ||
} | ||
}) | ||
}); | ||
} | ||
} | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2545942
6543