|
|
@@ -0,0 +1,941 @@
|
|
|
+<template>
|
|
|
+ <div class="cesium-three-fusion">
|
|
|
+ <!-- 控制面板 -->
|
|
|
+ <div class="control-panel">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :class="{ 'is-exploded': isExploded }"
|
|
|
+ @click="toggleExplosion"
|
|
|
+ >
|
|
|
+ <el-icon><Expand v-if="!isExploded" /><Fold v-else /></el-icon>
|
|
|
+ {{ isExploded ? '还原模型' : '炸开模型' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="warning"
|
|
|
+ @click="flyToModel"
|
|
|
+ >
|
|
|
+ <el-icon><Aim /></el-icon>
|
|
|
+ 定位模型
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="info"
|
|
|
+ @click="toggleWireframe"
|
|
|
+ >
|
|
|
+ <el-icon><Grid /></el-icon>
|
|
|
+ {{ isWireframe ? '实体模式' : '线框模式' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 爆炸程度滑块 -->
|
|
|
+ <div class="explosion-slider" v-if="isExploded || explosionFactor > 0">
|
|
|
+ <span class="slider-label">爆炸程度</span>
|
|
|
+ <el-slider
|
|
|
+ v-model="explosionFactor"
|
|
|
+ :min="0"
|
|
|
+ :max="3"
|
|
|
+ :step="0.1"
|
|
|
+ @input="updateExplosion"
|
|
|
+ />
|
|
|
+ <span class="slider-value">{{ explosionFactor.toFixed(1) }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ <!-- 模型信息面板 -->
|
|
|
+ <div class="model-info-panel" v-if="modelLoaded">
|
|
|
+ <h4>启闭机模型</h4>
|
|
|
+ <p>经度: {{ longitude }}</p>
|
|
|
+ <p>纬度: {{ latitude }}</p>
|
|
|
+ <p>高度: {{ height }}m</p>
|
|
|
+ <p>部件数量: {{ partCount }}</p>
|
|
|
+ <p>状态: {{ isExploded ? '已炸开' : '正常' }}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 说明面板 -->
|
|
|
+ <div class="instruction-panel">
|
|
|
+ <h4>操作说明</h4>
|
|
|
+ <ul>
|
|
|
+ <li>左键拖动:旋转视角</li>
|
|
|
+ <li>右键拖动:平移视角</li>
|
|
|
+ <li>滚轮:缩放</li>
|
|
|
+ <li>点击"炸开模型":查看拆解动画</li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
+import * as THREE from 'three'
|
|
|
+// 尝试不同的GLTFLoader导入方式
|
|
|
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|
|
+console.log('GLTFLoader 导入:', GLTFLoader)
|
|
|
+import { Expand, Fold, Aim, Grid } from '@element-plus/icons-vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ // 模型位置(经纬度)
|
|
|
+ longitude: {
|
|
|
+ type: Number,
|
|
|
+ default: 118.852918
|
|
|
+ },
|
|
|
+ latitude: {
|
|
|
+ type: Number,
|
|
|
+ default: 25.228317
|
|
|
+ },
|
|
|
+ height: {
|
|
|
+ type: Number,
|
|
|
+ default: 10
|
|
|
+ },
|
|
|
+ // 模型路径
|
|
|
+ modelPath: {
|
|
|
+ type: String,
|
|
|
+ default: '/models/启闭机.glb'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['model-loaded', 'explosion-complete', 'reset-complete'])
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const isExploded = ref(false)
|
|
|
+const isWireframe = ref(false)
|
|
|
+const explosionFactor = ref(0)
|
|
|
+const modelLoaded = ref(false)
|
|
|
+const partCount = ref(0)
|
|
|
+
|
|
|
+// Three.js 和 Cesium 相关变量
|
|
|
+let scene = null
|
|
|
+let camera = null
|
|
|
+let renderer = null
|
|
|
+let model = null
|
|
|
+let animationId = null
|
|
|
+let threeContainer = null
|
|
|
+let renderInterval = null // 添加渲染循环的interval ID
|
|
|
+
|
|
|
+// 存储原始位置和爆炸方向
|
|
|
+const originalTransforms = new Map()
|
|
|
+const explosionDirections = new Map()
|
|
|
+
|
|
|
+// 初始化 Three.js 场景并与 Cesium 融合
|
|
|
+const initThreeScene = () => {
|
|
|
+ console.log('开始初始化 Three.js 场景')
|
|
|
+ // 获取 Cesium 的 canvas
|
|
|
+ const cesiumCanvas = document.querySelector('.cesium-viewer canvas')
|
|
|
+ console.log('Cesium canvas:', cesiumCanvas)
|
|
|
+ if (!cesiumCanvas) {
|
|
|
+ console.error('Cesium canvas 未找到')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建 Three.js 场景
|
|
|
+ scene = new THREE.Scene()
|
|
|
+ console.log('Three.js 场景创建成功')
|
|
|
+
|
|
|
+ // 创建相机
|
|
|
+ camera = new THREE.PerspectiveCamera(60, cesiumCanvas.width / cesiumCanvas.height, 0.01, 10000000)
|
|
|
+ console.log('Three.js 相机创建成功')
|
|
|
+ console.log('相机配置:', {
|
|
|
+ fov: 60,
|
|
|
+ aspect: cesiumCanvas.width / cesiumCanvas.height,
|
|
|
+ near: 0.01,
|
|
|
+ far: 10000000
|
|
|
+ })
|
|
|
+
|
|
|
+ // 创建独立的 Three.js canvas
|
|
|
+ renderer = new THREE.WebGLRenderer({
|
|
|
+ antialias: true,
|
|
|
+ alpha: true,
|
|
|
+ preserveDrawingBuffer: true
|
|
|
+ })
|
|
|
+ console.log('Three.js 渲染器创建成功')
|
|
|
+
|
|
|
+ // 设置渲染器尺寸与 Cesium canvas 相同
|
|
|
+ renderer.setSize(cesiumCanvas.width, cesiumCanvas.height)
|
|
|
+ renderer.setPixelRatio(window.devicePixelRatio)
|
|
|
+ renderer.domElement.style.position = 'absolute'
|
|
|
+ renderer.domElement.style.top = '0'
|
|
|
+ renderer.domElement.style.left = '0'
|
|
|
+ renderer.domElement.style.width = '100%'
|
|
|
+ renderer.domElement.style.height = '100%'
|
|
|
+ renderer.domElement.style.pointerEvents = 'none'
|
|
|
+ renderer.domElement.style.zIndex = '100'
|
|
|
+
|
|
|
+ // 将 Three.js canvas 添加到 Cesium canvas 的父容器中
|
|
|
+ const cesiumContainer = cesiumCanvas.parentElement
|
|
|
+ cesiumContainer.appendChild(renderer.domElement)
|
|
|
+ console.log('Three.js canvas 已添加到 Cesium 容器')
|
|
|
+
|
|
|
+ renderer.autoClear = true // 自动清除画布
|
|
|
+ renderer.shadowMap.enabled = true
|
|
|
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap
|
|
|
+ renderer.setClearColor(0x000000, 0) // 设置透明背景
|
|
|
+
|
|
|
+ // 添加光源
|
|
|
+ setupLights()
|
|
|
+ console.log('Three.js 光源设置成功')
|
|
|
+
|
|
|
+ // 监听 Cesium 渲染事件
|
|
|
+ if (window.viewer) {
|
|
|
+ console.log('添加 Cesium 渲染事件监听器')
|
|
|
+ window.viewer.scene.postRender.addEventListener(syncCamera)
|
|
|
+ } else {
|
|
|
+ console.error('window.viewer 未定义')
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('Three.js 场景初始化完成')
|
|
|
+
|
|
|
+ // 添加窗口大小变化监听器
|
|
|
+ window.addEventListener('resize', handleResize)
|
|
|
+ console.log('窗口大小变化监听器已添加')
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// 处理窗口大小变化
|
|
|
+const handleResize = () => {
|
|
|
+ if (!renderer || !camera) return
|
|
|
+
|
|
|
+ const cesiumCanvas = document.querySelector('.cesium-viewer canvas')
|
|
|
+ if (!cesiumCanvas) return
|
|
|
+
|
|
|
+ // 更新相机宽高比
|
|
|
+ camera.aspect = cesiumCanvas.width / cesiumCanvas.height
|
|
|
+ camera.updateProjectionMatrix()
|
|
|
+
|
|
|
+ // 更新渲染器尺寸
|
|
|
+ renderer.setSize(cesiumCanvas.width, cesiumCanvas.height)
|
|
|
+
|
|
|
+ console.log('窗口大小已更新:', cesiumCanvas.width, cesiumCanvas.height)
|
|
|
+}
|
|
|
+
|
|
|
+// 设置光源
|
|
|
+const setupLights = () => {
|
|
|
+ // 环境光
|
|
|
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
|
|
|
+ scene.add(ambientLight)
|
|
|
+
|
|
|
+ // 主光源(模拟太阳光)
|
|
|
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0)
|
|
|
+ directionalLight.position.set(100, 200, 100)
|
|
|
+ directionalLight.castShadow = true
|
|
|
+ scene.add(directionalLight)
|
|
|
+
|
|
|
+ // 辅助光源
|
|
|
+ const fillLight = new THREE.DirectionalLight(0x4080ff, 0.3)
|
|
|
+ fillLight.position.set(-100, 50, -100)
|
|
|
+ scene.add(fillLight)
|
|
|
+}
|
|
|
+
|
|
|
+// 同步 Cesium 相机到 Three.js
|
|
|
+const syncCamera = () => {
|
|
|
+ if (!window.viewer || !camera || !scene || !renderer) return
|
|
|
+
|
|
|
+ const cesiumCamera = window.viewer.camera
|
|
|
+
|
|
|
+ // 同步相机位置
|
|
|
+ camera.position.set(
|
|
|
+ cesiumCamera.position.x,
|
|
|
+ cesiumCamera.position.y,
|
|
|
+ cesiumCamera.position.z
|
|
|
+ )
|
|
|
+
|
|
|
+ // 同步相机方向
|
|
|
+ const direction = cesiumCamera.direction
|
|
|
+ const up = cesiumCamera.up
|
|
|
+ camera.lookAt(
|
|
|
+ camera.position.x + direction.x,
|
|
|
+ camera.position.y + direction.y,
|
|
|
+ camera.position.z + direction.z
|
|
|
+ )
|
|
|
+ camera.up.set(up.x, up.y, up.z)
|
|
|
+
|
|
|
+ // 同步投影矩阵
|
|
|
+ camera.projectionMatrix.fromArray(cesiumCamera.frustum.projectionMatrix)
|
|
|
+
|
|
|
+ // 渲染 Three.js 场景
|
|
|
+ renderer.render(scene, camera)
|
|
|
+}
|
|
|
+
|
|
|
+// 将经纬度转换为 Three.js 世界坐标(使用 Cesium 坐标系)
|
|
|
+const latLonToWorld = (lon, lat, alt) => {
|
|
|
+ if (window.viewer) {
|
|
|
+ const cartesian = Cesium.Cartesian3.fromDegrees(lon, lat, alt)
|
|
|
+ console.log('Cesium 坐标转换:', {
|
|
|
+ lon, lat, alt,
|
|
|
+ cartesian: { x: cartesian.x, y: cartesian.y, z: cartesian.z }
|
|
|
+ })
|
|
|
+ return new THREE.Vector3(cartesian.x, cartesian.y, cartesian.z)
|
|
|
+ }
|
|
|
+ return new THREE.Vector3(0, 0, 0)
|
|
|
+}
|
|
|
+
|
|
|
+// 计算模型中心(优先使用底座作为中心)
|
|
|
+const getModelCenter = (object) => {
|
|
|
+ // 尝试找到底座部件
|
|
|
+ let base = null
|
|
|
+ object.traverse((child) => {
|
|
|
+ if (child.name && (child.name.includes('底座') || child.name.includes('base') || child.name.includes('Base'))) {
|
|
|
+ base = child
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 如果找到底座,以底座的位置为中心
|
|
|
+ if (base) {
|
|
|
+ const baseCenter = new THREE.Vector3()
|
|
|
+ if (base.isMesh) {
|
|
|
+ base.geometry.computeBoundingBox()
|
|
|
+ base.geometry.boundingBox.getCenter(baseCenter)
|
|
|
+ baseCenter.applyMatrix4(base.matrixWorld)
|
|
|
+ } else {
|
|
|
+ base.getWorldPosition(baseCenter)
|
|
|
+ }
|
|
|
+ console.log('使用底座作为爆炸中心:', base.name)
|
|
|
+ return baseCenter
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有找到底座,使用模型的边界框中心
|
|
|
+ const box = new THREE.Box3().setFromObject(object)
|
|
|
+ const center = box.getCenter(new THREE.Vector3())
|
|
|
+ console.log('使用模型边界框作为爆炸中心')
|
|
|
+ return center
|
|
|
+}
|
|
|
+
|
|
|
+// 计算爆炸方向(从模型中心向外)
|
|
|
+const calculateExplosionDirection = (object, center) => {
|
|
|
+ const objectCenter = new THREE.Vector3()
|
|
|
+
|
|
|
+ if (object.geometry) {
|
|
|
+ object.geometry.computeBoundingBox()
|
|
|
+ object.geometry.boundingBox.getCenter(objectCenter)
|
|
|
+ objectCenter.applyMatrix4(object.matrixWorld)
|
|
|
+ } else {
|
|
|
+ object.getWorldPosition(objectCenter)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算从模型中心到部件中心的方向
|
|
|
+ const direction = new THREE.Vector3().subVectors(objectCenter, center)
|
|
|
+
|
|
|
+ // 确保方向向量长度不为零
|
|
|
+ if (direction.length() > 0) {
|
|
|
+ direction.normalize()
|
|
|
+ } else {
|
|
|
+ // 如果方向向量长度为零,使用随机方向
|
|
|
+ direction.set(
|
|
|
+ Math.random() - 0.5,
|
|
|
+ Math.random() - 0.5,
|
|
|
+ Math.random() - 0.5
|
|
|
+ ).normalize()
|
|
|
+ }
|
|
|
+
|
|
|
+ return direction
|
|
|
+}
|
|
|
+
|
|
|
+// 递归收集所有可爆炸的部件
|
|
|
+const collectExplodableParts = (object, center) => {
|
|
|
+ const parts = []
|
|
|
+
|
|
|
+ // 遍历模型中的所有对象,包括嵌套的组和网格
|
|
|
+ object.traverse((child) => {
|
|
|
+ // 只收集有名称的对象,这些通常是用户在建模软件中创建的部件
|
|
|
+ if ((child.isGroup || (child.isMesh && child.geometry)) && child.name) {
|
|
|
+ // 保存原始变换
|
|
|
+ originalTransforms.set(child.uuid, {
|
|
|
+ position: child.position.clone(),
|
|
|
+ rotation: child.rotation.clone(),
|
|
|
+ scale: child.scale.clone()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 计算爆炸方向
|
|
|
+ const direction = calculateExplosionDirection(child, center)
|
|
|
+ explosionDirections.set(child.uuid, direction)
|
|
|
+
|
|
|
+ parts.push(child)
|
|
|
+ console.log('收集到部件:', child.name)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 如果没有收集到有名称的部件,尝试收集所有网格
|
|
|
+ if (parts.length === 0) {
|
|
|
+ object.traverse((child) => {
|
|
|
+ if (child.isMesh && child.geometry) {
|
|
|
+ // 保存原始变换
|
|
|
+ originalTransforms.set(child.uuid, {
|
|
|
+ position: child.position.clone(),
|
|
|
+ rotation: child.rotation.clone(),
|
|
|
+ scale: child.scale.clone()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 计算爆炸方向
|
|
|
+ const direction = calculateExplosionDirection(child, center)
|
|
|
+ explosionDirections.set(child.uuid, direction)
|
|
|
+
|
|
|
+ parts.push(child)
|
|
|
+ console.log('收集到网格:', child.uuid)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('收集到的可爆炸部件数量:', parts.length)
|
|
|
+ return parts
|
|
|
+}
|
|
|
+
|
|
|
+// 加载 GLB 模型
|
|
|
+const loadModel = () => {
|
|
|
+ console.log('开始加载模型:', props.modelPath)
|
|
|
+ if (!scene) {
|
|
|
+ console.error('场景未初始化')
|
|
|
+ ElMessage.error('场景未初始化')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const loader = new GLTFLoader()
|
|
|
+ console.log('GLTFLoader 实例创建成功')
|
|
|
+
|
|
|
+ console.log('尝试加载模型:', props.modelPath)
|
|
|
+ loader.load(
|
|
|
+ props.modelPath,
|
|
|
+ (gltf) => {
|
|
|
+ console.log('模型加载成功,开始处理:', gltf)
|
|
|
+ model = gltf.scene
|
|
|
+
|
|
|
+ // 计算模型位置(使用 Cesium 世界坐标)
|
|
|
+ // 先设置模型缩放,再计算边界框
|
|
|
+ const scale = 100 // 调整缩放值,使模型在地图上更明显
|
|
|
+ model.scale.set(scale, scale, scale)
|
|
|
+
|
|
|
+ // 获取模型的边界框,计算模型的中心点和底部位置
|
|
|
+ const boundingBox = new THREE.Box3().setFromObject(model)
|
|
|
+ const boxCenter = new THREE.Vector3()
|
|
|
+ boundingBox.getCenter(boxCenter)
|
|
|
+ const size = new THREE.Vector3()
|
|
|
+ boundingBox.getSize(size)
|
|
|
+
|
|
|
+ // 计算模型底部到中心点的距离
|
|
|
+ const bottomOffset = size.y / 2
|
|
|
+
|
|
|
+ // 调整高度为0,确保模型贴合地面
|
|
|
+ const adjustedHeight = 0
|
|
|
+ const position = latLonToWorld(props.longitude, props.latitude, adjustedHeight) // 调整高度为0,确保模型贴合地面
|
|
|
+ model.position.copy(position)
|
|
|
+
|
|
|
+ // 调整模型位置,确保底部贴合地面
|
|
|
+ model.position.y -= bottomOffset
|
|
|
+
|
|
|
+ console.log('模型边界框尺寸:', size)
|
|
|
+ console.log('模型底部偏移:', bottomOffset)
|
|
|
+ console.log('调整后的高度:', adjustedHeight)
|
|
|
+
|
|
|
+ // 模型缩放已经在前面设置过了
|
|
|
+ console.log('模型缩放值:', scale)
|
|
|
+ console.log('模型位置:', model.position)
|
|
|
+ console.log('模型坐标:', {
|
|
|
+ x: model.position.x,
|
|
|
+ y: model.position.y,
|
|
|
+ z: model.position.z
|
|
|
+ })
|
|
|
+ console.log('模型边界框:', new THREE.Box3().setFromObject(model))
|
|
|
+
|
|
|
+ // 计算地表对齐旋转,确保模型垂直于当地地表法线
|
|
|
+ if (window.viewer) {
|
|
|
+ // 获取当地坐标系(ENU - 东东北天)的变换矩阵
|
|
|
+ const cartesianPosition = Cesium.Cartesian3.fromDegrees(props.longitude, props.latitude, adjustedHeight)
|
|
|
+ const enuMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(cartesianPosition)
|
|
|
+
|
|
|
+ // 提取旋转部分并转换为 Three.js 矩阵
|
|
|
+ const threeMatrix = new THREE.Matrix4()
|
|
|
+
|
|
|
+ // 手动填充矩阵值(从 Cesium 的 Matrix4 转换)
|
|
|
+ threeMatrix.elements[0] = enuMatrix[0]
|
|
|
+ threeMatrix.elements[1] = enuMatrix[1]
|
|
|
+ threeMatrix.elements[2] = enuMatrix[2]
|
|
|
+ threeMatrix.elements[3] = enuMatrix[3]
|
|
|
+ threeMatrix.elements[4] = enuMatrix[4]
|
|
|
+ threeMatrix.elements[5] = enuMatrix[5]
|
|
|
+ threeMatrix.elements[6] = enuMatrix[6]
|
|
|
+ threeMatrix.elements[7] = enuMatrix[7]
|
|
|
+ threeMatrix.elements[8] = enuMatrix[8]
|
|
|
+ threeMatrix.elements[9] = enuMatrix[9]
|
|
|
+ threeMatrix.elements[10] = enuMatrix[10]
|
|
|
+ threeMatrix.elements[11] = enuMatrix[11]
|
|
|
+ threeMatrix.elements[12] = 0
|
|
|
+ threeMatrix.elements[13] = 0
|
|
|
+ threeMatrix.elements[14] = 0
|
|
|
+ threeMatrix.elements[15] = 1
|
|
|
+
|
|
|
+ // 从矩阵中提取旋转
|
|
|
+ const quaternion = new THREE.Quaternion()
|
|
|
+ quaternion.setFromRotationMatrix(threeMatrix)
|
|
|
+
|
|
|
+ // 应用旋转到模型
|
|
|
+ model.quaternion.copy(quaternion)
|
|
|
+
|
|
|
+ // 调整模型朝向,使其垂直于地面
|
|
|
+ // 绕 x 轴旋转 90 度,使模型垂直于地面
|
|
|
+ const xRotation = new THREE.Quaternion().setFromAxisAngle(
|
|
|
+ new THREE.Vector3(1, 0, 0),
|
|
|
+ Math.PI / 2
|
|
|
+ )
|
|
|
+ model.quaternion.multiply(xRotation)
|
|
|
+
|
|
|
+ console.log('模型四元数旋转:', model.quaternion)
|
|
|
+ } else {
|
|
|
+ // 备用旋转方案
|
|
|
+ model.rotation.x = Math.PI / 2 // 绕 x 轴旋转 90 度,使模型垂直于地面
|
|
|
+ model.rotation.y = 0
|
|
|
+ model.rotation.z = 0
|
|
|
+ console.log('模型旋转:', model.rotation)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 启用阴影和增强材质
|
|
|
+ model.traverse((child) => {
|
|
|
+ if (child.isMesh) {
|
|
|
+ child.castShadow = true
|
|
|
+ child.receiveShadow = true
|
|
|
+
|
|
|
+ if (child.material) {
|
|
|
+ child.material.metalness = 0.3
|
|
|
+ child.material.roughness = 0.7
|
|
|
+ child.material.side = THREE.DoubleSide
|
|
|
+ child.material.transparent = false
|
|
|
+ child.material.needsUpdate = true
|
|
|
+ child.material.depthTest = true
|
|
|
+ child.material.depthWrite = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ scene.add(model)
|
|
|
+
|
|
|
+ // 计算模型中心并收集可爆炸部件
|
|
|
+ const center = getModelCenter(model)
|
|
|
+ const parts = collectExplodableParts(model, center)
|
|
|
+ partCount.value = parts.length
|
|
|
+
|
|
|
+ modelLoaded.value = true
|
|
|
+ emit('model-loaded', { model, parts: parts.length })
|
|
|
+ ElMessage.success('启闭机模型加载成功')
|
|
|
+
|
|
|
+ console.log('模型加载成功:', {
|
|
|
+ position: position,
|
|
|
+ parts: parts.length,
|
|
|
+ center: center,
|
|
|
+ scale: scale,
|
|
|
+ model: model
|
|
|
+ })
|
|
|
+
|
|
|
+ // 检查模型是否在场景中
|
|
|
+ console.log('模型是否在场景中:', scene.children.includes(model))
|
|
|
+ console.log('场景中的对象数量:', scene.children.length)
|
|
|
+ console.log('Three.js 渲染器:', renderer)
|
|
|
+ console.log('Three.js 相机:', camera)
|
|
|
+ console.log('Three.js 场景:', scene)
|
|
|
+
|
|
|
+ // 强制渲染一次
|
|
|
+ setTimeout(() => {
|
|
|
+ if (renderer && scene && camera) {
|
|
|
+ console.log('强制渲染 Three.js 场景')
|
|
|
+ renderer.render(scene, camera)
|
|
|
+ }
|
|
|
+ }, 100)
|
|
|
+
|
|
|
+ // 自动定位到模型
|
|
|
+ flyToModel()
|
|
|
+ },
|
|
|
+ (progress) => {
|
|
|
+ const percent = (progress.loaded / progress.total * 100).toFixed(0)
|
|
|
+ console.log(`模型加载进度: ${percent}%`)
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ console.error('模型加载失败:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ console.error('错误类型:', error.type)
|
|
|
+ console.error('错误目标:', error.target)
|
|
|
+ ElMessage.error('模型加载失败: ' + error.message)
|
|
|
+ }
|
|
|
+ )
|
|
|
+ } catch (error) {
|
|
|
+ console.error('模型加载过程中发生异常:', error)
|
|
|
+ ElMessage.error('模型加载过程中发生异常: ' + error.message)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 更新爆炸效果
|
|
|
+const updateExplosion = () => {
|
|
|
+ if (!model) return
|
|
|
+
|
|
|
+ const factor = explosionFactor.value
|
|
|
+
|
|
|
+ // 只对收集到的可爆炸部件进行操作
|
|
|
+ originalTransforms.forEach((original, uuid) => {
|
|
|
+ const child = model.getObjectByProperty('uuid', uuid)
|
|
|
+ if (child) {
|
|
|
+ // 检查是否是底座,如果是底座则跳过移动
|
|
|
+ if (child.name && (child.name.includes('底座') || child.name.includes('base') || child.name.includes('Base'))) {
|
|
|
+ console.log('跳过底座移动:', child.name)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const direction = explosionDirections.get(uuid)
|
|
|
+
|
|
|
+ if (direction) {
|
|
|
+ // 计算爆炸位移(只向外扩开,距离适中)
|
|
|
+ const distance = factor * 1 // 进一步减小爆炸距离系数,使炸开程度为3时的距离只有原来的0.2
|
|
|
+ const offset = direction.clone().multiplyScalar(distance)
|
|
|
+
|
|
|
+ // 应用位移(只向外扩开,不添加旋转和随机性)
|
|
|
+ child.position.copy(original.position).add(offset)
|
|
|
+
|
|
|
+ // 如果是组,需要更新组内所有子元素的位置
|
|
|
+ if (child.isGroup) {
|
|
|
+ child.traverse((groupChild) => {
|
|
|
+ // 检查子元素是否是底座,如果是底座则跳过移动
|
|
|
+ if (groupChild.name && (groupChild.name.includes('底座') || groupChild.name.includes('base') || groupChild.name.includes('Base'))) {
|
|
|
+ console.log('跳过底座子元素移动:', groupChild.name)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (groupChild.isMesh && groupChild !== child) {
|
|
|
+ // 保存子元素的原始位置
|
|
|
+ if (!originalTransforms.has(groupChild.uuid)) {
|
|
|
+ originalTransforms.set(groupChild.uuid, {
|
|
|
+ position: groupChild.position.clone(),
|
|
|
+ rotation: groupChild.rotation.clone(),
|
|
|
+ scale: groupChild.scale.clone()
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 应用相同的位移
|
|
|
+ const groupChildOriginal = originalTransforms.get(groupChild.uuid)
|
|
|
+ groupChild.position.copy(groupChildOriginal.position).add(offset)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 切换爆炸状态
|
|
|
+const toggleExplosion = () => {
|
|
|
+ if (!model) {
|
|
|
+ ElMessage.warning('模型未加载')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ isExploded.value = !isExploded.value
|
|
|
+
|
|
|
+ if (isExploded.value) {
|
|
|
+ animateExplosion(1.5)
|
|
|
+ } else {
|
|
|
+ animateExplosion(0)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 爆炸/还原动画
|
|
|
+const animateExplosion = (targetFactor) => {
|
|
|
+ const startFactor = explosionFactor.value
|
|
|
+ const duration = 1200 // 1.2秒,更平滑
|
|
|
+ const startTime = Date.now()
|
|
|
+
|
|
|
+ const animate = () => {
|
|
|
+ const elapsed = Date.now() - startTime
|
|
|
+ const progress = Math.min(elapsed / duration, 1)
|
|
|
+
|
|
|
+ // 使用弹性缓动函数
|
|
|
+ const eased = progress < 0.5
|
|
|
+ ? 4 * progress * progress * progress
|
|
|
+ : 1 - Math.pow(-2 * progress + 2, 3) / 2
|
|
|
+
|
|
|
+ explosionFactor.value = startFactor + (targetFactor - startFactor) * eased
|
|
|
+ updateExplosion()
|
|
|
+
|
|
|
+ if (progress < 1) {
|
|
|
+ requestAnimationFrame(animate)
|
|
|
+ } else {
|
|
|
+ emit(isExploded.value ? 'explosion-complete' : 'reset-complete')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ animate()
|
|
|
+}
|
|
|
+
|
|
|
+// 飞行到模型位置
|
|
|
+const flyToModel = () => {
|
|
|
+ if (!window.viewer) return
|
|
|
+
|
|
|
+ window.viewer.camera.flyTo({
|
|
|
+ destination: Cesium.Cartesian3.fromDegrees(
|
|
|
+ props.longitude,
|
|
|
+ props.latitude,
|
|
|
+ props.height + 100
|
|
|
+ ),
|
|
|
+ orientation: {
|
|
|
+ heading: Cesium.Math.toRadians(0),
|
|
|
+ pitch: Cesium.Math.toRadians(-30),
|
|
|
+ roll: 0
|
|
|
+ },
|
|
|
+ duration: 2
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 切换线框模式
|
|
|
+const toggleWireframe = () => {
|
|
|
+ if (!model) return
|
|
|
+
|
|
|
+ isWireframe.value = !isWireframe.value
|
|
|
+
|
|
|
+ model.traverse((child) => {
|
|
|
+ if (child.isMesh && child.material) {
|
|
|
+ if (Array.isArray(child.material)) {
|
|
|
+ child.material.forEach(mat => {
|
|
|
+ mat.wireframe = isWireframe.value
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ child.material.wireframe = isWireframe.value
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 生命周期钩子
|
|
|
+onMounted(() => {
|
|
|
+ console.log('CesiumThreeFusion 组件已挂载')
|
|
|
+ // 等待 Cesium 初始化完成
|
|
|
+ const checkCesium = setInterval(() => {
|
|
|
+ console.log('检查 Cesium 初始化状态:', {
|
|
|
+ windowViewer: !!window.viewer,
|
|
|
+ canvasExists: !!document.querySelector('.cesium-viewer canvas')
|
|
|
+ })
|
|
|
+ if (window.viewer && document.querySelector('.cesium-viewer canvas')) {
|
|
|
+ clearInterval(checkCesium)
|
|
|
+ console.log('Cesium 初始化完成,开始初始化 Three.js 场景')
|
|
|
+ // 初始化 Three.js 场景
|
|
|
+ if (initThreeScene()) {
|
|
|
+ console.log('Three.js 场景初始化成功,准备加载模型')
|
|
|
+ // 延迟加载模型
|
|
|
+ setTimeout(() => {
|
|
|
+ loadModel()
|
|
|
+ }, 1000)
|
|
|
+ } else {
|
|
|
+ console.error('Three.js 场景初始化失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 500)
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (animationId) {
|
|
|
+ cancelAnimationFrame(animationId)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除 Cesium 事件监听
|
|
|
+ if (window.viewer) {
|
|
|
+ window.viewer.scene.postRender.removeEventListener(syncCamera)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除窗口大小变化监听器
|
|
|
+ window.removeEventListener('resize', handleResize)
|
|
|
+ console.log('窗口大小变化监听器已移除')
|
|
|
+
|
|
|
+ // 清理资源
|
|
|
+ originalTransforms.clear()
|
|
|
+ explosionDirections.clear()
|
|
|
+
|
|
|
+ if (model) {
|
|
|
+ scene.remove(model)
|
|
|
+ model = null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理渲染器
|
|
|
+ if (renderer) {
|
|
|
+ renderer.dispose()
|
|
|
+ renderer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除 Three.js canvas
|
|
|
+ const threeCanvas = renderer?.domElement
|
|
|
+ if (threeCanvas && threeCanvas.parentElement) {
|
|
|
+ threeCanvas.parentElement.removeChild(threeCanvas)
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('CesiumThreeFusion 组件已卸载')
|
|
|
+})
|
|
|
+
|
|
|
+// 暴露方法给父组件
|
|
|
+defineExpose({
|
|
|
+ loadModel,
|
|
|
+ toggleExplosion,
|
|
|
+ flyToModel,
|
|
|
+ toggleWireframe,
|
|
|
+ updateExplosion
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.cesium-three-fusion {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 100;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ left: 20px;
|
|
|
+ z-index: 1000;
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ pointer-events: auto;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel .el-button {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel .el-button.is-exploded {
|
|
|
+ background-color: #67c23a;
|
|
|
+ border-color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.explosion-slider {
|
|
|
+ position: absolute;
|
|
|
+ top: 90px;
|
|
|
+ left: 20px;
|
|
|
+ z-index: 1000;
|
|
|
+ width: 320px;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ padding: 15px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ pointer-events: auto;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+.slider-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ white-space: nowrap;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.slider-value {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: bold;
|
|
|
+ min-width: 40px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.explosion-slider :deep(.el-slider) {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.model-info-panel {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px;
|
|
|
+ z-index: 1000;
|
|
|
+ width: 220px;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
|
+ pointer-events: auto;
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+}
|
|
|
+
|
|
|
+.model-info-panel h4 {
|
|
|
+ margin: 0 0 12px 0;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #303133;
|
|
|
+ border-bottom: 2px solid #409eff;
|
|
|
+ padding-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.model-info-panel p {
|
|
|
+ margin: 8px 0;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.instruction-panel {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 20px;
|
|
|
+ left: 20px;
|
|
|
+ z-index: 1000;
|
|
|
+ width: 200px;
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ padding: 15px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
|
+ pointer-events: auto;
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+}
|
|
|
+
|
|
|
+.instruction-panel h4 {
|
|
|
+ margin: 0 0 10px 0;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #ffffff;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.instruction-panel ul {
|
|
|
+ margin: 0;
|
|
|
+ padding-left: 18px;
|
|
|
+ list-style: disc;
|
|
|
+}
|
|
|
+
|
|
|
+.instruction-panel li {
|
|
|
+ margin: 5px 0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式调整 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .control-panel {
|
|
|
+ top: 10px;
|
|
|
+ left: 10px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ padding: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .explosion-slider {
|
|
|
+ top: auto;
|
|
|
+ bottom: 150px;
|
|
|
+ left: 10px;
|
|
|
+ width: calc(100% - 20px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .model-info-panel {
|
|
|
+ top: auto;
|
|
|
+ bottom: 20px;
|
|
|
+ right: 10px;
|
|
|
+ left: 10px;
|
|
|
+ width: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .instruction-panel {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|