feat: 调整pad端模型加载
This commit is contained in:
parent
4602ec9e74
commit
9fbd2a6ddd
@ -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;
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
try {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await loadFiles()
|
await waitForContainerSize()
|
||||||
initScene();
|
initScene();
|
||||||
await loadRobot();
|
containerResizeObserver = new ResizeObserver(onResize)
|
||||||
animate();
|
containerResizeObserver.observe(viewerContainer.value)
|
||||||
window.addEventListener('resize', onResize);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user