267 lines
7.2 KiB
Vue
267 lines
7.2 KiB
Vue
<!--
|
||
* File: \src\components\Viewer.vue
|
||
* Project: viewer_for_ruwei
|
||
* File Created: Tuesday, 10th September 2024 10:44:34 am
|
||
* Author: Sun Jie (j.sun@supreium.com)
|
||
* -----
|
||
* Last Modified: Tuesday, 10th September 2024 10:44:35 am
|
||
* Modified By: Sun Jie (j.sun@supreium.com)
|
||
* -----
|
||
* Description: 3D 网格查看器组件,支持属性可视化切换
|
||
* -----
|
||
* Copyright (c) 2024 Supreium Co., Ltd , All Rights Reserved.
|
||
-->
|
||
|
||
<template>
|
||
<div class="viewer-wrapper">
|
||
<!-- 顶部属性切换工具栏 -->
|
||
<div class="viewer-toolbar">
|
||
<el-radio-group
|
||
v-model="selected"
|
||
size="small"
|
||
class="property-toggle-group"
|
||
v-if="properties && properties.length > 0"
|
||
>
|
||
<el-radio-button
|
||
v-for="item in properties"
|
||
:key="item"
|
||
:value="item"
|
||
>
|
||
{{ item }}
|
||
</el-radio-button>
|
||
</el-radio-group>
|
||
|
||
<!-- 视图控制按钮 -->
|
||
<div class="view-controls">
|
||
<el-tooltip content="适应视图" placement="bottom">
|
||
<button class="control-btn" @click="fitAll">
|
||
<el-icon><FullScreen /></el-icon>
|
||
</button>
|
||
</el-tooltip>
|
||
<el-tooltip content="等轴视图" placement="bottom">
|
||
<button class="control-btn" @click="viewAt">
|
||
<el-icon><View /></el-icon>
|
||
</button>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 3D 渲染容器 -->
|
||
<div class="viewer-container" ref="renderDiv">
|
||
<!-- 加载状态指示器 -->
|
||
<div v-if="isLoading" class="loading-overlay">
|
||
<el-icon class="is-loading loading-icon"><Loading /></el-icon>
|
||
<span class="loading-text">LOADING MODEL...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { FullScreen, Loading, View } from '@element-plus/icons-vue';
|
||
import axios from 'axios';
|
||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||
import { ViewHelper } from '../utils/viewHelper';
|
||
|
||
// 定义组件属性,默认使用 /example.json 作为数据源
|
||
const props = withDefaults(defineProps<{
|
||
dataPath?: string;
|
||
}>(), {
|
||
dataPath: '/example.json'
|
||
});
|
||
|
||
const renderDiv = ref<HTMLDivElement>();
|
||
const resizeListener = ref<Function[]>([]);
|
||
const destroyFunctions = ref<Function[]>([]);
|
||
|
||
const properties = ref<string[]>();
|
||
const selected = ref<string>("");
|
||
const isLoading = ref(true);
|
||
|
||
// 窗口尺寸调整回调
|
||
const resizeCallback = () => resizeListener.value.forEach((callback) => callback());
|
||
|
||
// 监听容器尺寸变化
|
||
const onDivResize = (currentDiv: HTMLDivElement, callback: ResizeObserverCallback) => {
|
||
resizeListener.value.push(callback);
|
||
const resizeObserver = new ResizeObserver(callback);
|
||
resizeObserver.observe(currentDiv!);
|
||
destroyFunctions.value.push(() => resizeObserver?.disconnect());
|
||
};
|
||
|
||
// 初始化视图
|
||
const init = async () => {
|
||
window.addEventListener("resize", resizeCallback);
|
||
await ViewHelper.instance.initView(renderDiv.value!);
|
||
onDivResize(renderDiv.value!, () => ViewHelper.instance.viewResize(renderDiv.value!.clientWidth, renderDiv.value!.clientHeight));
|
||
};
|
||
|
||
onMounted(async () => {
|
||
await init();
|
||
|
||
try {
|
||
// 加载网格数据
|
||
const mesh = (await axios.get(props.dataPath)).data;
|
||
// 创建视图对象
|
||
ViewHelper.instance.loadMesh(mesh.mesh, mesh.properties);
|
||
/**
|
||
* 如需过滤属性可使用:
|
||
* ViewHelper.instance.loadMesh(mesh.mesh, (mesh.properties as MeshPropertyType[]).filter((item) => item.name === "vonMises" || item.name === "displacement-magnitude"));
|
||
*/
|
||
// 设置等轴视图并适应
|
||
ViewHelper.instance.isometricView();
|
||
ViewHelper.instance.fitAll();
|
||
// 获取可用属性列表
|
||
properties.value = ViewHelper.instance.properties;
|
||
// 激活第一个属性,触发 watch
|
||
if (properties.value && properties.value.length > 0) {
|
||
selected.value = properties.value[0];
|
||
}
|
||
// 注册清理回调
|
||
destroyFunctions.value.push(() => ViewHelper.instance.clear());
|
||
} catch (error) {
|
||
console.error('加载模型数据失败:', error);
|
||
} finally {
|
||
isLoading.value = false;
|
||
}
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener("resize", resizeCallback);
|
||
destroyFunctions.value.forEach((fun) => fun());
|
||
});
|
||
|
||
// 监听属性选择变化
|
||
watch(selected, () => selected.value !== "" && ViewHelper.instance.activeProperty(selected.value))
|
||
|
||
// 适应视图
|
||
const fitAll = () => ViewHelper.instance.fitAll();
|
||
// 等轴视图
|
||
const viewAt = () => ViewHelper.instance.isometricView();
|
||
</script>
|
||
|
||
<style lang="css" scoped>
|
||
/* 查看器容器 */
|
||
.viewer-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #0b1120;
|
||
border: 1px solid rgba(6, 182, 212, 0.2);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 工具栏 */
|
||
.viewer-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.95) 0%, rgba(11, 17, 32, 0.9) 100%);
|
||
border-bottom: 1px solid rgba(6, 182, 212, 0.15);
|
||
gap: 16px;
|
||
}
|
||
|
||
/* 视图控制按钮组 */
|
||
.view-controls {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.control-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 6px;
|
||
background: rgba(15, 23, 42, 0.8);
|
||
border: 1px solid rgba(34, 211, 238, 0.3);
|
||
color: #22d3ee;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.control-btn:hover {
|
||
background: rgba(6, 182, 212, 0.2);
|
||
box-shadow: 0 0 12px rgba(6, 182, 212, 0.4);
|
||
transform: scale(1.05);
|
||
border-color: #22d3ee;
|
||
}
|
||
|
||
.control-btn:active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
/* 3D 渲染区域 */
|
||
.viewer-container {
|
||
flex: 1;
|
||
position: relative;
|
||
background: radial-gradient(ellipse at center, #0f172a 0%, #0b1120 100%);
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(11, 17, 32, 0.9);
|
||
backdrop-filter: blur(4px);
|
||
z-index: 10;
|
||
}
|
||
|
||
.loading-icon {
|
||
font-size: 32px;
|
||
color: #22d3ee;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.loading-text {
|
||
color: rgba(34, 211, 238, 0.8);
|
||
font-size: 12px;
|
||
letter-spacing: 0.2em;
|
||
font-family: monospace;
|
||
}
|
||
|
||
/* 属性切换 Toggle Group 样式 */
|
||
:deep(.property-toggle-group .el-radio-button__inner) {
|
||
background-color: transparent;
|
||
border: 1px solid rgba(34, 211, 238, 0.2);
|
||
color: #94a3b8;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
transition: all 0.3s ease;
|
||
padding: 6px 14px;
|
||
}
|
||
|
||
:deep(.property-toggle-group .el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
||
background-color: rgba(6, 182, 212, 0.15);
|
||
border-color: #06b6d4;
|
||
color: #22d3ee;
|
||
box-shadow: 0 0 10px rgba(6, 182, 212, 0.3);
|
||
}
|
||
|
||
:deep(.property-toggle-group .el-radio-button:first-child .el-radio-button__inner) {
|
||
border-radius: 4px 0 0 4px;
|
||
}
|
||
|
||
:deep(.property-toggle-group .el-radio-button:last-child .el-radio-button__inner) {
|
||
border-radius: 0 4px 4px 0;
|
||
}
|
||
|
||
:deep(.property-toggle-group .el-radio-button__inner:hover) {
|
||
color: #22d3ee;
|
||
background-color: rgba(34, 211, 238, 0.05);
|
||
}
|
||
|
||
/* 处理单个按钮时的圆角 */
|
||
:deep(.property-toggle-group .el-radio-button:only-child .el-radio-button__inner) {
|
||
border-radius: 4px;
|
||
}
|
||
</style> |