ThreeScene.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <template>
  2. <!-- 进度条 -->
  3. <!-- <el-progress
  4. :percentage="progress"
  5. :status="progress === 100 ? 'success' : ''"
  6. :stroke-width="10"
  7. :text-inside="true"
  8. style="width: 100%; margin-bottom: 20px;"
  9. /> -->
  10. <div ref="threeContainer" class="three-container" :style="{height: height}">
  11. <!-- 添加色卡Canvas -->
  12. <canvas
  13. v-if="showColorCard"
  14. ref="colorCardCanvas"
  15. class="color-card-canvas"
  16. :style="colorCardStyle"
  17. ></canvas>
  18. </div>
  19. </template>
  20. <script setup>
  21. import { ref, onMounted, onUnmounted, watch } from "vue"
  22. import * as THREE from "three"
  23. import { VTKParserFactory } from "@views/threejsView/utils/parsers/VTKParserFactory"
  24. import { UnstructuredGridRenderer } from "@views/threejsView/utils/renderers/UnstructuredGridRenderer"
  25. import { PolyDataRenderer } from "@views/threejsView/utils/renderers/PolyDataRenderer"
  26. import { xyzDataRenderer } from "@views/threejsView/utils/renderers/xyzDataRenderer"
  27. import { bdfDataRenderer } from "@views/threejsView/utils/renderers/bdfDataRenderer"
  28. import { CgnsJSONRenderer } from "@views/threejsView/utils/renderers/CgnsJSONRenderer";
  29. import { PltDataRenderer } from "@views/threejsView/utils/renderers/pltDataRenderer";
  30. import {
  31. initScene,
  32. initCamera,
  33. initRenderer,
  34. animateScene,
  35. cleanupScene,
  36. initControls,
  37. createAxesHelper
  38. } from "../utils/threeUtils"
  39. const props = defineProps({
  40. data: {
  41. type: Object,
  42. required: true
  43. },
  44. height: {
  45. type: String,
  46. required: true
  47. }
  48. })
  49. const threeContainer = ref(null);
  50. const colorCardCanvas = ref(null)
  51. const colorCardContext = ref(null)
  52. const progress = ref(0); // 进度条的值
  53. let scene, camera, renderer, controls
  54. // 初始化场景
  55. const init = () => {
  56. scene = initScene();
  57. camera = initCamera();
  58. renderer = initRenderer(threeContainer.value);
  59. controls = initControls(camera, renderer);
  60. // createAxesHelper(scene); // 添加坐标轴指示器
  61. }
  62. // 更新进度
  63. const updateProgress = (current, total) => {
  64. progress.value = Math.floor((current / total) * 100);
  65. };
  66. const adjustCameraToFit = (scene, camera) => {
  67. const box = new THREE.Box3().setFromObject(scene)
  68. const size = new THREE.Vector3()
  69. box.getSize(size)
  70. const maxDim = Math.max(size.x, size.y, size.z)
  71. const fov = camera.fov * (Math.PI / 180)
  72. let cameraZ = Math.abs(maxDim / Math.sin(fov / 2))
  73. const center = new THREE.Vector3()
  74. box.getCenter(center)
  75. camera.position.set(center.x, center.y, cameraZ)
  76. camera.lookAt(center)
  77. }
  78. // 渲染 VTK 数据
  79. const renderVTK = (data) => {
  80. if (!data) return
  81. // 根据文件格式选择解析器
  82. const parser = VTKParserFactory.createParser(data.data.datasetType)
  83. const parsedData = parser.parse(data)
  84. // 根据文件格式选择渲染器
  85. let dataRenderer
  86. switch (data.data.datasetType) {
  87. case "UNSTRUCTURED_GRID":
  88. dataRenderer = new UnstructuredGridRenderer()
  89. break;
  90. case "POLYDATA":
  91. dataRenderer = new PolyDataRenderer()
  92. break;
  93. case "xyz":
  94. dataRenderer = new xyzDataRenderer()
  95. break;
  96. case "bdf":
  97. dataRenderer = new bdfDataRenderer(updateProgress,() => {
  98. // 渲染完成后的回调
  99. adjustCameraForBdf(scene, camera);
  100. })
  101. break;
  102. case "cgns":
  103. dataRenderer = new CgnsJSONRenderer(updateProgress, () => {
  104. adjustCameraForCgns(scene, camera);
  105. });
  106. break;
  107. case "plt":
  108. dataRenderer = new PltDataRenderer(updateProgress, () => {
  109. adjustCameraForPlt(scene, camera);
  110. });
  111. break;
  112. default:
  113. console.log("11111")
  114. }
  115. // 渲染数据
  116. dataRenderer.render(parsedData, scene)
  117. // 根据数据类型调整相机位置
  118. if (data.datasetType === "UNSTRUCTURED_GRID") {
  119. adjustCameraForUnstructuredGrid(scene, camera);
  120. return;
  121. }
  122. if (data.datasetType === "POLYDATA") {
  123. adjustCameraForPolydata(scene, camera);
  124. return;
  125. }
  126. if (data.data.datasetType === "xyz"){
  127. adjustCameraForXYZ(scene, camera);
  128. return;
  129. }
  130. if (data.data.datasetType === "bdf"){
  131. adjustCameraForBdf(scene, camera);
  132. return;
  133. }
  134. if (data.data.datasetType === "cgns"){
  135. adjustCameraForCgns(scene, camera);
  136. return;
  137. }
  138. if (data.data.datasetType === "plt"){
  139. adjustCameraForPlt(scene, camera);
  140. return;
  141. }
  142. }
  143. // 计算色卡显示状态和样式
  144. const showColorCard = computed(() => {
  145. return props.data?.config?.colorCard?.visible
  146. })
  147. const colorCardStyle = computed(() => {
  148. if (!showColorCard.value) return {}
  149. const config = props.data.config.colorCard
  150. return {
  151. position: 'absolute',
  152. [config.orientation === 'vertical' ? 'right' : 'bottom']: '10px',
  153. [config.orientation === 'vertical' ? 'top' : 'left']: '50%',
  154. transform: config.orientation === 'vertical'
  155. ? 'translateY(-50%)'
  156. : 'translateX(-50%)',
  157. width: config.orientation === 'vertical'
  158. ? `${config.width * 100}px`
  159. : `${config.width * 100}%`,
  160. height: config.orientation === 'vertical'
  161. ? `${config.height * 100}%`
  162. : `${config.height * 100}px`,
  163. zIndex: 1000
  164. }
  165. })
  166. // 初始化色卡
  167. const initColorCard = () => {
  168. if (!colorCardCanvas.value) return
  169. colorCardContext.value = colorCardCanvas.value.getContext('2d')
  170. renderColorCard()
  171. }
  172. // 渲染色卡
  173. const renderColorCard = () => {
  174. if (!showColorCard.value || !colorCardContext.value) return
  175. const config = props.data.config.colorCard
  176. const ctx = colorCardContext.value
  177. const width = colorCardCanvas.value.width
  178. const height = colorCardCanvas.value.height
  179. // 清除画布
  180. ctx.clearRect(0, 0, width, height)
  181. // 创建渐变
  182. let gradient
  183. if (config.orientation === 'vertical') {
  184. gradient = ctx.createLinearGradient(0, height, 0, 0)
  185. } else {
  186. gradient = ctx.createLinearGradient(0, 0, width, 0)
  187. }
  188. // 添加色卡颜色
  189. gradient.addColorStop(0, '#0000ff') // 蓝色
  190. gradient.addColorStop(0.5, '#00ff00') // 绿色
  191. gradient.addColorStop(1, '#ff0000') // 红色
  192. // 填充渐变
  193. ctx.fillStyle = gradient
  194. ctx.fillRect(0, 0, width, height)
  195. // 添加刻度
  196. ctx.strokeStyle = '#000'
  197. ctx.lineWidth = 1
  198. ctx.font = `${config.fontSize}px ${config.font}`
  199. ctx.fillStyle = '#000'
  200. // 根据朝向绘制刻度
  201. if (config.orientation === 'vertical') {
  202. // 垂直色卡刻度
  203. const step = height / 10
  204. for (let i = 0; i <= 10; i++) {
  205. const y = i * step
  206. ctx.beginPath()
  207. ctx.moveTo(width * 0.7, y)
  208. ctx.lineTo(width, y)
  209. ctx.stroke()
  210. // 跳过指定层级的标签
  211. if (i % (config.skipLevels + 1) === 0) {
  212. const value = (1 - i / height * 1).toFixed(config.precision)
  213. ctx.fillText(value, 5, y + 5)
  214. }
  215. }
  216. } else {
  217. // 水平色卡刻度
  218. const step = width / 10
  219. for (let i = 0; i <= 10; i++) {
  220. const x = i * step
  221. ctx.beginPath()
  222. ctx.moveTo(x, 0)
  223. ctx.lineTo(x, height * 0.3)
  224. ctx.stroke()
  225. // 跳过指定层级的标签
  226. if (i % (config.skipLevels + 1) === 0) {
  227. const value = (i / width * 1).toFixed(config.precision)
  228. ctx.fillText(value, x - 10, height - 5)
  229. }
  230. }
  231. }
  232. // 添加标题
  233. if (config.showTitle) {
  234. ctx.font = `bold ${config.titleFontSize}px ${config.titleFont}`
  235. ctx.textAlign = 'center'
  236. const title = config.titleSource === 'custom'
  237. ? config.customTitle
  238. : 'Color Scale'
  239. if (config.orientation === 'vertical') {
  240. ctx.save()
  241. ctx.translate(20, height / 2)
  242. ctx.rotate(-Math.PI / 2)
  243. ctx.fillText(title, 0, 0)
  244. ctx.restore()
  245. } else {
  246. ctx.fillText(title, width / 2, height - 15)
  247. }
  248. }
  249. }
  250. // 监听 data 变化
  251. watch(
  252. () => props.data,
  253. (newData) => {
  254. if (showColorCard.value) {
  255. nextTick(() => {
  256. initColorCard()
  257. })
  258. }
  259. if (newData) {
  260. // 清空场景
  261. while (scene.children.length > 0) {
  262. scene.remove(scene.children[0])
  263. }
  264. // 重置进度
  265. progress.value = 0;
  266. // 重新渲染 VTK 数据
  267. renderVTK(newData)
  268. }
  269. },
  270. { immediate: true }
  271. )
  272. // VTK 数据
  273. onMounted(() => {
  274. nextTick(() => {
  275. init()
  276. if (showColorCard.value) {
  277. initColorCard()
  278. }
  279. animateScene(scene, camera, renderer, controls)
  280. // 监听窗口大小变化
  281. window.addEventListener("resize", onWindowResize)
  282. })
  283. })
  284. onUnmounted(() => {
  285. // 清理场景
  286. cleanupScene(renderer)
  287. // 移除窗口大小变化监听器
  288. window.removeEventListener("resize", onWindowResize)
  289. })
  290. const onWindowResize = () => {
  291. const width = threeContainer.value.clientWidth
  292. const height = threeContainer.value.clientHeight
  293. camera.aspect = width / height
  294. camera.updateProjectionMatrix()
  295. renderer.setSize(width, height)
  296. }
  297. const adjustCameraForUnstructuredGrid = (scene, camera) => {
  298. const box = new THREE.Box3().setFromObject(scene)
  299. const size = new THREE.Vector3()
  300. box.getSize(size)
  301. const maxDim = Math.max(size.x, size.y, size.z)
  302. const fov = camera.fov * (Math.PI / 180)
  303. let cameraZ = Math.abs(maxDim / Math.sin(fov / 2))
  304. const center = new THREE.Vector3()
  305. box.getCenter(center)
  306. camera.position.set(center.x, center.y, cameraZ)
  307. camera.lookAt(center)
  308. }
  309. const adjustCameraForPolydata = (scene, camera) => {
  310. const box = new THREE.Box3().setFromObject(scene)
  311. // 手动设置边界框
  312. box.set(new THREE.Vector3(-100, -100, -100), new THREE.Vector3(100, 100, 100))
  313. const size = new THREE.Vector3()
  314. box.getSize(size)
  315. console.log("Polydata bounding box:", box)
  316. console.log("Polydata size:", size)
  317. const maxDim = Math.max(size.x, size.y, size.z)
  318. const fov = camera.fov * (Math.PI / 180)
  319. let cameraZ = Math.abs(maxDim / Math.sin(fov / 2))
  320. const center = new THREE.Vector3()
  321. box.getCenter(center)
  322. camera.position.set(center.x, center.y, cameraZ * 0.8) // 调整相机距离
  323. camera.lookAt(center)
  324. }
  325. const adjustCameraForXYZ = (scene, camera) => {
  326. // 计算场景的边界框
  327. const box = new THREE.Box3().setFromObject(scene);
  328. // 获取边界框的尺寸
  329. const size = new THREE.Vector3();
  330. box.getSize(size);
  331. console.log("XYZ bounding box:", box);
  332. console.log("XYZ size:", size);
  333. // 计算最大尺寸
  334. const maxDim = Math.max(size.x, size.y, size.z);
  335. // 计算相机的距离
  336. const fov = camera.fov * (Math.PI / 180); // 将相机的视野角度转换为弧度
  337. let cameraZ = Math.abs(maxDim / Math.sin(fov / 2)); // 根据视野角度和最大尺寸计算相机距离
  338. // 获取边界框的中心点
  339. const center = new THREE.Vector3();
  340. box.getCenter(center);
  341. // 设置相机位置
  342. camera.position.set(center.x, center.y, cameraZ * 0.4); // 调整相机距离(可以根据需要调整倍数)
  343. camera.lookAt(center); // 让相机看向场景中心
  344. camera.updateProjectionMatrix(); // 更新相机的投影矩阵
  345. };
  346. /**
  347. * 调整相机位置以适应BDF数据
  348. * @param {THREE.Scene} scene - Three.js场景
  349. * @param {THREE.PerspectiveCamera} camera - 透视相机
  350. */
  351. const adjustCameraForBdf = (scene, camera) => {
  352. // 添加环境光(均匀照亮整个场景)
  353. const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  354. scene.add(ambientLight);
  355. // 添加平行光(模拟太阳光)
  356. const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
  357. directionalLight.position.set(1, 1, 1);
  358. scene.add(directionalLight);
  359. // 计算场景的边界框
  360. const box = new THREE.Box3().setFromObject(scene);
  361. // 获取边界框的尺寸
  362. const size = new THREE.Vector3();
  363. box.getSize(size);
  364. console.log("BDF bounding box:", box);
  365. console.log("BDF size:", size);
  366. // 计算最大尺寸
  367. const maxDim = Math.max(size.x, size.y, size.z);
  368. // 计算相机的距离
  369. const fov = camera.fov * (Math.PI / 180); // 将相机的视野角度转换为弧度
  370. let cameraZ = Math.abs(maxDim / Math.sin(fov / 2)); // 根据视野角度和最大尺寸计算相机距离
  371. // 获取边界框的中心点
  372. const center = new THREE.Vector3();
  373. box.getCenter(center);
  374. // 设置相机位置
  375. camera.position.set(center.x, center.y, cameraZ * 0.8); // 调整相机距离(可以根据需要调整倍数)
  376. camera.lookAt(center); // 让相机看向场景中心
  377. camera.updateProjectionMatrix(); // 更新相机的投影矩阵
  378. };
  379. /**
  380. * 调整相机位置以适应 JSON 数据
  381. * @param {THREE.Scene} scene - Three.js场景
  382. * @param {THREE.PerspectiveCamera} camera - 透视相机
  383. */
  384. const adjustCameraForCgns = (scene, camera) => {
  385. // 添加环境光(均匀照亮整个场景)
  386. const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  387. scene.add(ambientLight);
  388. // 添加平行光(模拟太阳光)
  389. const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
  390. directionalLight.position.set(1, 1, 1);
  391. scene.add(directionalLight);
  392. // 计算场景的边界框
  393. const box = new THREE.Box3().setFromObject(scene);
  394. // 获取边界框的尺寸
  395. const size = new THREE.Vector3();
  396. box.getSize(size);
  397. console.log("JSON bounding box:", box);
  398. console.log("JSON size:", size);
  399. // 计算最大尺寸
  400. const maxDim = Math.max(size.x, size.y, size.z);
  401. // 根据场景尺寸动态调整视野角度
  402. const dynamicFov = Math.min(75, 45 + (maxDim / 10)); // 动态调整 fov,确保场景大小合适
  403. camera.fov = dynamicFov;
  404. camera.updateProjectionMatrix();
  405. // 计算相机的距离
  406. const fov = camera.fov * (Math.PI / 180); // 将相机的视野角度转换为弧度
  407. let cameraZ = Math.abs(maxDim / Math.sin(fov / 2)); // 根据视野角度和最大尺寸计算相机距离
  408. // 获取边界框的中心点
  409. const center = new THREE.Vector3();
  410. box.getCenter(center);
  411. // 设置相机位置
  412. camera.position.set(center.x, center.y, cameraZ * 0.8); // 调整相机距离(可以根据需要调整倍数)
  413. camera.lookAt(center); // 让相机看向场景中心
  414. camera.updateProjectionMatrix(); // 更新相机的投影矩阵
  415. console.log("Adjusted camera position:", camera.position);
  416. console.log("Adjusted camera fov:", camera.fov);
  417. };
  418. /**
  419. * 调整相机位置和参数,使整个PLT模型在视图中可见
  420. * @param {THREE.Scene} scene - Three.js场景对象
  421. * @param {THREE.PerspectiveCamera} camera - 透视相机
  422. * @param {number} [paddingFactor] - 视图边距系数(1.0表示紧贴边界)
  423. */
  424. const adjustCameraForPlt = (scene, camera, params = {}) => {
  425. const {
  426. padding = 2.0,
  427. forceTopView = false
  428. } = params;
  429. // 1. 确保场景和相机已初始化
  430. if (!scene || !camera) {
  431. console.error('场景或相机未初始化');
  432. return;
  433. }
  434. // 2. 计算包围盒(包含可见网格)
  435. const box = new THREE.Box3();
  436. const visibleMeshes = [];
  437. scene.traverse(child => {
  438. if (child.isMesh && child.visible) {
  439. if (!child.geometry.boundingBox) {
  440. child.geometry.computeBoundingBox();
  441. }
  442. box.union(child.geometry.boundingBox);
  443. visibleMeshes.push(child);
  444. }
  445. });
  446. // 3. 处理空场景情况
  447. if (visibleMeshes.length === 0) {
  448. console.warn('没有可见网格,使用默认视角');
  449. camera.position.set(0, 10, 0);
  450. camera.lookAt(0, 0, 0);
  451. return;
  452. }
  453. // 4. 计算模型参数
  454. const size = box.getSize(new THREE.Vector3());
  455. const center = box.getCenter(new THREE.Vector3());
  456. const maxDim = Math.max(size.x, size.y, size.z);
  457. const distance = padding * maxDim;
  458. // 5. 设置相机位置(根据模型特征优化)
  459. if (forceTopView || size.y > size.x * 1.5) {
  460. // 高模型或强制顶视图
  461. camera.position.set(
  462. center.x,
  463. center.y + distance * 1.5,
  464. center.z
  465. );
  466. camera.up.set(0, 0, 1); // Z轴朝上
  467. } else {
  468. // 常规模型
  469. camera.position.set(
  470. center.x + distance * 0.7,
  471. center.y + distance * 0.5,
  472. center.z + distance * 0.7
  473. );
  474. camera.up.set(0, 1, 0); // Y轴朝上
  475. }
  476. // 6. 确保相机看向中心点
  477. camera.lookAt(center);
  478. camera.near = 0.1 * maxDim; // 动态调整近平面
  479. camera.far = 100 * maxDim; // 动态调整远平面
  480. camera.updateProjectionMatrix();
  481. // 7. 打印调试信息
  482. console.log(`相机调整完成:
  483. 模型中心:${center.toArray().map(v => v.toFixed(2))}
  484. 模型尺寸:${size.toArray().map(v => v.toFixed(2))}
  485. 相机位置:${camera.position.toArray().map(v => v.toFixed(2))}
  486. 视锥体:near=${camera.near.toFixed(2)}, far=${camera.far.toFixed(2)}`);
  487. };
  488. </script>
  489. <style>
  490. .three-container {
  491. width: 100%;
  492. /* height: calc(60vh - 6px); */
  493. border: 1px solid #ccc; /* 可选:添加边框以便查看容器范围 */
  494. }
  495. .el-checkbox__label{
  496. font-size: 12px;
  497. }
  498. .color-card-canvas {
  499. pointer-events: none; /* 允许点击穿透 */
  500. border: 1px solid #ccc;
  501. background: white;
  502. }
  503. </style>