fix editor data component refresh and transition update loop

This commit is contained in:
plum 2026-04-10 00:01:02 +08:00
parent 8b363e8eb4
commit ecddca00c9
9 changed files with 20 additions and 707 deletions

View File

@ -3,7 +3,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import * as monaco from 'monaco-editor';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
@ -74,6 +74,17 @@ onBeforeUnmount(() => {
}
});
watch(
() => props.source,
value => {
if (!editor) return;
const currentValue = editor.getValue();
if (value !== currentValue) {
editor.setValue(value);
}
}
);
async function initMonaco() {
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false,

View File

@ -4,20 +4,16 @@
<SceneTree />
</div>
<n-tabs default-value="bim" animated type="line" justify-content="space-around" class="!h-60%"
<n-tabs default-value="cad" animated type="line" justify-content="space-around" class="!h-60%"
pane-wrapper-class="layout-assets-tab-pane-wrapper">
<n-tab-pane name="cad" tab="CAD" display-directive="show">
<CadLibrary />
</n-tab-pane>
<n-tab-pane name="bim" tab="BIM" display-directive="show">
<BIMLibrary />
</n-tab-pane>
</n-tabs>
</div>
</template>
<script lang="ts" setup>
import BIMLibrary from "./assets/BIMLibrary.vue";
import SceneTree from "@/components/tree/SceneTree.vue";
import CadLibrary from "./assets/CadLibrary.vue";
</script>

View File

@ -1,339 +0,0 @@
<template>
<div id="bim-library" class="h-full flex flex-col">
<n-alert type="info" :show-icon="false" :bordered="false" class="mb-2 mx-2">
<n-button quaternary round type="primary" @click="showHistoryModal = true">
{{ t("layout.sider.History") }}
</n-button>
<n-button quaternary circle type="primary" @click="showBIMUpload = true">
<template #icon>
<n-icon>
<CloudUpload/>
</n-icon>
</template>
</n-button>
</n-alert>
<div class="flex-1 overflow-y-auto px-0.3rem">
<div class="grid grid-cols-[repeat(auto-fill,minmax(80px,1fr))] gap-2">
<n-card size="small" hoverable v-for="item in objectList" :key="item.id" @dblclick="addToScene(item)"
draggable="true" @dragstart="dragStart($event, item)" @dragend="dragEnd">
<template #cover>
<n-spin :show="item.conversionStatus === 0">
<template #description>
正在轻量化...
</template>
<img :src="item.thumbnail ? item.thumbnail : '/static/images/placeholder/占位图.png'"
:alt="item.fileName" draggable="false">
<n-tag :color="{ color: '#F1C3CC', textColor: '#D03050' }" :bordered="false" size="small"
class="absolute top-33px w-full" v-if="item.conversionStatus === 2">
轻量化失败
<template #icon>
<n-icon>
<CloseCircleSharp/>
</n-icon>
</template>
</n-tag>
</n-spin>
</template>
<n-tooltip placement="bottom" trigger="hover">
<template #trigger> {{ item.fileName }}</template>
<span> {{ item.fileName }} </span>
</n-tooltip>
</n-card>
</div>
</div>
<div class="flex justify-center mt-0.5rem">
<n-pagination v-bind="paginationReactive"></n-pagination>
</div>
<n-modal v-model:show="showHistoryModal" class="!w-60vw" preset="dialog" display-directive="show"
:title="t('bim[\'BIM lightweight\']') + t('layout.sider.History')">
<n-data-table class="mt-20px" size="small" :loading="tableLoading" :columns="columns"
:data="objectList"></n-data-table>
<div class="flex justify-end mt-0.5rem">
<n-pagination v-bind="paginationReactive"></n-pagination>
</div>
</n-modal>
<!-- BIM文件上传 -->
<BimUploadDialog v-model:show="showBIMUpload" ref="uploadDialogRef"/>
</div>
</template>
<script setup lang="ts">
import {ref, reactive, onMounted, h, onBeforeUnmount} from "vue";
import type {DataTableColumns} from 'naive-ui';
import {NButton, NTag, NIcon} from 'naive-ui'
import {CloudUpload, Reload, CheckmarkCircle, CloseOutline, CloseCircleSharp} from '@vicons/ionicons5';
import {t} from "@/language";
import {useDragStore} from "@/store/modules/drag";
import {fetchGetBim2GltfList} from "@/http/api/bim";
import {Bim2GltfWsData, WebSocketMessage} from "~/network";
import {Loader,Hooks} from "@astral3d/engine";
import {filterSize} from "@/utils/common/file";
import {onWebSocket, offWebSocket} from "@/hooks/useWebSocket";
import {useWebsocketStore} from "@/store/modules/websocket";
import {dateTimeFormat} from "@/utils/common/dateTime";
import BimUploadDialog from "./bimLibrary/BimUploadDialog.vue";
const websocketStore = useWebsocketStore();
let objectList = ref<IBIMData[]>([]);
const showHistoryModal = ref(false);
const tableLoading = ref(false);
const columns: DataTableColumns<IBIMData> = [
{
title: '文件名',
key: 'fileName'
},
{
title: 'bim文件体积',
key: 'bimFileSize',
render(row) {
return filterSize(row.bimFileSize);
}
},
{
title: 'gltf文件体积',
key: 'gltfFileSize',
render(row) {
return filterSize(row.gltfFileSize);
}
},
{
title: '转换时长',
key: 'conversionDuration',
render(row) {
return row.conversionDuration.toFixed(2) + "s";
}
},
{
title: '状态',
key: 'conversionStatus',
render(row) {
return h(
NTag,
{
bordered: false,
type: row.conversionStatus === 0 ? "warning" : row.conversionStatus === 1 ? "success" : "error",
},
{
default: () => row.conversionStatus === 0 ? "转换中" : row.conversionStatus === 1 ? "成功" : "失败",
icon: () => h(
NIcon, {
component: row.conversionStatus === 0 ? Reload : row.conversionStatus === 1 ? CheckmarkCircle : CloseOutline
}
)
}
)
}
},
{
title: '操作',
key: 'actions',
render(row) {
if (row.conversionStatus !== 1) return "";
return h(
NButton,
{
size: 'small',
onClick: () => addToScene(row)
},
{default: () => t("other.Load")}
)
}
}
];
let paginationReactive = reactive({
page: 1,
pageSize: 10,
pageCount: 1,
"on-update:page": (page: number) => {
paginationReactive.page = page;
getBim2GltfList();
}
})
const showBIMUpload = ref(false);
const uploadDialogRef = ref();
// BIM
async function getBim2GltfList() {
const res = await fetchGetBim2GltfList({
offset: (paginationReactive.page - 1) * paginationReactive.pageSize,
limit: paginationReactive.pageSize
});
objectList.value = res.data?.items || [];
paginationReactive.pageCount = res.data?.pages || 1;
}
async function addToScene(item) {
showHistoryModal.value = false;
let notice = window.$notification.info({
title: window.$t("scene['Get the scene data']") + "...",
content: window.$t("other.Loading") + "...",
closable: false,
})
// gltfFilePathgltf
fetch(`file/static/${item.gltfFilePath}`)
.then(res => res.blob())
.then(data => {
const file = new File([data as Blob], `${item.fileName}.glb`, {type: 'model/gltf-binary'});
notice.content = window.$t("scene['Parsing to editor']");
Loader.loadFiles([file], undefined).finally(() => {
setTimeout(() => {
notice.destroy();
Hooks.useDispatchSignal("sceneGraphChanged");
}, 800)
Hooks.useDispatchSignal("sceneGraphChanged");
})
})
.catch(() => {
notice.content = window.$t("scene['Failed to get scene data']");
setTimeout(() => {
notice.destroy();
}, 500)
return null;
})
}
//
const dragStore = useDragStore();
function dragStart(_, item) {
if (item.conversionStatus !== 1) return;
dragStore.setData(item)
}
function dragEnd() {
if (dragStore.getActionTarget !== "addToScene") return;
const data = dragStore.getData
if (data.conversionStatus !== 1) return;
addToScene(data);
dragStore.setActionTarget("");
}
// websocket bim2gltf
function Bim2GltfWsHandle(data: WebSocketMessage<Bim2GltfWsData>) {
if (data.type === "bim2gltf") {
//
if (data.subscriber === websocketStore.uname) {
let wsNotice = uploadDialogRef.value?.getNotice();
// 使uname
if (!wsNotice) {
wsNotice = window.$notification.info({
title: t("bim['BIM lightweight']"),
content: "",
closable: false,
})
}
if (data.data.conversionStatus === "progress") {
wsNotice.content = t("bim['BIM lightweight is in progress']") + "...";
} else if (data.data.conversionStatus === "completed") {
wsNotice.content = `${t("bim['BIM lightweight completed']")},${t("bim.In")} ${data.data.item.conversionDuration} ${t("bim.seconds")}`;
setTimeout(() => {
wsNotice?.destroy();
window.$dialog.info({
title: t("bim['BIM lightweight completed']"),
content: t("bim['Whether to load the BIM model into the scene?']"),
positiveText: window.$t('other.Load'),
negativeText: window.$t('other.Cancel'),
onPositiveClick: () => {
addToScene(data.data.item);
}
})
}, 800)
getBim2GltfList();
} else if (data.data.conversionStatus === "failed") {
wsNotice.content = t("bim['BIM lightweight failed']");
setTimeout(() => {
wsNotice?.destroy();
}, 1500)
getBim2GltfList();
}
} else {
if (data.data.conversionStatus !== "completed") return;
//
const n = window.$notification.info({
title: t("bim['BIM lightweight']"),
content: t("bim['New lightweight BIM model received, do you want to view it?']"),
duration: 5000,
closable: true,
meta: dateTimeFormat("yyyy-MM-dd HH:mm:ss"),
action: () =>
h(NButton, {
text: true,
type: 'primary',
onClick: () => {
addToScene(data.data.item);
n.destroy();
}
},
{
default: () => t("other.Load")
}
),
})
}
}
}
onMounted(() => {
getBim2GltfList();
// websocket
onWebSocket(Bim2GltfWsHandle);
})
onBeforeUnmount(() => {
offWebSocket(Bim2GltfWsHandle)
})
</script>
<style scoped lang="less">
#bim-library {
overflow-x: hidden;
.n-alert {
:deep(.n-alert-body) {
padding: 10px;
&__content {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
.n-card {
cursor: pointer;
:deep(.n-card-cover) {
img {
height: 89px;
object-fit: cover;
}
}
:deep(.n-card__content) {
padding: 3px 5px;
font-size: 13px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
</style>

View File

@ -1,277 +0,0 @@
<template>
<n-modal :show="show" @update:show="(b) => emits('update:show',b)" class="!w-500px" preset="card" :title="t('bim.BIM lightweight')">
<n-form label-placement="left" :model="BIMModel" :rules="BIMRules"
label-width="100px" label-align="right" ref="formRef"
require-mark-placement="right-hanging">
<!-- <n-form-item :label="t('bim[\'File name\']')" path="fileName">-->
<!-- <n-input v-model:value="BIMModel.fileName"-->
<!-- :placeholder="t('bim[\'Please enter the BIM file name\']')"/>-->
<!-- </n-form-item>-->
<n-form-item :label="t('bim.Thumbnail')">
<n-upload :default-upload="false" list-type="image-card" :max="1"
@change="thumbnailChange"/>
</n-form-item>
<n-form-item :label="t('bim[\'BIM file\']')" path="bimFile">
<n-upload ref="uploadBIMRef" :default-upload="false" directory-dnd :max="1"
:accept="'.' + NEED_CONVERT_BIM_MODEL.join(',.')" @change="bimChange">
<n-upload-dragger>
<div>
<n-icon size="48" :depth="3">
<ArchiveOutline/>
</n-icon>
</div>
<n-text style="font-size: 14px">
{{
t("bim['Click or drag the file to this area.Supported formats are:']") + ` ${NEED_CONVERT_BIM_MODEL.join("、")}`
}}
</n-text>
</n-upload-dragger>
</n-upload>
</n-form-item>
<n-collapse>
<template #arrow>
<n-icon>
<CaretForwardOutline />
</n-icon>
</template>
<n-collapse-item :title="t('bim[\'Conversion configuration\']')">
<!-- 极致轻量化 -->
<n-form-item :label="t('bim[\'Extreme lightweight\']')">
<n-switch v-model:value="BIMModel.options.Optimize" @change="handleOptimizeChange" />
</n-form-item>
<!-- 导出属性 -->
<n-form-item :label="t('bim[\'Export Property\']')">
<n-switch v-model:value="BIMModel.options.ExportProperty" :disabled="exportPropertyDisabled" />
</n-form-item>
<!-- 转换视图 -->
<n-form-item :label="t('bim[\'Conversion view\']')">
<n-radio-group v-model:value="BIMModel.options.View" name="View">
<n-radio-button value="Default">默认3D视图</n-radio-button>
<n-radio-button value="Name">按名称</n-radio-button>
</n-radio-group>
</n-form-item>
<!-- 视图名称 -->
<n-form-item :label="t('bim[\'View name\']')" v-if="BIMModel.options.View === 'Name'">
<n-input v-model:value="BIMModel.options.ViewName" :placeholder="t('bim[\'Please enter the conversion view name\']')"/>
</n-form-item>
<!-- 视觉样式 -->
<n-form-item :label="t('bim[\'Display style\']')">
<n-radio-group v-model:value="BIMModel.options.DisplayStyle" name="DisplayStyle">
<n-radio-button value="Colour">{{t("bim.Colour")}}</n-radio-button>
<n-radio-button value="Realistic">{{t("bim.Realistic")}}</n-radio-button>
<n-radio-button value="ViewDefault">{{t("bim['View default']")}}</n-radio-button>
</n-radio-group>
</n-form-item>
<!-- 坐标参考 -->
<n-form-item :label="t('bim[\'Coordinate reference\']')">
<n-radio-group v-model:value="BIMModel.options.CoordinateReference" name="CoordinateReference">
<n-radio-button value="Origin">{{t("bim.Origin")}}</n-radio-button>
<n-radio-button value="ProjectBasePoint">{{t("bim['Project base point']")}}</n-radio-button>
<n-radio-button value="MeasuringPoint">{{t("bim['Measuring point']")}}</n-radio-button>
</n-radio-group>
</n-form-item>
</n-collapse-item>
</n-collapse>
</n-form>
<div class="flex justify-end">
<n-button round type="primary" @click="submit">{{ t("bim['Upload and lightweight']") }}</n-button>
</div>
</n-modal>
</template>
<script setup lang="ts">
import {reactive, ref} from "vue";
import {NotificationReactive} from "naive-ui";
import {t} from "@/language";
import {ArchiveOutline, CaretForwardOutline} from "@vicons/ionicons5";
import {fetchUpload} from "@/http/api/sys";
import {fetchAddBim2Gltf, fetchUploadRvt} from "@/http/api/bim";
import {NEED_CONVERT_BIM_MODEL} from "@/utils/common/constant";
withDefaults(defineProps<{
show:boolean
}>(),{
show:false
})
const emits = defineEmits(["update:show"]);
const formRef = ref();
const uploadBIMRef = ref();
const exportPropertyDisabled = ref(false);
/* bim文件上传转换 */
const BIMModel = reactive<{
fileName: string,
thumbnail: File | null,
bimFile: File | null,
options:{
//
Optimize:boolean,
//
ExportProperty:boolean,
// - Default3D | Name
View:string,
// - View = "Name"
ViewName:string,
// - Colour: | Realistic- | ViewDefault-
DisplayStyle:string,
// - Origin: | ProjectBasePoint | MeasuringPoint
CoordinateReference:string
}
}>({
fileName: "",
thumbnail: null,
bimFile: null,
options:{
Optimize:false,
ExportProperty:true,
View:"Default",
ViewName:"",
DisplayStyle:"Colour",
CoordinateReference:"Origin"
}
})
const BIMRules = {
bimFile: {
required: true,
trigger: 'change',
validator: (_, value) => {
return new Promise<void>((resolve, reject) => {
// valueFile
if (value === '' || !(value instanceof File)) {
reject(Error(t("bim['Please upload the BIM file']")))
} else {
resolve()
}
})
}
}
}
//
function thumbnailChange({file}) {
if (file.status === "removed") {
BIMModel.thumbnail = null;
return;
}
BIMModel.thumbnail = file.file as File;
}
// bim
function bimChange({file}) {
if (file.status === "removed") {
BIMModel.bimFile = null;
formRef.value?.validate();
return;
}
if (!NEED_CONVERT_BIM_MODEL.includes(file.name.split(".").at(-1).toLowerCase())) {
window.$message?.error(t("prompt['This format is not supported, please upload again! Supported formats are:']") + ` ${NEED_CONVERT_BIM_MODEL.join("、")}`);
uploadBIMRef.value.clear();
return false
}
BIMModel.bimFile = file.file as File;
BIMModel.fileName = BIMModel.bimFile.name;
formRef.value?.validate();
}
// ()
function handleOptimizeChange() {
if(BIMModel.options.Optimize){
BIMModel.options.ExportProperty = false;
exportPropertyDisabled.value = true;
}else{
exportPropertyDisabled.value = false;
}
}
// ws 使notice
let wsNotice: null | NotificationReactive = null;
const getNotice = () => wsNotice;
//
function submit(e) {
if (import.meta.env.PROD) {
window.$message?.error(window.$t("prompt['Disable this function in the demonstration environment!']"));
return;
}
e.preventDefault();
formRef.value?.validate(async (errors) => {
if (!errors) {
emits("update:show",false);
wsNotice = window.$notification.info({
title: t("bim['BIM lightweight']"),
content: t("prompt.Uploading") + "...",
closable: false,
})
// 1.
let thumbnail = "";
if (BIMModel.thumbnail) {
const res = await fetchUpload({
file: BIMModel.thumbnail,
biz: "upload/bim/thumbnail"
})
if (res.data === null) {
window.$message?.error(t("bim['Failed to upload thumbnail']"));
} else {
thumbnail = res.data as string;
}
}
// 2. bim
const bimRes = await fetchUploadRvt({
file: BIMModel.bimFile,
});
if (bimRes.data === null) {
window.$message?.error(t("bim['Failed to upload BIM file']"));
wsNotice.content = `${t("bim['Failed to upload BIM file']")},${t("prompt['Please try again later!']")}`;
setTimeout(() => {
wsNotice?.destroy();
}, 1000)
return;
}
//3 Revit
wsNotice.content = t("bim['BIM lightweight is in progress']") + "...";
await fetchAddBim2Gltf({
bimFilePath: bimRes.data,
bimFileSize: (BIMModel.bimFile as File).size,
fileName: BIMModel.fileName,
fileSourceIp: "",
thumbnail,
conversionStatus: 0,
//
options:BIMModel.options
})
// reset
BIMModel.fileName = "";
BIMModel.thumbnail = null;
BIMModel.bimFile = null;
BIMModel.options = {
Optimize:false,
ExportProperty:true,
View:"Default",
ViewName:"",
DisplayStyle:"Colour",
CoordinateReference:"Origin"
}
}
})
}
defineExpose({getNotice})
</script>
<style scoped lang="less">
.n-form-item{
margin-bottom: 10px;
}
</style>

View File

@ -1,65 +0,0 @@
<template>
<n-card v-if="visible" class="absolute top-40px right-1 max-w-300px" content-style="padding: 5px 10px;">
<n-collapse accordion default-expanded-names="bim">
<template #arrow="{ collapsed }">
<n-icon v-if="collapsed">
<PlanetOutline/>
</n-icon>
<n-icon v-else>
<SunnyOutline/>
</n-icon>
</template>
<n-collapse-item title="&nbsp;BIM 构件信息" name="bim">
<div class="max-h-360px overflow-y-auto">
<n-descriptions v-for="(item, index) in info.parameters" :key="index" label-placement="left"
bordered size="small" :column="1">
<template #header>
<p class="py-2">{{ item.GroupName }}</p>
</template>
<n-descriptions-item v-for="(it, i) in item.Parameters" :key="i + it.name" :label="it.name">
{{ it.value }}
</n-descriptions-item>
</n-descriptions>
</div>
</n-collapse-item>
</n-collapse>
</n-card>
</template>
<script setup lang="ts">
import {PlanetOutline, SunnyOutline} from "@vicons/ionicons5";
import {ref, onBeforeUnmount, onMounted} from "vue";
import {Hooks} from "@astral3d/engine";
const visible = ref(false);
const info = ref({
parameters: [{GroupName: "", Parameters: [{name: "", value: ""}]}]
})
function objectSelected(object) {
if (!object) {
visible.value = false;
return;
}
if (!object.userData.BIM) {
if (!object.parent || !object.parent.userData.BIM) {
visible.value = false;
} else {
visible.value = true;
info.value = object.parent.userData.BIM;
}
} else {
visible.value = true;
info.value = object.userData.BIM;
}
}
onMounted(() => {
Hooks.useAddSignal("objectSelected", objectSelected);
})
onBeforeUnmount(() => {
Hooks.useRemoveSignal("objectSelected", objectSelected);
})
</script>

View File

@ -6,9 +6,6 @@
<ViewportInfo/>
</div>
<!-- RVT BIM 构件信息悬浮框 -->
<BIMProperties/>
<!-- IFC BIM 构件信息悬浮框 -->
<IFCProperties/>
@ -26,7 +23,6 @@ import {usePluginStore} from "@/store/modules/plugin";
import {installBuiltinPlugin} from "@/plugin";
import { clearBuffer } from "@/utils/wasm/optimize";
import ViewportInfo from "./ViewportInfo.vue";
import BIMProperties from "./BIMProperties.vue";
import IFCProperties from "./IFCProperties.vue";
const globalStore = useGlobalConfigStore();

View File

@ -19,26 +19,12 @@ const options = [
key: 'details',
icon: renderIcon(Information)
},
{
label: () => cpt("home.Rename").value,
key: 'rename',
icon: renderIcon(Edit)
},
{
label: () => cpt("home.Delete").value,
key: 'delete',
icon: renderIcon(Delete)
},
{
label: () => cpt("home.Release").value,
key: 'release',
icon: renderIcon(SendAlt)
},
{
label: () => cpt("layout.header.Export").value,
key: 'export',
icon: renderIcon(Export)
}
]
const detail = ref();
const detailVisible = ref(false);

View File

@ -158,7 +158,8 @@ export default defineConfig(async ({mode, command}) => {
// }),
viteStaticCopy({
targets: [
{ src: 'node_modules/@astral3d/engine/dist/libs/*', dest: 'assets/libs' }
{ src: 'node_modules/@astral3d/engine/dist/libs/*', dest: 'assets/libs' },
{ src: 'node_modules/@astral3d/engine/dist/resource/**/*', dest: 'resource' }
],
}),
...plugins

View File

@ -4,6 +4,7 @@ import {Timer} from 'three/examples/jsm/misc/Timer.js';
import {CSS2DRenderer} from "three/examples/jsm/renderers/CSS2DRenderer";
import {CSS3DRenderer} from "three/examples/jsm/renderers/CSS3DRenderer.js";
import {TransformControls} from "three/examples/jsm/controls/TransformControls.js";
import TWEEN from "three/examples/jsm/libs/tween.module.js";
import App from "../app/App";
import {ViewerOptions} from "./ViewerOptions";
import {PluginManager} from "@/core/plugin/plugin";
@ -1060,6 +1061,9 @@ export default class Viewer extends THREE.EventDispatcher<ViewerEventMap> {
this.dispatchEvent({type: 'beforeAnimation', delta: timeStamp});
let needRender = App.animationManager.update(timeStamp);
if (TWEEN.update()) {
needRender = true;
}
if (needRender) {
if (App.selected !== null && App.selected.animations.length > 0) {
// 避免某些蒙皮网格的帧延迟效应(e.g. Michelle.glb)