|
|
@@ -0,0 +1,1152 @@
|
|
|
+<template>
|
|
|
+ <el-dialog
|
|
|
+ v-model="dialogVisible"
|
|
|
+ :title="modelName"
|
|
|
+ width="80%"
|
|
|
+ :before-close="handleClose"
|
|
|
+ destroy-on-close
|
|
|
+ >
|
|
|
+ <div class="model-preview-container">
|
|
|
+ <div ref="modelContainer" class="model-container"></div>
|
|
|
+ <div class="model-info">
|
|
|
+ <h3>模型信息</h3>
|
|
|
+ <el-descriptions :column="1" border>
|
|
|
+ <el-descriptions-item label="模型名称">{{ modelData.name }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="模型类型">{{ getTypeDisplayName(modelData.type) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="经纬度">{{ modelData.location }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="提交单位">{{ modelData.uploadUnit }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="格式">{{ modelData.format }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="状态">{{ modelData.status }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间">{{ modelData.createTime }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="handleClose">关闭</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
|
+import * as THREE from 'three'
|
|
|
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|
|
+import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
|
|
|
+import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'
|
|
|
+import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'
|
|
|
+// 导入项目的request实例
|
|
|
+import request from '@/utils/request'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ visible: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ modelData: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({})
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['update:visible', 'close'])
|
|
|
+
|
|
|
+const modelContainer = ref(null)
|
|
|
+let scene = null
|
|
|
+let camera = null
|
|
|
+let renderer = null
|
|
|
+let model = null
|
|
|
+let controls = null
|
|
|
+let animationId = null
|
|
|
+const dialogVisible = ref(false)
|
|
|
+
|
|
|
+// 获取类型显示名称
|
|
|
+const getTypeDisplayName = (type) => {
|
|
|
+ const typeMapping = {
|
|
|
+ 'RESERVOIR': '水库',
|
|
|
+ 'HYDROPOWER': '水电站',
|
|
|
+ 'IRRIGATION': '灌溉',
|
|
|
+ 'FLOOD_CONTROL': '防洪',
|
|
|
+ 'CANAL': '渠道',
|
|
|
+ 'PUMPING_STATION': '泵站'
|
|
|
+ }
|
|
|
+ return typeMapping[type] || type
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化场景
|
|
|
+const initScene = () => {
|
|
|
+ if (!modelContainer.value) return
|
|
|
+
|
|
|
+ // 创建场景
|
|
|
+ scene = new THREE.Scene()
|
|
|
+ scene.background = new THREE.Color(0xf0f0f0)
|
|
|
+
|
|
|
+ // 创建相机
|
|
|
+ camera = new THREE.PerspectiveCamera(
|
|
|
+ 75,
|
|
|
+ modelContainer.value.clientWidth / modelContainer.value.clientHeight,
|
|
|
+ 0.1,
|
|
|
+ 1000
|
|
|
+ )
|
|
|
+ // 增加相机初始位置,让相机离模型更远,以便能看到整个模型
|
|
|
+ camera.position.z = 10
|
|
|
+ camera.position.y = 2
|
|
|
+
|
|
|
+ // 创建渲染器
|
|
|
+ renderer = new THREE.WebGLRenderer({ antialias: true })
|
|
|
+ renderer.setSize(modelContainer.value.clientWidth, modelContainer.value.clientHeight)
|
|
|
+ modelContainer.value.appendChild(renderer.domElement)
|
|
|
+
|
|
|
+ // 添加光源
|
|
|
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
|
|
|
+ scene.add(ambientLight)
|
|
|
+
|
|
|
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
|
|
+ directionalLight.position.set(1, 1, 1)
|
|
|
+ scene.add(directionalLight)
|
|
|
+
|
|
|
+ // 添加网格辅助线
|
|
|
+ const gridHelper = new THREE.GridHelper(10, 10)
|
|
|
+ scene.add(gridHelper)
|
|
|
+
|
|
|
+ // 加载模型
|
|
|
+ if (props.modelData) {
|
|
|
+ loadModel()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加鼠标控制
|
|
|
+ initControls()
|
|
|
+
|
|
|
+ // 添加三维坐标轴
|
|
|
+ addAxesHelper()
|
|
|
+
|
|
|
+ // 开始渲染
|
|
|
+ animate()
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', handleResize)
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化控制器
|
|
|
+const initControls = () => {
|
|
|
+ // 简单的鼠标控制实现 - 控制相机视角
|
|
|
+ let isDragging = false
|
|
|
+ let previousMousePosition = { x: 0, y: 0 }
|
|
|
+
|
|
|
+ // 相机目标点(看向的点)
|
|
|
+ let cameraTarget = new THREE.Vector3(0, 0, 0)
|
|
|
+
|
|
|
+ // 更新相机目标点为模型中心
|
|
|
+ const updateCameraTarget = () => {
|
|
|
+ if (model) {
|
|
|
+ model.updateMatrixWorld(true)
|
|
|
+ const box = new THREE.Box3().setFromObject(model)
|
|
|
+ cameraTarget = box.getCenter(new THREE.Vector3())
|
|
|
+ console.log('相机目标点已更新为模型中心:', cameraTarget)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化时更新目标点
|
|
|
+ updateCameraTarget()
|
|
|
+
|
|
|
+ const container = modelContainer.value
|
|
|
+ container.addEventListener('mousedown', (e) => {
|
|
|
+ isDragging = true
|
|
|
+ previousMousePosition = { x: e.offsetX, y: e.offsetY }
|
|
|
+ })
|
|
|
+
|
|
|
+ container.addEventListener('mousemove', (e) => {
|
|
|
+ if (!isDragging) return
|
|
|
+
|
|
|
+ const deltaMove = {
|
|
|
+ x: e.offsetX - previousMousePosition.x,
|
|
|
+ y: e.offsetY - previousMousePosition.y
|
|
|
+ }
|
|
|
+
|
|
|
+ // 控制相机围绕目标点旋转
|
|
|
+ if (camera) {
|
|
|
+ // 计算相机到目标点的距离
|
|
|
+ const distance = camera.position.distanceTo(cameraTarget)
|
|
|
+
|
|
|
+ // 限制垂直旋转范围,避免相机翻转
|
|
|
+ const currentPosition = new THREE.Vector3().copy(camera.position)
|
|
|
+ const direction = new THREE.Vector3().subVectors(currentPosition, cameraTarget).normalize()
|
|
|
+
|
|
|
+ // 水平旋转(围绕Y轴)
|
|
|
+ const horizontalRotation = new THREE.Matrix4().makeRotationY(-deltaMove.x * 0.01)
|
|
|
+ // 垂直旋转(围绕X轴,限制范围)
|
|
|
+ const verticalRotation = new THREE.Matrix4().makeRotationX(-deltaMove.y * 0.005)
|
|
|
+
|
|
|
+ // 应用旋转
|
|
|
+ const newDirection = direction.applyMatrix4(horizontalRotation).applyMatrix4(verticalRotation)
|
|
|
+
|
|
|
+ // 设置新的相机位置
|
|
|
+ camera.position.copy(cameraTarget).add(newDirection.multiplyScalar(distance))
|
|
|
+
|
|
|
+ // 相机看向目标点
|
|
|
+ camera.lookAt(cameraTarget)
|
|
|
+ }
|
|
|
+
|
|
|
+ previousMousePosition = {
|
|
|
+ x: e.offsetX,
|
|
|
+ y: e.offsetY
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ container.addEventListener('mouseup', () => {
|
|
|
+ isDragging = false
|
|
|
+ })
|
|
|
+
|
|
|
+ container.addEventListener('mouseleave', () => {
|
|
|
+ isDragging = false
|
|
|
+ })
|
|
|
+
|
|
|
+ // 滚轮缩放 - 控制相机距离目标点的距离
|
|
|
+ container.addEventListener('wheel', (e) => {
|
|
|
+ e.preventDefault()
|
|
|
+ if (camera) {
|
|
|
+ // 计算相机到目标点的向量
|
|
|
+ const direction = new THREE.Vector3().subVectors(camera.position, cameraTarget)
|
|
|
+ const distance = direction.length()
|
|
|
+
|
|
|
+ // 限制最小和最大距离
|
|
|
+ const minDistance = 0.5
|
|
|
+ const maxDistance = 50
|
|
|
+
|
|
|
+ // 计算新的距离
|
|
|
+ const newDistance = Math.max(minDistance, Math.min(maxDistance, distance + e.deltaY * 0.01))
|
|
|
+
|
|
|
+ // 设置新的相机位置
|
|
|
+ direction.normalize().multiplyScalar(newDistance)
|
|
|
+ camera.position.copy(cameraTarget).add(direction)
|
|
|
+
|
|
|
+ // 相机看向目标点
|
|
|
+ camera.lookAt(cameraTarget)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 添加三维坐标轴
|
|
|
+const addAxesHelper = () => {
|
|
|
+ // 移除现有的坐标轴
|
|
|
+ if (scene) {
|
|
|
+ scene.traverse((object) => {
|
|
|
+ if (object.isAxesHelper) {
|
|
|
+ scene.remove(object)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的坐标轴辅助器
|
|
|
+ if (scene) {
|
|
|
+ const axesHelper = new THREE.AxesHelper(2)
|
|
|
+ // 设置坐标轴位置在世界空间的固定位置
|
|
|
+ // 这个位置会显示在视口左下角
|
|
|
+ axesHelper.position.set(-4, -4, 0)
|
|
|
+ axesHelper.scale.set(1, 1, 1)
|
|
|
+
|
|
|
+ // 确保坐标轴不会受到场景其他变换的影响
|
|
|
+ axesHelper.updateMatrixWorld(true)
|
|
|
+
|
|
|
+ scene.add(axesHelper)
|
|
|
+ console.log('三维坐标轴已添加到视口左下角')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 加载模型
|
|
|
+const loadModel = () => {
|
|
|
+ console.log('========================================')
|
|
|
+ console.log('开始加载模型流程')
|
|
|
+ console.log('========================================')
|
|
|
+
|
|
|
+ if (!props.modelData || !props.modelData.format) {
|
|
|
+ console.error('模型数据不完整:', { modelData: props.modelData })
|
|
|
+ console.log('========================================')
|
|
|
+ console.log('加载模型流程结束 - 数据不完整')
|
|
|
+ console.log('========================================')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('模型基本信息:', {
|
|
|
+ modelId: props.modelData.id,
|
|
|
+ modelFormat: props.modelData.format,
|
|
|
+ modelName: props.modelData.name
|
|
|
+ })
|
|
|
+
|
|
|
+ // 打印完整的模型数据,查看是否包含文件路径或URL
|
|
|
+ console.log('完整模型数据:', props.modelData)
|
|
|
+ console.log('模型数据所有属性:', Object.keys(props.modelData))
|
|
|
+
|
|
|
+ const format = props.modelData.format.toUpperCase()
|
|
|
+
|
|
|
+ // 清除现有模型
|
|
|
+ if (model) {
|
|
|
+ scene.remove(model)
|
|
|
+ model = null
|
|
|
+ console.log('已清除现有模型')
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 检查模型数据中是否包含文件路径或URL
|
|
|
+ let modelUrl = null
|
|
|
+
|
|
|
+ // 尝试从模型数据中获取文件路径或URL
|
|
|
+ if (props.modelData.path) {
|
|
|
+ modelUrl = props.modelData.path
|
|
|
+ console.log('✓ 从modelData.path获取模型路径:', modelUrl)
|
|
|
+ } else if (props.modelData.url) {
|
|
|
+ modelUrl = props.modelData.url
|
|
|
+ console.log('✓ 从modelData.url获取模型路径:', modelUrl)
|
|
|
+ } else if (props.modelData.filePath) {
|
|
|
+ // 后端返回的filePath是相对路径,需要添加/profile前缀
|
|
|
+ modelUrl = `/profile/${props.modelData.filePath}`
|
|
|
+ console.log('✓ 从modelData.filePath获取模型路径并添加前缀:', props.modelData.filePath, '→', modelUrl)
|
|
|
+ } else if (props.modelData.file_path) {
|
|
|
+ // 检查file_path是否已经包含/profile前缀
|
|
|
+ if (props.modelData.file_path.startsWith('/profile')) {
|
|
|
+ // 已经包含/profile前缀,直接使用
|
|
|
+ modelUrl = props.modelData.file_path
|
|
|
+ console.log('✓ 从modelData.file_path获取模型路径(已包含/profile前缀):', modelUrl)
|
|
|
+ } else {
|
|
|
+ // 后端返回的file_path是相对路径,需要添加/profile前缀
|
|
|
+ modelUrl = `/profile/${props.modelData.file_path}`
|
|
|
+ console.log('✓ 从modelData.file_path获取模型路径并添加前缀:', props.modelData.file_path, '→', modelUrl)
|
|
|
+ }
|
|
|
+ } else if (props.modelData.fileUrl) {
|
|
|
+ modelUrl = props.modelData.fileUrl
|
|
|
+ console.log('✓ 从modelData.fileUrl获取模型路径:', modelUrl)
|
|
|
+ } else if (props.modelData.storagePath) {
|
|
|
+ modelUrl = props.modelData.storagePath
|
|
|
+ console.log('✓ 从modelData.storagePath获取模型路径:', modelUrl)
|
|
|
+ } else if (props.modelData.filename) {
|
|
|
+ // 直接使用filename构建路径,添加/profile/upload前缀
|
|
|
+ modelUrl = `/profile/upload/${props.modelData.filename}`
|
|
|
+ console.log('✓ 从modelData.filename获取模型路径:', modelUrl)
|
|
|
+ } else if (props.modelData.fileName) {
|
|
|
+ // 直接使用fileName构建路径,添加/profile/upload前缀
|
|
|
+ modelUrl = `/profile/upload/${props.modelData.fileName}`
|
|
|
+ console.log('✓ 从modelData.fileName获取模型路径:', modelUrl)
|
|
|
+ } else if (props.modelData.id) {
|
|
|
+ // 如果没有找到文件路径,尝试使用ID构建路径
|
|
|
+ // 尝试多种可能的静态文件路径,添加/profile前缀
|
|
|
+ const modelId = props.modelData.id
|
|
|
+ const possiblePaths = [
|
|
|
+ `/profile/upload/${modelId}.${props.modelData.format.toLowerCase()}`,
|
|
|
+ `/profile/model/${modelId}.${props.modelData.format.toLowerCase()}`
|
|
|
+ ]
|
|
|
+
|
|
|
+ // 尝试每个路径
|
|
|
+ console.log('开始尝试构建模型路径:')
|
|
|
+ for (const path of possiblePaths) {
|
|
|
+ console.log(' 尝试路径:', path)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用第一个可能的路径
|
|
|
+ modelUrl = possiblePaths[0]
|
|
|
+ console.log('✓ 使用ID构建模型路径:', modelUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果找到了模型路径
|
|
|
+ if (modelUrl) {
|
|
|
+ // 确保路径是完整的URL
|
|
|
+ if (!modelUrl.startsWith('http://') && !modelUrl.startsWith('https://')) {
|
|
|
+ // 检查是否已经是绝对路径(以/开头)
|
|
|
+ if (!modelUrl.startsWith('/')) {
|
|
|
+ // 如果是相对路径,添加基础路径
|
|
|
+ const oldUrl = modelUrl
|
|
|
+ modelUrl = `/${modelUrl}`
|
|
|
+ console.log('✓ 添加斜杠前缀:', oldUrl, '→', modelUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否需要添加/upload前缀
|
|
|
+ // 根据后端代码,FileUploadUtils.upload()返回的路径已经包含了upload前缀
|
|
|
+ // 所以这里不需要再添加/upload/model/前缀
|
|
|
+ console.log('✓ 路径处理完成:', modelUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('========================================')
|
|
|
+ console.log('最终确定的模型路径:', modelUrl)
|
|
|
+ console.log('========================================')
|
|
|
+
|
|
|
+ // 根据模型格式加载
|
|
|
+ switch (format) {
|
|
|
+ case 'GLTF':
|
|
|
+ case 'GLB':
|
|
|
+ console.log('开始加载GLTF/GLB模型')
|
|
|
+ loadGLTFModel(modelUrl)
|
|
|
+ break
|
|
|
+ case 'OBJ':
|
|
|
+ console.log('开始加载OBJ模型')
|
|
|
+ loadOBJModel(modelUrl)
|
|
|
+ break
|
|
|
+ case 'FBX':
|
|
|
+ console.log('开始加载FBX模型')
|
|
|
+ loadFBXModel(modelUrl)
|
|
|
+ break
|
|
|
+ case 'STL':
|
|
|
+ console.log('开始加载STL模型')
|
|
|
+ loadSTLModel(modelUrl)
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ console.error('不支持的模型格式:', format)
|
|
|
+ // 显示默认模型
|
|
|
+ createDefaultModel()
|
|
|
+ console.log('========================================')
|
|
|
+ console.log('加载模型流程结束 - 格式不支持')
|
|
|
+ console.log('========================================')
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.error('❌ 模型数据中未找到文件路径或URL')
|
|
|
+ // 显示默认模型
|
|
|
+ createDefaultModel()
|
|
|
+ console.log('========================================')
|
|
|
+ console.log('加载模型流程结束 - 未找到路径')
|
|
|
+ console.log('========================================')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 加载模型出错:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ // 显示默认模型
|
|
|
+ createDefaultModel()
|
|
|
+ console.log('========================================')
|
|
|
+ console.log('加载模型流程结束 - 发生错误')
|
|
|
+ console.log('========================================')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 加载GLTF/GLB模型
|
|
|
+const loadGLTFModel = (url) => {
|
|
|
+ console.log('开始加载GLTF/GLB模型:', url)
|
|
|
+
|
|
|
+ // 使用后端接口获取文件
|
|
|
+ // 直接传递包含/profile前缀的完整路径,后端会处理前缀
|
|
|
+ const resourcePath = url
|
|
|
+ const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
|
|
|
+ console.log('使用后端接口获取文件:', apiUrl)
|
|
|
+
|
|
|
+ request({
|
|
|
+ url: apiUrl,
|
|
|
+ method: 'get',
|
|
|
+ responseType: 'blob'
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ const blob = response
|
|
|
+ console.log('响应是Blob,大小:', blob.size)
|
|
|
+ console.log('Blob类型:', blob.type)
|
|
|
+
|
|
|
+ // 使用Blob创建临时URL
|
|
|
+ const blobUrl = URL.createObjectURL(blob)
|
|
|
+ console.log('创建Blob URL:', blobUrl)
|
|
|
+
|
|
|
+ // 使用Blob URL加载模型
|
|
|
+ const loader = new GLTFLoader()
|
|
|
+ loader.load(
|
|
|
+ blobUrl,
|
|
|
+ (gltf) => {
|
|
|
+ console.log('GLTF/GLB模型加载成功:', gltf)
|
|
|
+ model = gltf.scene
|
|
|
+ scene.add(model)
|
|
|
+ // 使用模型自身的原始坐标,不进行中心调整
|
|
|
+ console.log('模型添加到场景,使用原始坐标')
|
|
|
+ console.log('模型原始位置:', model.position)
|
|
|
+ console.log('模型原始旋转:', model.rotation)
|
|
|
+ console.log('模型原始缩放:', model.scale)
|
|
|
+ // 调整相机位置,使模型在视角中心且完整可见
|
|
|
+ adjustCameraForModel()
|
|
|
+ console.log('相机位置调整完成')
|
|
|
+ // 释放Blob URL
|
|
|
+ URL.revokeObjectURL(blobUrl)
|
|
|
+ },
|
|
|
+ (xhr) => {
|
|
|
+ console.log('GLTF/GLB模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ console.error('GLTF加载错误:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ // 释放Blob URL
|
|
|
+ URL.revokeObjectURL(blobUrl)
|
|
|
+ createDefaultModel()
|
|
|
+ }
|
|
|
+ )
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ console.error('后端接口请求错误:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ createDefaultModel()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 加载OBJ模型
|
|
|
+const loadOBJModel = (url) => {
|
|
|
+ console.log('开始加载OBJ模型:', url)
|
|
|
+
|
|
|
+ // 使用后端接口获取文件
|
|
|
+ // 直接传递包含/profile前缀的完整路径,后端会处理前缀
|
|
|
+ const resourcePath = url
|
|
|
+ const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
|
|
|
+ console.log('使用后端接口获取文件:', apiUrl)
|
|
|
+
|
|
|
+ request({
|
|
|
+ url: apiUrl,
|
|
|
+ method: 'get',
|
|
|
+ responseType: 'blob'
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ const blob = response
|
|
|
+ console.log('OBJ模型响应是Blob,大小:', blob.size)
|
|
|
+ console.log('Blob类型:', blob.type)
|
|
|
+
|
|
|
+ // 使用Blob创建临时URL
|
|
|
+ const blobUrl = URL.createObjectURL(blob)
|
|
|
+ console.log('创建OBJ Blob URL:', blobUrl)
|
|
|
+
|
|
|
+ // 使用Blob URL加载模型
|
|
|
+ const loader = new OBJLoader()
|
|
|
+ loader.load(
|
|
|
+ blobUrl,
|
|
|
+ (obj) => {
|
|
|
+ console.log('OBJ模型加载成功:', obj)
|
|
|
+ model = obj
|
|
|
+ // 添加材质
|
|
|
+ const material = new THREE.MeshStandardMaterial({ color: 0x409eff })
|
|
|
+ obj.traverse((child) => {
|
|
|
+ if (child.isMesh) {
|
|
|
+ child.material = material
|
|
|
+ }
|
|
|
+ })
|
|
|
+ scene.add(model)
|
|
|
+ // 使用模型自身的原始坐标,不进行中心调整
|
|
|
+ console.log('模型添加到场景,使用原始坐标')
|
|
|
+ console.log('模型原始位置:', model.position)
|
|
|
+ console.log('模型原始旋转:', model.rotation)
|
|
|
+ console.log('模型原始缩放:', model.scale)
|
|
|
+ // 调整相机位置,使模型在视角中心且完整可见
|
|
|
+ adjustCameraForModel()
|
|
|
+ console.log('相机位置调整完成')
|
|
|
+ // 释放Blob URL
|
|
|
+ URL.revokeObjectURL(blobUrl)
|
|
|
+ },
|
|
|
+ (xhr) => {
|
|
|
+ console.log('OBJ模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ console.error('OBJ加载错误:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ // 释放Blob URL
|
|
|
+ URL.revokeObjectURL(blobUrl)
|
|
|
+ createDefaultModel()
|
|
|
+ }
|
|
|
+ )
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ console.error('后端接口请求错误:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ createDefaultModel()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 处理FBX模型的特殊问题
|
|
|
+const processFBXModel = (fbxModel) => {
|
|
|
+ console.log('开始专业处理FBX模型')
|
|
|
+
|
|
|
+ // 1. FBX模型坐标系转换
|
|
|
+ console.log('处理FBX坐标系...')
|
|
|
+
|
|
|
+ // FBX通常使用Y-up坐标系,Three.js使用Y-up,所以不需要翻转
|
|
|
+ // 但需要确保缩放和位置正确
|
|
|
+
|
|
|
+ // 2. 统一模型缩放
|
|
|
+ console.log('处理FBX模型缩放...')
|
|
|
+ console.log('原始模型缩放:', fbxModel.scale)
|
|
|
+
|
|
|
+ // 计算模型的整体缩放因子
|
|
|
+ let maxScale = 1
|
|
|
+ fbxModel.traverse((child) => {
|
|
|
+ if (child.isMesh) {
|
|
|
+ maxScale = Math.max(maxScale, child.scale.x, child.scale.y, child.scale.z)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ console.log('最大缩放因子:', maxScale)
|
|
|
+
|
|
|
+ // 如果缩放因子过大或过小,进行统一调整
|
|
|
+ if (maxScale > 10 || maxScale < 0.1) {
|
|
|
+ const scaleFactor = 1 / maxScale
|
|
|
+ console.log('需要调整缩放,缩放因子:', scaleFactor)
|
|
|
+
|
|
|
+ fbxModel.traverse((child) => {
|
|
|
+ if (child.isMesh) {
|
|
|
+ child.scale.multiplyScalar(scaleFactor)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ console.log('缩放调整完成')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 强制更新所有矩阵
|
|
|
+ console.log('强制更新所有模型矩阵...')
|
|
|
+ fbxModel.traverse((child) => {
|
|
|
+ if (child.isObject3D) {
|
|
|
+ child.updateMatrix()
|
|
|
+ child.updateMatrixWorld(true)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 4. 计算模型边界框
|
|
|
+ console.log('计算FBX模型边界框...')
|
|
|
+ const box = new THREE.Box3().setFromObject(fbxModel)
|
|
|
+ const size = box.getSize(new THREE.Vector3())
|
|
|
+ const center = box.getCenter(new THREE.Vector3())
|
|
|
+
|
|
|
+ console.log('FBX模型边界框大小:', size)
|
|
|
+ console.log('FBX模型边界框中心:', center)
|
|
|
+
|
|
|
+ // 5. 处理材质
|
|
|
+ console.log('处理FBX模型材质...')
|
|
|
+ let materialCount = 0
|
|
|
+
|
|
|
+ fbxModel.traverse((child) => {
|
|
|
+ if (child.isMesh) {
|
|
|
+ console.log('处理网格:', child.name)
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!child.material ||
|
|
|
+ child.material.type === 'unknown' ||
|
|
|
+ child.material.type === 'MeshPhongMaterial') {
|
|
|
+ console.log('为', child.name, '添加默认材质')
|
|
|
+ const defaultMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ color: 0x409eff,
|
|
|
+ metalness: 0.3,
|
|
|
+ roughness: 0.7,
|
|
|
+ transparent: false,
|
|
|
+ opacity: 1
|
|
|
+ })
|
|
|
+ child.material = defaultMaterial
|
|
|
+ materialCount++
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理材质时出错:', error.message)
|
|
|
+ const fallbackMaterial = new THREE.MeshStandardMaterial({
|
|
|
+ color: 0x409eff,
|
|
|
+ metalness: 0.3,
|
|
|
+ roughness: 0.7
|
|
|
+ })
|
|
|
+ child.material = fallbackMaterial
|
|
|
+ materialCount++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ console.log('材质处理完成,添加了', materialCount, '个默认材质')
|
|
|
+
|
|
|
+ // 6. 最终矩阵更新
|
|
|
+ fbxModel.updateMatrixWorld(true)
|
|
|
+ console.log('FBX模型处理完成')
|
|
|
+
|
|
|
+ return {
|
|
|
+ model: fbxModel,
|
|
|
+ box: box,
|
|
|
+ size: size,
|
|
|
+ center: center
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 加载FBX模型
|
|
|
+const loadFBXModel = (url) => {
|
|
|
+ console.log('开始加载FBX模型:', url)
|
|
|
+
|
|
|
+ // 使用后端接口获取文件
|
|
|
+ // 直接传递包含/profile前缀的完整路径,后端会处理前缀
|
|
|
+ const resourcePath = url
|
|
|
+ const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
|
|
|
+ console.log('使用后端接口获取文件:', apiUrl)
|
|
|
+
|
|
|
+ request({
|
|
|
+ url: apiUrl,
|
|
|
+ method: 'get',
|
|
|
+ responseType: 'blob'
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ const blob = response
|
|
|
+ console.log('FBX模型响应是Blob,大小:', blob.size)
|
|
|
+ console.log('Blob类型:', blob.type)
|
|
|
+
|
|
|
+ // 使用Blob创建临时URL
|
|
|
+ const blobUrl = URL.createObjectURL(blob)
|
|
|
+ console.log('创建FBX Blob URL:', blobUrl)
|
|
|
+
|
|
|
+ // 使用Blob URL加载模型
|
|
|
+ const loader = new FBXLoader()
|
|
|
+ loader.load(
|
|
|
+ blobUrl,
|
|
|
+ (fbx) => {
|
|
|
+ console.log('FBX模型加载成功:', fbx)
|
|
|
+
|
|
|
+ // 专业处理FBX模型
|
|
|
+ const processed = processFBXModel(fbx)
|
|
|
+ model = processed.model
|
|
|
+
|
|
|
+ // 添加到场景
|
|
|
+ scene.add(model)
|
|
|
+ console.log('FBX模型添加到场景')
|
|
|
+
|
|
|
+ // 计算最终边界框
|
|
|
+ console.log('计算最终模型边界框...')
|
|
|
+ model.updateMatrixWorld(true)
|
|
|
+ const finalBox = new THREE.Box3().setFromObject(model)
|
|
|
+ const finalSize = finalBox.getSize(new THREE.Vector3())
|
|
|
+ const finalCenter = finalBox.getCenter(new THREE.Vector3())
|
|
|
+
|
|
|
+ console.log('最终模型边界框大小:', finalSize)
|
|
|
+ console.log('最终模型边界框中心:', finalCenter)
|
|
|
+ console.log('模型实际位置:', model.position)
|
|
|
+
|
|
|
+ // 调整相机位置
|
|
|
+ console.log('调整相机位置...')
|
|
|
+ adjustCameraForModel()
|
|
|
+ console.log('相机位置调整完成')
|
|
|
+
|
|
|
+ // 释放Blob URL
|
|
|
+ URL.revokeObjectURL(blobUrl)
|
|
|
+ console.log('FBX模型加载和处理完成')
|
|
|
+ },
|
|
|
+ (xhr) => {
|
|
|
+ console.log('FBX模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ console.error('FBX加载错误:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ // 释放Blob URL
|
|
|
+ URL.revokeObjectURL(blobUrl)
|
|
|
+ createDefaultModel()
|
|
|
+ }
|
|
|
+ )
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ console.error('后端接口请求错误:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ createDefaultModel()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 加载STL模型
|
|
|
+const loadSTLModel = (url) => {
|
|
|
+ console.log('开始加载STL模型:', url)
|
|
|
+
|
|
|
+ // 使用后端接口获取文件
|
|
|
+ // 直接传递包含/profile前缀的完整路径,后端会处理前缀
|
|
|
+ const resourcePath = url
|
|
|
+ const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
|
|
|
+ console.log('使用后端接口获取文件:', apiUrl)
|
|
|
+
|
|
|
+ request({
|
|
|
+ url: apiUrl,
|
|
|
+ method: 'get',
|
|
|
+ responseType: 'blob'
|
|
|
+ })
|
|
|
+ .then(response => {
|
|
|
+ const blob = response
|
|
|
+ console.log('STL模型响应是Blob,大小:', blob.size)
|
|
|
+ console.log('Blob类型:', blob.type)
|
|
|
+
|
|
|
+ // 使用Blob创建临时URL
|
|
|
+ const blobUrl = URL.createObjectURL(blob)
|
|
|
+ console.log('创建STL Blob URL:', blobUrl)
|
|
|
+
|
|
|
+ // 使用Blob URL加载模型
|
|
|
+ const loader = new STLLoader()
|
|
|
+ loader.load(
|
|
|
+ blobUrl,
|
|
|
+ (geometry) => {
|
|
|
+ console.log('STL模型加载成功,几何体顶点数:', geometry.attributes.position.count)
|
|
|
+ const material = new THREE.MeshStandardMaterial({ color: 0x409eff })
|
|
|
+ const mesh = new THREE.Mesh(geometry, material)
|
|
|
+ model = mesh
|
|
|
+ scene.add(mesh)
|
|
|
+ // 使用模型自身的原始坐标,不进行中心调整
|
|
|
+ console.log('模型添加到场景,使用原始坐标')
|
|
|
+ console.log('模型原始位置:', model.position)
|
|
|
+ console.log('模型原始旋转:', model.rotation)
|
|
|
+ console.log('模型原始缩放:', model.scale)
|
|
|
+ // 调整相机位置,使模型在视角中心且完整可见
|
|
|
+ adjustCameraForModel()
|
|
|
+ console.log('相机位置调整完成')
|
|
|
+ // 释放Blob URL
|
|
|
+ URL.revokeObjectURL(blobUrl)
|
|
|
+ },
|
|
|
+ (xhr) => {
|
|
|
+ console.log('STL模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ console.error('STL加载错误:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ // 释放Blob URL
|
|
|
+ URL.revokeObjectURL(blobUrl)
|
|
|
+ createDefaultModel()
|
|
|
+ }
|
|
|
+ )
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ console.error('后端接口请求错误:', error)
|
|
|
+ console.error('错误详情:', error.message)
|
|
|
+ console.error('错误堆栈:', error.stack)
|
|
|
+ createDefaultModel()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 创建默认模型
|
|
|
+const createDefaultModel = () => {
|
|
|
+ // 创建一个简单的立方体作为默认模型
|
|
|
+ const geometry = new THREE.BoxGeometry(2, 2, 2)
|
|
|
+ const material = new THREE.MeshStandardMaterial({
|
|
|
+ color: 0x409eff,
|
|
|
+ wireframe: true
|
|
|
+ })
|
|
|
+ model = new THREE.Mesh(geometry, material)
|
|
|
+ scene.add(model)
|
|
|
+}
|
|
|
+
|
|
|
+// 调整相机位置,使模型在视角中心且完整可见
|
|
|
+const adjustCameraForModel = () => {
|
|
|
+ if (!model || !camera) return
|
|
|
+
|
|
|
+ console.log('=======================================')
|
|
|
+ console.log('开始调整相机位置')
|
|
|
+ console.log('当前模型位置:', model.position)
|
|
|
+ console.log('当前相机位置:', camera.position)
|
|
|
+
|
|
|
+ // 增强:强制更新所有模型和子对象的矩阵
|
|
|
+ console.log('强制更新模型和子对象矩阵...')
|
|
|
+ model.traverse((child) => {
|
|
|
+ if (child.isObject3D) {
|
|
|
+ child.updateMatrix()
|
|
|
+ child.updateMatrixWorld(true)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ console.log('模型矩阵更新完成')
|
|
|
+
|
|
|
+ // 计算模型边界框
|
|
|
+ const box = new THREE.Box3().setFromObject(model)
|
|
|
+ const size = box.getSize(new THREE.Vector3())
|
|
|
+ const center = box.getCenter(new THREE.Vector3())
|
|
|
+
|
|
|
+ console.log('模型边界框大小:', size)
|
|
|
+ console.log('模型边界框中心:', center)
|
|
|
+ console.log('边界框最小值:', box.min)
|
|
|
+ console.log('边界框最大值:', box.max)
|
|
|
+
|
|
|
+ // 增强:检查边界框是否有效
|
|
|
+ if (box.isEmpty()) {
|
|
|
+ console.warn('模型边界框为空,使用默认相机位置')
|
|
|
+ // 使用默认相机位置
|
|
|
+ camera.position.set(5, 5, 5)
|
|
|
+ camera.lookAt(0, 0, 0)
|
|
|
+ camera.updateProjectionMatrix()
|
|
|
+ console.log('使用默认相机位置:', camera.position)
|
|
|
+ console.log('相机位置调整完成')
|
|
|
+ console.log('=======================================')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算模型对角线长度
|
|
|
+ const modelDiagonal = Math.sqrt(
|
|
|
+ size.x * size.x +
|
|
|
+ size.y * size.y +
|
|
|
+ size.z * size.z
|
|
|
+ )
|
|
|
+
|
|
|
+ console.log('模型对角线长度:', modelDiagonal)
|
|
|
+
|
|
|
+ // 根据模型大小计算合适的相机距离
|
|
|
+ const fov = camera.fov * (Math.PI / 180) // 转换为弧度
|
|
|
+
|
|
|
+ // 增强:使用动态安全系数,根据模型大小调整
|
|
|
+ let safetyFactor = 1.3
|
|
|
+ if (modelDiagonal < 5) {
|
|
|
+ safetyFactor = 1.5 // 小模型使用更大的安全系数
|
|
|
+ } else if (modelDiagonal > 20) {
|
|
|
+ safetyFactor = 1.1 // 大模型使用更小的安全系数
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('使用的安全系数:', safetyFactor)
|
|
|
+ const cameraDistance = (modelDiagonal / 2) / Math.tan(fov / 2) * safetyFactor
|
|
|
+
|
|
|
+ console.log('计算的相机距离:', cameraDistance)
|
|
|
+ console.log('当前相机到模型中心距离:', camera.position.distanceTo(center))
|
|
|
+
|
|
|
+ // 增强:确保相机距离不会太小
|
|
|
+ const minCameraDistance = Math.max(2, modelDiagonal * 0.5)
|
|
|
+ const finalCameraDistance = Math.max(minCameraDistance, cameraDistance)
|
|
|
+ console.log('最终相机距离:', finalCameraDistance, '(最小距离:', minCameraDistance, ')')
|
|
|
+
|
|
|
+ // 增强:对于FBX模型,确保相机位置计算考虑模型的实际位置
|
|
|
+ console.log('模型实际位置:', model.position)
|
|
|
+ console.log('边界框中心相对于模型位置:', center.clone().sub(model.position))
|
|
|
+
|
|
|
+ // 设置相机位置,从斜上方看向模型中心
|
|
|
+ // 这样可以获得更好的视角
|
|
|
+ const cameraHeight = finalCameraDistance * 0.4 // 相机高度,40%的距离
|
|
|
+
|
|
|
+ // 增强:考虑模型的实际位置
|
|
|
+ const cameraPosition = new THREE.Vector3(
|
|
|
+ center.x + finalCameraDistance * 0.2, // 稍微偏右
|
|
|
+ center.y + cameraHeight, // 上方
|
|
|
+ center.z + finalCameraDistance * 0.9 // 前方
|
|
|
+ )
|
|
|
+
|
|
|
+ console.log('计算的相机位置:', cameraPosition)
|
|
|
+ camera.position.copy(cameraPosition)
|
|
|
+
|
|
|
+ // 相机看向模型中心
|
|
|
+ camera.lookAt(center)
|
|
|
+
|
|
|
+ console.log('调整后相机位置:', camera.position)
|
|
|
+ console.log('相机看向:', center)
|
|
|
+ console.log('调整后相机到模型中心距离:', camera.position.distanceTo(center))
|
|
|
+
|
|
|
+ // 确保相机矩阵更新
|
|
|
+ camera.updateProjectionMatrix()
|
|
|
+ console.log('相机投影矩阵已更新')
|
|
|
+
|
|
|
+ // 增强:重置相机的旋转和缩放
|
|
|
+ console.log('重置相机旋转和缩放...')
|
|
|
+ camera.rotation.set(0, 0, 0)
|
|
|
+ camera.scale.set(1, 1, 1)
|
|
|
+ console.log('相机旋转和缩放已重置')
|
|
|
+
|
|
|
+ console.log('相机位置调整完成,现在应该能完整看到模型')
|
|
|
+ console.log('=======================================')
|
|
|
+}
|
|
|
+
|
|
|
+// 自动调整模型位置和大小(保留但不再使用)
|
|
|
+const centerModel = () => {
|
|
|
+ if (!model || !camera) return
|
|
|
+
|
|
|
+ console.log('=======================================')
|
|
|
+ console.log('开始调整模型位置和大小')
|
|
|
+ console.log('调整前模型位置:', model.position)
|
|
|
+ console.log('调整前模型缩放:', model.scale)
|
|
|
+ console.log('调整前模型旋转:', model.rotation)
|
|
|
+ console.log('调整前模型世界矩阵:', model.matrixWorld.elements)
|
|
|
+
|
|
|
+ // 重置模型旋转,确保计算边界框时不受旋转影响
|
|
|
+ model.rotation.set(0, 0, 0)
|
|
|
+ console.log('重置模型旋转后:', model.rotation)
|
|
|
+
|
|
|
+ // 确保模型矩阵更新
|
|
|
+ model.updateMatrixWorld(true)
|
|
|
+ console.log('模型矩阵已更新')
|
|
|
+ console.log('重置旋转后模型世界矩阵:', model.matrixWorld.elements)
|
|
|
+
|
|
|
+ // 计算模型边界框
|
|
|
+ const box = new THREE.Box3().setFromObject(model)
|
|
|
+ const size = box.getSize(new THREE.Vector3())
|
|
|
+ const center = box.getCenter(new THREE.Vector3())
|
|
|
+
|
|
|
+ console.log('模型边界框大小:', size)
|
|
|
+ console.log('模型边界框中心:', center)
|
|
|
+ console.log('边界框最小值:', box.min)
|
|
|
+ console.log('边界框最大值:', box.max)
|
|
|
+
|
|
|
+ // 直接设置模型位置到原点,确保在世界中心
|
|
|
+ console.log('准备调整模型位置到原点...')
|
|
|
+ console.log('计算的位置调整值:', -center.x, -center.y, -center.z)
|
|
|
+ model.position.set(-center.x, -center.y, -center.z)
|
|
|
+ console.log('模型调整后位置:', model.position)
|
|
|
+ console.log('模型位置是否接近原点:',
|
|
|
+ Math.abs(model.position.x) < 0.001 &&
|
|
|
+ Math.abs(model.position.y) < 0.001 &&
|
|
|
+ Math.abs(model.position.z) < 0.001
|
|
|
+ )
|
|
|
+
|
|
|
+ // 确保模型矩阵更新
|
|
|
+ model.updateMatrixWorld(true)
|
|
|
+ console.log('位置调整后模型矩阵已更新')
|
|
|
+ console.log('位置调整后模型世界矩阵:', model.matrixWorld.elements)
|
|
|
+
|
|
|
+ // 重新计算调整后的边界框,验证位置是否正确
|
|
|
+ const positionAdjustedBox = new THREE.Box3().setFromObject(model)
|
|
|
+ const positionAdjustedCenter = positionAdjustedBox.getCenter(new THREE.Vector3())
|
|
|
+ console.log('位置调整后模型边界框中心:', positionAdjustedCenter)
|
|
|
+ console.log('边界框中心是否接近原点:',
|
|
|
+ Math.abs(positionAdjustedCenter.x) < 0.001 &&
|
|
|
+ Math.abs(positionAdjustedCenter.y) < 0.001 &&
|
|
|
+ Math.abs(positionAdjustedCenter.z) < 0.001
|
|
|
+ )
|
|
|
+
|
|
|
+ // 自动调整模型大小 - 使用更大的基准值,确保模型不会太小
|
|
|
+ const maxSize = Math.max(size.x, size.y, size.z)
|
|
|
+ if (maxSize > 0) {
|
|
|
+ // 使用 5 作为基准值,而不是 3,让模型更大一些
|
|
|
+ const scale = 5 / maxSize
|
|
|
+ console.log('准备调整模型缩放...')
|
|
|
+ console.log('计算的缩放比例:', scale)
|
|
|
+ model.scale.set(scale, scale, scale)
|
|
|
+ console.log('模型调整后缩放:', model.scale)
|
|
|
+ console.log('调整后模型最大尺寸:', maxSize * scale)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保模型矩阵更新
|
|
|
+ model.updateMatrixWorld(true)
|
|
|
+ console.log('模型缩放后矩阵已更新')
|
|
|
+ console.log('缩放后模型世界矩阵:', model.matrixWorld.elements)
|
|
|
+
|
|
|
+ // 重新计算调整后的边界框
|
|
|
+ const adjustedBox = new THREE.Box3().setFromObject(model)
|
|
|
+ const adjustedSize = adjustedBox.getSize(new THREE.Vector3())
|
|
|
+ const adjustedCenter = adjustedBox.getCenter(new THREE.Vector3())
|
|
|
+ console.log('调整后模型边界框大小:', adjustedSize)
|
|
|
+ console.log('调整后模型边界框中心:', adjustedCenter)
|
|
|
+ console.log('最终边界框中心是否接近原点:',
|
|
|
+ Math.abs(adjustedCenter.x) < 0.001 &&
|
|
|
+ Math.abs(adjustedCenter.y) < 0.001 &&
|
|
|
+ Math.abs(adjustedCenter.z) < 0.001
|
|
|
+ )
|
|
|
+
|
|
|
+ // 动态调整相机位置,确保相机能够完整看到模型
|
|
|
+ const modelDiagonal = Math.sqrt(
|
|
|
+ adjustedSize.x * adjustedSize.x +
|
|
|
+ adjustedSize.y * adjustedSize.y +
|
|
|
+ adjustedSize.z * adjustedSize.z
|
|
|
+ )
|
|
|
+
|
|
|
+ // 根据模型对角线长度计算合适的相机距离
|
|
|
+ const fov = camera.fov * (Math.PI / 180) // 转换为弧度
|
|
|
+ const cameraDistance = (modelDiagonal / 2) / Math.tan(fov / 2) * 1.5 // 1.5 是安全系数
|
|
|
+
|
|
|
+ console.log('模型对角线长度:', modelDiagonal)
|
|
|
+ console.log('计算的相机距离:', cameraDistance)
|
|
|
+
|
|
|
+ // 设置相机位置,确保从适当的距离和角度观察模型
|
|
|
+ camera.position.set(0, cameraDistance * 0.3, cameraDistance)
|
|
|
+ camera.lookAt(0, 0, 0)
|
|
|
+
|
|
|
+ console.log('调整后相机位置:', camera.position)
|
|
|
+ console.log('相机看向:', new THREE.Vector3(0, 0, 0))
|
|
|
+ console.log('相机到原点距离:', camera.position.distanceTo(new THREE.Vector3(0, 0, 0)))
|
|
|
+
|
|
|
+ // 确保相机矩阵更新
|
|
|
+ camera.updateProjectionMatrix()
|
|
|
+ console.log('相机投影矩阵已更新')
|
|
|
+
|
|
|
+ console.log('模型位置调整完成,现在应该在世界坐标系原点')
|
|
|
+ console.log('=======================================')
|
|
|
+}
|
|
|
+
|
|
|
+// 渲染动画
|
|
|
+const animate = () => {
|
|
|
+ animationId = requestAnimationFrame(animate)
|
|
|
+ if (renderer && scene && camera) {
|
|
|
+ renderer.render(scene, camera)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理窗口大小变化
|
|
|
+const handleResize = () => {
|
|
|
+ if (!camera || !renderer || !modelContainer.value) return
|
|
|
+
|
|
|
+ const width = modelContainer.value.clientWidth
|
|
|
+ const height = modelContainer.value.clientHeight
|
|
|
+
|
|
|
+ camera.aspect = width / height
|
|
|
+ camera.updateProjectionMatrix()
|
|
|
+ renderer.setSize(width, height)
|
|
|
+}
|
|
|
+
|
|
|
+// 处理关闭
|
|
|
+const handleClose = () => {
|
|
|
+ dialogVisible.value = false
|
|
|
+ emit('update:visible', false)
|
|
|
+ emit('close')
|
|
|
+}
|
|
|
+
|
|
|
+// 监听可见性变化
|
|
|
+watch(() => props.visible, (newVal) => {
|
|
|
+ dialogVisible.value = newVal
|
|
|
+ if (newVal) {
|
|
|
+ // 延迟初始化,确保DOM已更新
|
|
|
+ setTimeout(() => {
|
|
|
+ initScene()
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 组件挂载时初始化
|
|
|
+onMounted(() => {
|
|
|
+ dialogVisible.value = props.visible
|
|
|
+ if (props.visible) {
|
|
|
+ initScene()
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 组件卸载时清理
|
|
|
+onUnmounted(() => {
|
|
|
+ if (animationId) {
|
|
|
+ cancelAnimationFrame(animationId)
|
|
|
+ }
|
|
|
+ if (renderer) {
|
|
|
+ renderer.dispose()
|
|
|
+ }
|
|
|
+ if (modelContainer.value) {
|
|
|
+ window.removeEventListener('resize', handleResize)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+const modelName = ref('')
|
|
|
+watch(() => props.modelData, (newVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ modelName.value = newVal.name || '模型预览'
|
|
|
+ }
|
|
|
+}, { immediate: true })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.model-preview-container {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ height: 600px;
|
|
|
+}
|
|
|
+
|
|
|
+.model-container {
|
|
|
+ flex: 1;
|
|
|
+ background-color: #fafafa;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.model-info {
|
|
|
+ width: 300px;
|
|
|
+ background-color: #ffffff;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.model-info h3 {
|
|
|
+ margin-top: 0;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .model-preview-container {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .model-info {
|
|
|
+ width: 100%;
|
|
|
+ max-height: 300px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|