feat: 调整pad端模型加载

This commit is contained in:
zhanghao 2026-08-05 17:09:58 +08:00
parent 4602ec9e74
commit 9fbd2a6ddd
3 changed files with 76 additions and 12 deletions

View File

@ -121,7 +121,8 @@ export class XacroAdapter {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const loader = new URDFLoader(); const loader = new URDFLoader();
loader.parseCollision = true; // 查看器只渲染 visual跳过碰撞网格可显著降低 Pad 的内存占用。
loader.parseCollision = false;
// Extract directory where URDF file is located // Extract directory where URDF file is located
const urdfDir = workingPath; const urdfDir = workingPath;

View File

@ -3,6 +3,8 @@
<div ref="viewerContainer" class="viewer-container"> <div ref="viewerContainer" class="viewer-container">
<!-- Three.js 渲染 --> <!-- Three.js 渲染 -->
</div> </div>
<div v-if="isLoading" class="viewer-status">模型加载中...</div>
<div v-else-if="loadError" class="viewer-status viewer-status--error">{{ loadError }}</div>
</div> </div>
</template> </template>
@ -15,6 +17,7 @@ import URDFLoader from 'urdf-loader';
import { XacroAdapter } from '@/adapters/XacroAdapter.js' import { XacroAdapter } from '@/adapters/XacroAdapter.js'
import { URDFAdapter } from '@/adapters/URDFAdapter.js' import { URDFAdapter } from '@/adapters/URDFAdapter.js'
import { getThemeColor } from '@/utils/themeColors.js' import { getThemeColor } from '@/utils/themeColors.js'
import { ElMessage } from 'element-plus'
const props = defineProps({ const props = defineProps({
modelPath: { modelPath: {
@ -49,6 +52,9 @@ const modelDirectory = computed(() => modelUrl.value.slice(0, modelUrl.value.las
const viewerContainer = ref(null); const viewerContainer = ref(null);
let scene, camera, renderer, controls; let scene, camera, renderer, controls;
let containerResizeObserver;
const isLoading = ref(true)
const loadError = ref('')
const animationId = ref(null); const animationId = ref(null);
const jointControls = reactive([]); const jointControls = reactive([]);
@ -65,8 +71,13 @@ const initScene = () => {
camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000); camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
camera.position.set(0, 0, 4); 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.setSize(width, height);
renderer.domElement.addEventListener('webglcontextlost', onWebGLContextLost)
container.appendChild(renderer.domElement); container.appendChild(renderer.domElement);
@ -116,6 +127,11 @@ const resetJoints = () => {
}; };
const fileMap = new Map() 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 resolveReferencedUrl = (reference, parentUrl) => {
const normalized = reference.replace(/\\/g, '/') const normalized = reference.replace(/\\/g, '/')
const rosPath = normalized.match(/^(?:file:\/\/)?\$\(find\s+[^)]+\)\/(.+)$/) const rosPath = normalized.match(/^(?:file:\/\/)?\$\(find\s+[^)]+\)\/(.+)$/)
@ -151,7 +167,10 @@ const loadFiles = async () => {
const references = [...content.matchAll(/\b(?:filename|url)=["']([^"']+)["']/gi)] const references = [...content.matchAll(/\b(?:filename|url)=["']([^"']+)["']/gi)]
references.forEach(([, reference]) => { references.forEach(([, reference]) => {
const referencedUrl = resolveReferencedUrl(reference, url) 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 parseUrdf = async (urdfContent) => {
const loader = new URDFLoader() const loader = new URDFLoader()
loader.parseCollision = true // Pad STL
loader.parseCollision = false
const packageNames = [...urdfContent.matchAll(/package:\/\/([^/]+)/g)].map(match => match[1]) const packageNames = [...urdfContent.matchAll(/package:\/\/([^/]+)/g)].map(match => match[1])
loader.packages = Object.fromEntries( loader.packages = Object.fromEntries(
@ -196,6 +216,7 @@ const loadRobot = async () => {
currentModel.value = model currentModel.value = model
robotInstance = robot; robotInstance = robot;
fileMap.clear()
// //
let jointEntries = []; 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 () => { onMounted(async () => {
await nextTick(); try {
await loadFiles() await nextTick();
initScene(); await waitForContainerSize()
await loadRobot(); initScene();
animate(); containerResizeObserver = new ResizeObserver(onResize)
window.addEventListener('resize', 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(() => { onBeforeUnmount(() => {
if (animationId.value) cancelAnimationFrame(animationId.value); if (animationId.value) cancelAnimationFrame(animationId.value);
window.removeEventListener('resize', onResize); window.removeEventListener('resize', onResize);
containerResizeObserver?.disconnect()
renderer?.domElement.removeEventListener('webglcontextlost', onWebGLContextLost)
renderer?.dispose(); renderer?.dispose();
controls?.dispose(); controls?.dispose();
}); });
@ -285,6 +334,20 @@ defineExpose({ updateJoint, resetJoints, jointControls });
height: 100% !important; 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;
}
}
} }

View File

@ -175,7 +175,7 @@ export const drawAdvancedAreaList = (canvasCtx, map, dataStore) => {
centroidWorldX /= vertices.length; // 求平均得到质心 X centroidWorldX /= vertices.length; // 求平均得到质心 X
centroidWorldY /= vertices.length; // 求平均得到质心 Y centroidWorldY /= vertices.length; // 求平均得到质心 Y
const centroidScreen = worldToScreen(centroidWorldX, centroidWorldY, store); const centroidScreen = worldToScreen(centroidWorldX, centroidWorldY, store);
canvasCtx.font = '500 8px "JetBrains Mono"'; // 标签字体 canvasCtx.font = '500 16px "JetBrains Mono"'; // 标签字体
canvasCtx.fillStyle = strokeColorCss; // 与描边同色 canvasCtx.fillStyle = strokeColorCss; // 与描边同色
canvasCtx.textAlign = 'center'; // 居中对齐 canvasCtx.textAlign = 'center'; // 居中对齐
canvasCtx.fillText(area.instanceName || '', centroidScreen.x, centroidScreen.y + 3); // 绘制文字 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.fillStyle = hexToCssRgba(pointColorHex, 0.7);
canvasCtx.textAlign = 'center'; canvasCtx.textAlign = 'center';
canvasCtx.fillText(advPoint.instanceName || '', pointScreen.x, pointScreen.y - diamondRadius - 4); canvasCtx.fillText(advPoint.instanceName || '', pointScreen.x, pointScreen.y - diamondRadius - 4);