👔 Update model utils to be able to load both urdf and xacro

This commit is contained in:
Rune Harlyk
2025-07-02 22:55:31 +02:00
parent 7c8c5b40a1
commit 01d46f283b
+67 -64
View File
@@ -1,89 +1,92 @@
import { Color, LoaderUtils, Vector3 } from 'three'; import { Color, LoaderUtils, Vector3 } from 'three'
import URDFLoader, { type URDFRobot } from 'urdf-loader'; import URDFLoader, { type URDFRobot } from 'urdf-loader'
import { XacroLoader } from 'xacro-parser'; import { XacroLoader } from 'xacro-parser'
import { Result } from '$lib/utilities'; import { Result } from '$lib/utilities'
import { jointNames, model } from '$lib/stores'; import { jointNames, model } from '$lib/stores'
import uzip from 'uzip'; import uzip from 'uzip'
import { fileService } from '$lib/services'; import { fileService } from '$lib/services'
let model_xml: XMLDocument; let model_xml: XMLDocument
export const populateModelCache = async () => { export const populateModelCache = async () => {
await cacheModelFiles(); await cacheModelFiles()
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro'); const modelRes = await loadModel('/yertle.URDF')
if (modelRes.isOk()) { if (modelRes.isOk()) {
const [urdf, JOINT_NAME] = modelRes.inner; const [urdf, JOINT_NAME] = modelRes.inner
jointNames.set(JOINT_NAME); jointNames.set(JOINT_NAME)
model.set(urdf); model.set(urdf)
} else { } else {
console.error(modelRes.inner, { exception: modelRes.exception }); console.error(modelRes.inner, { exception: modelRes.exception })
}
} }
};
export const cacheModelFiles = async () => { export const cacheModelFiles = async () => {
let data = await fetch('/stl.zip'); const data = await fetch('/URDF.zip')
var files = uzip.parse(await data.arrayBuffer()); const files = uzip.parse(await data.arrayBuffer())
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) { for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
const url = new URL(path, window.location.href); const url = new URL(path, window.location.href)
fileService.saveFile(url.toString(), data); fileService?.saveFile(url.toString(), data)
}
} }
};
export const loadModelAsync = async ( export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => {
url: string const urdfLoader = new URDFLoader()
): Promise<Result<[URDFRobot, string[]], string>> => { urdfLoader.workingPath = LoaderUtils.extractUrlBase(url)
return new Promise((resolve, reject) => {
const xacroLoader = new XacroLoader();
const urdfLoader = new URDFLoader();
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url);
xacroLoader.load( let xml = url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
url,
async (xml) => { if (typeof xml === 'string') {
model_xml = xml; xml = new window.DOMParser().parseFromString(xml, 'text/xml')
}
return new Promise(resolve => {
model_xml = xml
try { try {
const model = urdfLoader.parse(xml); const model = urdfLoader.parse(xml)
model.rotation.x = -Math.PI / 2; setupRobot(model)
model.rotation.z = Math.PI / 2;
model.traverse((c) => (c.castShadow = true));
model.updateMatrixWorld(true);
model.scale.setScalar(10);
const joints = Object.entries(model.joints) const joints = Object.entries(model.joints)
.filter((joint) => joint[1].jointType !== 'fixed') .filter(joint => joint[1].jointType !== 'fixed')
.map((joint) => joint[0]); .map(joint => joint[0])
resolve(Result.ok([model, joints])); resolve(Result.ok([model, joints]))
} catch (error) { } catch (error) {
resolve(Result.err('Failed to load model', error)); resolve(Result.err('Failed to load model', error))
} }
}, })
(error) => resolve(Result.err('Failed to load model', error))
);
});
};
export const toeWorldPositions = (robot: URDFRobot) => {
const toe_positions: Vector3[] = [];
robot.traverse((child) => {
if (child.name.includes('toe') && !child.name.includes('_link')) {
const worldPosition = new Vector3();
child.getWorldPosition(worldPosition);
toe_positions.push(worldPosition);
} }
});
return toe_positions;
};
export const footColor = () => { const loadXacro = async (url: string): Promise<XMLDocument> =>
const colorElem = model_xml.querySelector('material[name=foot_color] > color') as Element; new Promise((resolve, reject) => {
const colorAttrStr = colorElem.getAttribute('rgba') as string; new XacroLoader().load(url, resolve, reject)
})
function setupRobot(robot: URDFRobot) {
robot.rotation.x = -Math.PI / 2
robot.rotation.z = Math.PI / 2
robot.scale.setScalar(10)
robot.traverse(c => (c.castShadow = true))
robot.updateMatrixWorld(true)
}
export function getToeWorldPositions(robot: URDFRobot): Vector3[] {
const toes: Vector3[] = []
robot.traverse(c => {
if (c.name.includes('toe') && !c.name.includes('_link'))
toes.push(c.getWorldPosition(new Vector3()))
})
return toes
}
export const extractFootColor = () => {
const colorElem = model_xml.querySelector('material[name=foot_color] > color') as Element
const colorAttrStr = colorElem.getAttribute('rgba') as string
const colorStr = colorAttrStr const colorStr = colorAttrStr
.split(' ') .split(' ')
.slice(0, 3) .slice(0, 3)
.map((val) => Math.floor(+val * 255)) .map(val => Math.floor(+val * 255))
.join(', '); .join(', ')
return new Color(`rgb(${colorStr})`); return new Color(`rgb(${colorStr})`)
}; }