Files
vue-model-review/src/components/ThreeViewer2.vue

278 lines
7.8 KiB
Vue
Raw Normal View History

2026-04-27 09:57:00 +08:00
<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>