From 9fbd2a6dddce1a51392bf5041736e3cb627b3a52 Mon Sep 17 00:00:00 2001
From: zhanghao <774378400@qq.com>
Date: Wed, 5 Aug 2026 17:09:58 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4pad=E7=AB=AF=E6=A8=A1?=
=?UTF-8?q?=E5=9E=8B=E5=8A=A0=E8=BD=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/adapters/XacroAdapter.js | 3 +-
src/views/arm/UrdfView.vue | 81 ++++++++++++++++++++++++++++----
src/views/chassis/canvasUtils.js | 4 +-
3 files changed, 76 insertions(+), 12 deletions(-)
diff --git a/src/adapters/XacroAdapter.js b/src/adapters/XacroAdapter.js
index 29cec84..982c425 100644
--- a/src/adapters/XacroAdapter.js
+++ b/src/adapters/XacroAdapter.js
@@ -121,7 +121,8 @@ export class XacroAdapter {
return new Promise((resolve, reject) => {
const loader = new URDFLoader();
- loader.parseCollision = true;
+ // 查看器只渲染 visual;跳过碰撞网格可显著降低 Pad 的内存占用。
+ loader.parseCollision = false;
// Extract directory where URDF file is located
const urdfDir = workingPath;
diff --git a/src/views/arm/UrdfView.vue b/src/views/arm/UrdfView.vue
index 66de0bc..a04dc37 100644
--- a/src/views/arm/UrdfView.vue
+++ b/src/views/arm/UrdfView.vue
@@ -3,6 +3,8 @@
+ 模型加载中...
+ {{ loadError }}
@@ -15,6 +17,7 @@ import URDFLoader from 'urdf-loader';
import { XacroAdapter } from '@/adapters/XacroAdapter.js'
import { URDFAdapter } from '@/adapters/URDFAdapter.js'
import { getThemeColor } from '@/utils/themeColors.js'
+import { ElMessage } from 'element-plus'
const props = defineProps({
modelPath: {
@@ -49,6 +52,9 @@ const modelDirectory = computed(() => modelUrl.value.slice(0, modelUrl.value.las
const viewerContainer = ref(null);
let scene, camera, renderer, controls;
+let containerResizeObserver;
+const isLoading = ref(true)
+const loadError = ref('')
const animationId = ref(null);
const jointControls = reactive([]);
@@ -65,8 +71,13 @@ const initScene = () => {
camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
camera.position.set(0, 0, 4);
- renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
+ renderer = new THREE.WebGLRenderer({
+ antialias: true,
+ alpha: true,
+ powerPreference: 'high-performance'
+ });
renderer.setSize(width, height);
+ renderer.domElement.addEventListener('webglcontextlost', onWebGLContextLost)
container.appendChild(renderer.domElement);
@@ -116,6 +127,11 @@ const resetJoints = () => {
};
const fileMap = new Map()
+const isBrowserModelDependency = (url) => {
+ const path = url.split(/[?#]/, 1)[0]
+ return /\.(?:xacro|urdf|xml|stl|dae|obj|mtl|gltf|glb|jpg|jpeg|png|gif|bmp|tga|tiff|webp)$/i.test(path)
+}
+
const resolveReferencedUrl = (reference, parentUrl) => {
const normalized = reference.replace(/\\/g, '/')
const rosPath = normalized.match(/^(?:file:\/\/)?\$\(find\s+[^)]+\)\/(.+)$/)
@@ -151,7 +167,10 @@ const loadFiles = async () => {
const references = [...content.matchAll(/\b(?:filename|url)=["']([^"']+)["']/gi)]
references.forEach(([, reference]) => {
const referencedUrl = resolveReferencedUrl(reference, url)
- if (!visitedUrls.has(referencedUrl)) pendingUrls.push(referencedUrl)
+ // .so/.dll 等 Gazebo 原生插件不是浏览器模型依赖,不应由前端请求。
+ if (isBrowserModelDependency(referencedUrl) && !visitedUrls.has(referencedUrl)) {
+ pendingUrls.push(referencedUrl)
+ }
})
}
}
@@ -161,7 +180,8 @@ let robotInstance = null; // 保存 URDFRobot 对象
const parseUrdf = async (urdfContent) => {
const loader = new URDFLoader()
- loader.parseCollision = true
+ // 展示场景不需要碰撞网格,Pad 上重复解析大型 STL 容易耗尽内存。
+ loader.parseCollision = false
const packageNames = [...urdfContent.matchAll(/package:\/\/([^/]+)/g)].map(match => match[1])
loader.packages = Object.fromEntries(
@@ -196,6 +216,7 @@ const loadRobot = async () => {
currentModel.value = model
robotInstance = robot;
+ fileMap.clear()
// 获取关节条目数组
let jointEntries = [];
@@ -238,18 +259,46 @@ const animate = () => {
};
// ============ 生命周期 ============
+const waitForContainerSize = async () => {
+ for (let index = 0; index < 10; index += 1) {
+ await new Promise(resolve => requestAnimationFrame(resolve))
+ if (viewerContainer.value?.clientWidth && viewerContainer.value?.clientHeight) return
+ }
+ throw new Error('模型显示区域尺寸无效')
+}
+
+const onWebGLContextLost = (event) => {
+ event.preventDefault()
+ loadError.value = '模型渲染资源不足,请刷新页面后重试'
+ isLoading.value = false
+ ElMessage.error(loadError.value)
+}
+
onMounted(async () => {
- await nextTick();
- await loadFiles()
- initScene();
- await loadRobot();
- animate();
- window.addEventListener('resize', onResize);
+ try {
+ await nextTick();
+ await waitForContainerSize()
+ initScene();
+ containerResizeObserver = new ResizeObserver(onResize)
+ containerResizeObserver.observe(viewerContainer.value)
+ window.addEventListener('resize', onResize);
+ await loadFiles()
+ await loadRobot();
+ isLoading.value = false
+ animate();
+ } catch (error) {
+ console.error('[UrdfView] 模型加载失败:', error)
+ loadError.value = error?.message || '模型加载失败'
+ isLoading.value = false
+ ElMessage.error(loadError.value)
+ }
});
onBeforeUnmount(() => {
if (animationId.value) cancelAnimationFrame(animationId.value);
window.removeEventListener('resize', onResize);
+ containerResizeObserver?.disconnect()
+ renderer?.domElement.removeEventListener('webglcontextlost', onWebGLContextLost)
renderer?.dispose();
controls?.dispose();
});
@@ -285,6 +334,20 @@ defineExpose({ updateJoint, resetJoints, jointControls });
height: 100% !important;
}
}
+
+ .viewer-status {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $color-text-secondary;
+ pointer-events: none;
+
+ &--error {
+ color: $color-danger;
+ }
+ }
}
diff --git a/src/views/chassis/canvasUtils.js b/src/views/chassis/canvasUtils.js
index bb38f23..d12254f 100644
--- a/src/views/chassis/canvasUtils.js
+++ b/src/views/chassis/canvasUtils.js
@@ -175,7 +175,7 @@ export const drawAdvancedAreaList = (canvasCtx, map, dataStore) => {
centroidWorldX /= vertices.length; // 求平均得到质心 X
centroidWorldY /= vertices.length; // 求平均得到质心 Y
const centroidScreen = worldToScreen(centroidWorldX, centroidWorldY, store);
- canvasCtx.font = '500 8px "JetBrains Mono"'; // 标签字体
+ canvasCtx.font = '500 16px "JetBrains Mono"'; // 标签字体
canvasCtx.fillStyle = strokeColorCss; // 与描边同色
canvasCtx.textAlign = 'center'; // 居中对齐
canvasCtx.fillText(area.instanceName || '', centroidScreen.x, centroidScreen.y + 3); // 绘制文字
@@ -441,7 +441,7 @@ export const drawAdvancedPointList = (canvasCtx, map, dataStore) => {
}
// ── 名称标签 ──
- canvasCtx.font = '500 7px "JetBrains Mono"';
+ canvasCtx.font = '500 16px "JetBrains Mono"';
canvasCtx.fillStyle = hexToCssRgba(pointColorHex, 0.7);
canvasCtx.textAlign = 'center';
canvasCtx.fillText(advPoint.instanceName || '', pointScreen.x, pointScreen.y - diamondRadius - 4);