urdf-loader
Advanced tools
Comparing version 0.5.3 to 0.6.0
@@ -7,2 +7,16 @@ # Changelog | ||
## [0.6.0] - 2019-02-23 | ||
### Added | ||
- Added `setAngle` and `setAngles` function to the Robot node | ||
- Added `meshLoadFunc` and `urlModifierFunc` functions to `URDFViewer` component. | ||
### Changed | ||
- Changed `loadMeshCb` function API to take `url`, `manager`, and `done`. | ||
- Materials defined as "shared" by name in the URDF are shared among meshes, now. | ||
- The root link is now the same as the URDF Robot object. | ||
- Moved the `packages` parameter to the options object | ||
### Removed | ||
- Removed `urdfLoader` and `loadingManager` fields from `URDFViewer` component. | ||
## [0.5.3] - 2018-12-17 | ||
@@ -14,2 +28,3 @@ ### Added | ||
- Scale being overwritten on models that are loaded with pre-set scale values. | ||
- Loader not completing if a mesh could not be loaded. | ||
@@ -16,0 +31,0 @@ ## [0.5.2] - 2018-12-5 |
@@ -109,3 +109,3 @@ /* globals animToggle viewer setColor */ | ||
const fileNames = Object.keys(files).map(n => cleanFilePath(n)); | ||
viewer.loadingManager.setURLModifier(url => { | ||
viewer.urlModifierFunc = url => { | ||
@@ -135,3 +135,3 @@ // find the matching file given the requested url | ||
}); | ||
}; | ||
@@ -148,9 +148,2 @@ // set the source of the element to the most likely intended display model | ||
// remove the url modifier function is it doesn't affect other actions | ||
viewer.addEventListener( | ||
'geometry-loaded', | ||
() => viewer.loadingManager.setURLModifier(null), | ||
{ once: true } | ||
); | ||
}); | ||
@@ -157,0 +150,0 @@ |
@@ -189,5 +189,6 @@ /* globals viewer THREE */ | ||
const modelLoader = new THREE.ModelLoader(viewer.loadingManager); | ||
viewer.urdfLoader.defaultMeshLoader = (path, ext, done) => { | ||
modelLoader.load(path, res => done(res.model)); | ||
viewer.loadMeshFunc = (path, manager, done) => { | ||
new THREE.ModelLoader(manager).load(path, res => done(res.model), null, err => done(null, err)); | ||
}; | ||
@@ -194,0 +195,0 @@ |
{ | ||
"name": "urdf-loader", | ||
"version": "0.5.3", | ||
"version": "0.6.0", | ||
"description": "URDF Loader for THREE.js and webcomponent viewer", | ||
@@ -45,6 +45,6 @@ "main": "umd/URDFLoader.js", | ||
"concurrently": "^4.0.1", | ||
"eslint": "^5.4.0", | ||
"jest": "^23.5.0", | ||
"jest-cli": "^23.5.0", | ||
"nyc": "^12.0.2", | ||
"eslint": "^5.14.1", | ||
"jest": "^24.1.0", | ||
"jest-cli": "^24.1.0", | ||
"nyc": "^13.3.0", | ||
"opn-cli": "^3.1.0", | ||
@@ -56,3 +56,3 @@ "puppeteer": "^1.7.0", | ||
"static-server": "^3.0.0", | ||
"three": "^0.94.0", | ||
"three": "^0.101.1", | ||
"threejs-model-loader": "0.0.1", | ||
@@ -59,0 +59,0 @@ "wc-loader": "^1.1.12", |
@@ -21,8 +21,8 @@ # javascript urdf-loader | ||
'T12/urdf/T12.URDF', // The path to the URDF within the package OR absolute | ||
{ | ||
packageName : '.../package/dir/' // The equivelant of a (list of) ROS package(s):// directory | ||
}, | ||
robot => { }, // The robot is loaded! | ||
{ | ||
loadMeshCb: (path, ext, done) => { }, // Callback for each mesh for custom mesh processing and loading code | ||
packages: { | ||
packageName : '.../package/dir/' // The equivalent of a (list of) ROS package(s):// directory | ||
}, | ||
loadMeshCb: (path, manager, done) => { }, // Callback for each mesh for custom mesh processing and loading code | ||
} | ||
@@ -46,3 +46,3 @@ ); | ||
### load(urdfpath, packages, onComplete, options) | ||
### load(urdfpath, onComplete, options) | ||
@@ -57,6 +57,16 @@ Loads and builds the specified URDF robot in THREE.js | ||
#### packages : String | Object | ||
#### onComplete(robot) : Function | ||
_required_ | ||
Callback that is called once the urdf robots have been loaded. The loaded robot is passed to the function. | ||
See `URDFRobot` documentation. | ||
#### options : Object | ||
_optional_ | ||
##### options.packages : String | Object | ||
The path representing the `package://` directory(s) to load `package://` relative files. | ||
@@ -75,18 +85,12 @@ | ||
#### onComplete(robot) : Function | ||
##### options.loadMeshCb(pathToModel, manager, onComplete) : Function | ||
_required_ | ||
An optional function that can be used to override the default mesh loading functionality. The default loader is specified at `URDFLoader.defaultMeshLoader`. | ||
Callback that is called once the urdf robots have been loaded. The loaded robot is passed to the function. | ||
`pathToModel` is the url to load the model from. | ||
See `URDFRobot` documentation. | ||
`manager` is the THREE.js `LoadingManager` used by the `URDFLoader`. | ||
#### options : Object | ||
`onComplete` is called with the mesh once the geometry has been loaded. | ||
_optional_ | ||
##### options.loadMeshCb(pathToModel, fileExtension, onComplete) : Function | ||
An optional function that can be used to override the default mesh loading functionality. The default loader is specified at `URDFLoader.defaultMeshLoader`. `onComplete` is called with the mesh once the geometry has been loaded. | ||
##### options.fetchOptions : Object | ||
@@ -102,3 +106,3 @@ | ||
### parse(urdfContent, packages, onComplete, options) : THREE.Object3D | ||
### parse(urdfContent, options) : THREE.Object3D | ||
@@ -113,8 +117,2 @@ Parses URDF content and returns the robot model. | ||
#### packages : String | Object | ||
_required_ | ||
See `load`. | ||
#### onComplete(robot) : Function | ||
@@ -134,24 +132,8 @@ | ||
## URDFRobot | ||
## URDFJoint : THREE.Object3D | ||
Object that describes the URDF Robot. An extension of `THREE.Object3D`. | ||
An object representing a robot joint. | ||
#### name : String | ||
The name of the robot described in the `<robot>` tag. | ||
#### links : Object | ||
A dictionary of `linkName : URDFLink` with all links in the robot. | ||
#### joints : Object | ||
A dictionary of `jointName : URDFJoint` with all joints in the robot. | ||
## URDFJoint | ||
An object representing a robot joint. An extension of `THREE.Object3D`. | ||
#### name : String | ||
The name of the joint. | ||
@@ -187,3 +169,3 @@ | ||
## URDFLink | ||
## URDFLink : THREE.Object3D | ||
@@ -194,2 +176,18 @@ #### name | ||
## URDFRobot : URDFLink | ||
Object that describes the URDF Robot. | ||
#### robotName : String | ||
The name of the robot described in the `<robot>` tag. | ||
#### links : Object | ||
A dictionary of `linkName : URDFLink` with all links in the robot. | ||
#### joints : Object | ||
A dictionary of `jointName : URDFJoint` with all joints in the robot. | ||
## urdf-viewer Element | ||
@@ -317,3 +315,3 @@ ```html | ||
Run `npm run server`. | ||
Run `npm start`. | ||
@@ -320,0 +318,0 @@ Visit `localhost:9080/javascript/example/` to view the page. |
@@ -47,6 +47,2 @@ import * as THREE from 'three'; | ||
get loadingManager() { return this._loadingManager = this._loadingManager || new THREE.LoadingManager(); } | ||
get urdfLoader() { return this._urdfLoader = this._urdfLoader || new URDFLoader(this.loadingManager); } | ||
get angles() { | ||
@@ -75,2 +71,4 @@ | ||
this.robot = null; | ||
this.loadMeshFunc = null; | ||
this.urlModifierFunc = null; | ||
@@ -92,2 +90,3 @@ // Scene setup | ||
dirLight.castShadow = true; | ||
dirLight.shadow.bias = -0.00001; | ||
scene.add(dirLight); | ||
@@ -145,5 +144,2 @@ scene.add(dirLight.target); | ||
// redraw when something new has loaded | ||
this.loadingManager.onLoad = () => this.recenter(); | ||
const _renderLoop = () => { | ||
@@ -291,17 +287,16 @@ | ||
// Set the joint with jointname to | ||
// Set the joint with jointName to | ||
// angle in degrees | ||
setAngle(jointname, angle) { | ||
setAngle(jointName, angle) { | ||
if (!this.robot) return; | ||
if (!this.robot.joints[jointName]) return; | ||
const joint = this.robot.joints[jointname]; | ||
if (joint && joint.angle !== angle) { | ||
joint.setAngle(angle); | ||
const origAngle = this.robot.joints[jointName].angle; | ||
const newAngle = this.robot.setAngle(jointName, angle); | ||
if (origAngle !== newAngle) { | ||
this.redraw(); | ||
} | ||
this.dispatchEvent(new CustomEvent('angle-change', { bubles: true, cancelable: true, detail: jointname })); | ||
this.dispatchEvent(new CustomEvent('angle-change', { bubbles: true, cancelable: true, detail: jointName })); | ||
@@ -404,3 +399,3 @@ } | ||
if (c.type === 'Mesh') { | ||
if (c.isMesh) { | ||
@@ -462,45 +457,43 @@ c.castShadow = true; | ||
this.urdfLoader.load( | ||
urdf, | ||
pkg, | ||
let robot = null; | ||
const manager = new THREE.LoadingManager(); | ||
manager.onLoad = () => { | ||
robot => { | ||
// If another request has come in to load a new | ||
// robot, then ignore this one | ||
if (this._requestId !== requestId) { | ||
// If another request has come in to load a new | ||
// robot, then ignore this one | ||
if (this._requestId !== requestId) { | ||
robot.traverse(c => c.dispose && c.dispose()); | ||
return; | ||
robot.traverse(c => c.dispose && c.dispose()); | ||
return; | ||
} | ||
} | ||
this.robot = robot; | ||
this.world.add(robot); | ||
updateMaterials(robot); | ||
this.robot = robot; | ||
this.world.add(robot); | ||
updateMaterials(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('geometry-loaded', { bubbles: true, cancelable: true, composed: true })); | ||
this.dispatchEvent(new CustomEvent('urdf-processed', { bubbles: true, cancelable: true, composed: true })); | ||
this.dispatchEvent(new CustomEvent('geometry-loaded', { bubbles: true, cancelable: true, composed: true })); | ||
this.recenter(); | ||
this.recenter(); | ||
}; | ||
}, | ||
if (this.urlModifierFunc) { | ||
// options | ||
{ | ||
loadMeshCb: (path, ext, done) => { | ||
manager.setURLModifier(this.urlModifierFunc); | ||
// Load meshes and enable shadow casting | ||
this.urdfLoader.defaultMeshLoader(path, ext, mesh => { | ||
} | ||
updateMaterials(mesh); | ||
done(mesh); | ||
this.recenter(); | ||
new URDFLoader(manager).load( | ||
urdf, | ||
model => robot = model, | ||
}); | ||
// options | ||
{ | ||
}, | ||
packages: pkg, | ||
loadMeshCb: this.loadMeshFunc, | ||
fetchOptions: { mode: 'cors', credentials: 'same-origin' }, | ||
@@ -507,0 +500,0 @@ |
import { Object3D, Quaternion } from 'three'; | ||
class URDFRobot extends Object3D { | ||
constructor(...args) { | ||
super(...args); | ||
this.isURDFRobot = true; | ||
this.type = 'URDFRobot'; | ||
this.urdfNode = null; | ||
this.links = null; | ||
this.joints = null; | ||
} | ||
copy(source, recursive) { | ||
super.copy(source, recursive); | ||
this.links = {}; | ||
this.joints = {}; | ||
this.traverse(c => { | ||
if (c.isURDFJoint && c.name in source.joints) { | ||
this.joints[c.name] = c; | ||
} | ||
if (c.isURDFLink && c.name in source.links) { | ||
this.links[c.name] = c; | ||
} | ||
}); | ||
return this; | ||
} | ||
} | ||
class URDFLink extends Object3D { | ||
@@ -223,2 +180,69 @@ | ||
class URDFRobot extends URDFLink { | ||
constructor(...args) { | ||
super(...args); | ||
this.isURDFRobot = true; | ||
this.urdfNode = null; | ||
this.urdfRobotNode = null; | ||
this.robotName = null; | ||
this.links = null; | ||
this.joints = null; | ||
} | ||
copy(source, recursive) { | ||
super.copy(source, recursive); | ||
this.urdfRobotNode = source.urdfRobotNode; | ||
this.robotName = source.robotName; | ||
this.links = {}; | ||
this.joints = {}; | ||
this.traverse(c => { | ||
if (c.isURDFJoint && c.name in source.joints) { | ||
this.joints[c.name] = c; | ||
} | ||
if (c.isURDFLink && c.name in source.links) { | ||
this.links[c.name] = c; | ||
} | ||
}); | ||
return this; | ||
} | ||
setAngle(jointName, ...angle) { | ||
const joint = this.joints[jointName]; | ||
if (joint) { | ||
return joint.setAngle(...angle); | ||
} | ||
return null; | ||
} | ||
setAngles(angles) { | ||
// TODO: How to handle other, multi-dimensional joint types? | ||
for (const name in angles) this.setAngle(name, angles[name]); | ||
} | ||
} | ||
export { URDFRobot, URDFLink, URDFJoint }; |
@@ -31,29 +31,30 @@ import * as THREE from 'three'; | ||
/* URDFLoader Class */ | ||
// Loads and reads a URDF file into a THREEjs Object3D format | ||
export default | ||
class URDFLoader { | ||
// take a vector "x y z" and process it into | ||
// an array [x, y, z] | ||
function processTuple(val) { | ||
// Cached mesh loaders | ||
get STLLoader() { | ||
if (!val) return [0, 0, 0]; | ||
return val.trim().split(/\s+/g).map(num => parseFloat(num)); | ||
this._stlloader = this._stlloader || new STLLoader(this.manager); | ||
return this._stlloader; | ||
} | ||
} | ||
// applies a rotation a threejs object in URDF order | ||
function applyRotation(obj, rpy, additive = false) { | ||
get DAELoader() { | ||
// if additive is true the rotation is applied in | ||
// addition to the existing rotation | ||
if (!additive) obj.rotation.set(0, 0, 0); | ||
this._daeloader = this._daeloader || new ColladaLoader(this.manager); | ||
return this._daeloader; | ||
tempEuler.set(rpy[0], rpy[1], rpy[2], 'ZYX'); | ||
tempQuaternion.setFromEuler(tempEuler); | ||
tempQuaternion.multiply(obj.quaternion); | ||
obj.quaternion.copy(tempQuaternion); | ||
} | ||
} | ||
get TextureLoader() { | ||
/* URDFLoader Class */ | ||
// Loads and reads a URDF file into a THREEjs Object3D format | ||
export default | ||
class URDFLoader { | ||
this._textureloader = this._textureloader || new THREE.TextureLoader(this.manager); | ||
return this._textureloader; | ||
} | ||
constructor(manager) { | ||
@@ -65,47 +66,10 @@ | ||
/* Utilities */ | ||
// forEach and filter function wrappers because | ||
// HTMLCollection does not the by default | ||
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)); | ||
} | ||
// applies a rotation a threejs object in URDF order | ||
_applyRotation(obj, rpy, additive = false) { | ||
// if additive is true the rotation is applied in | ||
// addition to the existing rotation | ||
if (!additive) obj.rotation.set(0, 0, 0); | ||
tempEuler.set(rpy[0], rpy[1], rpy[2], 'ZYX'); | ||
tempQuaternion.setFromEuler(tempEuler); | ||
tempQuaternion.multiply(obj.quaternion); | ||
obj.quaternion.copy(tempQuaternion); | ||
} | ||
/* Public API */ | ||
// urdf: The path to the URDF within the package OR absolute | ||
// packages: The equivelant of a (list of) ROS package(s):// directory | ||
// onComplete: Callback that is passed the model once loaded | ||
load(urdf, packages, onComplete, options) { | ||
load(urdf, onComplete, options) { | ||
// Check if a full URI is specified before | ||
// prepending the package info | ||
const manager = this.manager; | ||
const workingPath = THREE.LoaderUtils.extractUrlBase(urdf); | ||
@@ -116,459 +80,417 @@ const urdfPath = this.manager.resolveURL(urdf); | ||
manager.itemStart(urdfPath); | ||
fetch(urdfPath, options.fetchOptions) | ||
.then(res => res.text()) | ||
.then(data => this.parse(data, packages, onComplete, options)); | ||
.then(data => { | ||
} | ||
const model = this.parse(data, options); | ||
onComplete(model); | ||
manager.itemEnd(urdfPath); | ||
parse(content, packages, onComplete, options) { | ||
}) | ||
.catch(e => { | ||
options = Object.assign({ | ||
// TODO: Add onProgress and onError functions here | ||
console.error('URDFLoader: Error parsing file.', e); | ||
manager.itemError(urdfPath); | ||
manager.itemEnd(urdfPath); | ||
loadMeshCb: this.defaultMeshLoader.bind(this), | ||
workingPath: '', | ||
}); | ||
}, options); | ||
} | ||
let result = null; | ||
let meshCount = 0; | ||
const loadMeshFunc = (path, ext, done) => { | ||
parse(content, options = {}) { | ||
meshCount++; | ||
options.loadMeshCb(path, ext, (...args) => { | ||
const packages = options.packages || ''; | ||
const loadMeshCb = options.loadMeshCb || this.defaultMeshLoader.bind(this); | ||
const workingPath = options.workingPath || ''; | ||
const manager = this.manager; | ||
const linkMap = {}; | ||
const jointMap = {}; | ||
const materialMap = {}; | ||
done(...args); | ||
meshCount--; | ||
if (meshCount === 0) { | ||
// Resolves the path of mesh files | ||
function resolvePath(path) { | ||
requestAnimationFrame(() => { | ||
if (typeof onComplete === 'function') { | ||
onComplete(result); | ||
} | ||
}); | ||
if (!/^package:\/\//.test(path)) { | ||
} | ||
return workingPath ? workingPath + path : path; | ||
}); | ||
} | ||
}; | ||
result = this._processUrdf(content, packages, options.workingPath, loadMeshFunc); | ||
// Remove "package://" keyword and split meshPath at the first slash | ||
const [targetPkg, relPath] = path.replace(/^package:\/\//, '').split(/\/(.+)/); | ||
if (meshCount === 0 && typeof onComplete === 'function') { | ||
if (typeof packages === 'string') { | ||
onComplete(result); | ||
onComplete = null; | ||
// "pkg" is one single package | ||
if (packages.endsWith(targetPkg)) { | ||
} | ||
// "pkg" is the target package | ||
return packages + '/' + relPath; | ||
return result; | ||
} else { | ||
} | ||
// Assume "pkg" is the target package's parent directory | ||
return packages + '/' + targetPkg + '/' + relPath; | ||
// Default mesh loading function | ||
defaultMeshLoader(path, ext, done) { | ||
} | ||
if (/\.stl$/i.test(path)) { | ||
} else if (typeof packages === 'object') { | ||
this.STLLoader.load(path, geom => { | ||
const mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial()); | ||
done(mesh); | ||
}); | ||
// "pkg" is a map of packages | ||
if (targetPkg in packages) { | ||
} else if (/\.dae$/i.test(path)) { | ||
return packages[targetPkg] + '/' + relPath; | ||
this.DAELoader.load(path, dae => done(dae.scene)); | ||
} else { | ||
} else { | ||
console.error(`URDFLoader : ${ targetPkg } not found in provided package list.`); | ||
return null; | ||
console.warn(`URDFLoader: Could not load model at ${ path }.\nNo loader available`); | ||
} | ||
} | ||
} | ||
} | ||
// Process the URDF text format | ||
function processUrdf(data) { | ||
/* Private Functions */ | ||
const parser = new DOMParser(); | ||
const urdf = parser.parseFromString(data, 'text/xml'); | ||
const children = [ ...urdf.children ]; | ||
// Resolves the path of mesh files | ||
_resolvePackagePath(pkg, meshPath, currPath) { | ||
const robotNode = children.filter(c => c.nodeName === 'robot').pop(); | ||
return processRobot(robotNode); | ||
if (!/^package:\/\//.test(meshPath)) { | ||
} | ||
return currPath !== undefined ? currPath + meshPath : meshPath; | ||
// Process the <robot> node | ||
function processRobot(robot) { | ||
} | ||
const robotNodes = [ ...robot.children ]; | ||
const links = robotNodes.filter(c => c.nodeName.toLowerCase() === 'link'); | ||
const joints = robotNodes.filter(c => c.nodeName.toLowerCase() === 'joint'); | ||
const materials = robotNodes.filter(c => c.nodeName.toLowerCase() === 'material'); | ||
const obj = new URDFRobot(); | ||
// Remove "package://" keyword and split meshPath at the first slash | ||
const [targetPkg, relPath] = meshPath.replace(/^package:\/\//, '').split(/\/(.+)/); | ||
obj.robotName = robot.getAttribute('name'); | ||
obj.urdfRobotNode = robot; | ||
if (typeof pkg === 'string') { | ||
// Create the <material> map | ||
materials.forEach(m => { | ||
// "pkg" is one single package | ||
if (pkg.endsWith(targetPkg)) { | ||
const name = m.getAttribute('name'); | ||
materialMap[name] = processMaterial(m); | ||
// "pkg" is the target package | ||
return pkg + '/' + relPath; | ||
}); | ||
} else { | ||
// Create the <link> map | ||
links.forEach(l => { | ||
// Assume "pkg" is the target package's parent directory | ||
return pkg + '/' + targetPkg + '/' + relPath; | ||
const name = l.getAttribute('name'); | ||
const isRoot = robot.querySelector(`child[link="${ name }"]`) === null; | ||
linkMap[name] = processLink(l, isRoot ? obj : null); | ||
} | ||
}); | ||
} else if (typeof pkg === 'object') { | ||
// Create the <joint> map | ||
joints.forEach(j => { | ||
// "pkg" is a map of packages | ||
if (targetPkg in pkg) { | ||
const name = j.getAttribute('name'); | ||
jointMap[name] = processJoint(j); | ||
return pkg[targetPkg] + '/' + relPath; | ||
}); | ||
} else { | ||
obj.joints = jointMap; | ||
obj.links = linkMap; | ||
console.error(`URDFLoader : ${ targetPkg } not found in provided package list!`); | ||
return null; | ||
return obj; | ||
} | ||
} | ||
} | ||
// Process the URDF text format | ||
_processUrdf(data, packages, path, loadMeshCb) { | ||
// Process joint nodes and parent them | ||
function processJoint(joint) { | ||
const parser = new DOMParser(); | ||
const urdf = parser.parseFromString(data, 'text/xml'); | ||
const children = [ ...joint.children ]; | ||
const jointType = joint.getAttribute('type'); | ||
const obj = new URDFJoint(); | ||
obj.urdfNode = joint; | ||
obj.name = joint.getAttribute('name'); | ||
obj.jointType = jointType; | ||
const robottag = this.filter(urdf.children, c => c.nodeName === 'robot').pop(); | ||
return this._processRobot(robottag, packages, path, loadMeshCb); | ||
let parent = null; | ||
let child = null; | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
} | ||
// Extract the attributes | ||
children.forEach(n => { | ||
// Process the <robot> node | ||
_processRobot(robot, packages, path, loadMeshCb) { | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'origin') { | ||
const materials = robot.querySelectorAll('material'); | ||
const links = []; | ||
const joints = []; | ||
const obj = new URDFRobot(); | ||
obj.name = robot.getAttribute('name'); | ||
xyz = processTuple(n.getAttribute('xyz')); | ||
rpy = processTuple(n.getAttribute('rpy')); | ||
// Process the <joint> and <link> nodes | ||
this.forEach(robot.children, n => { | ||
} else if (type === 'child') { | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'link') links.push(n); | ||
else if (type === 'joint') joints.push(n); | ||
child = linkMap[n.getAttribute('link')]; | ||
}); | ||
} else if (type === 'parent') { | ||
// Create the <material> map | ||
const materialMap = {}; | ||
this.forEach(materials, m => { | ||
parent = linkMap[n.getAttribute('link')]; | ||
const name = m.getAttribute('name'); | ||
if (!materialMap[name]) { | ||
} else if (type === 'limit') { | ||
materialMap[name] = {}; | ||
this.forEach(m.children, c => { | ||
obj.limit.lower = parseFloat(n.getAttribute('lower') || obj.limit.lower); | ||
obj.limit.upper = parseFloat(n.getAttribute('upper') || obj.limit.upper); | ||
this._processMaterial( | ||
materialMap[name], | ||
c, | ||
packages, | ||
path | ||
); | ||
} | ||
}); | ||
}); | ||
} | ||
// Join the links | ||
parent.add(obj); | ||
obj.add(child); | ||
applyRotation(obj, rpy); | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
}); | ||
// Set up the rotate function | ||
const axisNode = children.filter(n => n.nodeName.toLowerCase() === 'axis')[0]; | ||
// Create the <link> map | ||
const linkMap = {}; | ||
this.forEach(links, l => { | ||
if (axisNode) { | ||
const name = l.getAttribute('name'); | ||
linkMap[name] = this._processLink(l, materialMap, packages, path, loadMeshCb); | ||
const axisXYZ = axisNode.getAttribute('xyz').split(/\s+/g).map(num => parseFloat(num)); | ||
obj.axis = new THREE.Vector3(axisXYZ[0], axisXYZ[1], axisXYZ[2]); | ||
obj.axis.normalize(); | ||
}); | ||
} | ||
// Create the <joint> map | ||
const jointMap = {}; | ||
this.forEach(joints, j => { | ||
return obj; | ||
const name = j.getAttribute('name'); | ||
jointMap[name] = this._processJoint(j, linkMap); | ||
} | ||
}); | ||
// Process the <link> nodes | ||
function processLink(link, target = null) { | ||
for (const key in linkMap) { | ||
if (target === null) { | ||
if (linkMap[key].parent == null) { | ||
target = new URDFLink(); | ||
obj.add(linkMap[key]); | ||
} | ||
} | ||
const children = [ ...link.children ]; | ||
const visualNodes = children.filter(n => n.nodeName.toLowerCase() === 'visual'); | ||
target.name = link.getAttribute('name'); | ||
target.urdfNode = link; | ||
obj.joints = jointMap; | ||
obj.links = linkMap; | ||
visualNodes.forEach(vn => processVisualNode(vn, target, materialMap)); | ||
return obj; | ||
return target; | ||
} | ||
} | ||
// Process joint nodes and parent them | ||
_processJoint(joint, linkMap) { | ||
function processMaterial(node) { | ||
const jointType = joint.getAttribute('type'); | ||
const obj = new URDFJoint(); | ||
obj.urdfNode = joint; | ||
obj.name = joint.getAttribute('name'); | ||
obj.jointType = jointType; | ||
const matNodes = [ ...node.children ]; | ||
const material = new THREE.MeshPhongMaterial(); | ||
let parent = null; | ||
let child = null; | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
material.name = node.getAttribute('name') || ''; | ||
matNodes.forEach(n => { | ||
// Extract the attributes | ||
this.forEach(joint.children, n => { | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'color') { | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'origin') { | ||
const rgba = | ||
n | ||
.getAttribute('rgba') | ||
.split(/\s/g) | ||
.map(v => parseFloat(v)); | ||
xyz = this._processTuple(n.getAttribute('xyz')); | ||
rpy = this._processTuple(n.getAttribute('rpy')); | ||
material.color.setRGB(rgba[0], rgba[1], rgba[2]); | ||
material.opacity = rgba[3]; | ||
material.transparent = rgba[3] < 1; | ||
} else if (type === 'child') { | ||
} else if (type === 'texture') { | ||
child = linkMap[n.getAttribute('link')]; | ||
const loader = new THREE.TextureLoader(manager); | ||
const filename = n.getAttribute('filename'); | ||
const filePath = resolvePath(filename); | ||
material.map = loader.load(filePath); | ||
} else if (type === 'parent') { | ||
} | ||
}); | ||
parent = linkMap[n.getAttribute('link')]; | ||
return material; | ||
} else if (type === 'limit') { | ||
obj.limit.lower = parseFloat(n.getAttribute('lower') || obj.limit.lower); | ||
obj.limit.upper = parseFloat(n.getAttribute('upper') || obj.limit.upper); | ||
} | ||
}); | ||
// Join the links | ||
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 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.axis = new THREE.Vector3(axisxyz[0], axisxyz[1], axisxyz[2]); | ||
obj.axis.normalize(); | ||
} | ||
return obj; | ||
// Process the visual nodes into meshes | ||
function processVisualNode(vn, linkObj, materialMap) { | ||
} | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
let scale = [1, 1, 1]; | ||
// Process the <link> nodes | ||
_processLink(link, materialMap, packages, path, loadMeshCb) { | ||
const children = [ ...vn.children ]; | ||
let material = null; | ||
let primitiveModel = null; | ||
const visualNodes = this.filter(link.children, n => n.nodeName.toLowerCase() === 'visual'); | ||
const obj = new URDFLink(); | ||
obj.name = link.getAttribute('name'); | ||
obj.urdfNode = link; | ||
// get the material first | ||
const materialNode = children.filter(n => n.nodeName.toLowerCase() === 'material')[0]; | ||
if (materialNode) { | ||
this.forEach(visualNodes, vn => this._processVisualNode(vn, obj, materialMap, packages, path, loadMeshCb)); | ||
const name = materialNode.getAttribute('name'); | ||
if (name && name in materialMap) { | ||
return obj; | ||
material = materialMap[name]; | ||
} | ||
} else { | ||
_processMaterial(material, node, packages, path) { | ||
material = processMaterial(materialNode); | ||
const type = node.nodeName.toLowerCase(); | ||
if (type === 'color') { | ||
} | ||
const rgba = | ||
node | ||
.getAttribute('rgba') | ||
.split(/\s/g) | ||
.map(v => parseFloat(v)); | ||
} else { | ||
this._copyMaterialAttributes( | ||
material, | ||
{ | ||
color: new THREE.Color(rgba[0], rgba[1], rgba[2]), | ||
opacity: rgba[3], | ||
transparent: rgba[3] < 1, | ||
}); | ||
material = new THREE.MeshPhongMaterial(); | ||
} else if (type === 'texture') { | ||
} | ||
const filename = node.getAttribute('filename'); | ||
const filePath = this._resolvePackagePath(packages, filename, path); | ||
this._copyMaterialAttributes( | ||
material, | ||
{ | ||
map: this.TextureLoader.load(filePath), | ||
}); | ||
children.forEach(n => { | ||
} | ||
} | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'geometry') { | ||
_copyMaterialAttributes(material, materialAttributes) { | ||
const geoType = n.children[0].nodeName.toLowerCase(); | ||
if (geoType === 'mesh') { | ||
if ('color' in materialAttributes) { | ||
const filename = n.children[0].getAttribute('filename'); | ||
const filePath = resolvePath(filename); | ||
material.color = materialAttributes.color.clone(); | ||
material.opacity = materialAttributes.opacity; | ||
material.transparent = materialAttributes.transparent; | ||
// file path is null if a package directory is not provided. | ||
if (filePath !== null) { | ||
} | ||
const scaleAttr = n.children[0].getAttribute('scale'); | ||
if (scaleAttr) scale = processTuple(scaleAttr); | ||
if ('map' in materialAttributes) { | ||
loadMeshCb(filePath, manager, (obj, err) => { | ||
material.map = materialAttributes.map.clone(); | ||
if (err) { | ||
} | ||
console.error('URDFLoader: Error loading mesh.', err); | ||
} | ||
} else if (obj) { | ||
// Process the visual nodes into meshes | ||
_processVisualNode(vn, linkObj, materialMap, packages, path, loadMeshCb) { | ||
if (obj instanceof THREE.Mesh) { | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
let scale = [1, 1, 1]; | ||
obj.material = material; | ||
const material = new THREE.MeshPhongMaterial(); | ||
let primitiveModel = null; | ||
this.forEach(vn.children, n => { | ||
} | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'geometry') { | ||
linkObj.add(obj); | ||
const geoType = n.children[0].nodeName.toLowerCase(); | ||
if (geoType === 'mesh') { | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
obj.rotation.set(0, 0, 0); | ||
const filename = n.children[0].getAttribute('filename'); | ||
const filePath = this._resolvePackagePath(packages, filename, path); | ||
// multiply the existing scale by the scale components because | ||
// the loaded model could have important scale values already applied | ||
// to the root. Collada files, for example, can load in with a scale | ||
// to convert the model units to meters. | ||
obj.scale.x *= scale[0]; | ||
obj.scale.y *= scale[1]; | ||
obj.scale.z *= scale[2]; | ||
// file path is null if a package directory is not provided. | ||
if (filePath !== null) { | ||
applyRotation(obj, rpy); | ||
const ext = filePath.match(/.*\.([A-Z0-9]+)$/i).pop() || ''; | ||
const scaleAttr = n.children[0].getAttribute('scale'); | ||
if (scaleAttr) scale = this._processTuple(scaleAttr); | ||
loadMeshCb(filePath, ext, obj => { | ||
if (obj) { | ||
if (obj instanceof THREE.Mesh) { | ||
obj.material.copy(material); | ||
} | ||
linkObj.add(obj); | ||
}); | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
obj.rotation.set(0, 0, 0); | ||
} | ||
// multiply the existing scale by the scale components because | ||
// the loaded model could have important scale values already applied | ||
// to the root. Collada files, for example, can load in with a scale | ||
// to convert the model units to meters. | ||
obj.scale.x *= scale[0]; | ||
obj.scale.y *= scale[1]; | ||
obj.scale.z *= scale[2]; | ||
} else if (geoType === 'box') { | ||
this._applyRotation(obj, rpy); | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.BoxBufferGeometry(1, 1, 1); | ||
primitiveModel.material = material; | ||
} | ||
const size = processTuple(n.children[0].getAttribute('size')); | ||
}); | ||
linkObj.add(primitiveModel); | ||
primitiveModel.scale.set(size[0], size[1], size[2]); | ||
} | ||
} else if (geoType === 'sphere') { | ||
} else if (geoType === 'box') { | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.SphereBufferGeometry(1, 30, 30); | ||
primitiveModel.material = material; | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.BoxBufferGeometry(1, 1, 1); | ||
primitiveModel.material = material; | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
primitiveModel.scale.set(radius, radius, radius); | ||
const size = this._processTuple(n.children[0].getAttribute('size')); | ||
linkObj.add(primitiveModel); | ||
linkObj.add(primitiveModel); | ||
primitiveModel.scale.set(size[0], size[1], size[2]); | ||
} else if (geoType === 'cylinder') { | ||
} else if (geoType === 'sphere') { | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.CylinderBufferGeometry(1, 1, 1, 30); | ||
primitiveModel.material = material; | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.SphereBufferGeometry(1, 30, 30); | ||
primitiveModel.material = material; | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
const length = parseFloat(n.children[0].getAttribute('length')) || 0; | ||
primitiveModel.scale.set(radius, length, radius); | ||
primitiveModel.rotation.set(Math.PI / 2, 0, 0); | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
primitiveModel.scale.set(radius, radius, radius); | ||
linkObj.add(primitiveModel); | ||
linkObj.add(primitiveModel); | ||
} | ||
} else if (geoType === 'cylinder') { | ||
} else if (type === 'origin') { | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.CylinderBufferGeometry(1, 1, 1, 30); | ||
primitiveModel.material = material; | ||
xyz = processTuple(n.getAttribute('xyz')); | ||
rpy = processTuple(n.getAttribute('rpy')); | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
const length = parseFloat(n.children[0].getAttribute('length')) || 0; | ||
primitiveModel.scale.set(radius, length, radius); | ||
primitiveModel.rotation.set(Math.PI / 2, 0, 0); | ||
} | ||
linkObj.add(primitiveModel); | ||
}); | ||
} | ||
// apply the position and rotation to the primitive geometry after | ||
// the fact because it's guaranteed to have been scraped from the child | ||
// nodes by this point | ||
if (primitiveModel) { | ||
} else if (type === 'origin') { | ||
applyRotation(primitiveModel, rpy, true); | ||
primitiveModel.position.set(xyz[0], xyz[1], xyz[2]); | ||
xyz = this._processTuple(n.getAttribute('xyz')); | ||
rpy = this._processTuple(n.getAttribute('rpy')); | ||
} | ||
} else if (type === 'material') { | ||
} | ||
const materialName = n.getAttribute('name'); | ||
if (materialName) { | ||
return processUrdf(content); | ||
this._copyMaterialAttributes(material, materialMap[materialName]); | ||
} | ||
} else { | ||
// Default mesh loading function | ||
defaultMeshLoader(path, manager, done) { | ||
this.forEach(n.children, c => { | ||
if (/\.stl$/i.test(path)) { | ||
this._processMaterial(material, c, packages, path); | ||
const loader = new STLLoader(manager); | ||
loader.load(path, geom => { | ||
const mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial()); | ||
done(mesh); | ||
}); | ||
}); | ||
} else if (/\.dae$/i.test(path)) { | ||
} | ||
const loader = new ColladaLoader(manager); | ||
loader.load(path, dae => done(dae.scene)); | ||
} | ||
}); | ||
} else { | ||
// apply the position and rotation to the primitive geometry after | ||
// the fact because it's guaranteed to have been scraped from the child | ||
// nodes by this point | ||
if (primitiveModel) { | ||
console.warn(`URDFLoader: Could not load model at ${ path }.\nNo loader available`); | ||
this._applyRotation(primitiveModel, rpy, true); | ||
primitiveModel.position.set(xyz[0], xyz[1], xyz[2]); | ||
} | ||
@@ -575,0 +497,0 @@ |
@@ -50,6 +50,2 @@ (function (global, factory) { | ||
get loadingManager() { return this._loadingManager = this._loadingManager || new THREE.LoadingManager(); } | ||
get urdfLoader() { return this._urdfLoader = this._urdfLoader || new URDFLoader(this.loadingManager); } | ||
get angles() { | ||
@@ -78,2 +74,4 @@ | ||
this.robot = null; | ||
this.loadMeshFunc = null; | ||
this.urlModifierFunc = null; | ||
@@ -95,2 +93,3 @@ // Scene setup | ||
dirLight.castShadow = true; | ||
dirLight.shadow.bias = -0.00001; | ||
scene.add(dirLight); | ||
@@ -148,5 +147,2 @@ scene.add(dirLight.target); | ||
// redraw when something new has loaded | ||
this.loadingManager.onLoad = () => this.recenter(); | ||
const _renderLoop = () => { | ||
@@ -294,17 +290,16 @@ | ||
// Set the joint with jointname to | ||
// Set the joint with jointName to | ||
// angle in degrees | ||
setAngle(jointname, angle) { | ||
setAngle(jointName, angle) { | ||
if (!this.robot) return; | ||
if (!this.robot.joints[jointName]) return; | ||
const joint = this.robot.joints[jointname]; | ||
if (joint && joint.angle !== angle) { | ||
joint.setAngle(angle); | ||
const origAngle = this.robot.joints[jointName].angle; | ||
const newAngle = this.robot.setAngle(jointName, angle); | ||
if (origAngle !== newAngle) { | ||
this.redraw(); | ||
} | ||
this.dispatchEvent(new CustomEvent('angle-change', { bubles: true, cancelable: true, detail: jointname })); | ||
this.dispatchEvent(new CustomEvent('angle-change', { bubbles: true, cancelable: true, detail: jointName })); | ||
@@ -407,3 +402,3 @@ } | ||
if (c.type === 'Mesh') { | ||
if (c.isMesh) { | ||
@@ -465,45 +460,43 @@ c.castShadow = true; | ||
this.urdfLoader.load( | ||
urdf, | ||
pkg, | ||
let robot = null; | ||
const manager = new THREE.LoadingManager(); | ||
manager.onLoad = () => { | ||
robot => { | ||
// If another request has come in to load a new | ||
// robot, then ignore this one | ||
if (this._requestId !== requestId) { | ||
// If another request has come in to load a new | ||
// robot, then ignore this one | ||
if (this._requestId !== requestId) { | ||
robot.traverse(c => c.dispose && c.dispose()); | ||
return; | ||
robot.traverse(c => c.dispose && c.dispose()); | ||
return; | ||
} | ||
} | ||
this.robot = robot; | ||
this.world.add(robot); | ||
updateMaterials(robot); | ||
this.robot = robot; | ||
this.world.add(robot); | ||
updateMaterials(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('geometry-loaded', { bubbles: true, cancelable: true, composed: true })); | ||
this.dispatchEvent(new CustomEvent('urdf-processed', { bubbles: true, cancelable: true, composed: true })); | ||
this.dispatchEvent(new CustomEvent('geometry-loaded', { bubbles: true, cancelable: true, composed: true })); | ||
this.recenter(); | ||
this.recenter(); | ||
}; | ||
}, | ||
if (this.urlModifierFunc) { | ||
// options | ||
{ | ||
loadMeshCb: (path, ext, done) => { | ||
manager.setURLModifier(this.urlModifierFunc); | ||
// Load meshes and enable shadow casting | ||
this.urdfLoader.defaultMeshLoader(path, ext, mesh => { | ||
} | ||
updateMaterials(mesh); | ||
done(mesh); | ||
this.recenter(); | ||
new URDFLoader(manager).load( | ||
urdf, | ||
model => robot = model, | ||
}); | ||
// options | ||
{ | ||
}, | ||
packages: pkg, | ||
loadMeshCb: this.loadMeshFunc, | ||
fetchOptions: { mode: 'cors', credentials: 'same-origin' }, | ||
@@ -510,0 +503,0 @@ |
@@ -7,45 +7,2 @@ (function (global, factory) { | ||
class URDFRobot extends THREE.Object3D { | ||
constructor(...args) { | ||
super(...args); | ||
this.isURDFRobot = true; | ||
this.type = 'URDFRobot'; | ||
this.urdfNode = null; | ||
this.links = null; | ||
this.joints = null; | ||
} | ||
copy(source, recursive) { | ||
super.copy(source, recursive); | ||
this.links = {}; | ||
this.joints = {}; | ||
this.traverse(c => { | ||
if (c.isURDFJoint && c.name in source.joints) { | ||
this.joints[c.name] = c; | ||
} | ||
if (c.isURDFLink && c.name in source.links) { | ||
this.links[c.name] = c; | ||
} | ||
}); | ||
return this; | ||
} | ||
} | ||
class URDFLink extends THREE.Object3D { | ||
@@ -228,2 +185,69 @@ | ||
class URDFRobot extends URDFLink { | ||
constructor(...args) { | ||
super(...args); | ||
this.isURDFRobot = true; | ||
this.urdfNode = null; | ||
this.urdfRobotNode = null; | ||
this.robotName = null; | ||
this.links = null; | ||
this.joints = null; | ||
} | ||
copy(source, recursive) { | ||
super.copy(source, recursive); | ||
this.urdfRobotNode = source.urdfRobotNode; | ||
this.robotName = source.robotName; | ||
this.links = {}; | ||
this.joints = {}; | ||
this.traverse(c => { | ||
if (c.isURDFJoint && c.name in source.joints) { | ||
this.joints[c.name] = c; | ||
} | ||
if (c.isURDFLink && c.name in source.links) { | ||
this.links[c.name] = c; | ||
} | ||
}); | ||
return this; | ||
} | ||
setAngle(jointName, ...angle) { | ||
const joint = this.joints[jointName]; | ||
if (joint) { | ||
return joint.setAngle(...angle); | ||
} | ||
return null; | ||
} | ||
setAngles(angles) { | ||
// TODO: How to handle other, multi-dimensional joint types? | ||
for (const name in angles) this.setAngle(name, angles[name]); | ||
} | ||
} | ||
/* | ||
@@ -254,28 +278,29 @@ Reference coordinate frames for THREE.js and ROS. | ||
/* URDFLoader Class */ | ||
// Loads and reads a URDF file into a THREEjs Object3D format | ||
class URDFLoader { | ||
// take a vector "x y z" and process it into | ||
// an array [x, y, z] | ||
function processTuple(val) { | ||
// Cached mesh loaders | ||
get STLLoader() { | ||
if (!val) return [0, 0, 0]; | ||
return val.trim().split(/\s+/g).map(num => parseFloat(num)); | ||
this._stlloader = this._stlloader || new STLLoader.STLLoader(this.manager); | ||
return this._stlloader; | ||
} | ||
} | ||
// applies a rotation a threejs object in URDF order | ||
function applyRotation(obj, rpy, additive = false) { | ||
get DAELoader() { | ||
// if additive is true the rotation is applied in | ||
// addition to the existing rotation | ||
if (!additive) obj.rotation.set(0, 0, 0); | ||
this._daeloader = this._daeloader || new ColladaLoader.ColladaLoader(this.manager); | ||
return this._daeloader; | ||
tempEuler.set(rpy[0], rpy[1], rpy[2], 'ZYX'); | ||
tempQuaternion.setFromEuler(tempEuler); | ||
tempQuaternion.multiply(obj.quaternion); | ||
obj.quaternion.copy(tempQuaternion); | ||
} | ||
} | ||
get TextureLoader() { | ||
/* URDFLoader Class */ | ||
// Loads and reads a URDF file into a THREEjs Object3D format | ||
class URDFLoader { | ||
this._textureloader = this._textureloader || new THREE.TextureLoader(this.manager); | ||
return this._textureloader; | ||
} | ||
constructor(manager) { | ||
@@ -287,47 +312,10 @@ | ||
/* Utilities */ | ||
// forEach and filter function wrappers because | ||
// HTMLCollection does not the by default | ||
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)); | ||
} | ||
// applies a rotation a threejs object in URDF order | ||
_applyRotation(obj, rpy, additive = false) { | ||
// if additive is true the rotation is applied in | ||
// addition to the existing rotation | ||
if (!additive) obj.rotation.set(0, 0, 0); | ||
tempEuler.set(rpy[0], rpy[1], rpy[2], 'ZYX'); | ||
tempQuaternion.setFromEuler(tempEuler); | ||
tempQuaternion.multiply(obj.quaternion); | ||
obj.quaternion.copy(tempQuaternion); | ||
} | ||
/* Public API */ | ||
// urdf: The path to the URDF within the package OR absolute | ||
// packages: The equivelant of a (list of) ROS package(s):// directory | ||
// onComplete: Callback that is passed the model once loaded | ||
load(urdf, packages, onComplete, options) { | ||
load(urdf, onComplete, options) { | ||
// Check if a full URI is specified before | ||
// prepending the package info | ||
const manager = this.manager; | ||
const workingPath = THREE.LoaderUtils.extractUrlBase(urdf); | ||
@@ -338,459 +326,417 @@ const urdfPath = this.manager.resolveURL(urdf); | ||
manager.itemStart(urdfPath); | ||
fetch(urdfPath, options.fetchOptions) | ||
.then(res => res.text()) | ||
.then(data => this.parse(data, packages, onComplete, options)); | ||
.then(data => { | ||
} | ||
const model = this.parse(data, options); | ||
onComplete(model); | ||
manager.itemEnd(urdfPath); | ||
parse(content, packages, onComplete, options) { | ||
}) | ||
.catch(e => { | ||
options = Object.assign({ | ||
// TODO: Add onProgress and onError functions here | ||
console.error('URDFLoader: Error parsing file.', e); | ||
manager.itemError(urdfPath); | ||
manager.itemEnd(urdfPath); | ||
loadMeshCb: this.defaultMeshLoader.bind(this), | ||
workingPath: '', | ||
}); | ||
}, options); | ||
} | ||
let result = null; | ||
let meshCount = 0; | ||
const loadMeshFunc = (path, ext, done) => { | ||
parse(content, options = {}) { | ||
meshCount++; | ||
options.loadMeshCb(path, ext, (...args) => { | ||
const packages = options.packages || ''; | ||
const loadMeshCb = options.loadMeshCb || this.defaultMeshLoader.bind(this); | ||
const workingPath = options.workingPath || ''; | ||
const manager = this.manager; | ||
const linkMap = {}; | ||
const jointMap = {}; | ||
const materialMap = {}; | ||
done(...args); | ||
meshCount--; | ||
if (meshCount === 0) { | ||
// Resolves the path of mesh files | ||
function resolvePath(path) { | ||
requestAnimationFrame(() => { | ||
if (typeof onComplete === 'function') { | ||
onComplete(result); | ||
} | ||
}); | ||
if (!/^package:\/\//.test(path)) { | ||
} | ||
return workingPath ? workingPath + path : path; | ||
}); | ||
} | ||
}; | ||
result = this._processUrdf(content, packages, options.workingPath, loadMeshFunc); | ||
// Remove "package://" keyword and split meshPath at the first slash | ||
const [targetPkg, relPath] = path.replace(/^package:\/\//, '').split(/\/(.+)/); | ||
if (meshCount === 0 && typeof onComplete === 'function') { | ||
if (typeof packages === 'string') { | ||
onComplete(result); | ||
onComplete = null; | ||
// "pkg" is one single package | ||
if (packages.endsWith(targetPkg)) { | ||
} | ||
// "pkg" is the target package | ||
return packages + '/' + relPath; | ||
return result; | ||
} else { | ||
} | ||
// Assume "pkg" is the target package's parent directory | ||
return packages + '/' + targetPkg + '/' + relPath; | ||
// Default mesh loading function | ||
defaultMeshLoader(path, ext, done) { | ||
} | ||
if (/\.stl$/i.test(path)) { | ||
} else if (typeof packages === 'object') { | ||
this.STLLoader.load(path, geom => { | ||
const mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial()); | ||
done(mesh); | ||
}); | ||
// "pkg" is a map of packages | ||
if (targetPkg in packages) { | ||
} else if (/\.dae$/i.test(path)) { | ||
return packages[targetPkg] + '/' + relPath; | ||
this.DAELoader.load(path, dae => done(dae.scene)); | ||
} else { | ||
} else { | ||
console.error(`URDFLoader : ${ targetPkg } not found in provided package list.`); | ||
return null; | ||
console.warn(`URDFLoader: Could not load model at ${ path }.\nNo loader available`); | ||
} | ||
} | ||
} | ||
} | ||
// Process the URDF text format | ||
function processUrdf(data) { | ||
/* Private Functions */ | ||
const parser = new DOMParser(); | ||
const urdf = parser.parseFromString(data, 'text/xml'); | ||
const children = [ ...urdf.children ]; | ||
// Resolves the path of mesh files | ||
_resolvePackagePath(pkg, meshPath, currPath) { | ||
const robotNode = children.filter(c => c.nodeName === 'robot').pop(); | ||
return processRobot(robotNode); | ||
if (!/^package:\/\//.test(meshPath)) { | ||
} | ||
return currPath !== undefined ? currPath + meshPath : meshPath; | ||
// Process the <robot> node | ||
function processRobot(robot) { | ||
} | ||
const robotNodes = [ ...robot.children ]; | ||
const links = robotNodes.filter(c => c.nodeName.toLowerCase() === 'link'); | ||
const joints = robotNodes.filter(c => c.nodeName.toLowerCase() === 'joint'); | ||
const materials = robotNodes.filter(c => c.nodeName.toLowerCase() === 'material'); | ||
const obj = new URDFRobot(); | ||
// Remove "package://" keyword and split meshPath at the first slash | ||
const [targetPkg, relPath] = meshPath.replace(/^package:\/\//, '').split(/\/(.+)/); | ||
obj.robotName = robot.getAttribute('name'); | ||
obj.urdfRobotNode = robot; | ||
if (typeof pkg === 'string') { | ||
// Create the <material> map | ||
materials.forEach(m => { | ||
// "pkg" is one single package | ||
if (pkg.endsWith(targetPkg)) { | ||
const name = m.getAttribute('name'); | ||
materialMap[name] = processMaterial(m); | ||
// "pkg" is the target package | ||
return pkg + '/' + relPath; | ||
}); | ||
} else { | ||
// Create the <link> map | ||
links.forEach(l => { | ||
// Assume "pkg" is the target package's parent directory | ||
return pkg + '/' + targetPkg + '/' + relPath; | ||
const name = l.getAttribute('name'); | ||
const isRoot = robot.querySelector(`child[link="${ name }"]`) === null; | ||
linkMap[name] = processLink(l, isRoot ? obj : null); | ||
} | ||
}); | ||
} else if (typeof pkg === 'object') { | ||
// Create the <joint> map | ||
joints.forEach(j => { | ||
// "pkg" is a map of packages | ||
if (targetPkg in pkg) { | ||
const name = j.getAttribute('name'); | ||
jointMap[name] = processJoint(j); | ||
return pkg[targetPkg] + '/' + relPath; | ||
}); | ||
} else { | ||
obj.joints = jointMap; | ||
obj.links = linkMap; | ||
console.error(`URDFLoader : ${ targetPkg } not found in provided package list!`); | ||
return null; | ||
return obj; | ||
} | ||
} | ||
} | ||
// Process the URDF text format | ||
_processUrdf(data, packages, path, loadMeshCb) { | ||
// Process joint nodes and parent them | ||
function processJoint(joint) { | ||
const parser = new DOMParser(); | ||
const urdf = parser.parseFromString(data, 'text/xml'); | ||
const children = [ ...joint.children ]; | ||
const jointType = joint.getAttribute('type'); | ||
const obj = new URDFJoint(); | ||
obj.urdfNode = joint; | ||
obj.name = joint.getAttribute('name'); | ||
obj.jointType = jointType; | ||
const robottag = this.filter(urdf.children, c => c.nodeName === 'robot').pop(); | ||
return this._processRobot(robottag, packages, path, loadMeshCb); | ||
let parent = null; | ||
let child = null; | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
} | ||
// Extract the attributes | ||
children.forEach(n => { | ||
// Process the <robot> node | ||
_processRobot(robot, packages, path, loadMeshCb) { | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'origin') { | ||
const materials = robot.querySelectorAll('material'); | ||
const links = []; | ||
const joints = []; | ||
const obj = new URDFRobot(); | ||
obj.name = robot.getAttribute('name'); | ||
xyz = processTuple(n.getAttribute('xyz')); | ||
rpy = processTuple(n.getAttribute('rpy')); | ||
// Process the <joint> and <link> nodes | ||
this.forEach(robot.children, n => { | ||
} else if (type === 'child') { | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'link') links.push(n); | ||
else if (type === 'joint') joints.push(n); | ||
child = linkMap[n.getAttribute('link')]; | ||
}); | ||
} else if (type === 'parent') { | ||
// Create the <material> map | ||
const materialMap = {}; | ||
this.forEach(materials, m => { | ||
parent = linkMap[n.getAttribute('link')]; | ||
const name = m.getAttribute('name'); | ||
if (!materialMap[name]) { | ||
} else if (type === 'limit') { | ||
materialMap[name] = {}; | ||
this.forEach(m.children, c => { | ||
obj.limit.lower = parseFloat(n.getAttribute('lower') || obj.limit.lower); | ||
obj.limit.upper = parseFloat(n.getAttribute('upper') || obj.limit.upper); | ||
this._processMaterial( | ||
materialMap[name], | ||
c, | ||
packages, | ||
path | ||
); | ||
} | ||
}); | ||
}); | ||
} | ||
// Join the links | ||
parent.add(obj); | ||
obj.add(child); | ||
applyRotation(obj, rpy); | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
}); | ||
// Set up the rotate function | ||
const axisNode = children.filter(n => n.nodeName.toLowerCase() === 'axis')[0]; | ||
// Create the <link> map | ||
const linkMap = {}; | ||
this.forEach(links, l => { | ||
if (axisNode) { | ||
const name = l.getAttribute('name'); | ||
linkMap[name] = this._processLink(l, materialMap, packages, path, loadMeshCb); | ||
const axisXYZ = axisNode.getAttribute('xyz').split(/\s+/g).map(num => parseFloat(num)); | ||
obj.axis = new THREE.Vector3(axisXYZ[0], axisXYZ[1], axisXYZ[2]); | ||
obj.axis.normalize(); | ||
}); | ||
} | ||
// Create the <joint> map | ||
const jointMap = {}; | ||
this.forEach(joints, j => { | ||
return obj; | ||
const name = j.getAttribute('name'); | ||
jointMap[name] = this._processJoint(j, linkMap); | ||
} | ||
}); | ||
// Process the <link> nodes | ||
function processLink(link, target = null) { | ||
for (const key in linkMap) { | ||
if (target === null) { | ||
if (linkMap[key].parent == null) { | ||
target = new URDFLink(); | ||
obj.add(linkMap[key]); | ||
} | ||
} | ||
const children = [ ...link.children ]; | ||
const visualNodes = children.filter(n => n.nodeName.toLowerCase() === 'visual'); | ||
target.name = link.getAttribute('name'); | ||
target.urdfNode = link; | ||
obj.joints = jointMap; | ||
obj.links = linkMap; | ||
visualNodes.forEach(vn => processVisualNode(vn, target, materialMap)); | ||
return obj; | ||
return target; | ||
} | ||
} | ||
// Process joint nodes and parent them | ||
_processJoint(joint, linkMap) { | ||
function processMaterial(node) { | ||
const jointType = joint.getAttribute('type'); | ||
const obj = new URDFJoint(); | ||
obj.urdfNode = joint; | ||
obj.name = joint.getAttribute('name'); | ||
obj.jointType = jointType; | ||
const matNodes = [ ...node.children ]; | ||
const material = new THREE.MeshPhongMaterial(); | ||
let parent = null; | ||
let child = null; | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
material.name = node.getAttribute('name') || ''; | ||
matNodes.forEach(n => { | ||
// Extract the attributes | ||
this.forEach(joint.children, n => { | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'color') { | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'origin') { | ||
const rgba = | ||
n | ||
.getAttribute('rgba') | ||
.split(/\s/g) | ||
.map(v => parseFloat(v)); | ||
xyz = this._processTuple(n.getAttribute('xyz')); | ||
rpy = this._processTuple(n.getAttribute('rpy')); | ||
material.color.setRGB(rgba[0], rgba[1], rgba[2]); | ||
material.opacity = rgba[3]; | ||
material.transparent = rgba[3] < 1; | ||
} else if (type === 'child') { | ||
} else if (type === 'texture') { | ||
child = linkMap[n.getAttribute('link')]; | ||
const loader = new THREE.TextureLoader(manager); | ||
const filename = n.getAttribute('filename'); | ||
const filePath = resolvePath(filename); | ||
material.map = loader.load(filePath); | ||
} else if (type === 'parent') { | ||
} | ||
}); | ||
parent = linkMap[n.getAttribute('link')]; | ||
return material; | ||
} else if (type === 'limit') { | ||
obj.limit.lower = parseFloat(n.getAttribute('lower') || obj.limit.lower); | ||
obj.limit.upper = parseFloat(n.getAttribute('upper') || obj.limit.upper); | ||
} | ||
}); | ||
// Join the links | ||
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 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.axis = new THREE.Vector3(axisxyz[0], axisxyz[1], axisxyz[2]); | ||
obj.axis.normalize(); | ||
} | ||
return obj; | ||
// Process the visual nodes into meshes | ||
function processVisualNode(vn, linkObj, materialMap) { | ||
} | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
let scale = [1, 1, 1]; | ||
// Process the <link> nodes | ||
_processLink(link, materialMap, packages, path, loadMeshCb) { | ||
const children = [ ...vn.children ]; | ||
let material = null; | ||
let primitiveModel = null; | ||
const visualNodes = this.filter(link.children, n => n.nodeName.toLowerCase() === 'visual'); | ||
const obj = new URDFLink(); | ||
obj.name = link.getAttribute('name'); | ||
obj.urdfNode = link; | ||
// get the material first | ||
const materialNode = children.filter(n => n.nodeName.toLowerCase() === 'material')[0]; | ||
if (materialNode) { | ||
this.forEach(visualNodes, vn => this._processVisualNode(vn, obj, materialMap, packages, path, loadMeshCb)); | ||
const name = materialNode.getAttribute('name'); | ||
if (name && name in materialMap) { | ||
return obj; | ||
material = materialMap[name]; | ||
} | ||
} else { | ||
_processMaterial(material, node, packages, path) { | ||
material = processMaterial(materialNode); | ||
const type = node.nodeName.toLowerCase(); | ||
if (type === 'color') { | ||
} | ||
const rgba = | ||
node | ||
.getAttribute('rgba') | ||
.split(/\s/g) | ||
.map(v => parseFloat(v)); | ||
} else { | ||
this._copyMaterialAttributes( | ||
material, | ||
{ | ||
color: new THREE.Color(rgba[0], rgba[1], rgba[2]), | ||
opacity: rgba[3], | ||
transparent: rgba[3] < 1, | ||
}); | ||
material = new THREE.MeshPhongMaterial(); | ||
} else if (type === 'texture') { | ||
} | ||
const filename = node.getAttribute('filename'); | ||
const filePath = this._resolvePackagePath(packages, filename, path); | ||
this._copyMaterialAttributes( | ||
material, | ||
{ | ||
map: this.TextureLoader.load(filePath), | ||
}); | ||
children.forEach(n => { | ||
} | ||
} | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'geometry') { | ||
_copyMaterialAttributes(material, materialAttributes) { | ||
const geoType = n.children[0].nodeName.toLowerCase(); | ||
if (geoType === 'mesh') { | ||
if ('color' in materialAttributes) { | ||
const filename = n.children[0].getAttribute('filename'); | ||
const filePath = resolvePath(filename); | ||
material.color = materialAttributes.color.clone(); | ||
material.opacity = materialAttributes.opacity; | ||
material.transparent = materialAttributes.transparent; | ||
// file path is null if a package directory is not provided. | ||
if (filePath !== null) { | ||
} | ||
const scaleAttr = n.children[0].getAttribute('scale'); | ||
if (scaleAttr) scale = processTuple(scaleAttr); | ||
if ('map' in materialAttributes) { | ||
loadMeshCb(filePath, manager, (obj, err) => { | ||
material.map = materialAttributes.map.clone(); | ||
if (err) { | ||
} | ||
console.error('URDFLoader: Error loading mesh.', err); | ||
} | ||
} else if (obj) { | ||
// Process the visual nodes into meshes | ||
_processVisualNode(vn, linkObj, materialMap, packages, path, loadMeshCb) { | ||
if (obj instanceof THREE.Mesh) { | ||
let xyz = [0, 0, 0]; | ||
let rpy = [0, 0, 0]; | ||
let scale = [1, 1, 1]; | ||
obj.material = material; | ||
const material = new THREE.MeshPhongMaterial(); | ||
let primitiveModel = null; | ||
this.forEach(vn.children, n => { | ||
} | ||
const type = n.nodeName.toLowerCase(); | ||
if (type === 'geometry') { | ||
linkObj.add(obj); | ||
const geoType = n.children[0].nodeName.toLowerCase(); | ||
if (geoType === 'mesh') { | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
obj.rotation.set(0, 0, 0); | ||
const filename = n.children[0].getAttribute('filename'); | ||
const filePath = this._resolvePackagePath(packages, filename, path); | ||
// multiply the existing scale by the scale components because | ||
// the loaded model could have important scale values already applied | ||
// to the root. Collada files, for example, can load in with a scale | ||
// to convert the model units to meters. | ||
obj.scale.x *= scale[0]; | ||
obj.scale.y *= scale[1]; | ||
obj.scale.z *= scale[2]; | ||
// file path is null if a package directory is not provided. | ||
if (filePath !== null) { | ||
applyRotation(obj, rpy); | ||
const ext = filePath.match(/.*\.([A-Z0-9]+)$/i).pop() || ''; | ||
const scaleAttr = n.children[0].getAttribute('scale'); | ||
if (scaleAttr) scale = this._processTuple(scaleAttr); | ||
loadMeshCb(filePath, ext, obj => { | ||
if (obj) { | ||
if (obj instanceof THREE.Mesh) { | ||
obj.material.copy(material); | ||
} | ||
linkObj.add(obj); | ||
}); | ||
obj.position.set(xyz[0], xyz[1], xyz[2]); | ||
obj.rotation.set(0, 0, 0); | ||
} | ||
// multiply the existing scale by the scale components because | ||
// the loaded model could have important scale values already applied | ||
// to the root. Collada files, for example, can load in with a scale | ||
// to convert the model units to meters. | ||
obj.scale.x *= scale[0]; | ||
obj.scale.y *= scale[1]; | ||
obj.scale.z *= scale[2]; | ||
} else if (geoType === 'box') { | ||
this._applyRotation(obj, rpy); | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.BoxBufferGeometry(1, 1, 1); | ||
primitiveModel.material = material; | ||
} | ||
const size = processTuple(n.children[0].getAttribute('size')); | ||
}); | ||
linkObj.add(primitiveModel); | ||
primitiveModel.scale.set(size[0], size[1], size[2]); | ||
} | ||
} else if (geoType === 'sphere') { | ||
} else if (geoType === 'box') { | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.SphereBufferGeometry(1, 30, 30); | ||
primitiveModel.material = material; | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.BoxBufferGeometry(1, 1, 1); | ||
primitiveModel.material = material; | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
primitiveModel.scale.set(radius, radius, radius); | ||
const size = this._processTuple(n.children[0].getAttribute('size')); | ||
linkObj.add(primitiveModel); | ||
linkObj.add(primitiveModel); | ||
primitiveModel.scale.set(size[0], size[1], size[2]); | ||
} else if (geoType === 'cylinder') { | ||
} else if (geoType === 'sphere') { | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.CylinderBufferGeometry(1, 1, 1, 30); | ||
primitiveModel.material = material; | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.SphereBufferGeometry(1, 30, 30); | ||
primitiveModel.material = material; | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
const length = parseFloat(n.children[0].getAttribute('length')) || 0; | ||
primitiveModel.scale.set(radius, length, radius); | ||
primitiveModel.rotation.set(Math.PI / 2, 0, 0); | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
primitiveModel.scale.set(radius, radius, radius); | ||
linkObj.add(primitiveModel); | ||
linkObj.add(primitiveModel); | ||
} | ||
} else if (geoType === 'cylinder') { | ||
} else if (type === 'origin') { | ||
primitiveModel = new THREE.Mesh(); | ||
primitiveModel.geometry = new THREE.CylinderBufferGeometry(1, 1, 1, 30); | ||
primitiveModel.material = material; | ||
xyz = processTuple(n.getAttribute('xyz')); | ||
rpy = processTuple(n.getAttribute('rpy')); | ||
const radius = parseFloat(n.children[0].getAttribute('radius')) || 0; | ||
const length = parseFloat(n.children[0].getAttribute('length')) || 0; | ||
primitiveModel.scale.set(radius, length, radius); | ||
primitiveModel.rotation.set(Math.PI / 2, 0, 0); | ||
} | ||
linkObj.add(primitiveModel); | ||
}); | ||
} | ||
// apply the position and rotation to the primitive geometry after | ||
// the fact because it's guaranteed to have been scraped from the child | ||
// nodes by this point | ||
if (primitiveModel) { | ||
} else if (type === 'origin') { | ||
applyRotation(primitiveModel, rpy, true); | ||
primitiveModel.position.set(xyz[0], xyz[1], xyz[2]); | ||
xyz = this._processTuple(n.getAttribute('xyz')); | ||
rpy = this._processTuple(n.getAttribute('rpy')); | ||
} | ||
} else if (type === 'material') { | ||
} | ||
const materialName = n.getAttribute('name'); | ||
if (materialName) { | ||
return processUrdf(content); | ||
this._copyMaterialAttributes(material, materialMap[materialName]); | ||
} | ||
} else { | ||
// Default mesh loading function | ||
defaultMeshLoader(path, manager, done) { | ||
this.forEach(n.children, c => { | ||
if (/\.stl$/i.test(path)) { | ||
this._processMaterial(material, c, packages, path); | ||
const loader = new STLLoader.STLLoader(manager); | ||
loader.load(path, geom => { | ||
const mesh = new THREE.Mesh(geom, new THREE.MeshPhongMaterial()); | ||
done(mesh); | ||
}); | ||
}); | ||
} else if (/\.dae$/i.test(path)) { | ||
} | ||
const loader = new ColladaLoader.ColladaLoader(manager); | ||
loader.load(path, dae => done(dae.scene)); | ||
} | ||
}); | ||
} else { | ||
// apply the position and rotation to the primitive geometry after | ||
// the fact because it's guaranteed to have been scraped from the child | ||
// nodes by this point | ||
if (primitiveModel) { | ||
console.warn(`URDFLoader: Could not load model at ${ path }.\nNo loader available`); | ||
this._applyRotation(primitiveModel, rpy, true); | ||
primitiveModel.position.set(xyz[0], xyz[1], xyz[2]); | ||
} | ||
@@ -797,0 +743,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
238161
2689
327