Files
vue-model-review/src/components/ThreeViewer2.vue
2026-04-27 09:57:00 +08:00

278 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="three-viewer">
<div ref="container" class="canvas-container"></div>
<div class="annotations-container" ref="annotationsContainer"></div>
</div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
export default {
name: 'ThreeViewer',
props: {
modelPath: {
type: String,
required: true
},
annotations: {
type: Array,
default: () => []
}
},
data() {
return {
scene: null,
camera: null,
renderer: null,
labelRenderer: null,
controls: null,
model: null,
annotationObjects: [],
selectedPart: null
}
},
mounted() {
this.initThree()
this.loadModel()
this.animate()
window.addEventListener('resize', this.onWindowResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.onWindowResize)
if (this.renderer) {
this.renderer.dispose()
}
if (this.labelRenderer) {
this.labelRenderer.domElement.remove()
}
},
methods: {
initThree() {
// 场景
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0xf0f0f0)
// 相机
const container = this.$refs.container
const width = container.clientWidth
const height = container.clientHeight
this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
this.camera.position.set(5, 5, 5)
// 渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setSize(width, height)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.renderer.shadowMap.enabled = true
container.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)
// 控制器
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
this.controls.dampingFactor = 0.05
// 光照
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 gridHelper = new THREE.GridHelper(10, 10)
// this.scene.add(gridHelper)
},
loadModel() {
const loader = new GLTFLoader()
console.log(this.modelPath)
loader.load(
this.modelPath,
(gltf) => {
this.model = gltf.scene
// 计算模型边界并居中
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')
},
(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 = []
// 创建标注点的预设位置(圆形分布)
const radius = 2
const angleStep = (Math.PI * 2) / this.annotations.length
this.annotations.forEach((annotation, index) => {
const angle = angleStep * index
const x = Math.cos(angle) * radius
const z = Math.sin(angle) * radius
const y = 1 + Math.sin(index) * 0.5
// 创建标注标签
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;
`
labelDiv.addEventListener('click', () => {
this.onAnnotationClick(annotation, index)
})
const label = new CSS2DObject(labelDiv)
label.position.set(x, y, z)
label.userData = { annotation, index }
this.scene.add(label)
this.annotationObjects.push(label)
// 创建连接线
const points = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(x, y, z)
]
const geometry = new THREE.BufferGeometry().setFromPoints(points)
const material = new THREE.LineBasicMaterial({
color: 0x409eff,
transparent: true,
opacity: 0.5
})
const line = new THREE.Line(geometry, material)
this.scene.add(line)
this.annotationObjects.push(line)
})
},
onAnnotationClick(annotation, index) {
this.highlightPart(index)
this.$emit('annotation-click', annotation, index)
},
highlightPart(index) {
// 重置之前的高亮
if (this.selectedPart !== null) {
this.annotationObjects.forEach((obj, i) => {
if (obj.isLine) {
obj.material.opacity = 0.5
} 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)'
}
}
})
},
animate() {
requestAnimationFrame(this.animate)
this.controls.update()
this.renderer.render(this.scene, this.camera)
this.labelRenderer.render(this.scene, this.camera)
},
onWindowResize() {
const container = this.$refs.container
const width = container.clientWidth
const height = container.clientHeight
this.camera.aspect = width / height
this.camera.updateProjectionMatrix()
this.renderer.setSize(width, height)
this.labelRenderer.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;
}
</style>