feat: 开发移动AGV节点
This commit is contained in:
parent
751cb3cd8e
commit
6dfa2be64e
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "CMVR-IOT-UI",
|
||||
"name": "changan-inspection",
|
||||
"version": "3.8.8",
|
||||
"description": "CMVR-IOT平台管理系统前端UI框架",
|
||||
"description": "长安巡检项目",
|
||||
"author": "CMVR",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@ -1186,10 +1186,6 @@
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- <template #footer>
|
||||
<el-button @click="cancel">取消</el-button>
|
||||
<el-button type="primary" @click="confirm">保存</el-button>
|
||||
</template> -->
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
|
||||
@ -363,12 +363,28 @@ function handler(params) {
|
||||
{
|
||||
collapseTitle: "机器人",
|
||||
nodeList: [
|
||||
{
|
||||
icon: videoSvg,
|
||||
name: "AGV控制",
|
||||
type: "serviceNode",
|
||||
desc: "控制机器人的底盘",
|
||||
action: 'AGV_MOVE_TO_POINT',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true },
|
||||
{ name: "x", type: "input", input: "", disabled: true },
|
||||
{ name: "y", type: "input", input: "", disabled: true },
|
||||
{ name: "theta", type: "input", input: "", disabled: true },
|
||||
],
|
||||
outputType: 'json'
|
||||
},
|
||||
{
|
||||
icon: videoSvg,
|
||||
name: "机械臂",
|
||||
type: "serviceNode",
|
||||
desc: "控制机器人的手臂",
|
||||
action: 'ALM',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true }
|
||||
],
|
||||
|
||||
@ -34,89 +34,13 @@ let scene, camera, renderer, controls;
|
||||
const animationId = ref(null);
|
||||
const jointControls = reactive([]);
|
||||
|
||||
|
||||
// ============ 工具函数 ============
|
||||
const cleanMeshPath = (path) => {
|
||||
if (!path) return '';
|
||||
path = path.replace(/^file:\/\//, '');
|
||||
path = path.replace(/^package:\/\/[^/]+\//, '');
|
||||
const findMatch = path.match(/\$\(find\s+([^)]+)\)/);
|
||||
if (findMatch) path = path.replace(/\$\(find\s+[^)]+\)/, '');
|
||||
path = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
return path;
|
||||
};
|
||||
|
||||
const parseXYZ = (str) => {
|
||||
if (!str) return { x: 0, y: 0, z: 0 };
|
||||
const [x, y, z] = str.trim().split(/\s+/).map(Number);
|
||||
return { x: x || 0, y: y || 0, z: z || 0 };
|
||||
};
|
||||
|
||||
const parseRPY = (str) => {
|
||||
if (!str) return { roll: 0, pitch: 0, yaw: 0 };
|
||||
const [r, p, y] = str.trim().split(/\s+/).map(Number);
|
||||
return { roll: r || 0, pitch: p || 0, yaw: y || 0 };
|
||||
};
|
||||
|
||||
// ★ 正确转换 RPY → 四元数(URDF 固定轴 XYZ = Three.js 局部轴 ZYX)
|
||||
const rpyToQuaternion = (rpy) => {
|
||||
const euler = new THREE.Euler(rpy.roll, rpy.pitch, rpy.yaw, 'ZYX');
|
||||
return new THREE.Quaternion().setFromEuler(euler);
|
||||
};
|
||||
|
||||
// ============ STL 加载 ============
|
||||
const loadSTLWithRetry = (url, group, visualPos, visualQuat) => {
|
||||
return new Promise((resolve) => {
|
||||
const loader = new STLLoader();
|
||||
const loadMesh = (attemptUrl) => {
|
||||
loader.load(
|
||||
attemptUrl,
|
||||
(geometry) => {
|
||||
geometry.computeVertexNormals();
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: props.modelColor,
|
||||
roughness: 0.4,
|
||||
metalness: 0.3,
|
||||
});
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.set(visualPos.x, visualPos.y, visualPos.z);
|
||||
mesh.quaternion.copy(visualQuat); // 使用四元数
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
while (group.children.length > 0) {
|
||||
const child = group.children[0];
|
||||
if (child.isMesh) {
|
||||
child.geometry?.dispose();
|
||||
child.material?.dispose();
|
||||
}
|
||||
group.remove(child);
|
||||
}
|
||||
group.add(mesh);
|
||||
resolve(mesh);
|
||||
},
|
||||
undefined,
|
||||
(error) => {
|
||||
console.warn(`加载STL失败: ${attemptUrl}`, error);
|
||||
if (!attemptUrl.startsWith('/') && !attemptUrl.startsWith('http')) {
|
||||
loadMesh('/' + attemptUrl);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
loadMesh(url);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ============ 场景初始化 ============
|
||||
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(0x1a1a2e);
|
||||
scene.background = new THREE.Color('#000d2e');
|
||||
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
||||
camera.position.set(2, 2, 3);
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
@ -145,9 +69,9 @@ const initScene = () => {
|
||||
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 grid = new THREE.GridHelper(3, 20, 0x444466, 0x333355);
|
||||
// grid.position.y = -0.5;
|
||||
// scene.add(grid);
|
||||
};
|
||||
|
||||
|
||||
@ -174,7 +98,6 @@ const updateJoint = (jointName, angle) => {
|
||||
if (control) control.value = angle;
|
||||
};
|
||||
|
||||
|
||||
const resetJoints = () => {
|
||||
for (const joint of jointControls) {
|
||||
joint.value = 0;
|
||||
@ -225,7 +148,6 @@ const loadRobot = async () => {
|
||||
|
||||
const robot = model.threeObject; // 这就是 URDFRobot
|
||||
|
||||
|
||||
// 将机器人添加到场景
|
||||
scene.add(robot); // ⚠️ 注意:是 robot.scene,不是 robot
|
||||
robot.rotation.x = -Math.PI / 2;
|
||||
@ -246,8 +168,6 @@ const loadRobot = async () => {
|
||||
console.error('未知的 joints 结构:', robot.joints);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 填充关节控制列表
|
||||
for (const [name, joint] of jointEntries) {
|
||||
if (joint._jointType === 'fixed') continue;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user