CMVR-IOT-UI/src/adapters/URDFAdapter.js

462 lines
19 KiB
JavaScript
Raw Normal View History

2026-07-06 11:11:59 +08:00
/**
* URDF Adapter
* Converts urdf-loaders result to unified model
*/
import * as THREE from 'three';
import { UnifiedRobotModel, Link, Joint, JointLimits, VisualGeometry, CollisionGeometry, InertialProperties } from '../models/UnifiedRobotModel.js';
export class URDFAdapter {
/**
* Convert urdf-loaders robot object to unified model
* @param {THREE.Group} robot - Robot object returned by urdf-loaders
* @param {string} urdfXML - Original URDF XML content (optional, for extracting inertial data)
* @returns {UnifiedRobotModel}
*/
static convert(robot, urdfXML = null) {
const model = new UnifiedRobotModel();
model.name = robot.name || 'robot';
model.threeObject = robot;
// Mark model type as URDF
if (!robot.userData) robot.userData = {};
robot.userData.type = 'urdf';
if (!robot.links || !robot.joints) {
console.warn('URDF model missing links or joints information');
return model;
}
// If XML provided, parse inertial data
let inertialData = {};
if (urdfXML) {
inertialData = this.parseInertialFromXML(urdfXML);
}
// Convert links
Object.values(robot.links).forEach(urdfLink => {
const link = this.convertLink(urdfLink);
// If urdf-loader didn't parse inertial, get from XML
if (!link.inertial && inertialData[urdfLink.name]) {
link.inertial = inertialData[urdfLink.name];
}
model.addLink(link);
});
// Convert joints
Object.values(robot.joints).forEach(urdfJoint => {
const joint = this.convertJoint(urdfJoint);
model.addJoint(joint);
});
// If XML provided, supplement effort and velocity from XML (urdf-loaders may not have parsed)
if (urdfXML) {
this.supplementJointLimitsFromXML(model, urdfXML);
}
// Find root link (link that is not a child of any joint)
const allChildren = new Set(
Array.from(model.joints.values()).map(j => j.child).filter(c => c)
);
const rootLinks = Array.from(model.links.keys()).filter(
name => !allChildren.has(name)
);
if (rootLinks.length > 0) {
model.rootLink = rootLinks[0];
}
// Enhance materials for better lighting (MuJoCo style)
this.enhanceMaterials(robot);
return model;
}
/**
* Enhance materials for better lighting (MuJoCo style)
* Applies consistent shininess and specular properties to all materials
* Directly modifies materials in place to ensure changes persist
*/
static enhanceMaterials(robotObject) {
robotObject.traverse((child) => {
if (child.isMesh && child.material) {
// Handle material arrays
if (Array.isArray(child.material)) {
child.material = child.material.map(mat => {
return this.enhanceSingleMaterial(mat);
});
} else {
child.material = this.enhanceSingleMaterial(child.material);
}
}
});
}
/**
* Enhance a single material with better lighting properties
* Returns enhanced material (may be cloned or modified in place)
* Saves original properties for lighting toggle
*/
static enhanceSingleMaterial(material) {
if (material.isMeshPhongMaterial || material.isMeshStandardMaterial) {
// Save original properties if not already saved (for lighting toggle)
if (material.userData.originalShininess === undefined) {
material.userData.originalShininess = material.shininess !== undefined ? material.shininess : 30;
// IMPORTANT: Save original color to preserve URDF material colors
if (!material.userData.originalColor && material.color) {
material.userData.originalColor = material.color.clone();
}
// Save original specular - if material had no specular, save null
if (!material.specular) {
material.userData.originalSpecular = null;
} else if (material.specular.isColor) {
const spec = material.specular;
if (spec.r < 0.1 && spec.g < 0.1 && spec.b < 0.1) {
material.userData.originalSpecular = null; // Likely default
} else {
material.userData.originalSpecular = spec.clone();
}
} else if (typeof material.specular === 'number') {
if (material.specular === 0x111111 || material.specular < 0x111111) {
material.userData.originalSpecular = null;
} else {
material.userData.originalSpecular = new THREE.Color(material.specular);
}
} else {
material.userData.originalSpecular = null;
}
}
// IMPORTANT: Preserve original color from URDF material definitions
// Do not modify material.color - urdf-loaders already sets the correct color
// Apply enhanced lighting (default enabled)
// Increase shininess for better highlights
if (material.shininess === undefined || material.shininess < 50) {
material.shininess = 50;
}
// Enhance specular reflection - ensure it's a Color object with proper values
if (!material.specular) {
material.specular = new THREE.Color(0.3, 0.3, 0.3);
} else if (material.specular.isColor) {
// If it's already a Color object, update values
if (material.specular.r < 0.2 || material.specular.g < 0.2 || material.specular.b < 0.2) {
material.specular.setRGB(0.3, 0.3, 0.3);
}
} else if (typeof material.specular === 'number') {
// Convert number to Color object
if (material.specular < 0x333333) {
material.specular = new THREE.Color(0.3, 0.3, 0.3);
} else {
// Convert hex to Color
material.specular = new THREE.Color(material.specular);
if (material.specular.r < 0.2) {
material.specular.setRGB(0.3, 0.3, 0.3);
}
}
}
// Mark material as needing update
material.needsUpdate = true;
return material;
} else if (material.type === 'MeshBasicMaterial') {
// Convert MeshBasicMaterial to MeshPhongMaterial
const oldMaterial = material;
const envMap = typeof window !== 'undefined' && window.app?.sceneManager?.environmentManager?.getEnvironmentMap();
const newMaterial = new THREE.MeshPhongMaterial({
color: oldMaterial.color,
map: oldMaterial.map,
transparent: oldMaterial.transparent,
opacity: oldMaterial.opacity,
side: oldMaterial.side,
shininess: 50,
specular: new THREE.Color(0.3, 0.3, 0.3),
envMap: envMap || null,
reflectivity: envMap ? 0.3 : 0
});
// Save original properties for lighting toggle
newMaterial.userData.originalShininess = 30;
newMaterial.userData.originalSpecular = null; // MeshBasicMaterial had no specular
return newMaterial;
}
return material;
}
static convertLink(urdfLink) {
const link = new Link(urdfLink.name);
link.threeObject = urdfLink;
// Convert inertial properties
if (urdfLink.inertial) {
link.inertial = this.convertInertial(urdfLink.inertial);
}
// Note: urdf-loaders has already converted visual and collision to Three.js objects
// We mainly extract metadata, actual meshes are in threeObject
return link;
}
static convertJoint(urdfJoint) {
// URDF joint object has jointType property (not type)
const jointType = urdfJoint.jointType || urdfJoint.type || 'fixed';
const joint = new Joint(urdfJoint.name, jointType);
// Extract parent name (urdf-loader returns Three.js object reference)
joint.parent = urdfJoint.parent?.name || null;
// child may not be in urdfJoint.child, but in Three.js children array
// In Three.js scene graph, Joint is parent node, Child Link is Joint's child node
if (urdfJoint.child && urdfJoint.child.name) {
joint.child = urdfJoint.child.name;
} else if (urdfJoint.children && urdfJoint.children.length > 0) {
// Find Link object from children array
const childLink = urdfJoint.children.find(child =>
child.isURDFLink || child.type === 'URDFLink'
);
if (childLink) {
joint.child = childLink.name;
}
}
joint.threeObject = urdfJoint;
// Convert origin
if (urdfJoint.origin) {
joint.origin = {
xyz: urdfJoint.origin.xyz || [0, 0, 0],
rpy: urdfJoint.origin.rpy || [0, 0, 0]
};
}
// Convert axis
if (urdfJoint.axis) {
joint.axis = { xyz: urdfJoint.axis.xyz || [0, 0, 1] };
}
// Convert limits
if (urdfJoint.limit) {
joint.limits = this.convertLimits(urdfJoint.limit);
}
// Get current value
if (urdfJoint.angle !== undefined) {
joint.currentValue = urdfJoint.angle;
} else if (urdfJoint.jointValue !== undefined) {
joint.currentValue = urdfJoint.jointValue;
}
return joint;
}
static convertLimits(limit) {
const limits = new JointLimits();
if (limit.lower !== undefined) limits.lower = limit.lower;
if (limit.upper !== undefined) limits.upper = limit.upper;
if (limit.effort !== undefined) limits.effort = limit.effort;
if (limit.velocity !== undefined) limits.velocity = limit.velocity;
return limits;
}
/**
* Supplement joint effort and velocity information from URDF XML
* (because urdf-loaders may not have parsed these attributes)
*/
static supplementJointLimitsFromXML(model, urdfXML) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(urdfXML, 'text/xml');
const jointElements = doc.querySelectorAll('joint');
jointElements.forEach(jointEl => {
const jointName = jointEl.getAttribute('name');
const joint = model.joints.get(jointName);
if (joint) {
const limitEl = jointEl.querySelector('limit');
if (limitEl) {
if (!joint.limits) {
joint.limits = new JointLimits();
}
// Read effort and velocity
const effort = limitEl.getAttribute('effort');
const velocity = limitEl.getAttribute('velocity');
if (effort !== null) {
joint.limits.effort = parseFloat(effort);
}
if (velocity !== null) {
joint.limits.velocity = parseFloat(velocity);
}
// Also ensure lower and upper are correctly set
const lower = limitEl.getAttribute('lower');
const upper = limitEl.getAttribute('upper');
if (lower !== null) {
joint.limits.lower = parseFloat(lower);
}
if (upper !== null) {
joint.limits.upper = parseFloat(upper);
}
}
}
});
} catch (error) {
console.error('Failed to supplement joint limit information from XML:', error);
}
}
static convertInertial(inertial) {
const props = new InertialProperties();
if (inertial.mass !== undefined) props.mass = inertial.mass;
if (inertial.origin) {
props.origin = {
xyz: inertial.origin.xyz || [0, 0, 0],
rpy: inertial.origin.rpy || [0, 0, 0]
};
}
if (inertial.ixx !== undefined) props.ixx = inertial.ixx;
if (inertial.iyy !== undefined) props.iyy = inertial.iyy;
if (inertial.izz !== undefined) props.izz = inertial.izz;
if (inertial.ixy !== undefined) props.ixy = inertial.ixy;
if (inertial.ixz !== undefined) props.ixz = inertial.ixz;
if (inertial.iyz !== undefined) props.iyz = inertial.iyz;
return props;
}
/**
* Parse inertial data from URDF XML
* @param {string} xmlContent - URDF XML content
* @returns {Object} Mapping from link names to inertial data
*/
static parseInertialFromXML(xmlContent) {
const inertialData = {};
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlContent, 'text/xml');
// Get all link elements
const links = xmlDoc.getElementsByTagName('link');
for (let i = 0; i < links.length; i++) {
const linkElement = links[i];
const linkName = linkElement.getAttribute('name');
// Find inertial element
const inertialElement = linkElement.getElementsByTagName('inertial')[0];
if (!inertialElement) continue;
const inertial = new InertialProperties();
// Parse mass
const massElement = inertialElement.getElementsByTagName('mass')[0];
if (massElement) {
inertial.mass = parseFloat(massElement.getAttribute('value'));
}
// Parse origin
const originElement = inertialElement.getElementsByTagName('origin')[0];
if (originElement) {
const xyz = originElement.getAttribute('xyz');
const rpy = originElement.getAttribute('rpy');
inertial.origin = {
xyz: xyz ? xyz.split(' ').map(parseFloat) : [0, 0, 0],
rpy: rpy ? rpy.split(' ').map(parseFloat) : [0, 0, 0]
};
} else {
inertial.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
}
// Parse inertia
const inertiaElement = inertialElement.getElementsByTagName('inertia')[0];
if (inertiaElement) {
inertial.ixx = parseFloat(inertiaElement.getAttribute('ixx')) || 0;
inertial.iyy = parseFloat(inertiaElement.getAttribute('iyy')) || 0;
inertial.izz = parseFloat(inertiaElement.getAttribute('izz')) || 0;
inertial.ixy = parseFloat(inertiaElement.getAttribute('ixy')) || 0;
inertial.ixz = parseFloat(inertiaElement.getAttribute('ixz')) || 0;
inertial.iyz = parseFloat(inertiaElement.getAttribute('iyz')) || 0;
}
inertialData[linkName] = inertial;
}
} catch (error) {
console.error('Failed to parse URDF XML inertial data:', error);
}
return inertialData;
}
/**
* Set joint angle (using urdf-loaders' setJointValue method)
* Reference URDFClasses.js setJointValue implementation
*/
static setJointAngle(joint, angle, ignoreLimits = false) {
joint.currentValue = angle;
// URDF format: use urdf-loader's setJointValue method
if (joint.threeObject) {
// If ignoring limits, temporarily modify URDF joint object's limit values
let originalLimits = null;
if (ignoreLimits && joint.threeObject.limit) {
originalLimits = {
lower: joint.threeObject.limit.lower,
upper: joint.threeObject.limit.upper
};
joint.threeObject.limit.lower = -Math.PI * 2;
joint.threeObject.limit.upper = Math.PI * 2;
}
// Prefer setJointValue method (urdf-loader's standard method)
if (typeof joint.threeObject.setJointValue === 'function') {
joint.threeObject.setJointValue(angle);
// Restore original limits
if (originalLimits && joint.threeObject.limit) {
joint.threeObject.limit.lower = originalLimits.lower;
joint.threeObject.limit.upper = originalLimits.upper;
}
return;
} else if (typeof joint.threeObject.setAngle === 'function') {
joint.threeObject.setAngle(angle);
// Restore original limits
if (originalLimits && joint.threeObject.limit) {
joint.threeObject.limit.lower = originalLimits.lower;
joint.threeObject.limit.upper = originalLimits.upper;
}
return;
}
// Restore original limits (if none of the above methods executed)
if (originalLimits && joint.threeObject.limit) {
joint.threeObject.limit.lower = originalLimits.lower;
joint.threeObject.limit.upper = originalLimits.upper;
}
// If none available, manually set rotation
// Note: For URDF, we usually shouldn't reach here, as urdf-loader should provide setJointValue
console.warn(`Joint ${joint.name} has no setJointValue or setAngle method, attempting manual setup`);
if (joint.type === 'revolute' || joint.type === 'continuous') {
const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(0, 0, 1);
// Note: joint.threeObject may not be a Three.js object, cannot directly set rotation
if (joint.threeObject.rotation) {
joint.threeObject.rotation.setFromAxisAngle(axis, angle);
}
} else if (joint.type === 'prismatic') {
const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(1, 0, 0);
if (joint.threeObject.position) {
joint.threeObject.position.copy(axis.multiplyScalar(angle));
}
}
} else {
console.warn(`Joint ${joint.name} has no threeObject`);
}
}
}