inspection-host-computer/src/views/arm/UrdfView.vue

230 lines
6.0 KiB
Vue
Raw Normal View History

2026-06-29 16:19:37 +08:00
<template>
<div class="elfin-viewer">
<div ref="viewerContainer" class="viewer-container">
<!-- Three.js 渲染 -->
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick, reactive } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import { XacroAdapter } from '@/adapters/XacroAdapter.js'
2026-06-30 17:32:54 +08:00
import { fileNameMap } from './config'
const props = defineProps({
filePath: {
type: String,
default: '/inspection/elfin10/'
},
fileName: {
type: String,
default: 'elfin10.urdf.xacro'
}
})
2026-06-29 16:19:37 +08:00
const viewerContainer = ref(null);
let scene, camera, renderer, controls;
const animationId = ref(null);
const jointControls = reactive([]);
// ============ 场景初始化 ============
const initScene = () => {
const container = viewerContainer.value;
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
2026-06-30 17:32:54 +08:00
2026-06-29 16:19:37 +08:00
scene = new THREE.Scene();
scene.background = new THREE.Color('#060a12');
2026-06-30 17:32:54 +08:00
camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
camera.position.set(0, 0, 4);
2026-06-29 16:19:37 +08:00
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(width, height);
2026-06-30 17:32:54 +08:00
2026-06-29 16:19:37 +08:00
container.appendChild(renderer.domElement);
2026-06-30 17:32:54 +08:00
2026-06-29 16:19:37 +08:00
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.target.set(0, 0.5, 0);
controls.update();
2026-06-30 17:32:54 +08:00
const ambient = new THREE.AmbientLight(0xffffff, 0.6)
2026-06-29 16:19:37 +08:00
scene.add(ambient);
2026-06-30 17:32:54 +08:00
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(5, 10, 7);
directionalLight.castShadow = true;
scene.add(directionalLight);
};
2026-06-29 16:19:37 +08:00
// ============ 关节控制(无需修改) ============
const updateJoint = (jointName, angle) => {
if (!robotInstance) return;
const joint = robotInstance.joints[jointName];
if (!joint) {
console.warn(`未找到关节: ${jointName}`);
return;
}
// ✅ 使用 setJointValue 触发变换矩阵更新
if (joint._jointType === 'revolute' || joint._jointType === 'continuous') {
joint.setJointValue(angle);
} else if (joint._jointType === 'prismatic') {
joint.setJointValue(angle);
}
// 同步滑块显示值
const control = jointControls.find(c => c.name === jointName);
if (control) control.value = angle;
};
const resetJoints = () => {
for (const joint of jointControls) {
joint.value = 0;
updateJoint(joint.name, 0);
}
};
const fileMap = new Map()
const loadFiles = async () => {
2026-06-30 17:32:54 +08:00
const fileNames = fileNameMap[props.filePath]
2026-06-29 16:19:37 +08:00
const fetchPromises = fileNames.map(async (name) => {
2026-06-30 17:32:54 +08:00
const url = `${props.filePath}${name}`
2026-06-29 16:19:37 +08:00
const response = await fetch(url)
const blob = await response.blob()
return new File([blob], name, { type: blob.type })
})
const fileList = await Promise.all(fetchPromises)
fileList.forEach(item => {
2026-06-30 17:32:54 +08:00
fileMap.set(props.filePath + item.name, item);
2026-06-29 16:19:37 +08:00
})
}
const currentModel = ref()
let robotInstance = null; // 保存 URDFRobot 对象
const loadRobot = async () => {
2026-06-30 17:32:54 +08:00
const xacroUrl = `${props.filePath}${props.fileName}`;
2026-06-29 16:19:37 +08:00
const response = await fetch(xacroUrl);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const xacroContent = await response.text();
2026-06-30 17:32:54 +08:00
const file = fileMap.get(xacroUrl)
let model
if (props.fileName.split('.').pop() === 'xacro') {
model = await XacroAdapter.parse(xacroContent, file.name, fileMap, file);
}
2026-06-29 16:19:37 +08:00
2026-06-30 17:32:54 +08:00
const robot = model.threeObject;
2026-06-29 16:19:37 +08:00
// 将机器人添加到场景
2026-06-30 17:32:54 +08:00
scene.add(robot);
2026-06-29 16:19:37 +08:00
robot.rotation.x = -Math.PI / 2;
currentModel.value = model
robotInstance = robot;
// 获取关节条目数组
let jointEntries = [];
if (robot.joints instanceof Map) {
jointEntries = [...robot.joints.entries()];
} else if (typeof robot.joints === 'object' && robot.joints !== null) {
jointEntries = Object.entries(robot.joints);
} else if (Array.isArray(robot.joints)) {
jointEntries = robot.joints.map((j, i) => [i, j]); // 索引作为名称
} else {
console.error('未知的 joints 结构:', robot.joints);
}
// 填充关节控制列表
for (const [name, joint] of jointEntries) {
if (joint._jointType === 'fixed') continue;
const limits = joint.limits || {};
let lower = limits.lower !== undefined ? limits.lower : -Math.PI;
let upper = limits.upper !== undefined ? limits.upper : Math.PI;
// 如果是连续关节,范围设为 -PI ~ PI
if (joint._jointType === 'continuous') {
lower = -Math.PI;
upper = Math.PI;
}
// 初始角度一般为 0
jointControls.push({
name,
min: lower,
max: upper,
value: 0, // 因为 joint.angle 初始为 0
});
}
};
// ============ 动画循环 ============
const animate = () => {
animationId.value = requestAnimationFrame(animate);
controls?.update();
renderer?.render(scene, camera);
};
// ============ 生命周期 ============
onMounted(async () => {
await nextTick();
await loadFiles()
initScene();
await loadRobot();
animate();
window.addEventListener('resize', onResize);
});
onBeforeUnmount(() => {
if (animationId.value) cancelAnimationFrame(animationId.value);
window.removeEventListener('resize', onResize);
renderer?.dispose();
controls?.dispose();
});
const onResize = () => {
const container = viewerContainer.value;
if (!container || !camera || !renderer) return;
const width = container.clientWidth;
const height = container.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
};
defineExpose({ updateJoint, resetJoints, jointControls });
</script>
<style lang="scss" scoped>
.elfin-viewer {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: #0d0d1a;
.viewer-container {
width: 100%;
height: 100%;
canvas {
display: block;
width: 100% !important;
height: 100% !important;
}
}
}
</style>