Commit 43809eefdb32641325d4f6ddbe30cc7228d69c95

Authored by xp.Huang
2 parents 78610649 f94a8090

Merge branch 'fix/3d-model-load' into 'main_dev'

perf: 优化3d模型加载

See merge request yunteng/thingskit-view!309
dist.zip deleted 100644 → 0
No preview for this file type
  1 +import { UIPanel, UIButton } from './libs/ui.js';
  2 +import html2canvas from 'html2canvas'
  3 +import useMessage from './MessageDialog.js'
  4 +import { saveOrUpdateThreeJsModel } from './libs/http/api.js'
  5 +import { fetchRouteParamsLocation } from '@/utils'
  6 +import { uploadFile } from '@/api/external/contentSave/content'
  7 +import { base64toFile } from '../utils/Base64ToFile.js'
  8 +import { useSpin } from '../js/libs/spin/useSpin.js'
  9 +
  10 +function MenubarSave(editor) {
  11 + const strings = editor.strings
  12 +
  13 + const container = new UIPanel();
  14 + container.setClass('menu right');
  15 + container.setStyle('height', ['100%'])
  16 + container.setStyle('display', ['flex'])
  17 +
  18 + const saveButton = new UIButton(strings.getKey('menubar/file/save'))
  19 + container.add(saveButton)
  20 +
  21 + saveButton.onClick(async function () {
  22 +
  23 + // 获取缩略图片
  24 + const range = document.querySelector('#viewport').children[3]
  25 + const { spin, stop } = useSpin()
  26 + const { success, error } = useMessage()
  27 + try {
  28 + spin()
  29 + // 生成图片
  30 + const canvasImage = await html2canvas(range, {
  31 + backgroundColor: null,
  32 + allowTaint: true,
  33 + useCORS: true,
  34 + logging: false
  35 + })
  36 + // 上传预览图
  37 + const uploadParams = new FormData()
  38 + uploadParams.append(
  39 + 'file',
  40 + base64toFile(canvasImage.toDataURL(), `${fetchRouteParamsLocation()}_index_preview.png`)
  41 + )
  42 + const uploadRes = await uploadFile(uploadParams)
  43 + const file_json = editor.toJSON()
  44 + const paramsStr = window.location.search
  45 + const params = new URLSearchParams(paramsStr)
  46 + const file_uuid = params.get('three_file_uuid')
  47 + await saveOrUpdateThreeJsModel({
  48 + id: file_uuid,
  49 + imageUrl: uploadRes?.fileDownloadUri,
  50 + data: file_json
  51 + })
  52 + success('保存成功')
  53 + } catch (e) {
  54 + error(e?.message || e?.msg || '网络错误')
  55 + } finally {
  56 + stop()
  57 + }
  58 +
  59 + })
  60 +
  61 + return container;
  62 +}
  63 +
  64 +export { MenubarSave };
... ...
... ... @@ -5,20 +5,22 @@ import { MenubarEdit } from './Menubar.Edit.js';
5 5 import { MenubarFile } from './Menubar.File.js';
6 6 import { MenubarView } from './Menubar.View.js';
7 7 import { MenubarHelp } from './Menubar.Help.js';
8   -import { MenubarStatus } from './Menubar.Status.js';
  8 +// import { MenubarStatus } from './Menubar.Status.js';
  9 +import { MenubarSave } from './Menubar.Save.js'
9 10
10   -function Menubar( editor ) {
  11 +function Menubar(editor) {
11 12
12 13 const container = new UIPanel();
13   - container.setId( 'menubar' );
  14 + container.setId('menubar');
14 15
15   - container.add( new MenubarFile( editor ) );
16   - container.add( new MenubarEdit( editor ) );
17   - container.add( new MenubarAdd( editor ) );
18   - container.add( new MenubarView( editor ) );
19   - container.add( new MenubarHelp( editor ) );
  16 + container.add(new MenubarFile(editor));
  17 + container.add(new MenubarEdit(editor));
  18 + container.add(new MenubarAdd(editor));
  19 + container.add(new MenubarView(editor));
  20 + container.add(new MenubarHelp(editor));
20 21
21 22 // container.add( new MenubarStatus( editor ) );
  23 + container.add(new MenubarSave(editor))
22 24
23 25 return container;
24 26
... ...
  1 +import { defHttp } from '@/utils/external/http/axios';
  2 +import { ModelComponentItemType } from './model';
  3 +
  4 +
  5 +
  6 +
  7 +export const doGet3DComponentJson = (id: string, ) => {
  8 + return defHttp.get<ModelComponentItemType | null>(
  9 + {
  10 + url: `/3d_component/json/${id}/root.json`
  11 + }
  12 + )
  13 +}
... ...
  1 +export interface ModelComponentItemType {
  2 + camera: Recordable
  3 + environment: Nullable<string>
  4 + history: Recordable
  5 + metadata: Recordable
  6 + project: Recordable
  7 + scene: Recordable
  8 + scripts: Recordable
  9 +}
... ...
... ... @@ -284,6 +284,8 @@ export default {
284 284 after: 'After rendering',
285 285 doesDomComponent: 'At this time, the component DOM does not yet exist',
286 286 alreadyDomComponent: 'At this point, the component DOM already exists',
287   -
  287 + },
  288 + threeModel: {
  289 + loadError: 'Load error occurred'
288 290 }
289 291 }
... ...
... ... @@ -286,5 +286,8 @@ export default {
286 286 after: '渲染之后',//After rendering
287 287 doesDomComponent: '此时组件 DOM 还未存在',//At this time, the component DOM does not yet exist
288 288 alreadyDomComponent: '此时组件 DOM 已经存在',//At this point, the component DOM already exists
  289 + },
  290 + threeModel: {
  291 + loadError: '加载发生错误'
289 292 }
290 293 }
... ...
1 1 <template>
2 2 <div class="go-content-box" :style="{ border: !borderConfig.show ? 'none' : '' }">
3   - <div>
4   - <vue3dLoader
5   - ref="vue3dLoaderRef"
6   - :requestHeader="headers"
7   - :backgroundColor="backgroundColor"
8   - :backgroundAlpha="backgroundAlpha"
9   - :webGLRendererOptions="webGLRendererOptions"
10   - :height="h"
11   - :width="w"
12   - :filePath="dataset"
13   - @process="onProcess"
14   - @load="onLoad"
15   - :intersectRecursive="true"
16   - />
17   - <div v-show="show" class="process">
18   - <n-spin :show="show">
19   - <template #description> 拼命加载中... </template>
20   - </n-spin>
21   - </div>
22   - </div>
  3 + <NSpin :show="show" style="width: 100%; height: 100%;">
  4 + <div v-show="!loadErrorMsg" ref="modelContainerElRef" :style="{ display: show ? 'hidden' : 'block' }"></div>
  5 + <NResult v-show="loadErrorMsg" status="500" :title="t('external.threeModel.loadError')"
  6 + :description="loadErrorMsg || ''">
  7 + </NResult>
  8 + </NSpin>
23 9 </div>
24 10 </template>
25 11 <script setup lang="ts">
26   -import { PropType, toRefs, ref, nextTick } from 'vue'
  12 +import { PropType, toRefs, unref, shallowRef, onMounted, onBeforeMount, ref } from 'vue'
27 13 import { CreateComponentType } from '@/packages/index.d'
28   -import { vue3dLoader } from 'vue-3d-loader'
29   -import { getJwtToken } from '@/utils/external/auth'
  14 +import { ModelLoader } from './modelLoader'
  15 +import { doGet3DComponentJson } from '@/api/external/3dModel'
  16 +import { NSpin, NResult } from 'naive-ui'
  17 +const t = window['$t']
30 18
31 19 const props = defineProps({
32 20 chartConfig: {
... ... @@ -35,39 +23,37 @@ const props = defineProps({
35 23 }
36 24 })
37 25
38   -const vue3dLoaderRef = ref(null)
39 26
40   -const headers = {
41   - 'x-authorization': `Bearer ${getJwtToken()}`
42   -}
43   -const webGLRendererOptions = {
44   - alpha: true, // 透明
45   - antialias: true, // 抗锯齿
46   - // fix: 预览三维图像需加上preserveDrawingBuffer: true
47   - preserveDrawingBuffer: true //缩略图生效需开启
48   -}
  27 +const show = ref(false)
  28 +const loadErrorMsg = ref<Nullable<string>>()
49 29
50   -//三维模型加载
51   -const show = ref(true)
  30 +const { borderConfig, dataset } = toRefs(props.chartConfig.option) as Recordable
52 31
53   -const { dataset, backgroundColor, backgroundAlpha, borderConfig } = toRefs(props.chartConfig.option) as Recordable
54 32
55   -const onLoad = async () => {
56   - //加载完成
57   - await nextTick()
58   - if (dataset.value) {
  33 +const modelContainerElRef = shallowRef<HTMLDivElement>()
  34 +
  35 +const { w, h } = toRefs(props.chartConfig.attr)
  36 +
  37 +const loader = new ModelLoader({ width: unref(w), height: unref(h) })
  38 +
  39 +onBeforeMount(async () => {
  40 + try {
  41 + show.value = true
  42 + loadErrorMsg.value = null
  43 + const sceneJSON = await doGet3DComponentJson(unref(dataset))
  44 + await Promise.all([loader.parseScene(sceneJSON?.scene || {}), loader.parseCamera(sceneJSON?.camera || {})])
  45 + } catch (e) {
  46 + loadErrorMsg.value = (e as any)?.toString()
  47 + } finally {
59 48 show.value = false
60 49 }
61   -}
62 50
63   -//三维模型进度条
64   -const process = ref(0)
65   -
66   -const onProcess = (event: Recordable) => {
67   - process.value = Math.floor((event.loaded / event.total) * 100)
68   -}
  51 +})
69 52
70   -const { w, h } = toRefs(props.chartConfig.attr)
  53 +onMounted(() => {
  54 + unref(modelContainerElRef)?.appendChild(loader.renderer.domElement)
  55 + loader.addControls()
  56 +})
71 57 </script>
72 58
73 59 <style lang="scss" scoped>
... ... @@ -76,13 +62,14 @@ const { w, h } = toRefs(props.chartConfig.attr)
76 62 border-width: v-bind('borderConfig.size + "px"');
77 63 border-style: solid;
78 64 border-color: v-bind('borderConfig.color');
  65 +
79 66 .process {
80 67 position: absolute;
81 68 top: 50%;
82 69 left: 50%;
83 70 width: 50%;
84 71 transform: translate(-50%, -50%);
85   - display:flex;
  72 + display: flex;
86 73 align-items: center;
87 74 justify-content: center;
88 75 }
... ...
  1 +import { Camera, Scene, PerspectiveCamera, WebGLRenderer, ObjectLoader, PMREMGenerator, } from "three";
  2 +
  3 +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
  4 +
  5 +interface ModelLoaderConstructorOptions {
  6 + width?: number
  7 + height?: number
  8 +}
  9 +
  10 +export class ModelLoader {
  11 +
  12 + renderer: WebGLRenderer
  13 + scene: Scene
  14 + camera: Camera
  15 + controls?: OrbitControls
  16 +
  17 + private pmremGenerator?: PMREMGenerator
  18 + private objectLoader: ObjectLoader
  19 + private el?: HTMLElement
  20 +
  21 + constructor(
  22 + {
  23 + width = window.innerWidth,
  24 + height = window.innerHeight
  25 + }: ModelLoaderConstructorOptions = {}
  26 + ) {
  27 + this.renderer = new WebGLRenderer({ alpha: true, antialias: true })
  28 + this.scene = new Scene()
  29 + this.camera = new PerspectiveCamera()
  30 + this.objectLoader = new ObjectLoader()
  31 + this.renderer.setPixelRatio(window.devicePixelRatio)
  32 + this.renderer.setSize(width, height)
  33 + this.renderer.setAnimationLoop(this.animate.bind(this))
  34 + }
  35 +
  36 + private animate() {
  37 + this.renderer.render(this.scene, this.camera)
  38 + }
  39 +
  40 + addControls() {
  41 + this.controls = new OrbitControls(this.camera, this.renderer.domElement)
  42 + // 启用阻尼效果
  43 + this.controls.enableDamping = true;
  44 + // 阻尼系数
  45 + this.controls.dampingFactor = 0.25;
  46 + // 禁止屏幕空间的平移
  47 + this.controls.screenSpacePanning = false;
  48 + // 设置最小距离
  49 + this.controls.minDistance = 0.5;
  50 + // 设置最大距离
  51 + this.controls.maxDistance = 500;
  52 + // 限制垂直旋转角度
  53 + this.controls.maxPolarAngle = Math.PI / 2;
  54 + }
  55 +
  56 + append(el: HTMLElement) {
  57 + this.el = el
  58 + this.el.appendChild(this.renderer.domElement)
  59 + }
  60 +
  61 + setScene(scene: Scene) {
  62 + this.scene = scene
  63 + }
  64 +
  65 + async parseScene(object: Recordable) {
  66 + this.scene = await this.objectLoader.parseAsync(object)
  67 + }
  68 +
  69 + async parseCamera(object: Recordable) {
  70 + this.camera = await this.objectLoader.parseAsync(object)
  71 + if (this.controls) {
  72 + this.controls.object = this.camera
  73 + this.controls.update()
  74 + }
  75 + }
  76 +
  77 + // setModelviewer() {
  78 + // this.pmremGenerator = new PMREMGenerator(this.renderer);
  79 + // this.pmremGenerator.compileEquirectangularShader();
  80 + // this.scene.environment = this.pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
  81 + // }
  82 +
  83 +}
... ...
1 1 <template>
2 2 <!-- 侧边栏和数据分发处理 -->
3 3 <div class="go-chart-common">
4   - <n-menu
5   - v-show="hidePackageOneCategory"
6   - class="chart-menu-width"
7   - v-model:value="selectValue"
8   - :options="packages.menuOptions"
9   - :icon-size="16"
10   - :indent="18"
11   - @update:value="clickItemHandle"
12   - ></n-menu>
  4 + <n-menu v-show="hidePackageOneCategory" class="chart-menu-width" v-model:value="selectValue"
  5 + :options="packages.menuOptions" :icon-size="16" :indent="18" @update:value="clickItemHandle"></n-menu>
13 6 <div class="chart-content-list">
14 7 <n-scrollbar trigger="none">
15 8 <charts-item-box :menuOptions="packages.selectOptions" @deletePhoto="deleteHandle"></charts-item-box>
... ... @@ -37,7 +30,7 @@ const props = defineProps({
37 30 selectOptions: {
38 31 type: Object,
39 32 // eslint-disable-next-line @typescript-eslint/no-empty-function
40   - default: () => {}
  33 + default: () => { }
41 34 }
42 35 })
43 36
... ... @@ -134,7 +127,7 @@ watch(
134 127 redirectComponent: 'Decorates/Three/Loader',
135 128 isThreeModel: true,
136 129 threeModelContent: modelItem.content,
137   - threeModelFilePath: `${threeFilePath}${VITE_GLOB_API_URL}${VITE_GLOB_API_URL_PREFIX}/3d_component/json/${modelItem.id}/scene.json`,
  130 + threeModelFilePath: modelItem.id,
138 131 threeModelImageUrl: modelItem.imageUrl
139 132 }
140 133 })
... ... @@ -189,14 +182,17 @@ const clickItemHandle = (key: string) => {
189 182 /* 此高度与 ContentBox 组件关联*/
190 183 $topHeight: 40px;
191 184 $menuWidth: 65px;
  185 +
192 186 @include go('chart-common') {
193 187 display: flex;
194 188 height: calc(100vh - #{$--header-height} - #{$topHeight});
  189 +
195 190 .chart-menu-width {
196 191 width: $menuWidth;
197 192 flex-shrink: 0;
198 193 @include fetch-bg-color('background-color2-shallow');
199 194 }
  195 +
200 196 .chart-content-list {
201 197 width: 200px;
202 198 flex: 1;
... ... @@ -204,14 +200,17 @@ $menuWidth: 65px;
204 200 flex-direction: column;
205 201 align-items: center;
206 202 }
  203 +
207 204 @include deep() {
208 205 .n-menu-item {
209 206 height: 30px;
  207 +
210 208 &.n-menu-item--selected {
211 209 &::before {
212 210 background-color: rgba(0, 0, 0, 0);
213 211 }
214 212 }
  213 +
215 214 .n-menu-item-content {
216 215 text-align: center;
217 216 padding: 0px 14px !important;
... ...