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);