暂存本地
This commit is contained in:
277
src/components/ThreeViewer2.vue
Normal file
277
src/components/ThreeViewer2.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user