123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- <template>
- <!-- 进度条 -->
- <!-- <el-progress
- :percentage="progress"
- :status="progress === 100 ? 'success' : ''"
- :stroke-width="10"
- :text-inside="true"
- style="width: 100%; margin-bottom: 20px;"
- /> -->
- <div ref="threeContainer" class="three-container" :style="{height: height}">
- <!-- 添加色卡Canvas -->
- <canvas
- v-if="showColorCard"
- ref="colorCardCanvas"
- class="color-card-canvas"
- :style="colorCardStyle"
- ></canvas>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, onUnmounted, watch } from "vue"
- import * as THREE from "three"
- import { VTKParserFactory } from "@views/threejsView/utils/parsers/VTKParserFactory"
- import { UnstructuredGridRenderer } from "@views/threejsView/utils/renderers/UnstructuredGridRenderer"
- import { PolyDataRenderer } from "@views/threejsView/utils/renderers/PolyDataRenderer"
- import { xyzDataRenderer } from "@views/threejsView/utils/renderers/xyzDataRenderer"
- import { bdfDataRenderer } from "@views/threejsView/utils/renderers/bdfDataRenderer"
- import { CgnsJSONRenderer } from "@views/threejsView/utils/renderers/CgnsJSONRenderer";
- import { PltDataRenderer } from "@views/threejsView/utils/renderers/pltDataRenderer";
- import {
- initScene,
- initCamera,
- initRenderer,
- animateScene,
- cleanupScene,
- initControls,
- createAxesHelper
- } from "../utils/threeUtils"
- const props = defineProps({
- data: {
- type: Object,
- required: true
- },
- height: {
- type: String,
- required: true
- }
- })
- const threeContainer = ref(null);
- const colorCardCanvas = ref(null)
- const colorCardContext = ref(null)
- const progress = ref(0); // 进度条的值
- let scene, camera, renderer, controls
- // 初始化场景
- const init = () => {
- scene = initScene();
- camera = initCamera();
- renderer = initRenderer(threeContainer.value);
- controls = initControls(camera, renderer);
- // createAxesHelper(scene); // 添加坐标轴指示器
- }
- // 更新进度
- const updateProgress = (current, total) => {
- progress.value = Math.floor((current / total) * 100);
- };
- const adjustCameraToFit = (scene, camera) => {
- const box = new THREE.Box3().setFromObject(scene)
- const size = new THREE.Vector3()
- box.getSize(size)
- const maxDim = Math.max(size.x, size.y, size.z)
- const fov = camera.fov * (Math.PI / 180)
- let cameraZ = Math.abs(maxDim / Math.sin(fov / 2))
- const center = new THREE.Vector3()
- box.getCenter(center)
- camera.position.set(center.x, center.y, cameraZ)
- camera.lookAt(center)
- }
- // 渲染 VTK 数据
- const renderVTK = (data) => {
- if (!data) return
- // 根据文件格式选择解析器
- const parser = VTKParserFactory.createParser(data.data.datasetType)
- const parsedData = parser.parse(data)
- // 根据文件格式选择渲染器
- let dataRenderer
- switch (data.data.datasetType) {
- case "UNSTRUCTURED_GRID":
- dataRenderer = new UnstructuredGridRenderer()
- break;
- case "POLYDATA":
- dataRenderer = new PolyDataRenderer()
- break;
- case "xyz":
- dataRenderer = new xyzDataRenderer()
- break;
- case "bdf":
- dataRenderer = new bdfDataRenderer(updateProgress,() => {
- // 渲染完成后的回调
- adjustCameraForBdf(scene, camera);
- })
- break;
- case "cgns":
- dataRenderer = new CgnsJSONRenderer(updateProgress, () => {
- adjustCameraForCgns(scene, camera);
- });
- break;
- case "plt":
- dataRenderer = new PltDataRenderer(updateProgress, () => {
- adjustCameraForPlt(scene, camera);
- });
- break;
- default:
- console.log("11111")
-
- }
- // 渲染数据
- dataRenderer.render(parsedData, scene)
- // 根据数据类型调整相机位置
- if (data.datasetType === "UNSTRUCTURED_GRID") {
- adjustCameraForUnstructuredGrid(scene, camera);
- return;
- }
- if (data.datasetType === "POLYDATA") {
- adjustCameraForPolydata(scene, camera);
- return;
- }
- if (data.data.datasetType === "xyz"){
- adjustCameraForXYZ(scene, camera);
- return;
- }
- if (data.data.datasetType === "bdf"){
- adjustCameraForBdf(scene, camera);
- return;
- }
- if (data.data.datasetType === "cgns"){
- adjustCameraForCgns(scene, camera);
- return;
- }
- if (data.data.datasetType === "plt"){
- adjustCameraForPlt(scene, camera);
- return;
- }
- }
- // 计算色卡显示状态和样式
- const showColorCard = computed(() => {
- return props.data?.config?.colorCard?.visible
- })
- const colorCardStyle = computed(() => {
- if (!showColorCard.value) return {}
- const config = props.data.config.colorCard
- return {
- position: 'absolute',
- [config.orientation === 'vertical' ? 'right' : 'bottom']: '10px',
- [config.orientation === 'vertical' ? 'top' : 'left']: '50%',
- transform: config.orientation === 'vertical'
- ? 'translateY(-50%)'
- : 'translateX(-50%)',
- width: config.orientation === 'vertical'
- ? `${config.width * 100}px`
- : `${config.width * 100}%`,
- height: config.orientation === 'vertical'
- ? `${config.height * 100}%`
- : `${config.height * 100}px`,
- zIndex: 1000
- }
- })
- // 初始化色卡
- const initColorCard = () => {
- if (!colorCardCanvas.value) return
- colorCardContext.value = colorCardCanvas.value.getContext('2d')
- renderColorCard()
- }
- // 渲染色卡
- const renderColorCard = () => {
- if (!showColorCard.value || !colorCardContext.value) return
-
- const config = props.data.config.colorCard
- const ctx = colorCardContext.value
- const width = colorCardCanvas.value.width
- const height = colorCardCanvas.value.height
-
- // 清除画布
- ctx.clearRect(0, 0, width, height)
-
- // 创建渐变
- let gradient
- if (config.orientation === 'vertical') {
- gradient = ctx.createLinearGradient(0, height, 0, 0)
- } else {
- gradient = ctx.createLinearGradient(0, 0, width, 0)
- }
-
- // 添加色卡颜色
- gradient.addColorStop(0, '#0000ff') // 蓝色
- gradient.addColorStop(0.5, '#00ff00') // 绿色
- gradient.addColorStop(1, '#ff0000') // 红色
-
- // 填充渐变
- ctx.fillStyle = gradient
- ctx.fillRect(0, 0, width, height)
-
- // 添加刻度
- ctx.strokeStyle = '#000'
- ctx.lineWidth = 1
- ctx.font = `${config.fontSize}px ${config.font}`
- ctx.fillStyle = '#000'
-
- // 根据朝向绘制刻度
- if (config.orientation === 'vertical') {
- // 垂直色卡刻度
- const step = height / 10
- for (let i = 0; i <= 10; i++) {
- const y = i * step
- ctx.beginPath()
- ctx.moveTo(width * 0.7, y)
- ctx.lineTo(width, y)
- ctx.stroke()
-
- // 跳过指定层级的标签
- if (i % (config.skipLevels + 1) === 0) {
- const value = (1 - i / height * 1).toFixed(config.precision)
- ctx.fillText(value, 5, y + 5)
- }
- }
- } else {
- // 水平色卡刻度
- const step = width / 10
- for (let i = 0; i <= 10; i++) {
- const x = i * step
- ctx.beginPath()
- ctx.moveTo(x, 0)
- ctx.lineTo(x, height * 0.3)
- ctx.stroke()
-
- // 跳过指定层级的标签
- if (i % (config.skipLevels + 1) === 0) {
- const value = (i / width * 1).toFixed(config.precision)
- ctx.fillText(value, x - 10, height - 5)
- }
- }
- }
-
- // 添加标题
- if (config.showTitle) {
- ctx.font = `bold ${config.titleFontSize}px ${config.titleFont}`
- ctx.textAlign = 'center'
- const title = config.titleSource === 'custom'
- ? config.customTitle
- : 'Color Scale'
-
- if (config.orientation === 'vertical') {
- ctx.save()
- ctx.translate(20, height / 2)
- ctx.rotate(-Math.PI / 2)
- ctx.fillText(title, 0, 0)
- ctx.restore()
- } else {
- ctx.fillText(title, width / 2, height - 15)
- }
- }
- }
- // 监听 data 变化
- watch(
- () => props.data,
- (newData) => {
- if (showColorCard.value) {
- nextTick(() => {
- initColorCard()
- })
- }
- if (newData) {
- // 清空场景
- while (scene.children.length > 0) {
- scene.remove(scene.children[0])
- }
- // 重置进度
- progress.value = 0;
- // 重新渲染 VTK 数据
- renderVTK(newData)
- }
- },
- { immediate: true }
- )
- // VTK 数据
- onMounted(() => {
- nextTick(() => {
- init()
- if (showColorCard.value) {
- initColorCard()
- }
- animateScene(scene, camera, renderer, controls)
- // 监听窗口大小变化
- window.addEventListener("resize", onWindowResize)
- })
- })
- onUnmounted(() => {
- // 清理场景
- cleanupScene(renderer)
- // 移除窗口大小变化监听器
- window.removeEventListener("resize", onWindowResize)
- })
- const onWindowResize = () => {
- const width = threeContainer.value.clientWidth
- const height = threeContainer.value.clientHeight
- camera.aspect = width / height
- camera.updateProjectionMatrix()
- renderer.setSize(width, height)
- }
- const adjustCameraForUnstructuredGrid = (scene, camera) => {
- const box = new THREE.Box3().setFromObject(scene)
- const size = new THREE.Vector3()
- box.getSize(size)
- const maxDim = Math.max(size.x, size.y, size.z)
- const fov = camera.fov * (Math.PI / 180)
- let cameraZ = Math.abs(maxDim / Math.sin(fov / 2))
- const center = new THREE.Vector3()
- box.getCenter(center)
- camera.position.set(center.x, center.y, cameraZ)
- camera.lookAt(center)
- }
- const adjustCameraForPolydata = (scene, camera) => {
- const box = new THREE.Box3().setFromObject(scene)
- // 手动设置边界框
- box.set(new THREE.Vector3(-100, -100, -100), new THREE.Vector3(100, 100, 100))
- const size = new THREE.Vector3()
- box.getSize(size)
- console.log("Polydata bounding box:", box)
- console.log("Polydata size:", size)
- const maxDim = Math.max(size.x, size.y, size.z)
- const fov = camera.fov * (Math.PI / 180)
- let cameraZ = Math.abs(maxDim / Math.sin(fov / 2))
- const center = new THREE.Vector3()
- box.getCenter(center)
- camera.position.set(center.x, center.y, cameraZ * 0.8) // 调整相机距离
- camera.lookAt(center)
- }
- const adjustCameraForXYZ = (scene, camera) => {
- // 计算场景的边界框
- const box = new THREE.Box3().setFromObject(scene);
- // 获取边界框的尺寸
- const size = new THREE.Vector3();
- box.getSize(size);
- console.log("XYZ bounding box:", box);
- console.log("XYZ size:", size);
- // 计算最大尺寸
- const maxDim = Math.max(size.x, size.y, size.z);
- // 计算相机的距离
- const fov = camera.fov * (Math.PI / 180); // 将相机的视野角度转换为弧度
- let cameraZ = Math.abs(maxDim / Math.sin(fov / 2)); // 根据视野角度和最大尺寸计算相机距离
- // 获取边界框的中心点
- const center = new THREE.Vector3();
- box.getCenter(center);
- // 设置相机位置
- camera.position.set(center.x, center.y, cameraZ * 0.4); // 调整相机距离(可以根据需要调整倍数)
- camera.lookAt(center); // 让相机看向场景中心
- camera.updateProjectionMatrix(); // 更新相机的投影矩阵
- };
- /**
- * 调整相机位置以适应BDF数据
- * @param {THREE.Scene} scene - Three.js场景
- * @param {THREE.PerspectiveCamera} camera - 透视相机
- */
- const adjustCameraForBdf = (scene, camera) => {
- // 添加环境光(均匀照亮整个场景)
- 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 box = new THREE.Box3().setFromObject(scene);
- // 获取边界框的尺寸
- const size = new THREE.Vector3();
- box.getSize(size);
- console.log("BDF bounding box:", box);
- console.log("BDF size:", size);
- // 计算最大尺寸
- const maxDim = Math.max(size.x, size.y, size.z);
- // 计算相机的距离
- const fov = camera.fov * (Math.PI / 180); // 将相机的视野角度转换为弧度
- let cameraZ = Math.abs(maxDim / Math.sin(fov / 2)); // 根据视野角度和最大尺寸计算相机距离
- // 获取边界框的中心点
- const center = new THREE.Vector3();
- box.getCenter(center);
- // 设置相机位置
- camera.position.set(center.x, center.y, cameraZ * 0.8); // 调整相机距离(可以根据需要调整倍数)
- camera.lookAt(center); // 让相机看向场景中心
- camera.updateProjectionMatrix(); // 更新相机的投影矩阵
- };
- /**
- * 调整相机位置以适应 JSON 数据
- * @param {THREE.Scene} scene - Three.js场景
- * @param {THREE.PerspectiveCamera} camera - 透视相机
- */
- const adjustCameraForCgns = (scene, camera) => {
- // 添加环境光(均匀照亮整个场景)
- 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 box = new THREE.Box3().setFromObject(scene);
- // 获取边界框的尺寸
- const size = new THREE.Vector3();
- box.getSize(size);
- console.log("JSON bounding box:", box);
- console.log("JSON size:", size);
- // 计算最大尺寸
- const maxDim = Math.max(size.x, size.y, size.z);
- // 根据场景尺寸动态调整视野角度
- const dynamicFov = Math.min(75, 45 + (maxDim / 10)); // 动态调整 fov,确保场景大小合适
- camera.fov = dynamicFov;
- camera.updateProjectionMatrix();
- // 计算相机的距离
- const fov = camera.fov * (Math.PI / 180); // 将相机的视野角度转换为弧度
- let cameraZ = Math.abs(maxDim / Math.sin(fov / 2)); // 根据视野角度和最大尺寸计算相机距离
- // 获取边界框的中心点
- const center = new THREE.Vector3();
- box.getCenter(center);
- // 设置相机位置
- camera.position.set(center.x, center.y, cameraZ * 0.8); // 调整相机距离(可以根据需要调整倍数)
- camera.lookAt(center); // 让相机看向场景中心
- camera.updateProjectionMatrix(); // 更新相机的投影矩阵
- console.log("Adjusted camera position:", camera.position);
- console.log("Adjusted camera fov:", camera.fov);
- };
- /**
- * 调整相机位置和参数,使整个PLT模型在视图中可见
- * @param {THREE.Scene} scene - Three.js场景对象
- * @param {THREE.PerspectiveCamera} camera - 透视相机
- * @param {number} [paddingFactor] - 视图边距系数(1.0表示紧贴边界)
- */
- const adjustCameraForPlt = (scene, camera, params = {}) => {
- const {
- padding = 2.0,
- forceTopView = false
- } = params;
- // 1. 确保场景和相机已初始化
- if (!scene || !camera) {
- console.error('场景或相机未初始化');
- return;
- }
- // 2. 计算包围盒(包含可见网格)
- const box = new THREE.Box3();
- const visibleMeshes = [];
-
- scene.traverse(child => {
- if (child.isMesh && child.visible) {
- if (!child.geometry.boundingBox) {
- child.geometry.computeBoundingBox();
- }
- box.union(child.geometry.boundingBox);
- visibleMeshes.push(child);
- }
- });
- // 3. 处理空场景情况
- if (visibleMeshes.length === 0) {
- console.warn('没有可见网格,使用默认视角');
- camera.position.set(0, 10, 0);
- camera.lookAt(0, 0, 0);
- return;
- }
- // 4. 计算模型参数
- const size = box.getSize(new THREE.Vector3());
- const center = box.getCenter(new THREE.Vector3());
- const maxDim = Math.max(size.x, size.y, size.z);
- const distance = padding * maxDim;
- // 5. 设置相机位置(根据模型特征优化)
- if (forceTopView || size.y > size.x * 1.5) {
- // 高模型或强制顶视图
- camera.position.set(
- center.x,
- center.y + distance * 1.5,
- center.z
- );
- camera.up.set(0, 0, 1); // Z轴朝上
- } else {
- // 常规模型
- camera.position.set(
- center.x + distance * 0.7,
- center.y + distance * 0.5,
- center.z + distance * 0.7
- );
- camera.up.set(0, 1, 0); // Y轴朝上
- }
- // 6. 确保相机看向中心点
- camera.lookAt(center);
- camera.near = 0.1 * maxDim; // 动态调整近平面
- camera.far = 100 * maxDim; // 动态调整远平面
- camera.updateProjectionMatrix();
- // 7. 打印调试信息
- console.log(`相机调整完成:
- 模型中心:${center.toArray().map(v => v.toFixed(2))}
- 模型尺寸:${size.toArray().map(v => v.toFixed(2))}
- 相机位置:${camera.position.toArray().map(v => v.toFixed(2))}
- 视锥体:near=${camera.near.toFixed(2)}, far=${camera.far.toFixed(2)}`);
- };
- </script>
- <style>
- .three-container {
- width: 100%;
- /* height: calc(60vh - 6px); */
- border: 1px solid #ccc; /* 可选:添加边框以便查看容器范围 */
- }
- .el-checkbox__label{
- font-size: 12px;
- }
- .color-card-canvas {
- pointer-events: none; /* 允许点击穿透 */
- border: 1px solid #ccc;
- background: white;
- }
- </style>
|