inspection-host-computer/src/views/arm/UrdfView.vue
2026-06-29 16:19:37 +08:00

237 lines
6.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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'
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;
scene = new THREE.Scene();
scene.background = new THREE.Color('#060a12');
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
camera.position.set(2, 2, 3);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
container.appendChild(renderer.domElement);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.target.set(0, 0.5, 0);
controls.update();
const ambient = new THREE.AmbientLight(0x404060, 0.6);
scene.add(ambient);
const main = new THREE.DirectionalLight(0xffffff, 1.0);
main.position.set(5, 10, 7);
main.castShadow = true;
scene.add(main);
const fill = new THREE.DirectionalLight(0x8888ff, 0.4);
fill.position.set(-5, 0, 5);
scene.add(fill);
const rim = new THREE.DirectionalLight(0xffffff, 0.3);
rim.position.set(0, -5, 5);
scene.add(rim);
// const grid = new THREE.GridHelper(3, 20, 0x444466, 0x333355);
// grid.position.y = -0.5;
// scene.add(grid);
};
// ============ 关节控制(无需修改) ============
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 () => {
const fileNames = [
'elfin10.urdf.xacro',
'elfin10_gazebo.xacro',
'elfin10_transmission.xacro',
'meshes/elfin10/elfin_base.STL',
'meshes/elfin10/elfin_end_link.STL',
'meshes/elfin10/elfin_link1.STL',
'meshes/elfin10/elfin_link2.STL',
'meshes/elfin10/elfin_link3.STL',
'meshes/elfin10/elfin_link4.STL',
'meshes/elfin10/elfin_link5.STL',
'meshes/elfin10/elfin_link6.STL'
]
const fetchPromises = fileNames.map(async (name) => {
const url = `/inspection/elfin10/${name}`
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 => {
fileMap.set('elfin10/' + item.name, item);
})
}
const currentModel = ref()
let robotInstance = null; // 保存 URDFRobot 对象
const loadRobot = async () => {
const xacroUrl = `/inspection/elfin10/elfin10.urdf.xacro`;
const response = await fetch(xacroUrl);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const xacroContent = await response.text();
const file = fileMap.get('elfin10/elfin10.urdf.xacro')
const model = await XacroAdapter.parse(xacroContent, file.name, fileMap, file);
const robot = model.threeObject; // 这就是 URDFRobot
// 将机器人添加到场景
scene.add(robot); // ⚠️ 注意:是 robot.scene不是 robot
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>