Commit 43809eefdb32641325d4f6ddbe30cc7228d69c95
Merge branch 'fix/3d-model-load' into 'main_dev'
perf: 优化3d模型加载 See merge request yunteng/thingskit-view!309
Showing
10 changed files
with
231 additions
and
69 deletions
dist.zip
deleted
100644 → 0
No preview for this file type
editor/js/Menubar.Save.js
0 → 100644
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,20 +5,22 @@ import { MenubarEdit } from './Menubar.Edit.js'; | ||
5 | import { MenubarFile } from './Menubar.File.js'; | 5 | import { MenubarFile } from './Menubar.File.js'; |
6 | import { MenubarView } from './Menubar.View.js'; | 6 | import { MenubarView } from './Menubar.View.js'; |
7 | import { MenubarHelp } from './Menubar.Help.js'; | 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 | const container = new UIPanel(); | 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 | // container.add( new MenubarStatus( editor ) ); | 22 | // container.add( new MenubarStatus( editor ) ); |
23 | + container.add(new MenubarSave(editor)) | ||
22 | 24 | ||
23 | return container; | 25 | return container; |
24 | 26 |
src/api/external/3dModel/index.ts
0 → 100644
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 | +} |
src/api/external/3dModel/model/index.ts
0 → 100644
@@ -284,6 +284,8 @@ export default { | @@ -284,6 +284,8 @@ export default { | ||
284 | after: 'After rendering', | 284 | after: 'After rendering', |
285 | doesDomComponent: 'At this time, the component DOM does not yet exist', | 285 | doesDomComponent: 'At this time, the component DOM does not yet exist', |
286 | alreadyDomComponent: 'At this point, the component DOM already exists', | 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,5 +286,8 @@ export default { | ||
286 | after: '渲染之后',//After rendering | 286 | after: '渲染之后',//After rendering |
287 | doesDomComponent: '此时组件 DOM 还未存在',//At this time, the component DOM does not yet exist | 287 | doesDomComponent: '此时组件 DOM 还未存在',//At this time, the component DOM does not yet exist |
288 | alreadyDomComponent: '此时组件 DOM 已经存在',//At this point, the component DOM already exists | 288 | alreadyDomComponent: '此时组件 DOM 已经存在',//At this point, the component DOM already exists |
289 | + }, | ||
290 | + threeModel: { | ||
291 | + loadError: '加载发生错误' | ||
289 | } | 292 | } |
290 | } | 293 | } |
1 | <template> | 1 | <template> |
2 | <div class="go-content-box" :style="{ border: !borderConfig.show ? 'none' : '' }"> | 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 | </div> | 9 | </div> |
24 | </template> | 10 | </template> |
25 | <script setup lang="ts"> | 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 | import { CreateComponentType } from '@/packages/index.d' | 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 | const props = defineProps({ | 19 | const props = defineProps({ |
32 | chartConfig: { | 20 | chartConfig: { |
@@ -35,39 +23,37 @@ const props = defineProps({ | @@ -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 | show.value = false | 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 | </script> | 57 | </script> |
72 | 58 | ||
73 | <style lang="scss" scoped> | 59 | <style lang="scss" scoped> |
@@ -76,13 +62,14 @@ const { w, h } = toRefs(props.chartConfig.attr) | @@ -76,13 +62,14 @@ const { w, h } = toRefs(props.chartConfig.attr) | ||
76 | border-width: v-bind('borderConfig.size + "px"'); | 62 | border-width: v-bind('borderConfig.size + "px"'); |
77 | border-style: solid; | 63 | border-style: solid; |
78 | border-color: v-bind('borderConfig.color'); | 64 | border-color: v-bind('borderConfig.color'); |
65 | + | ||
79 | .process { | 66 | .process { |
80 | position: absolute; | 67 | position: absolute; |
81 | top: 50%; | 68 | top: 50%; |
82 | left: 50%; | 69 | left: 50%; |
83 | width: 50%; | 70 | width: 50%; |
84 | transform: translate(-50%, -50%); | 71 | transform: translate(-50%, -50%); |
85 | - display:flex; | 72 | + display: flex; |
86 | align-items: center; | 73 | align-items: center; |
87 | justify-content: center; | 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 | <template> | 1 | <template> |
2 | <!-- 侧边栏和数据分发处理 --> | 2 | <!-- 侧边栏和数据分发处理 --> |
3 | <div class="go-chart-common"> | 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 | <div class="chart-content-list"> | 6 | <div class="chart-content-list"> |
14 | <n-scrollbar trigger="none"> | 7 | <n-scrollbar trigger="none"> |
15 | <charts-item-box :menuOptions="packages.selectOptions" @deletePhoto="deleteHandle"></charts-item-box> | 8 | <charts-item-box :menuOptions="packages.selectOptions" @deletePhoto="deleteHandle"></charts-item-box> |
@@ -37,7 +30,7 @@ const props = defineProps({ | @@ -37,7 +30,7 @@ const props = defineProps({ | ||
37 | selectOptions: { | 30 | selectOptions: { |
38 | type: Object, | 31 | type: Object, |
39 | // eslint-disable-next-line @typescript-eslint/no-empty-function | 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,7 +127,7 @@ watch( | ||
134 | redirectComponent: 'Decorates/Three/Loader', | 127 | redirectComponent: 'Decorates/Three/Loader', |
135 | isThreeModel: true, | 128 | isThreeModel: true, |
136 | threeModelContent: modelItem.content, | 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 | threeModelImageUrl: modelItem.imageUrl | 131 | threeModelImageUrl: modelItem.imageUrl |
139 | } | 132 | } |
140 | }) | 133 | }) |
@@ -189,14 +182,17 @@ const clickItemHandle = (key: string) => { | @@ -189,14 +182,17 @@ const clickItemHandle = (key: string) => { | ||
189 | /* 此高度与 ContentBox 组件关联*/ | 182 | /* 此高度与 ContentBox 组件关联*/ |
190 | $topHeight: 40px; | 183 | $topHeight: 40px; |
191 | $menuWidth: 65px; | 184 | $menuWidth: 65px; |
185 | + | ||
192 | @include go('chart-common') { | 186 | @include go('chart-common') { |
193 | display: flex; | 187 | display: flex; |
194 | height: calc(100vh - #{$--header-height} - #{$topHeight}); | 188 | height: calc(100vh - #{$--header-height} - #{$topHeight}); |
189 | + | ||
195 | .chart-menu-width { | 190 | .chart-menu-width { |
196 | width: $menuWidth; | 191 | width: $menuWidth; |
197 | flex-shrink: 0; | 192 | flex-shrink: 0; |
198 | @include fetch-bg-color('background-color2-shallow'); | 193 | @include fetch-bg-color('background-color2-shallow'); |
199 | } | 194 | } |
195 | + | ||
200 | .chart-content-list { | 196 | .chart-content-list { |
201 | width: 200px; | 197 | width: 200px; |
202 | flex: 1; | 198 | flex: 1; |
@@ -204,14 +200,17 @@ $menuWidth: 65px; | @@ -204,14 +200,17 @@ $menuWidth: 65px; | ||
204 | flex-direction: column; | 200 | flex-direction: column; |
205 | align-items: center; | 201 | align-items: center; |
206 | } | 202 | } |
203 | + | ||
207 | @include deep() { | 204 | @include deep() { |
208 | .n-menu-item { | 205 | .n-menu-item { |
209 | height: 30px; | 206 | height: 30px; |
207 | + | ||
210 | &.n-menu-item--selected { | 208 | &.n-menu-item--selected { |
211 | &::before { | 209 | &::before { |
212 | background-color: rgba(0, 0, 0, 0); | 210 | background-color: rgba(0, 0, 0, 0); |
213 | } | 211 | } |
214 | } | 212 | } |
213 | + | ||
215 | .n-menu-item-content { | 214 | .n-menu-item-content { |
216 | text-align: center; | 215 | text-align: center; |
217 | padding: 0px 14px !important; | 216 | padding: 0px 14px !important; |