ModelPreview.vue 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625
  1. <template>
  2. <el-dialog
  3. v-model="dialogVisible"
  4. :title="modelName"
  5. width="80%"
  6. :before-close="handleClose"
  7. destroy-on-close
  8. >
  9. <div class="model-preview-container">
  10. <div ref="modelContainer" class="model-container">
  11. <!-- 光照控制按钮 -->
  12. <div class="light-control-btn" @click="toggleLightControl">
  13. <el-icon><Sunny /></el-icon>
  14. </div>
  15. <!-- 光照控制面板 -->
  16. <div v-if="showLightControl" class="light-control-panel">
  17. <h4>光照控制</h4>
  18. <!-- 材质透明度调整 -->
  19. <div class="control-item">
  20. <label>材质不透明度</label>
  21. <el-slider
  22. v-model="materialOpacity"
  23. :min="0.1"
  24. :max="1"
  25. :step="0.05"
  26. @change="updateMaterialOpacity"
  27. />
  28. <span class="value">{{ materialOpacity.toFixed(2) }}</span>
  29. </div>
  30. <!-- 环境光强度 -->
  31. <div class="control-item">
  32. <label>环境光强度</label>
  33. <el-slider
  34. v-model="lightSettings.ambientIntensity"
  35. :min="0"
  36. :max="5"
  37. :step="0.1"
  38. @change="updateLights"
  39. />
  40. <span class="value">{{ lightSettings.ambientIntensity.toFixed(1) }}</span>
  41. </div>
  42. <!-- 主平行光强度 -->
  43. <div class="control-item">
  44. <label>主光源强度</label>
  45. <el-slider
  46. v-model="lightSettings.directionalIntensity"
  47. :min="0"
  48. :max="10"
  49. :step="0.1"
  50. @change="updateLights"
  51. />
  52. <span class="value">{{ lightSettings.directionalIntensity.toFixed(1) }}</span>
  53. </div>
  54. <!-- 辅助光强度 -->
  55. <div class="control-item">
  56. <label>辅助光强度</label>
  57. <el-slider
  58. v-model="lightSettings.directionalIntensity2"
  59. :min="0"
  60. :max="5"
  61. :step="0.1"
  62. @change="updateLights"
  63. />
  64. <span class="value">{{ lightSettings.directionalIntensity2.toFixed(1) }}</span>
  65. </div>
  66. <!-- 主光源角度 -->
  67. <div class="control-item">
  68. <label>主光源角度</label>
  69. <div class="angle-controls">
  70. <div>
  71. <span>X轴</span>
  72. <el-slider
  73. v-model="lightSettings.directionalPosition.x"
  74. :min="-5"
  75. :max="5"
  76. :step="0.1"
  77. @change="updateLights"
  78. />
  79. <span class="value">{{ lightSettings.directionalPosition.x.toFixed(1) }}</span>
  80. </div>
  81. <div>
  82. <span>Y轴</span>
  83. <el-slider
  84. v-model="lightSettings.directionalPosition.y"
  85. :min="-5"
  86. :max="5"
  87. :step="0.1"
  88. @change="updateLights"
  89. />
  90. <span class="value">{{ lightSettings.directionalPosition.y.toFixed(1) }}</span>
  91. </div>
  92. <div>
  93. <span>Z轴</span>
  94. <el-slider
  95. v-model="lightSettings.directionalPosition.z"
  96. :min="-5"
  97. :max="5"
  98. :step="0.1"
  99. @change="updateLights"
  100. />
  101. <span class="value">{{ lightSettings.directionalPosition.z.toFixed(1) }}</span>
  102. </div>
  103. </div>
  104. </div>
  105. </div>
  106. </div>
  107. <div class="model-info">
  108. <h3>模型信息</h3>
  109. <el-descriptions :column="1" border>
  110. <el-descriptions-item label="模型名称">{{ modelData.name }}</el-descriptions-item>
  111. <el-descriptions-item label="模型类型">{{ getTypeDisplayName(modelData.type) }}</el-descriptions-item>
  112. <el-descriptions-item label="经纬度">{{ modelData.location }}</el-descriptions-item>
  113. <el-descriptions-item label="提交单位">{{ modelData.uploadUnit }}</el-descriptions-item>
  114. <el-descriptions-item label="格式">{{ modelData.format }}</el-descriptions-item>
  115. <el-descriptions-item label="状态">{{ modelData.status }}</el-descriptions-item>
  116. <el-descriptions-item label="创建时间">{{ modelData.createTime }}</el-descriptions-item>
  117. </el-descriptions>
  118. </div>
  119. </div>
  120. <template #footer>
  121. <el-button @click="handleClose">关闭</el-button>
  122. </template>
  123. </el-dialog>
  124. </template>
  125. <script setup>
  126. import { ref, onMounted, onUnmounted, watch } from 'vue'
  127. import * as THREE from 'three'
  128. import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
  129. import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
  130. import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'
  131. import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'
  132. // 导入项目的request实例
  133. import request from '@/utils/request'
  134. import { Sunny } from '@element-plus/icons-vue'
  135. const props = defineProps({
  136. visible: {
  137. type: Boolean,
  138. default: false
  139. },
  140. modelData: {
  141. type: Object,
  142. default: () => ({})
  143. }
  144. })
  145. const emit = defineEmits(['update:visible', 'close'])
  146. const modelContainer = ref(null)
  147. let scene = null
  148. let camera = null
  149. let renderer = null
  150. let model = null
  151. let controls = null
  152. let animationId = null
  153. const dialogVisible = ref(false)
  154. // 光照控制相关变量
  155. const showLightControl = ref(false)
  156. let ambientLight = null
  157. let directionalLight = null
  158. let directionalLight2 = null
  159. const lightSettings = ref({
  160. ambientIntensity: 2.0,
  161. directionalIntensity: 4.0,
  162. directionalIntensity2: 2.0,
  163. directionalPosition: {
  164. x: 1,
  165. y: 1,
  166. z: 1
  167. }
  168. })
  169. const materialOpacity = ref(1.0)
  170. // 获取类型显示名称
  171. const getTypeDisplayName = (type) => {
  172. const typeMapping = {
  173. 'RESERVOIR': '水库',
  174. 'HYDROPOWER': '水电站',
  175. 'IRRIGATION': '灌溉',
  176. 'FLOOD_CONTROL': '防洪',
  177. 'CANAL': '渠道',
  178. 'PUMPING_STATION': '泵站'
  179. }
  180. return typeMapping[type] || type
  181. }
  182. // 初始化场景
  183. const initScene = () => {
  184. if (!modelContainer.value) return
  185. // 创建场景
  186. scene = new THREE.Scene()
  187. scene.background = new THREE.Color(0xf0f0f0)
  188. // 创建相机
  189. camera = new THREE.PerspectiveCamera(
  190. 75,
  191. modelContainer.value.clientWidth / modelContainer.value.clientHeight,
  192. 0.1,
  193. 1000
  194. )
  195. // 增加相机初始位置,让相机离模型更远,以便能看到整个模型
  196. camera.position.z = 10
  197. camera.position.y = 2
  198. // 创建渲染器
  199. renderer = new THREE.WebGLRenderer({ antialias: true })
  200. renderer.setSize(modelContainer.value.clientWidth, modelContainer.value.clientHeight)
  201. modelContainer.value.appendChild(renderer.domElement)
  202. // 添加光源
  203. ambientLight = new THREE.AmbientLight(0xffffff, lightSettings.value.ambientIntensity)
  204. scene.add(ambientLight)
  205. directionalLight = new THREE.DirectionalLight(0xffffff, lightSettings.value.directionalIntensity)
  206. directionalLight.position.set(
  207. lightSettings.value.directionalPosition.x,
  208. lightSettings.value.directionalPosition.y,
  209. lightSettings.value.directionalPosition.z
  210. )
  211. scene.add(directionalLight)
  212. // 添加第二个平行光,从不同角度照射,增强照明效果
  213. directionalLight2 = new THREE.DirectionalLight(0xffffff, lightSettings.value.directionalIntensity2)
  214. directionalLight2.position.set(-1, 1, -1)
  215. scene.add(directionalLight2)
  216. // 添加网格辅助线
  217. const gridHelper = new THREE.GridHelper(10, 10)
  218. scene.add(gridHelper)
  219. // 加载模型
  220. if (props.modelData) {
  221. loadModel()
  222. }
  223. // 添加鼠标控制
  224. initControls()
  225. // 添加三维坐标轴
  226. addAxesHelper()
  227. // 开始渲染
  228. animate()
  229. // 监听窗口大小变化
  230. window.addEventListener('resize', handleResize)
  231. // 添加光照控制面板事件阻止
  232. addLightControlEventPrevent()
  233. }
  234. // 初始化控制器
  235. const initControls = () => {
  236. // 简单的鼠标控制实现 - 控制相机视角
  237. let isDragging = false
  238. let isPanning = false // 中键平移状态
  239. let previousMousePosition = { x: 0, y: 0 }
  240. // 相机目标点(看向的点)
  241. let cameraTarget = new THREE.Vector3(0, 0, 0)
  242. // 更新相机目标点为模型中心
  243. const updateCameraTarget = () => {
  244. if (model) {
  245. model.updateMatrixWorld(true)
  246. const box = new THREE.Box3().setFromObject(model)
  247. cameraTarget = box.getCenter(new THREE.Vector3())
  248. console.log('相机目标点已更新为模型中心:', cameraTarget)
  249. }
  250. }
  251. // 初始化时更新目标点
  252. updateCameraTarget()
  253. const container = modelContainer.value
  254. // 检查事件是否来自光照控制面板
  255. const isFromLightControl = (e) => {
  256. let target = e.target
  257. while (target) {
  258. if (target.classList && (target.classList.contains('light-control-panel') || target.classList.contains('light-control-btn'))) {
  259. return true
  260. }
  261. target = target.parentElement
  262. }
  263. return false
  264. }
  265. container.addEventListener('mousedown', (e) => {
  266. // 如果事件来自光照控制面板,不处理
  267. if (isFromLightControl(e)) return
  268. // 检查是否是中键
  269. if (e.button === 1) { // 中键
  270. isPanning = true
  271. } else if (e.button === 0) { // 左键
  272. isDragging = true
  273. }
  274. previousMousePosition = { x: e.offsetX, y: e.offsetY }
  275. })
  276. container.addEventListener('mousemove', (e) => {
  277. // 如果事件来自光照控制面板,不处理
  278. if (isFromLightControl(e)) return
  279. const deltaMove = {
  280. x: e.offsetX - previousMousePosition.x,
  281. y: e.offsetY - previousMousePosition.y
  282. }
  283. // 中键平移相机
  284. if (isPanning && camera) {
  285. // 计算平移距离
  286. const distance = camera.position.distanceTo(cameraTarget)
  287. const panSpeed = distance * 0.001
  288. // 计算相机的右方向和上方向
  289. const right = new THREE.Vector3()
  290. const up = new THREE.Vector3()
  291. camera.getWorldDirection(right)
  292. right.crossVectors(camera.up, right).normalize()
  293. up.copy(camera.up).normalize()
  294. // 计算平移向量
  295. const pan = new THREE.Vector3()
  296. pan.addScaledVector(right, deltaMove.x * panSpeed)
  297. pan.addScaledVector(up, deltaMove.y * panSpeed)
  298. // 平移相机和目标点
  299. camera.position.add(pan)
  300. cameraTarget.add(pan)
  301. // 相机看向目标点
  302. camera.lookAt(cameraTarget)
  303. }
  304. // 左键旋转相机
  305. else if (isDragging && camera) {
  306. // 控制相机围绕目标点旋转
  307. // 计算相机到目标点的距离
  308. const distance = camera.position.distanceTo(cameraTarget)
  309. // 限制垂直旋转范围,避免相机翻转
  310. const currentPosition = new THREE.Vector3().copy(camera.position)
  311. const direction = new THREE.Vector3().subVectors(currentPosition, cameraTarget).normalize()
  312. // 水平旋转(围绕Y轴)
  313. const horizontalRotation = new THREE.Matrix4().makeRotationY(-deltaMove.x * 0.01)
  314. // 垂直旋转(围绕X轴,限制范围)
  315. const verticalRotation = new THREE.Matrix4().makeRotationX(-deltaMove.y * 0.005)
  316. // 应用旋转
  317. const newDirection = direction.applyMatrix4(horizontalRotation).applyMatrix4(verticalRotation)
  318. // 设置新的相机位置
  319. camera.position.copy(cameraTarget).add(newDirection.multiplyScalar(distance))
  320. // 相机看向目标点
  321. camera.lookAt(cameraTarget)
  322. }
  323. previousMousePosition = {
  324. x: e.offsetX,
  325. y: e.offsetY
  326. }
  327. })
  328. container.addEventListener('mouseup', (e) => {
  329. // 如果事件来自光照控制面板,不处理
  330. if (isFromLightControl(e)) return
  331. isDragging = false
  332. isPanning = false
  333. })
  334. container.addEventListener('mouseleave', (e) => {
  335. // 如果事件来自光照控制面板,不处理
  336. if (isFromLightControl(e)) return
  337. isDragging = false
  338. isPanning = false
  339. })
  340. // 滚轮缩放 - 控制相机距离目标点的距离
  341. container.addEventListener('wheel', (e) => {
  342. // 如果事件来自光照控制面板,不处理
  343. if (isFromLightControl(e)) return
  344. e.preventDefault()
  345. if (camera) {
  346. // 计算相机到目标点的向量
  347. const direction = new THREE.Vector3().subVectors(camera.position, cameraTarget)
  348. const distance = direction.length()
  349. // 限制最小和最大距离
  350. const minDistance = 0.5
  351. const maxDistance = 50
  352. // 计算新的距离
  353. const newDistance = Math.max(minDistance, Math.min(maxDistance, distance + e.deltaY * 0.01))
  354. // 设置新的相机位置
  355. direction.normalize().multiplyScalar(newDistance)
  356. camera.position.copy(cameraTarget).add(direction)
  357. // 相机看向目标点
  358. camera.lookAt(cameraTarget)
  359. }
  360. })
  361. }
  362. // 为光照控制面板添加事件阻止
  363. const addLightControlEventPrevent = () => {
  364. const container = modelContainer.value
  365. if (!container) return
  366. // 为光照控制面板添加事件监听器,阻止事件传播
  367. const lightControlPanel = container.querySelector('.light-control-panel')
  368. const lightControlBtn = container.querySelector('.light-control-btn')
  369. const preventEvents = (element) => {
  370. if (element) {
  371. element.addEventListener('mousedown', (e) => {
  372. e.stopPropagation()
  373. })
  374. element.addEventListener('mousemove', (e) => {
  375. e.stopPropagation()
  376. })
  377. element.addEventListener('mouseup', (e) => {
  378. e.stopPropagation()
  379. })
  380. element.addEventListener('wheel', (e) => {
  381. e.stopPropagation()
  382. e.preventDefault()
  383. })
  384. }
  385. }
  386. preventEvents(lightControlPanel)
  387. preventEvents(lightControlBtn)
  388. }
  389. // 添加三维坐标轴
  390. const addAxesHelper = () => {
  391. // 移除现有的坐标轴
  392. if (scene) {
  393. scene.traverse((object) => {
  394. if (object.isAxesHelper || object.userData.isCustomAxes) {
  395. scene.remove(object)
  396. }
  397. })
  398. }
  399. // 创建带箭头的坐标轴
  400. if (scene) {
  401. const axisLength = 1
  402. const arrowSize = 0.1
  403. // X轴 - 红色
  404. const xArrow = new THREE.ArrowHelper(
  405. new THREE.Vector3(1, 0, 0), // 方向
  406. new THREE.Vector3(0, 0, 0), // 起点
  407. axisLength, // 长度
  408. 0xff0000, // 颜色
  409. arrowSize, // 箭头大小
  410. arrowSize // 箭头宽度
  411. )
  412. xArrow.userData.isCustomAxes = true
  413. scene.add(xArrow)
  414. // Y轴 - 绿色
  415. const yArrow = new THREE.ArrowHelper(
  416. new THREE.Vector3(0, 1, 0),
  417. new THREE.Vector3(0, 0, 0),
  418. axisLength,
  419. 0x00ff00,
  420. arrowSize,
  421. arrowSize
  422. )
  423. yArrow.userData.isCustomAxes = true
  424. scene.add(yArrow)
  425. // Z轴 - 蓝色
  426. const zArrow = new THREE.ArrowHelper(
  427. new THREE.Vector3(0, 0, 1),
  428. new THREE.Vector3(0, 0, 0),
  429. axisLength,
  430. 0x0000ff,
  431. arrowSize,
  432. arrowSize
  433. )
  434. zArrow.userData.isCustomAxes = true
  435. scene.add(zArrow)
  436. console.log('带箭头的三维坐标轴已添加到场景原点')
  437. }
  438. }
  439. // 加载模型
  440. const loadModel = () => {
  441. console.log('========================================')
  442. console.log('开始加载模型流程')
  443. console.log('========================================')
  444. if (!props.modelData || !props.modelData.format) {
  445. console.error('模型数据不完整:', { modelData: props.modelData })
  446. console.log('========================================')
  447. console.log('加载模型流程结束 - 数据不完整')
  448. console.log('========================================')
  449. return
  450. }
  451. console.log('模型基本信息:', {
  452. modelId: props.modelData.id,
  453. modelFormat: props.modelData.format,
  454. modelName: props.modelData.name
  455. })
  456. // 打印完整的模型数据,查看是否包含文件路径或URL
  457. console.log('完整模型数据:', props.modelData)
  458. console.log('模型数据所有属性:', Object.keys(props.modelData))
  459. const format = props.modelData.format.toUpperCase()
  460. // 清除现有模型
  461. if (model) {
  462. scene.remove(model)
  463. model = null
  464. console.log('已清除现有模型')
  465. }
  466. try {
  467. // 检查模型数据中是否包含文件路径或URL
  468. let modelUrl = null
  469. // 尝试从模型数据中获取文件路径或URL
  470. if (props.modelData.path) {
  471. modelUrl = props.modelData.path
  472. console.log('✓ 从modelData.path获取模型路径:', modelUrl)
  473. } else if (props.modelData.url) {
  474. modelUrl = props.modelData.url
  475. console.log('✓ 从modelData.url获取模型路径:', modelUrl)
  476. } else if (props.modelData.filePath) {
  477. // 后端返回的filePath是相对路径,需要添加/profile前缀
  478. modelUrl = `/profile/${props.modelData.filePath}`
  479. console.log('✓ 从modelData.filePath获取模型路径并添加前缀:', props.modelData.filePath, '→', modelUrl)
  480. } else if (props.modelData.file_path) {
  481. // 检查file_path是否已经包含/profile前缀
  482. if (props.modelData.file_path.startsWith('/profile')) {
  483. // 已经包含/profile前缀,直接使用
  484. modelUrl = props.modelData.file_path
  485. console.log('✓ 从modelData.file_path获取模型路径(已包含/profile前缀):', modelUrl)
  486. } else {
  487. // 后端返回的file_path是相对路径,需要添加/profile前缀
  488. modelUrl = `/profile/${props.modelData.file_path}`
  489. console.log('✓ 从modelData.file_path获取模型路径并添加前缀:', props.modelData.file_path, '→', modelUrl)
  490. }
  491. } else if (props.modelData.fileUrl) {
  492. modelUrl = props.modelData.fileUrl
  493. console.log('✓ 从modelData.fileUrl获取模型路径:', modelUrl)
  494. } else if (props.modelData.storagePath) {
  495. modelUrl = props.modelData.storagePath
  496. console.log('✓ 从modelData.storagePath获取模型路径:', modelUrl)
  497. } else if (props.modelData.filename) {
  498. // 直接使用filename构建路径,添加/profile/upload前缀
  499. modelUrl = `/profile/upload/${props.modelData.filename}`
  500. console.log('✓ 从modelData.filename获取模型路径:', modelUrl)
  501. } else if (props.modelData.fileName) {
  502. // 直接使用fileName构建路径,添加/profile/upload前缀
  503. modelUrl = `/profile/upload/${props.modelData.fileName}`
  504. console.log('✓ 从modelData.fileName获取模型路径:', modelUrl)
  505. } else if (props.modelData.id) {
  506. // 如果没有找到文件路径,尝试使用ID构建路径
  507. // 尝试多种可能的静态文件路径,添加/profile前缀
  508. const modelId = props.modelData.id
  509. const possiblePaths = [
  510. `/profile/upload/${modelId}.${props.modelData.format.toLowerCase()}`,
  511. `/profile/model/${modelId}.${props.modelData.format.toLowerCase()}`
  512. ]
  513. // 尝试每个路径
  514. console.log('开始尝试构建模型路径:')
  515. for (const path of possiblePaths) {
  516. console.log(' 尝试路径:', path)
  517. }
  518. // 使用第一个可能的路径
  519. modelUrl = possiblePaths[0]
  520. console.log('✓ 使用ID构建模型路径:', modelUrl)
  521. }
  522. // 如果找到了模型路径
  523. if (modelUrl) {
  524. // 确保路径是完整的URL
  525. if (!modelUrl.startsWith('http://') && !modelUrl.startsWith('https://')) {
  526. // 检查是否已经是绝对路径(以/开头)
  527. if (!modelUrl.startsWith('/')) {
  528. // 如果是相对路径,添加基础路径
  529. const oldUrl = modelUrl
  530. modelUrl = `/${modelUrl}`
  531. console.log('✓ 添加斜杠前缀:', oldUrl, '→', modelUrl)
  532. }
  533. // 检查是否需要添加/upload前缀
  534. // 根据后端代码,FileUploadUtils.upload()返回的路径已经包含了upload前缀
  535. // 所以这里不需要再添加/upload/model/前缀
  536. console.log('✓ 路径处理完成:', modelUrl)
  537. }
  538. console.log('========================================')
  539. console.log('最终确定的模型路径:', modelUrl)
  540. console.log('========================================')
  541. // 根据模型格式加载
  542. switch (format) {
  543. case 'GLTF':
  544. case 'GLB':
  545. console.log('开始加载GLTF/GLB模型')
  546. loadGLTFModel(modelUrl)
  547. break
  548. case 'OBJ':
  549. console.log('开始加载OBJ模型')
  550. loadOBJModel(modelUrl)
  551. break
  552. case 'FBX':
  553. console.log('开始加载FBX模型')
  554. loadFBXModel(modelUrl)
  555. break
  556. case 'STL':
  557. console.log('开始加载STL模型')
  558. loadSTLModel(modelUrl)
  559. break
  560. default:
  561. console.error('不支持的模型格式:', format)
  562. // 显示默认模型
  563. createDefaultModel()
  564. console.log('========================================')
  565. console.log('加载模型流程结束 - 格式不支持')
  566. console.log('========================================')
  567. }
  568. } else {
  569. console.error('❌ 模型数据中未找到文件路径或URL')
  570. // 显示默认模型
  571. createDefaultModel()
  572. console.log('========================================')
  573. console.log('加载模型流程结束 - 未找到路径')
  574. console.log('========================================')
  575. }
  576. } catch (error) {
  577. console.error('❌ 加载模型出错:', error)
  578. console.error('错误详情:', error.message)
  579. console.error('错误堆栈:', error.stack)
  580. // 显示默认模型
  581. createDefaultModel()
  582. console.log('========================================')
  583. console.log('加载模型流程结束 - 发生错误')
  584. console.log('========================================')
  585. }
  586. }
  587. // 加载GLTF/GLB模型
  588. const loadGLTFModel = (url) => {
  589. console.log('开始加载GLTF/GLB模型:', url)
  590. // 使用后端接口获取文件
  591. // 直接传递包含/profile前缀的完整路径,后端会处理前缀
  592. const resourcePath = url
  593. const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
  594. console.log('使用后端接口获取文件:', apiUrl)
  595. request({
  596. url: apiUrl,
  597. method: 'get',
  598. responseType: 'blob'
  599. })
  600. .then(response => {
  601. const blob = response
  602. console.log('响应是Blob,大小:', blob.size)
  603. console.log('Blob类型:', blob.type)
  604. // 使用Blob创建临时URL
  605. const blobUrl = URL.createObjectURL(blob)
  606. console.log('创建Blob URL:', blobUrl)
  607. // 使用Blob URL加载模型
  608. const loader = new GLTFLoader()
  609. loader.load(
  610. blobUrl,
  611. (gltf) => {
  612. console.log('GLTF/GLB模型加载成功:', gltf)
  613. model = gltf.scene
  614. scene.add(model)
  615. // 使用模型自身的原始坐标,不进行中心调整
  616. console.log('模型添加到场景,使用原始坐标')
  617. console.log('模型原始位置:', model.position)
  618. console.log('模型原始旋转:', model.rotation)
  619. console.log('模型原始缩放:', model.scale)
  620. // 调整相机位置,使模型在视角中心且完整可见
  621. adjustCameraForModel()
  622. console.log('相机位置调整完成')
  623. // 修复半透明材质太透的问题
  624. fixTransparentMaterials(model)
  625. // 释放Blob URL
  626. URL.revokeObjectURL(blobUrl)
  627. },
  628. (xhr) => {
  629. console.log('GLTF/GLB模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
  630. },
  631. (error) => {
  632. console.error('GLTF加载错误:', error)
  633. console.error('错误详情:', error.message)
  634. console.error('错误堆栈:', error.stack)
  635. // 释放Blob URL
  636. URL.revokeObjectURL(blobUrl)
  637. createDefaultModel()
  638. }
  639. )
  640. })
  641. .catch(error => {
  642. console.error('后端接口请求错误:', error)
  643. console.error('错误详情:', error.message)
  644. console.error('错误堆栈:', error.stack)
  645. createDefaultModel()
  646. })
  647. }
  648. // 加载OBJ模型
  649. const loadOBJModel = (url) => {
  650. console.log('开始加载OBJ模型:', url)
  651. // 使用后端接口获取文件
  652. // 直接传递包含/profile前缀的完整路径,后端会处理前缀
  653. const resourcePath = url
  654. const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
  655. console.log('使用后端接口获取文件:', apiUrl)
  656. request({
  657. url: apiUrl,
  658. method: 'get',
  659. responseType: 'blob'
  660. })
  661. .then(response => {
  662. const blob = response
  663. console.log('OBJ模型响应是Blob,大小:', blob.size)
  664. console.log('Blob类型:', blob.type)
  665. // 使用Blob创建临时URL
  666. const blobUrl = URL.createObjectURL(blob)
  667. console.log('创建OBJ Blob URL:', blobUrl)
  668. // 使用Blob URL加载模型
  669. const loader = new OBJLoader()
  670. loader.load(
  671. blobUrl,
  672. (obj) => {
  673. console.log('OBJ模型加载成功:', obj)
  674. model = obj
  675. // 添加材质
  676. const material = new THREE.MeshStandardMaterial({ color: 0x409eff })
  677. obj.traverse((child) => {
  678. if (child.isMesh) {
  679. child.material = material
  680. }
  681. })
  682. scene.add(model)
  683. // 使用模型自身的原始坐标,不进行中心调整
  684. console.log('模型添加到场景,使用原始坐标')
  685. console.log('模型原始位置:', model.position)
  686. console.log('模型原始旋转:', model.rotation)
  687. console.log('模型原始缩放:', model.scale)
  688. // 调整相机位置,使模型在视角中心且完整可见
  689. adjustCameraForModel()
  690. console.log('相机位置调整完成')
  691. // 修复半透明材质太透的问题
  692. fixTransparentMaterials(model)
  693. // 释放Blob URL
  694. URL.revokeObjectURL(blobUrl)
  695. },
  696. (xhr) => {
  697. console.log('OBJ模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
  698. },
  699. (error) => {
  700. console.error('OBJ加载错误:', error)
  701. console.error('错误详情:', error.message)
  702. console.error('错误堆栈:', error.stack)
  703. // 释放Blob URL
  704. URL.revokeObjectURL(blobUrl)
  705. createDefaultModel()
  706. }
  707. )
  708. })
  709. .catch(error => {
  710. console.error('后端接口请求错误:', error)
  711. console.error('错误详情:', error.message)
  712. console.error('错误堆栈:', error.stack)
  713. createDefaultModel()
  714. })
  715. }
  716. // 处理FBX模型的特殊问题
  717. const processFBXModel = (fbxModel) => {
  718. console.log('开始专业处理FBX模型')
  719. // 1. FBX模型坐标系转换
  720. console.log('处理FBX坐标系...')
  721. // FBX通常使用Y-up坐标系,Three.js使用Y-up,所以不需要翻转
  722. // 但需要确保缩放和位置正确
  723. // 2. 统一模型缩放
  724. console.log('处理FBX模型缩放...')
  725. console.log('原始模型缩放:', fbxModel.scale)
  726. // 计算模型的边界框,用于确定合适的缩放
  727. let tempBox = new THREE.Box3().setFromObject(fbxModel)
  728. let tempSize = tempBox.getSize(new THREE.Vector3())
  729. console.log('原始模型边界框大小:', tempSize)
  730. // 计算模型的整体尺寸
  731. const maxDimension = Math.max(tempSize.x, tempSize.y, tempSize.z)
  732. console.log('模型最大尺寸:', maxDimension)
  733. // 计算合适的缩放因子,确保模型大小适中
  734. let scaleFactor = 1
  735. if (maxDimension > 10) {
  736. scaleFactor = 5 / maxDimension
  737. console.log('模型过大,需要缩小,缩放因子:', scaleFactor)
  738. } else if (maxDimension < 1) {
  739. scaleFactor = 2 / maxDimension
  740. console.log('模型过小,需要放大,缩放因子:', scaleFactor)
  741. }
  742. // 应用缩放因子到整个模型
  743. if (scaleFactor !== 1) {
  744. fbxModel.scale.multiplyScalar(scaleFactor)
  745. console.log('缩放调整完成,新的模型缩放:', fbxModel.scale)
  746. }
  747. // 3. 强制更新所有矩阵
  748. console.log('强制更新所有模型矩阵...')
  749. fbxModel.traverse((child) => {
  750. if (child.isObject3D) {
  751. child.updateMatrix()
  752. child.updateMatrixWorld(true)
  753. }
  754. })
  755. // 4. 计算模型边界框
  756. console.log('计算FBX模型边界框...')
  757. const box = new THREE.Box3().setFromObject(fbxModel)
  758. const size = box.getSize(new THREE.Vector3())
  759. const center = box.getCenter(new THREE.Vector3())
  760. console.log('FBX模型边界框大小:', size)
  761. console.log('FBX模型边界框中心:', center)
  762. // 5. 处理材质
  763. console.log('处理FBX模型材质...')
  764. let materialCount = 0
  765. fbxModel.traverse((child) => {
  766. if (child.isMesh) {
  767. console.log('处理网格:', child.name)
  768. try {
  769. // 为所有FBX网格添加默认材质,确保每个网格都有材质
  770. console.log('为', child.name, '添加默认材质')
  771. const defaultMaterial = new THREE.MeshStandardMaterial({
  772. color: 0x409eff,
  773. metalness: 0.3,
  774. roughness: 0.7,
  775. transparent: false,
  776. opacity: 1,
  777. side: THREE.FrontSide
  778. })
  779. child.material = defaultMaterial
  780. materialCount++
  781. } catch (error) {
  782. console.error('处理材质时出错:', error.message)
  783. const fallbackMaterial = new THREE.MeshStandardMaterial({
  784. color: 0x409eff,
  785. metalness: 0.3,
  786. roughness: 0.7,
  787. side: THREE.FrontSide
  788. })
  789. child.material = fallbackMaterial
  790. materialCount++
  791. }
  792. }
  793. })
  794. console.log('材质处理完成,添加了', materialCount, '个默认材质')
  795. // 6. 最终矩阵更新
  796. fbxModel.updateMatrixWorld(true)
  797. console.log('FBX模型处理完成')
  798. return {
  799. model: fbxModel,
  800. box: box,
  801. size: size,
  802. center: center
  803. }
  804. }
  805. // 加载FBX模型
  806. const loadFBXModel = (url) => {
  807. console.log('开始加载FBX模型:', url)
  808. // 使用后端接口获取文件
  809. // 直接传递包含/profile前缀的完整路径,后端会处理前缀
  810. const resourcePath = url
  811. const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
  812. console.log('使用后端接口获取文件:', apiUrl)
  813. request({
  814. url: apiUrl,
  815. method: 'get',
  816. responseType: 'blob'
  817. })
  818. .then(response => {
  819. const blob = response
  820. console.log('FBX模型响应是Blob,大小:', blob.size)
  821. console.log('Blob类型:', blob.type)
  822. // 使用Blob创建临时URL
  823. const blobUrl = URL.createObjectURL(blob)
  824. console.log('创建FBX Blob URL:', blobUrl)
  825. // 使用Blob URL加载模型
  826. const loader = new FBXLoader()
  827. loader.load(
  828. blobUrl,
  829. (fbx) => {
  830. console.log('FBX模型加载成功:', fbx)
  831. // 专业处理FBX模型
  832. const processed = processFBXModel(fbx)
  833. model = processed.model
  834. // 添加到场景
  835. scene.add(model)
  836. console.log('FBX模型添加到场景')
  837. // 计算最终边界框
  838. console.log('计算最终模型边界框...')
  839. model.updateMatrixWorld(true)
  840. const finalBox = new THREE.Box3().setFromObject(model)
  841. const finalSize = finalBox.getSize(new THREE.Vector3())
  842. const finalCenter = finalBox.getCenter(new THREE.Vector3())
  843. console.log('最终模型边界框大小:', finalSize)
  844. console.log('最终模型边界框中心:', finalCenter)
  845. console.log('模型实际位置:', model.position)
  846. // 调整相机位置
  847. console.log('调整相机位置...')
  848. adjustCameraForModel()
  849. console.log('相机位置调整完成')
  850. // 修复半透明材质太透的问题
  851. fixTransparentMaterials(model)
  852. // 释放Blob URL
  853. URL.revokeObjectURL(blobUrl)
  854. console.log('FBX模型加载和处理完成')
  855. },
  856. (xhr) => {
  857. console.log('FBX模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
  858. },
  859. (error) => {
  860. console.error('FBX加载错误:', error)
  861. console.error('错误详情:', error.message)
  862. console.error('错误堆栈:', error.stack)
  863. // 释放Blob URL
  864. URL.revokeObjectURL(blobUrl)
  865. createDefaultModel()
  866. }
  867. )
  868. })
  869. .catch(error => {
  870. console.error('后端接口请求错误:', error)
  871. console.error('错误详情:', error.message)
  872. console.error('错误堆栈:', error.stack)
  873. createDefaultModel()
  874. })
  875. }
  876. // 加载STL模型
  877. const loadSTLModel = (url) => {
  878. console.log('开始加载STL模型:', url)
  879. // 使用后端接口获取文件
  880. // 直接传递包含/profile前缀的完整路径,后端会处理前缀
  881. const resourcePath = url
  882. const apiUrl = `/common/download/resource?resource=${encodeURIComponent(resourcePath)}`
  883. console.log('使用后端接口获取文件:', apiUrl)
  884. request({
  885. url: apiUrl,
  886. method: 'get',
  887. responseType: 'blob'
  888. })
  889. .then(response => {
  890. const blob = response
  891. console.log('STL模型响应是Blob,大小:', blob.size)
  892. console.log('Blob类型:', blob.type)
  893. // 使用Blob创建临时URL
  894. const blobUrl = URL.createObjectURL(blob)
  895. console.log('创建STL Blob URL:', blobUrl)
  896. // 使用Blob URL加载模型
  897. const loader = new STLLoader()
  898. loader.load(
  899. blobUrl,
  900. (geometry) => {
  901. console.log('STL模型加载成功,几何体顶点数:', geometry.attributes.position.count)
  902. const material = new THREE.MeshStandardMaterial({ color: 0x409eff })
  903. const mesh = new THREE.Mesh(geometry, material)
  904. model = mesh
  905. scene.add(mesh)
  906. // 使用模型自身的原始坐标,不进行中心调整
  907. console.log('模型添加到场景,使用原始坐标')
  908. console.log('模型原始位置:', model.position)
  909. console.log('模型原始旋转:', model.rotation)
  910. console.log('模型原始缩放:', model.scale)
  911. // 调整相机位置,使模型在视角中心且完整可见
  912. adjustCameraForModel()
  913. console.log('相机位置调整完成')
  914. // 修复半透明材质太透的问题
  915. fixTransparentMaterials(model)
  916. // 释放Blob URL
  917. URL.revokeObjectURL(blobUrl)
  918. },
  919. (xhr) => {
  920. console.log('STL模型加载进度:', (xhr.loaded / xhr.total * 100) + '% loaded')
  921. },
  922. (error) => {
  923. console.error('STL加载错误:', error)
  924. console.error('错误详情:', error.message)
  925. console.error('错误堆栈:', error.stack)
  926. // 释放Blob URL
  927. URL.revokeObjectURL(blobUrl)
  928. createDefaultModel()
  929. }
  930. )
  931. })
  932. .catch(error => {
  933. console.error('后端接口请求错误:', error)
  934. console.error('错误详情:', error.message)
  935. console.error('错误堆栈:', error.stack)
  936. createDefaultModel()
  937. })
  938. }
  939. // 创建默认模型
  940. const createDefaultModel = () => {
  941. // 创建一个简单的立方体作为默认模型
  942. const geometry = new THREE.BoxGeometry(2, 2, 2)
  943. const material = new THREE.MeshStandardMaterial({
  944. color: 0x409eff,
  945. wireframe: true
  946. })
  947. model = new THREE.Mesh(geometry, material)
  948. scene.add(model)
  949. }
  950. // 调整相机位置,使模型在视角中心且完整可见
  951. const adjustCameraForModel = () => {
  952. if (!model || !camera) return
  953. console.log('=======================================')
  954. console.log('开始调整相机位置')
  955. console.log('当前模型位置:', model.position)
  956. console.log('当前相机位置:', camera.position)
  957. // 增强:强制更新所有模型和子对象的矩阵
  958. console.log('强制更新模型和子对象矩阵...')
  959. model.traverse((child) => {
  960. if (child.isObject3D) {
  961. child.updateMatrix()
  962. child.updateMatrixWorld(true)
  963. }
  964. })
  965. console.log('模型矩阵更新完成')
  966. // 计算模型边界框
  967. const box = new THREE.Box3().setFromObject(model)
  968. const size = box.getSize(new THREE.Vector3())
  969. const center = box.getCenter(new THREE.Vector3())
  970. console.log('模型边界框大小:', size)
  971. console.log('模型边界框中心:', center)
  972. console.log('边界框最小值:', box.min)
  973. console.log('边界框最大值:', box.max)
  974. // 增强:检查边界框是否有效
  975. if (box.isEmpty()) {
  976. console.warn('模型边界框为空,使用默认相机位置')
  977. // 使用默认相机位置
  978. camera.position.set(5, 5, 5)
  979. camera.lookAt(0, 0, 0)
  980. camera.updateProjectionMatrix()
  981. console.log('使用默认相机位置:', camera.position)
  982. console.log('相机位置调整完成')
  983. console.log('=======================================')
  984. return
  985. }
  986. // 计算模型对角线长度
  987. const modelDiagonal = Math.sqrt(
  988. size.x * size.x +
  989. size.y * size.y +
  990. size.z * size.z
  991. )
  992. console.log('模型对角线长度:', modelDiagonal)
  993. // 根据模型大小计算合适的相机距离
  994. const fov = camera.fov * (Math.PI / 180) // 转换为弧度
  995. // 增强:使用动态安全系数,根据模型大小调整
  996. let safetyFactor = 1.3
  997. if (modelDiagonal < 5) {
  998. safetyFactor = 1.5 // 小模型使用更大的安全系数
  999. } else if (modelDiagonal > 20) {
  1000. safetyFactor = 1.1 // 大模型使用更小的安全系数
  1001. }
  1002. console.log('使用的安全系数:', safetyFactor)
  1003. const cameraDistance = (modelDiagonal / 2) / Math.tan(fov / 2) * safetyFactor
  1004. console.log('计算的相机距离:', cameraDistance)
  1005. console.log('当前相机到模型中心距离:', camera.position.distanceTo(center))
  1006. // 增强:确保相机距离不会太小
  1007. const minCameraDistance = Math.max(2, modelDiagonal * 0.5)
  1008. const finalCameraDistance = Math.max(minCameraDistance, cameraDistance)
  1009. console.log('最终相机距离:', finalCameraDistance, '(最小距离:', minCameraDistance, ')')
  1010. // 增强:对于FBX模型,确保相机位置计算考虑模型的实际位置
  1011. console.log('模型实际位置:', model.position)
  1012. console.log('边界框中心相对于模型位置:', center.clone().sub(model.position))
  1013. // 设置相机位置,从斜上方看向模型中心
  1014. // 这样可以获得更好的视角
  1015. const cameraHeight = finalCameraDistance * 0.4 // 相机高度,40%的距离
  1016. // 增强:考虑模型的实际位置
  1017. const cameraPosition = new THREE.Vector3(
  1018. center.x + finalCameraDistance * 0.2, // 稍微偏右
  1019. center.y + cameraHeight, // 上方
  1020. center.z + finalCameraDistance * 0.9 // 前方
  1021. )
  1022. console.log('计算的相机位置:', cameraPosition)
  1023. camera.position.copy(cameraPosition)
  1024. // 相机看向模型中心
  1025. camera.lookAt(center)
  1026. console.log('调整后相机位置:', camera.position)
  1027. console.log('相机看向:', center)
  1028. console.log('调整后相机到模型中心距离:', camera.position.distanceTo(center))
  1029. // 确保相机矩阵更新
  1030. camera.updateProjectionMatrix()
  1031. console.log('相机投影矩阵已更新')
  1032. // 增强:重置相机的旋转和缩放
  1033. console.log('重置相机旋转和缩放...')
  1034. camera.rotation.set(0, 0, 0)
  1035. camera.scale.set(1, 1, 1)
  1036. console.log('相机旋转和缩放已重置')
  1037. console.log('相机位置调整完成,现在应该能完整看到模型')
  1038. console.log('=======================================')
  1039. }
  1040. // 自动调整模型位置和大小(保留但不再使用)
  1041. const centerModel = () => {
  1042. if (!model || !camera) return
  1043. console.log('=======================================')
  1044. console.log('开始调整模型位置和大小')
  1045. console.log('调整前模型位置:', model.position)
  1046. console.log('调整前模型缩放:', model.scale)
  1047. console.log('调整前模型旋转:', model.rotation)
  1048. console.log('调整前模型世界矩阵:', model.matrixWorld.elements)
  1049. // 重置模型旋转,确保计算边界框时不受旋转影响
  1050. model.rotation.set(0, 0, 0)
  1051. console.log('重置模型旋转后:', model.rotation)
  1052. // 确保模型矩阵更新
  1053. model.updateMatrixWorld(true)
  1054. console.log('模型矩阵已更新')
  1055. console.log('重置旋转后模型世界矩阵:', model.matrixWorld.elements)
  1056. // 计算模型边界框
  1057. const box = new THREE.Box3().setFromObject(model)
  1058. const size = box.getSize(new THREE.Vector3())
  1059. const center = box.getCenter(new THREE.Vector3())
  1060. console.log('模型边界框大小:', size)
  1061. console.log('模型边界框中心:', center)
  1062. console.log('边界框最小值:', box.min)
  1063. console.log('边界框最大值:', box.max)
  1064. // 直接设置模型位置到原点,确保在世界中心
  1065. console.log('准备调整模型位置到原点...')
  1066. console.log('计算的位置调整值:', -center.x, -center.y, -center.z)
  1067. model.position.set(-center.x, -center.y, -center.z)
  1068. console.log('模型调整后位置:', model.position)
  1069. console.log('模型位置是否接近原点:',
  1070. Math.abs(model.position.x) < 0.001 &&
  1071. Math.abs(model.position.y) < 0.001 &&
  1072. Math.abs(model.position.z) < 0.001
  1073. )
  1074. // 确保模型矩阵更新
  1075. model.updateMatrixWorld(true)
  1076. console.log('位置调整后模型矩阵已更新')
  1077. console.log('位置调整后模型世界矩阵:', model.matrixWorld.elements)
  1078. // 重新计算调整后的边界框,验证位置是否正确
  1079. const positionAdjustedBox = new THREE.Box3().setFromObject(model)
  1080. const positionAdjustedCenter = positionAdjustedBox.getCenter(new THREE.Vector3())
  1081. console.log('位置调整后模型边界框中心:', positionAdjustedCenter)
  1082. console.log('边界框中心是否接近原点:',
  1083. Math.abs(positionAdjustedCenter.x) < 0.001 &&
  1084. Math.abs(positionAdjustedCenter.y) < 0.001 &&
  1085. Math.abs(positionAdjustedCenter.z) < 0.001
  1086. )
  1087. // 自动调整模型大小 - 使用更大的基准值,确保模型不会太小
  1088. const maxSize = Math.max(size.x, size.y, size.z)
  1089. if (maxSize > 0) {
  1090. // 使用 5 作为基准值,而不是 3,让模型更大一些
  1091. const scale = 5 / maxSize
  1092. console.log('准备调整模型缩放...')
  1093. console.log('计算的缩放比例:', scale)
  1094. model.scale.set(scale, scale, scale)
  1095. console.log('模型调整后缩放:', model.scale)
  1096. console.log('调整后模型最大尺寸:', maxSize * scale)
  1097. }
  1098. // 确保模型矩阵更新
  1099. model.updateMatrixWorld(true)
  1100. console.log('模型缩放后矩阵已更新')
  1101. console.log('缩放后模型世界矩阵:', model.matrixWorld.elements)
  1102. // 重新计算调整后的边界框
  1103. const adjustedBox = new THREE.Box3().setFromObject(model)
  1104. const adjustedSize = adjustedBox.getSize(new THREE.Vector3())
  1105. const adjustedCenter = adjustedBox.getCenter(new THREE.Vector3())
  1106. console.log('调整后模型边界框大小:', adjustedSize)
  1107. console.log('调整后模型边界框中心:', adjustedCenter)
  1108. console.log('最终边界框中心是否接近原点:',
  1109. Math.abs(adjustedCenter.x) < 0.001 &&
  1110. Math.abs(adjustedCenter.y) < 0.001 &&
  1111. Math.abs(adjustedCenter.z) < 0.001
  1112. )
  1113. // 动态调整相机位置,确保相机能够完整看到模型
  1114. const modelDiagonal = Math.sqrt(
  1115. adjustedSize.x * adjustedSize.x +
  1116. adjustedSize.y * adjustedSize.y +
  1117. adjustedSize.z * adjustedSize.z
  1118. )
  1119. // 根据模型对角线长度计算合适的相机距离
  1120. const fov = camera.fov * (Math.PI / 180) // 转换为弧度
  1121. const cameraDistance = (modelDiagonal / 2) / Math.tan(fov / 2) * 1.5 // 1.5 是安全系数
  1122. console.log('模型对角线长度:', modelDiagonal)
  1123. console.log('计算的相机距离:', cameraDistance)
  1124. // 设置相机位置,确保从适当的距离和角度观察模型
  1125. camera.position.set(0, cameraDistance * 0.3, cameraDistance)
  1126. camera.lookAt(0, 0, 0)
  1127. console.log('调整后相机位置:', camera.position)
  1128. console.log('相机看向:', new THREE.Vector3(0, 0, 0))
  1129. console.log('相机到原点距离:', camera.position.distanceTo(new THREE.Vector3(0, 0, 0)))
  1130. // 确保相机矩阵更新
  1131. camera.updateProjectionMatrix()
  1132. console.log('相机投影矩阵已更新')
  1133. console.log('模型位置调整完成,现在应该在世界坐标系原点')
  1134. console.log('=======================================')
  1135. }
  1136. // 渲染动画
  1137. const animate = () => {
  1138. animationId = requestAnimationFrame(animate)
  1139. if (renderer && scene && camera) {
  1140. renderer.render(scene, camera)
  1141. }
  1142. }
  1143. // 处理窗口大小变化
  1144. const handleResize = () => {
  1145. if (!camera || !renderer || !modelContainer.value) return
  1146. const width = modelContainer.value.clientWidth
  1147. const height = modelContainer.value.clientHeight
  1148. camera.aspect = width / height
  1149. camera.updateProjectionMatrix()
  1150. renderer.setSize(width, height)
  1151. }
  1152. // 处理关闭
  1153. const handleClose = () => {
  1154. dialogVisible.value = false
  1155. emit('update:visible', false)
  1156. emit('close')
  1157. }
  1158. // 监听可见性变化
  1159. watch(() => props.visible, (newVal) => {
  1160. dialogVisible.value = newVal
  1161. if (newVal) {
  1162. // 延迟初始化,确保DOM已更新
  1163. setTimeout(() => {
  1164. initScene()
  1165. }, 100)
  1166. }
  1167. })
  1168. // 组件挂载时初始化
  1169. onMounted(() => {
  1170. dialogVisible.value = props.visible
  1171. if (props.visible) {
  1172. initScene()
  1173. }
  1174. })
  1175. // 组件卸载时清理
  1176. onUnmounted(() => {
  1177. if (animationId) {
  1178. cancelAnimationFrame(animationId)
  1179. }
  1180. if (renderer) {
  1181. renderer.dispose()
  1182. }
  1183. if (modelContainer.value) {
  1184. window.removeEventListener('resize', handleResize)
  1185. }
  1186. })
  1187. // 计算属性
  1188. const modelName = ref('')
  1189. watch(() => props.modelData, (newVal) => {
  1190. if (newVal) {
  1191. modelName.value = newVal.name || '模型预览'
  1192. }
  1193. }, { immediate: true })
  1194. // 光照控制方法
  1195. const toggleLightControl = () => {
  1196. showLightControl.value = !showLightControl.value
  1197. }
  1198. // 更新光源设置
  1199. const updateLights = () => {
  1200. // 保存相机当前位置和目标点
  1201. let cameraPosition = null
  1202. let cameraTarget = null
  1203. if (camera) {
  1204. cameraPosition = camera.position.clone()
  1205. // 获取相机看向的点
  1206. const vector = new THREE.Vector3()
  1207. camera.getWorldDirection(vector)
  1208. cameraTarget = camera.position.clone().add(vector)
  1209. }
  1210. // 更新光源属性
  1211. if (ambientLight) {
  1212. ambientLight.intensity = lightSettings.value.ambientIntensity
  1213. }
  1214. if (directionalLight) {
  1215. directionalLight.intensity = lightSettings.value.directionalIntensity
  1216. directionalLight.position.set(
  1217. lightSettings.value.directionalPosition.x,
  1218. lightSettings.value.directionalPosition.y,
  1219. lightSettings.value.directionalPosition.z
  1220. )
  1221. }
  1222. if (directionalLight2) {
  1223. directionalLight2.intensity = lightSettings.value.directionalIntensity2
  1224. }
  1225. // 恢复相机位置和目标点
  1226. if (camera && cameraPosition) {
  1227. camera.position.copy(cameraPosition)
  1228. camera.lookAt(cameraTarget)
  1229. camera.updateProjectionMatrix()
  1230. }
  1231. }
  1232. // 修复半透明材质太透的问题
  1233. const fixTransparentMaterials = (modelObject) => {
  1234. if (!modelObject) return
  1235. let fixedCount = 0
  1236. modelObject.traverse((child) => {
  1237. if (child.isMesh && child.material) {
  1238. const materials = Array.isArray(child.material) ? child.material : [child.material]
  1239. materials.forEach((material) => {
  1240. // 检查是否使用了透明度贴图
  1241. const hasAlphaMap = material.alphaMap && material.alphaMap !== null
  1242. const isTransparent = material.transparent === true
  1243. const opacity = material.opacity
  1244. // 如果有透明度贴图或设置了transparent
  1245. if (hasAlphaMap || isTransparent) {
  1246. // 确保透明模式开启
  1247. material.transparent = true
  1248. // 启用双面渲染,确保半透明效果正确显示
  1249. material.side = THREE.DoubleSide
  1250. // 如果opacity太低或等于1(全透/全不透),设置为一个合适的值
  1251. if (opacity === undefined || opacity >= 1 || opacity < 0.1) {
  1252. material.opacity = 0.8
  1253. fixedCount++
  1254. }
  1255. // 修复depthWrite问题 - 对于半透明材质,depthWrite通常应为false
  1256. material.depthWrite = false
  1257. material.needsUpdate = true
  1258. }
  1259. })
  1260. }
  1261. })
  1262. console.log('修复了', fixedCount, '个半透明材质的透明度')
  1263. }
  1264. // 更新模型材质的不透明度
  1265. const updateMaterialOpacity = () => {
  1266. if (!model) return
  1267. const opacity = materialOpacity.value
  1268. model.traverse((child) => {
  1269. if (child.isMesh && child.material) {
  1270. const materials = Array.isArray(child.material) ? child.material : [child.material]
  1271. materials.forEach((material) => {
  1272. if (material.transparent === true || material.opacity < 1) {
  1273. material.opacity = opacity
  1274. material.needsUpdate = true
  1275. }
  1276. })
  1277. }
  1278. })
  1279. }
  1280. </script>
  1281. <style scoped>
  1282. .model-preview-container {
  1283. display: flex;
  1284. gap: 20px;
  1285. height: 600px;
  1286. }
  1287. .model-container {
  1288. flex: 1;
  1289. background-color: #fafafa;
  1290. border-radius: 8px;
  1291. overflow: hidden;
  1292. position: relative;
  1293. }
  1294. .model-info {
  1295. width: 300px;
  1296. background-color: #ffffff;
  1297. border-radius: 8px;
  1298. padding: 20px;
  1299. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1300. overflow-y: auto;
  1301. }
  1302. .model-info h3 {
  1303. margin-top: 0;
  1304. margin-bottom: 20px;
  1305. font-size: 18px;
  1306. font-weight: 600;
  1307. color: #303133;
  1308. }
  1309. @media (max-width: 768px) {
  1310. .model-preview-container {
  1311. flex-direction: column;
  1312. }
  1313. .model-info {
  1314. width: 100%;
  1315. max-height: 300px;
  1316. }
  1317. }
  1318. /* 光照控制样式 */
  1319. .light-control-btn {
  1320. position: absolute;
  1321. top: 10px;
  1322. right: 10px;
  1323. width: 40px;
  1324. height: 40px;
  1325. background-color: rgba(255, 255, 255, 0.9);
  1326. border-radius: 50%;
  1327. display: flex;
  1328. align-items: center;
  1329. justify-content: center;
  1330. cursor: pointer;
  1331. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  1332. z-index: 1000;
  1333. transition: all 0.3s ease;
  1334. }
  1335. .light-control-btn:hover {
  1336. background-color: #ffffff;
  1337. transform: scale(1.1);
  1338. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  1339. }
  1340. .light-control-panel {
  1341. position: absolute;
  1342. top: 60px;
  1343. right: 10px;
  1344. width: 300px;
  1345. background-color: rgba(255, 255, 255, 0.95);
  1346. border-radius: 8px;
  1347. padding: 20px;
  1348. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  1349. z-index: 1000;
  1350. backdrop-filter: blur(5px);
  1351. pointer-events: auto;
  1352. user-select: none;
  1353. }
  1354. .light-control-panel h4 {
  1355. margin: 0 0 15px 0;
  1356. font-size: 16px;
  1357. font-weight: 600;
  1358. color: #303133;
  1359. }
  1360. .control-item {
  1361. margin-bottom: 20px;
  1362. }
  1363. .control-item label {
  1364. display: block;
  1365. margin-bottom: 8px;
  1366. font-size: 14px;
  1367. font-weight: 500;
  1368. color: #606266;
  1369. }
  1370. .control-item .value {
  1371. position: absolute;
  1372. right: 10px;
  1373. top: 50%;
  1374. transform: translateY(-50%);
  1375. font-size: 12px;
  1376. color: #909399;
  1377. min-width: 40px;
  1378. text-align: right;
  1379. }
  1380. .angle-controls > div {
  1381. position: relative;
  1382. margin-bottom: 12px;
  1383. }
  1384. .angle-controls > div > span {
  1385. display: inline-block;
  1386. width: 40px;
  1387. font-size: 12px;
  1388. color: #909399;
  1389. margin-right: 10px;
  1390. }
  1391. .angle-controls .el-slider {
  1392. display: inline-block;
  1393. width: calc(100% - 100px);
  1394. vertical-align: middle;
  1395. }
  1396. .angle-controls .value {
  1397. position: static;
  1398. display: inline-block;
  1399. transform: none;
  1400. vertical-align: middle;
  1401. }
  1402. /* 响应式调整 */
  1403. @media (max-width: 768px) {
  1404. .light-control-panel {
  1405. width: 250px;
  1406. right: 5px;
  1407. }
  1408. .angle-controls .el-slider {
  1409. width: calc(100% - 90px);
  1410. }
  1411. }
  1412. </style>