Commit f94a8090d164b846bea76b6fe0c393a48619aaa3
Committed by
xp.Huang
1 parent
78610649
perf: 优化3d模型加载
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 | 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 | ... | ... |
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 | 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; | ... | ... |