598 lines
17 KiB
Vue
598 lines
17 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="three-viewer">
|
|||
|
|
<div ref="container" class="canvas-container"></div>
|
|||
|
|
<div class="annotations-container" ref="annotationsContainer"></div>
|
|||
|
|
|
|||
|
|
<!-- 调试面板 -->
|
|||
|
|
<div v-if="debugMode" class="debug-panel">
|
|||
|
|
<h3>调试模式</h3>
|
|||
|
|
<div class="debug-info">
|
|||
|
|
<p>点击模型查看部件信息</p>
|
|||
|
|
<div v-if="selectedMeshInfo">
|
|||
|
|
<p><strong>部件名称:</strong> {{ selectedMeshInfo.name || '未命名' }}</p>
|
|||
|
|
<p><strong>坐标:</strong></p>
|
|||
|
|
<pre>{{ JSON.stringify(selectedMeshInfo.position, null, 2) }}</pre>
|
|||
|
|
<button @click="copyPosition">复制坐标</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="mesh-list">
|
|||
|
|
<h4>模型部件列表:</h4>
|
|||
|
|
<ul>
|
|||
|
|
<li v-for="(mesh, index) in meshList" :key="index">
|
|||
|
|
{{ mesh.name || `未命名_${index}` }}
|
|||
|
|
</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import * as THREE from 'three';
|
|||
|
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|||
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|||
|
|
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer';
|
|||
|
|
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
|||
|
|
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
|
|||
|
|
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
|
|||
|
|
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'ThreeViewerDebug',
|
|||
|
|
props: {
|
|||
|
|
modelPath: {
|
|||
|
|
type: String,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
annotations: {
|
|||
|
|
type: Array,
|
|||
|
|
default: () => []
|
|||
|
|
},
|
|||
|
|
debugMode: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
scene: null,
|
|||
|
|
camera: null,
|
|||
|
|
renderer: null,
|
|||
|
|
labelRenderer: null,
|
|||
|
|
controls: null,
|
|||
|
|
model: null,
|
|||
|
|
composer: null,
|
|||
|
|
outlinePass: null,
|
|||
|
|
frameId: null,
|
|||
|
|
sceneDomElement: null,
|
|||
|
|
annotationObjects: [],
|
|||
|
|
selectedPart: null,
|
|||
|
|
partMeshMap: {},
|
|||
|
|
meshList: [],
|
|||
|
|
selectedMeshInfo: null,
|
|||
|
|
raycaster: new THREE.Raycaster(),
|
|||
|
|
mouse: new THREE.Vector2()
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
watch: {
|
|||
|
|
modelPath: {
|
|||
|
|
handler(v) {
|
|||
|
|
if (v) {
|
|||
|
|
this.cleanup();
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.init();
|
|||
|
|
}, 300);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
immediate: true
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
mounted() {
|
|||
|
|
this.resizeObserver = new ResizeObserver(this.onWindowResize);
|
|||
|
|
this.resizeObserver.observe(this.$refs.container);
|
|||
|
|
|
|||
|
|
// 添加点击事件
|
|||
|
|
window.addEventListener('click', this.onMouseClick, false);
|
|||
|
|
},
|
|||
|
|
beforeDestroy() {
|
|||
|
|
this.cleanup();
|
|||
|
|
window.removeEventListener('click', this.onMouseClick, false);
|
|||
|
|
if (this.resizeObserver) {
|
|||
|
|
this.resizeObserver.disconnect();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
cleanup() {
|
|||
|
|
if (this.frameId) {
|
|||
|
|
cancelAnimationFrame(this.frameId);
|
|||
|
|
}
|
|||
|
|
if (this.scene) {
|
|||
|
|
this.scene.clear();
|
|||
|
|
this.scene = null;
|
|||
|
|
}
|
|||
|
|
if (this.renderer) {
|
|||
|
|
this.renderer.dispose();
|
|||
|
|
this.renderer = null;
|
|||
|
|
}
|
|||
|
|
if (this.labelRenderer) {
|
|||
|
|
this.labelRenderer.domElement.remove();
|
|||
|
|
this.labelRenderer = null;
|
|||
|
|
}
|
|||
|
|
if (this.sceneDomElement) {
|
|||
|
|
this.sceneDomElement.innerHTML = '';
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
init() {
|
|||
|
|
this.sceneDomElement = this.$refs.container;
|
|||
|
|
|
|||
|
|
// 创建场景
|
|||
|
|
this.scene = new THREE.Scene();
|
|||
|
|
this.scene.background = new THREE.Color(0xf0f0f0);
|
|||
|
|
|
|||
|
|
// 创建相机
|
|||
|
|
const width = this.sceneDomElement.clientWidth;
|
|||
|
|
const height = this.sceneDomElement.clientHeight;
|
|||
|
|
this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
|||
|
|
this.camera.position.set(5, 5, 5);
|
|||
|
|
|
|||
|
|
// WebGL渲染器
|
|||
|
|
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|||
|
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|||
|
|
this.renderer.setSize(width, height);
|
|||
|
|
this.renderer.shadowMap.enabled = true;
|
|||
|
|
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
|||
|
|
this.sceneDomElement.appendChild(this.renderer.domElement);
|
|||
|
|
|
|||
|
|
// CSS2D渲染器
|
|||
|
|
this.labelRenderer = new CSS2DRenderer();
|
|||
|
|
this.labelRenderer.setSize(width, height);
|
|||
|
|
this.labelRenderer.domElement.style.position = 'absolute';
|
|||
|
|
this.labelRenderer.domElement.style.top = '0';
|
|||
|
|
this.labelRenderer.domElement.style.pointerEvents = 'none';
|
|||
|
|
this.$refs.annotationsContainer.appendChild(this.labelRenderer.domElement);
|
|||
|
|
|
|||
|
|
// 光照
|
|||
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|||
|
|
this.scene.add(ambientLight);
|
|||
|
|
|
|||
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|||
|
|
directionalLight.position.set(5, 10, 5);
|
|||
|
|
directionalLight.castShadow = true;
|
|||
|
|
this.scene.add(directionalLight);
|
|||
|
|
|
|||
|
|
// 环境光
|
|||
|
|
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
|
|||
|
|
const env = pmremGenerator.fromScene(new RoomEnvironment(this.renderer), 0.04).texture;
|
|||
|
|
this.scene.environment = env;
|
|||
|
|
|
|||
|
|
// 控制器
|
|||
|
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
|||
|
|
this.controls.enableDamping = true;
|
|||
|
|
this.controls.dampingFactor = 0.05;
|
|||
|
|
|
|||
|
|
// 加载模型
|
|||
|
|
this.loadModel();
|
|||
|
|
|
|||
|
|
// 开始动画
|
|||
|
|
this.animate();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
loadModel() {
|
|||
|
|
const loader = new GLTFLoader();
|
|||
|
|
|
|||
|
|
loader.load(
|
|||
|
|
this.modelPath,
|
|||
|
|
(gltf) => {
|
|||
|
|
this.model = gltf.scene;
|
|||
|
|
|
|||
|
|
// 收集所有mesh信息
|
|||
|
|
this.partMeshMap = {};
|
|||
|
|
this.meshList = [];
|
|||
|
|
|
|||
|
|
this.model.traverse((child) => {
|
|||
|
|
if (child.isMesh) {
|
|||
|
|
child.castShadow = true;
|
|||
|
|
child.receiveShadow = true;
|
|||
|
|
|
|||
|
|
// 存储mesh引用
|
|||
|
|
const meshInfo = {
|
|||
|
|
name: child.name,
|
|||
|
|
mesh: child,
|
|||
|
|
parent: child.parent
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.meshList.push(meshInfo);
|
|||
|
|
|
|||
|
|
if (child.name) {
|
|||
|
|
this.partMeshMap[child.name] = child;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('模型部件列表:', this.meshList.map(m => m.name || '未命名'));
|
|||
|
|
console.log('部件映射表:', Object.keys(this.partMeshMap));
|
|||
|
|
|
|||
|
|
// 计算模型边界并居中
|
|||
|
|
const box = new THREE.Box3().setFromObject(this.model);
|
|||
|
|
const center = box.getCenter(new THREE.Vector3());
|
|||
|
|
const size = box.getSize(new THREE.Vector3());
|
|||
|
|
|
|||
|
|
this.model.position.sub(center);
|
|||
|
|
|
|||
|
|
// 调整相机
|
|||
|
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|||
|
|
const fov = this.camera.fov * (Math.PI / 180);
|
|||
|
|
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
|
|||
|
|
cameraZ *= 2.5;
|
|||
|
|
|
|||
|
|
this.camera.position.set(cameraZ, cameraZ, cameraZ);
|
|||
|
|
this.camera.lookAt(0, 0, 0);
|
|||
|
|
this.controls.target.set(0, 0, 0);
|
|||
|
|
|
|||
|
|
this.scene.add(this.model);
|
|||
|
|
|
|||
|
|
// 创建标注
|
|||
|
|
this.createAnnotations();
|
|||
|
|
|
|||
|
|
this.$emit('model-loaded', {
|
|||
|
|
meshList: this.meshList,
|
|||
|
|
partMeshMap: Object.keys(this.partMeshMap)
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
(progress) => {
|
|||
|
|
const percent = (progress.loaded / progress.total) * 100;
|
|||
|
|
this.$emit('loading-progress', percent);
|
|||
|
|
},
|
|||
|
|
(error) => {
|
|||
|
|
console.error('模型加载失败:', error);
|
|||
|
|
this.$emit('model-error', error);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
createAnnotations() {
|
|||
|
|
// 清除旧标注
|
|||
|
|
this.annotationObjects.forEach(obj => {
|
|||
|
|
this.scene.remove(obj);
|
|||
|
|
});
|
|||
|
|
this.annotationObjects = [];
|
|||
|
|
|
|||
|
|
this.annotations.forEach((annotation, index) => {
|
|||
|
|
// 尝试通过名称匹配找到对应的mesh
|
|||
|
|
const mesh = this.findMeshByName(annotation.name);
|
|||
|
|
|
|||
|
|
let position = new THREE.Vector3();
|
|||
|
|
|
|||
|
|
if (mesh) {
|
|||
|
|
// 如果找到对应mesh,使用其位置
|
|||
|
|
mesh.getWorldPosition(position);
|
|||
|
|
|
|||
|
|
// 获取mesh的包围盒,标注放在上方
|
|||
|
|
const box = new THREE.Box3().setFromObject(mesh);
|
|||
|
|
const size = box.getSize(new THREE.Vector3());
|
|||
|
|
position.y += size.y / 2 + 0.3;
|
|||
|
|
|
|||
|
|
console.log(`找到匹配: ${annotation.name} -> ${mesh.name}`, position);
|
|||
|
|
} else {
|
|||
|
|
// 如果没找到,使用圆形分布
|
|||
|
|
const radius = 2;
|
|||
|
|
const angleStep = (Math.PI * 2) / this.annotations.length;
|
|||
|
|
const angle = angleStep * index;
|
|||
|
|
position.set(
|
|||
|
|
Math.cos(angle) * radius,
|
|||
|
|
1 + Math.sin(index) * 0.5,
|
|||
|
|
Math.sin(angle) * radius
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
console.warn(`未找到匹配: ${annotation.name},使用默认位置`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建标注标签
|
|||
|
|
const labelDiv = document.createElement('div');
|
|||
|
|
labelDiv.className = 'annotation-label';
|
|||
|
|
labelDiv.textContent = `${index + 1} ${annotation.name}`;
|
|||
|
|
labelDiv.style.cssText = `
|
|||
|
|
background: rgba(64, 158, 255, 0.9);
|
|||
|
|
color: white;
|
|||
|
|
padding: 5px 10px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
pointer-events: auto;
|
|||
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
labelDiv.addEventListener('click', () => {
|
|||
|
|
this.onAnnotationClick(annotation, index, mesh);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const label = new CSS2DObject(labelDiv);
|
|||
|
|
label.position.copy(position);
|
|||
|
|
label.userData = { annotation, index, mesh };
|
|||
|
|
|
|||
|
|
this.scene.add(label);
|
|||
|
|
this.annotationObjects.push(label);
|
|||
|
|
|
|||
|
|
// 创建连接线
|
|||
|
|
if (mesh) {
|
|||
|
|
const meshPos = new THREE.Vector3();
|
|||
|
|
mesh.getWorldPosition(meshPos);
|
|||
|
|
|
|||
|
|
const points = [meshPos, position];
|
|||
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|||
|
|
const material = new THREE.LineBasicMaterial({
|
|||
|
|
color: 0x409eff,
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.6
|
|||
|
|
});
|
|||
|
|
const line = new THREE.Line(geometry, material);
|
|||
|
|
this.scene.add(line);
|
|||
|
|
this.annotationObjects.push(line);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
findMeshByName(annotationName) {
|
|||
|
|
// 精确匹配
|
|||
|
|
if (this.partMeshMap[annotationName]) {
|
|||
|
|
return this.partMeshMap[annotationName];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模糊匹配
|
|||
|
|
for (let meshName in this.partMeshMap) {
|
|||
|
|
if (meshName.includes(annotationName) || annotationName.includes(meshName)) {
|
|||
|
|
return this.partMeshMap[meshName];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 尝试匹配关键词
|
|||
|
|
const keywords = annotationName.split(/[,、\s]+/);
|
|||
|
|
for (let keyword of keywords) {
|
|||
|
|
if (keyword.length > 1) {
|
|||
|
|
for (let meshName in this.partMeshMap) {
|
|||
|
|
if (meshName.includes(keyword)) {
|
|||
|
|
return this.partMeshMap[meshName];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onAnnotationClick(annotation, index, mesh) {
|
|||
|
|
this.highlightPart(index, mesh);
|
|||
|
|
this.$emit('annotation-click', annotation, index);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
highlightPart(index, mesh) {
|
|||
|
|
// 重置之前的高亮
|
|||
|
|
this.annotationObjects.forEach((obj) => {
|
|||
|
|
if (obj.isLine) {
|
|||
|
|
obj.material.opacity = 0.6;
|
|||
|
|
obj.material.color.setHex(0x409eff);
|
|||
|
|
} else if (obj.element) {
|
|||
|
|
obj.element.style.background = 'rgba(64, 158, 255, 0.9)';
|
|||
|
|
obj.element.style.transform = 'scale(1)';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 高亮选中的标注
|
|||
|
|
this.selectedPart = index;
|
|||
|
|
this.annotationObjects.forEach((obj, i) => {
|
|||
|
|
const objIndex = Math.floor(i / 2);
|
|||
|
|
if (objIndex === index) {
|
|||
|
|
if (obj.isLine) {
|
|||
|
|
obj.material.opacity = 1;
|
|||
|
|
obj.material.color.setHex(0xff6b00);
|
|||
|
|
} else if (obj.element) {
|
|||
|
|
obj.element.style.background = 'rgba(255, 107, 0, 0.95)';
|
|||
|
|
obj.element.style.transform = 'scale(1.1)';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 高亮3D部件
|
|||
|
|
if (mesh) {
|
|||
|
|
this.highlightMesh(mesh);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
highlightMesh(mesh) {
|
|||
|
|
// 创建高亮效果
|
|||
|
|
if (!this.composer) {
|
|||
|
|
this.composer = new EffectComposer(this.renderer);
|
|||
|
|
const renderPass = new RenderPass(this.scene, this.camera);
|
|||
|
|
this.composer.addPass(renderPass);
|
|||
|
|
|
|||
|
|
this.outlinePass = new OutlinePass(
|
|||
|
|
new THREE.Vector2(this.sceneDomElement.clientWidth, this.sceneDomElement.clientHeight),
|
|||
|
|
this.scene,
|
|||
|
|
this.camera
|
|||
|
|
);
|
|||
|
|
this.outlinePass.edgeStrength = 5.0;
|
|||
|
|
this.outlinePass.edgeGlow = 0.5;
|
|||
|
|
this.outlinePass.edgeThickness = 2.0;
|
|||
|
|
this.outlinePass.pulsePeriod = 2;
|
|||
|
|
this.outlinePass.visibleEdgeColor.set(0xff6b00);
|
|||
|
|
this.composer.addPass(this.outlinePass);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.outlinePass.selectedObjects = [mesh.parent || mesh];
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onMouseClick(event) {
|
|||
|
|
if (!this.debugMode || !this.sceneDomElement) return;
|
|||
|
|
|
|||
|
|
const rect = this.sceneDomElement.getBoundingClientRect();
|
|||
|
|
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|||
|
|
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|||
|
|
|
|||
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|||
|
|
const intersects = this.raycaster.intersectObjects(this.model.children, true);
|
|||
|
|
|
|||
|
|
if (intersects.length > 0) {
|
|||
|
|
const mesh = intersects[0].object;
|
|||
|
|
const point = intersects[0].point;
|
|||
|
|
|
|||
|
|
this.selectedMeshInfo = {
|
|||
|
|
name: mesh.name,
|
|||
|
|
position: {
|
|||
|
|
x: parseFloat(point.x.toFixed(3)),
|
|||
|
|
y: parseFloat(point.y.toFixed(3)),
|
|||
|
|
z: parseFloat(point.z.toFixed(3))
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
console.log('点击的部件:', this.selectedMeshInfo);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
copyPosition() {
|
|||
|
|
if (this.selectedMeshInfo) {
|
|||
|
|
const text = JSON.stringify(this.selectedMeshInfo.position, null, 2);
|
|||
|
|
navigator.clipboard.writeText(text).then(() => {
|
|||
|
|
this.$message.success('坐标已复制到剪贴板');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
animate() {
|
|||
|
|
this.frameId = requestAnimationFrame(this.animate);
|
|||
|
|
|
|||
|
|
if (this.controls) {
|
|||
|
|
this.controls.update();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.renderer && this.scene && this.camera) {
|
|||
|
|
if (this.composer && this.outlinePass && this.outlinePass.selectedObjects.length > 0) {
|
|||
|
|
this.composer.render();
|
|||
|
|
} else {
|
|||
|
|
this.renderer.render(this.scene, this.camera);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.labelRenderer && this.scene && this.camera) {
|
|||
|
|
this.labelRenderer.render(this.scene, this.camera);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onWindowResize() {
|
|||
|
|
if (this.camera && this.renderer && this.sceneDomElement) {
|
|||
|
|
const width = this.sceneDomElement.clientWidth;
|
|||
|
|
const height = this.sceneDomElement.clientHeight;
|
|||
|
|
|
|||
|
|
this.camera.aspect = width / height;
|
|||
|
|
this.camera.updateProjectionMatrix();
|
|||
|
|
|
|||
|
|
this.renderer.setSize(width, height);
|
|||
|
|
this.labelRenderer.setSize(width, height);
|
|||
|
|
|
|||
|
|
if (this.composer) {
|
|||
|
|
this.composer.setSize(width, height);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.three-viewer {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.canvas-container {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.annotations-container {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.debug-panel {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 10px;
|
|||
|
|
right: 10px;
|
|||
|
|
background: rgba(255, 255, 255, 0.95);
|
|||
|
|
padding: 15px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
max-width: 300px;
|
|||
|
|
max-height: 80%;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|||
|
|
pointer-events: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.debug-panel h3 {
|
|||
|
|
margin: 0 0 10px 0;
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: #303133;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.debug-panel h4 {
|
|||
|
|
margin: 10px 0 5px 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #606266;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.debug-info {
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
padding-bottom: 15px;
|
|||
|
|
border-bottom: 1px solid #dcdfe6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.debug-info p {
|
|||
|
|
margin: 5px 0;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #606266;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.debug-info pre {
|
|||
|
|
background: #f5f7fa;
|
|||
|
|
padding: 8px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
margin: 5px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.debug-info button {
|
|||
|
|
margin-top: 8px;
|
|||
|
|
padding: 5px 12px;
|
|||
|
|
background: #409eff;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.debug-info button:hover {
|
|||
|
|
background: #66b1ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mesh-list ul {
|
|||
|
|
margin: 0;
|
|||
|
|
padding-left: 20px;
|
|||
|
|
max-height: 300px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mesh-list li {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #606266;
|
|||
|
|
margin: 3px 0;
|
|||
|
|
word-break: break-all;
|
|||
|
|
}
|
|||
|
|
</style>
|