8 changed files with 563 additions and 106 deletions
@ -0,0 +1,336 @@ |
|||||
|
import _ from 'lodash' |
||||
|
import * as THREE from 'three' |
||||
|
import { AxesHelper, GridHelper, OrthographicCamera, Scene, WebGLRenderer } from 'three' |
||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' |
||||
|
import Stats from 'three/examples/jsm/libs/stats.module' |
||||
|
import type WorldModel from '@/designer/WorldModel.ts' |
||||
|
import $ from 'jquery' |
||||
|
import { watch } from 'vue' |
||||
|
|
||||
|
/** |
||||
|
* 编辑器对象 |
||||
|
* 这是非双向绑定的设计器对象,不记录状态,只记录全局使用到的对象,(实体类使用) |
||||
|
*/ |
||||
|
export default class Viewport { |
||||
|
viewerDom: HTMLElement |
||||
|
scene: Scene |
||||
|
camera: OrthographicCamera |
||||
|
renderer: WebGLRenderer |
||||
|
axesHelper: AxesHelper |
||||
|
gridHelper: GridHelper |
||||
|
statsControls: Stats |
||||
|
controls: OrbitControls |
||||
|
worldModel: WorldModel |
||||
|
|
||||
|
/** |
||||
|
* 监听窗口大小变化 |
||||
|
*/ |
||||
|
resizeObserver?: ResizeObserver |
||||
|
|
||||
|
unwatchList: (() => void)[] = [] |
||||
|
|
||||
|
state: ViewportState = { |
||||
|
currentFloor: null, |
||||
|
isReady: false, |
||||
|
cursorMode: 'normal', |
||||
|
camera: { |
||||
|
position: { x: 0, y: 0, z: 0 }, |
||||
|
rotation: { x: 0, y: 0, z: 0 } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
constructor(worldModel: WorldModel) { |
||||
|
this.worldModel = worldModel |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化 THREE 渲染器 |
||||
|
*/ |
||||
|
initThree(viewerDom: HTMLElement) { |
||||
|
this.viewerDom = viewerDom |
||||
|
this.worldModel.registerViewport(this) |
||||
|
|
||||
|
// 场景
|
||||
|
const scene = this.worldModel.getSceneByFloor(this.state.currentFloor) |
||||
|
this.scene = scene |
||||
|
|
||||
|
// 渲染器
|
||||
|
const renderer = new THREE.WebGLRenderer({ |
||||
|
logarithmicDepthBuffer: true, |
||||
|
antialias: true, |
||||
|
alpha: true, |
||||
|
precision: 'mediump', |
||||
|
premultipliedAlpha: true, |
||||
|
preserveDrawingBuffer: false, |
||||
|
powerPreference: 'high-performance' |
||||
|
}) |
||||
|
//@ts-ignore
|
||||
|
renderer.outputEncoding = THREE.SRGBColorSpace |
||||
|
renderer.clearDepth() |
||||
|
renderer.shadowMap.enabled = true |
||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping |
||||
|
renderer.setPixelRatio(window.devicePixelRatio) |
||||
|
renderer.setSize(viewerDom.getBoundingClientRect().width, viewerDom.getBoundingClientRect().height) |
||||
|
viewerDom.appendChild(renderer.domElement) |
||||
|
this.renderer = renderer |
||||
|
|
||||
|
// 创建正交摄像机
|
||||
|
this.initMode2DCamera() |
||||
|
|
||||
|
// 辅助线
|
||||
|
this.axesHelper = new THREE.AxesHelper(3) |
||||
|
this.scene.add(this.axesHelper) |
||||
|
|
||||
|
this.gridHelper = new THREE.GridHelper(500, 500) |
||||
|
const gridHelper = this.gridHelper |
||||
|
gridHelper.material = new THREE.LineBasicMaterial({ |
||||
|
color: 0x888888, |
||||
|
opacity: 0.8, |
||||
|
transparent: true |
||||
|
}) |
||||
|
|
||||
|
scene.add(gridHelper) |
||||
|
|
||||
|
// 光照
|
||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5) |
||||
|
scene.add(ambientLight) |
||||
|
|
||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) |
||||
|
directionalLight.position.set(5, 5, 5).multiplyScalar(3) |
||||
|
directionalLight.castShadow = true |
||||
|
scene.add(directionalLight) |
||||
|
|
||||
|
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1) |
||||
|
scene.add(hemisphereLight) |
||||
|
|
||||
|
// 性能监控
|
||||
|
const statsControls = new Stats() |
||||
|
this.statsControls = statsControls |
||||
|
statsControls.showPanel(0) |
||||
|
statsControls.dom.style.position = 'absolute' |
||||
|
statsControls.dom.style.top = '2px' |
||||
|
statsControls.dom.style.left = '0' |
||||
|
viewerDom.appendChild(statsControls.dom) |
||||
|
$(statsControls.dom).children().css('height', '28px') |
||||
|
|
||||
|
// 创建几何体和材质
|
||||
|
const geometry = new THREE.BoxGeometry(1, 1, 1) |
||||
|
const material = new THREE.MeshStandardMaterial({ |
||||
|
color: 0xcccccc, |
||||
|
metalness: 0.9, |
||||
|
roughness: 0.1 |
||||
|
}) |
||||
|
|
||||
|
const cube = new THREE.Mesh(geometry, material) |
||||
|
scene.add(cube) |
||||
|
this.camera.position.z = 5 |
||||
|
|
||||
|
this.animate() |
||||
|
|
||||
|
const unWatchFn = watch(() => this.state.camera.position.y, (newVal) => { |
||||
|
if (this.state.isReady) { |
||||
|
this.updateGridVisibility() |
||||
|
} |
||||
|
}) |
||||
|
this.unwatchList.push(unWatchFn) |
||||
|
|
||||
|
if (this.resizeObserver) { |
||||
|
this.resizeObserver.unobserve(this.viewerDom) |
||||
|
} |
||||
|
this.resizeObserver = new ResizeObserver(this.handleResize.bind(this)) |
||||
|
this.resizeObserver.observe(this.viewerDom) |
||||
|
|
||||
|
this.state.isReady = true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化2D相机 |
||||
|
*/ |
||||
|
initMode2DCamera() { |
||||
|
if (this.camera) { |
||||
|
this.scene.remove(this.camera) |
||||
|
} |
||||
|
|
||||
|
// ============================ 创建正交相机
|
||||
|
const viewerDom = this.viewerDom |
||||
|
const cameraNew = new THREE.OrthographicCamera( |
||||
|
viewerDom.clientWidth / -2, |
||||
|
viewerDom.clientWidth / 2, |
||||
|
viewerDom.clientHeight / 2, |
||||
|
viewerDom.clientHeight / -2, |
||||
|
1, |
||||
|
500 |
||||
|
) |
||||
|
cameraNew.position.set(0, 100, 0) |
||||
|
cameraNew.lookAt(0, 0, 0) |
||||
|
cameraNew.zoom = 30 |
||||
|
this.camera = cameraNew |
||||
|
this.scene.add(this.camera) |
||||
|
|
||||
|
// ============================ 创建控制器
|
||||
|
const controlsNew = new OrbitControls( |
||||
|
this.camera, |
||||
|
this.renderer.domElement |
||||
|
) |
||||
|
controlsNew.enableDamping = false |
||||
|
controlsNew.enableZoom = true |
||||
|
controlsNew.enableRotate = false |
||||
|
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN } // 鼠标中键平移
|
||||
|
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动
|
||||
|
controlsNew.listenToKeyEvents(viewerDom) // 监听键盘事件
|
||||
|
controlsNew.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' } |
||||
|
controlsNew.panSpeed = 1 |
||||
|
controlsNew.keyPanSpeed = 20 // normal 7
|
||||
|
controlsNew.minDistance = 0.1 |
||||
|
controlsNew.maxDistance = 1000 |
||||
|
this.controls = controlsNew |
||||
|
controlsNew.addEventListener('change', this.syncCameraState.bind(this)) |
||||
|
|
||||
|
this.camera.updateProjectionMatrix() |
||||
|
|
||||
|
this.syncCameraState() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 动画循环 |
||||
|
*/ |
||||
|
animate() { |
||||
|
requestAnimationFrame(this.animate.bind(this)) |
||||
|
this.renderView() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 渲染视图 |
||||
|
*/ |
||||
|
renderView() { |
||||
|
this.statsControls?.update() |
||||
|
this.renderer?.render(this.scene, this.camera) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 同步相机状态到全局状态 |
||||
|
*/ |
||||
|
syncCameraState() { |
||||
|
if (this.camera) { |
||||
|
const camera = this.camera |
||||
|
|
||||
|
this.state.camera.position.x = camera.position.x |
||||
|
this.state.camera.position.y = this.getEffectiveViewDistance() |
||||
|
this.state.camera.position.z = camera.position.z |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算相机到目标的有效视距 |
||||
|
*/ |
||||
|
getEffectiveViewDistance() { |
||||
|
if (!this.camera) { |
||||
|
return 10 |
||||
|
} |
||||
|
const camera = this.camera |
||||
|
const viewHeight = (camera.top - camera.bottom) / camera.zoom |
||||
|
// 假设我们希望匹配一个虚拟的透视相机(通常使用45度fov作为参考)
|
||||
|
const referenceFOV = 45 // 参考视场角
|
||||
|
return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2)) |
||||
|
} |
||||
|
|
||||
|
handleResize(entries: any) { |
||||
|
for (let entry of entries) { |
||||
|
// entry.contentRect包含了元素的尺寸信息
|
||||
|
console.log('Element size changed:', entry.contentRect) |
||||
|
|
||||
|
const width = entry.contentRect.width |
||||
|
const height = entry.contentRect.height |
||||
|
|
||||
|
if (this.camera instanceof THREE.PerspectiveCamera) { |
||||
|
this.camera.aspect = width / height |
||||
|
this.camera.updateProjectionMatrix() |
||||
|
|
||||
|
} else if (this.camera instanceof THREE.OrthographicCamera) { |
||||
|
this.camera.left = width / -2 |
||||
|
this.camera.right = width / 2 |
||||
|
this.camera.top = height / 2 |
||||
|
this.camera.bottom = height / -2 |
||||
|
this.camera.updateProjectionMatrix() |
||||
|
} |
||||
|
|
||||
|
this.renderer.setSize(width, height) |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据可视化范围更新网格的透明度 |
||||
|
*/ |
||||
|
updateGridVisibility() { |
||||
|
const cameraDistance = this.state.camera.position.y |
||||
|
const maxVisibleDistance = 60 // 网格完全可见的最大距离
|
||||
|
const fadeStartDistance = 15 // 开始淡出的距离
|
||||
|
|
||||
|
// 计算透明度(0~1)
|
||||
|
let opacity = 1 |
||||
|
if (cameraDistance > fadeStartDistance) { |
||||
|
opacity = 1 - Math.min((cameraDistance - fadeStartDistance) / (maxVisibleDistance - fadeStartDistance), 1) |
||||
|
} |
||||
|
|
||||
|
// 修改网格材质透明度
|
||||
|
this.gridHelper.material.opacity = opacity |
||||
|
this.gridHelper.visible = opacity > 0 |
||||
|
console.log('opacity', opacity) |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
this.state.isReady = false |
||||
|
|
||||
|
if (this.unwatchList) { |
||||
|
_.forEach(this.unwatchList, (unWatchFn => { |
||||
|
unWatchFn() |
||||
|
})) |
||||
|
this.unwatchList = [] |
||||
|
} |
||||
|
|
||||
|
if (this.resizeObserver) { |
||||
|
this.resizeObserver.unobserve(this.viewerDom) |
||||
|
this.resizeObserver.disconnect() |
||||
|
this.resizeObserver = undefined |
||||
|
} |
||||
|
|
||||
|
this.worldModel.unregisterViewport(this) |
||||
|
|
||||
|
if (this.statsControls) { |
||||
|
this.statsControls.dom.remove() |
||||
|
} |
||||
|
|
||||
|
if (this.renderer) { |
||||
|
this.renderer.dispose() |
||||
|
this.renderer.forceContextLoss() |
||||
|
console.log('WebGL disposed, memory:', this.renderer.info.memory) |
||||
|
this.renderer.domElement = null |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export interface ViewportState { |
||||
|
/** |
||||
|
* 当前楼层 |
||||
|
*/ |
||||
|
currentFloor: string |
||||
|
|
||||
|
/** |
||||
|
* 是否准备完成 |
||||
|
*/ |
||||
|
isReady: boolean |
||||
|
|
||||
|
/** |
||||
|
* 鼠标模式 |
||||
|
*/ |
||||
|
cursorMode: 'normal' | 'ALink' | 'SLink' | 'PointCallback' | 'PointAdd' | 'LinkAdd' | 'LinkAdd2' | 'Ruler' | 'selectByRec', |
||||
|
|
||||
|
/** |
||||
|
* 相机状态 |
||||
|
*/ |
||||
|
camera: { |
||||
|
position: { x: number, y: number, z: number }, |
||||
|
rotation: { x: number, y: number, z: number } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
import _ from 'lodash' |
||||
|
import Example1 from './example1' |
||||
|
import { markRaw, reactive } from 'vue' |
||||
|
import { Scene } from 'three' |
||||
|
import type Viewport from '@/designer/Viewport.ts' |
||||
|
import * as THREE from 'three' |
||||
|
|
||||
|
/** |
||||
|
* 世界模型 |
||||
|
*/ |
||||
|
export default class WorldModel { |
||||
|
data: any = null |
||||
|
allLevels: any = null |
||||
|
sceneMap = new Map<string, Scene>() |
||||
|
viewPorts: Viewport[] = [] |
||||
|
|
||||
|
constructor() { |
||||
|
this.init() |
||||
|
this.open() |
||||
|
} |
||||
|
|
||||
|
init() { |
||||
|
window['worldModel'] = this |
||||
|
} |
||||
|
|
||||
|
open() { |
||||
|
if (this.sceneMap.size > 0) { |
||||
|
// 释放旧场景
|
||||
|
this.sceneMap.forEach((scene: Scene) => { |
||||
|
this.sceneDispose(scene) |
||||
|
}) |
||||
|
} |
||||
|
if (this.viewPorts.length > 0) { |
||||
|
// 注销视口
|
||||
|
this.viewPorts.forEach((viewport: Viewport) => { |
||||
|
this.unregisterViewport(viewport) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
system.msg('打开世界地图完成') |
||||
|
this.data = markRaw(Example1) |
||||
|
this.allLevels = reactive(this.data.allLevels) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取当前楼层的场景, 如果没有则创建一个新的场景 |
||||
|
* @param floor |
||||
|
*/ |
||||
|
getSceneByFloor(floor: string) { |
||||
|
if (this.sceneMap.has(floor)) { |
||||
|
return this.sceneMap.get(floor) |
||||
|
} else { |
||||
|
const scene = this.createScene(floor) |
||||
|
|
||||
|
this.sceneMap.set(floor, scene) |
||||
|
return scene |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建一个新的场景 |
||||
|
*/ |
||||
|
createScene(floor: string) { |
||||
|
const scene = new Scene() |
||||
|
scene.background = new THREE.Color(0xeeeeee) |
||||
|
return scene |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 注册视口 |
||||
|
*/ |
||||
|
registerViewport(viewport: Viewport) { |
||||
|
this.viewPorts = this.viewPorts || [] |
||||
|
this.viewPorts.push(viewport) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 注销视口 |
||||
|
*/ |
||||
|
unregisterViewport(viewport: Viewport) { |
||||
|
const index = this.viewPorts.indexOf(viewport) |
||||
|
if (index > -1) { |
||||
|
this.viewPorts.splice(index, 1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 销毁场景, 释放全部 WebGL 资源 |
||||
|
*/ |
||||
|
sceneDispose(scene: Scene = null) { |
||||
|
// 移除旧模型
|
||||
|
if (!scene) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
scene.traverse((obj: any) => { |
||||
|
// 释放几何体
|
||||
|
if (obj.geometry) { |
||||
|
obj.geometry.dispose() |
||||
|
} |
||||
|
|
||||
|
// 释放材质
|
||||
|
if (obj.material) { |
||||
|
if (Array.isArray(obj.material)) { |
||||
|
obj.material.forEach(m => m.dispose()) |
||||
|
} else { |
||||
|
obj.material.dispose() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 释放纹理
|
||||
|
if (obj.texture) { |
||||
|
obj.texture.dispose() |
||||
|
} |
||||
|
|
||||
|
// 释放渲染目标
|
||||
|
if (obj.renderTarget) { |
||||
|
obj.renderTarget.dispose() |
||||
|
} |
||||
|
|
||||
|
// 移除事件监听(如 OrbitControls)
|
||||
|
if (obj.dispose) { |
||||
|
obj.dispose() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// 清空场景
|
||||
|
scene.children = [] |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
import { renderIcon } from '@/utils/webutils.ts' |
||||
|
import { defineComponent, markRaw } from 'vue' |
||||
|
import Viewport from '@/designer/Viewport.ts' |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'Model2DEditor', |
||||
|
data() { |
||||
|
const viewport = new Viewport(worldModel) |
||||
|
|
||||
|
return { |
||||
|
viewport: viewport, |
||||
|
view: { |
||||
|
searchKeyword: '' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
window['editor'] = this |
||||
|
}, |
||||
|
beforeMount() { |
||||
|
this.initByFloor('') |
||||
|
delete window['editor'] |
||||
|
}, |
||||
|
methods: { |
||||
|
renderIcon, |
||||
|
initByFloor(floor) { |
||||
|
const viewportOrigin = this.viewport |
||||
|
if (viewportOrigin && viewportOrigin.state.isReady) { |
||||
|
viewportOrigin.destroy() |
||||
|
this.viewport = null |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
delete window['editor'] |
||||
|
delete window['viewport'] |
||||
|
delete window['scene'] |
||||
|
delete window['renderer'] |
||||
|
delete window['camera'] |
||||
|
delete window['renderer'] |
||||
|
delete window['controls'] |
||||
|
|
||||
|
if (!floor) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const viewerDom = this.$refs.canvasContainer |
||||
|
const viewport = markRaw(new Viewport(worldModel)) |
||||
|
this.viewport = viewport |
||||
|
|
||||
|
viewport.initThree(viewerDom) |
||||
|
|
||||
|
window['viewport'] = viewport |
||||
|
window['scene'] = viewport.scene |
||||
|
window['renderer'] = viewport.renderer |
||||
|
window['camera'] = viewport.camera |
||||
|
window['renderer'] = viewport.renderer |
||||
|
window['controls'] = viewport.controls |
||||
|
|
||||
|
viewerDom?.focus() |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
'state.currentFloor'(newVal, oldVal) { |
||||
|
this.initByFloor(newVal) |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
/** |
||||
|
* @returns {ViewportState|{}} |
||||
|
*/ |
||||
|
state() { |
||||
|
return this.viewport?.state || {} |
||||
|
}, |
||||
|
allLevels() { |
||||
|
return worldModel.allLevels |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
@ -1,40 +0,0 @@ |
|||||
import { renderIcon } from '@/utils/webutils.ts' |
|
||||
import { defineComponent } from 'vue' |
|
||||
import ThreeJsEditor from './ThreeJsEditor.vue' |
|
||||
|
|
||||
export default defineComponent({ |
|
||||
name: 'Model2DEditor', |
|
||||
components: { ThreeJsEditor }, |
|
||||
data() { |
|
||||
return { |
|
||||
oper: { |
|
||||
currentMode: 'normal' |
|
||||
}, |
|
||||
view: { |
|
||||
currentLevel: '', |
|
||||
searchKeyword: '' |
|
||||
} |
|
||||
} as IData |
|
||||
}, |
|
||||
methods: { |
|
||||
renderIcon |
|
||||
}, |
|
||||
computed: { |
|
||||
editorState() { |
|
||||
return designer.editorState |
|
||||
}, |
|
||||
allLevels() { |
|
||||
return designer.allLevels |
|
||||
} |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
export interface IData { |
|
||||
oper: { |
|
||||
currentMode: 'normal' | 'ALink' | 'SLink' | 'PointCallback' | 'PointAdd' | 'LinkAdd' | 'LinkAdd2' | 'Ruler' | 'selectByRec', |
|
||||
}, |
|
||||
view: { |
|
||||
currentLevel: string, |
|
||||
searchKeyword: string, |
|
||||
}, |
|
||||
} |
|
||||
@ -1,49 +0,0 @@ |
|||||
<template> |
|
||||
<div class="canvas-container" ref="canvasContainer" tabindex="1" /> |
|
||||
</template> |
|
||||
<script setup lang="ts"> |
|
||||
import { getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref } from 'vue' |
|
||||
import Viewport from '@/designer/Viewport.ts' |
|
||||
|
|
||||
// DOM refs |
|
||||
const canvasContainer = ref<HTMLElement>(null) |
|
||||
|
|
||||
// Three.js 场景相关 |
|
||||
let viewport: Viewport |
|
||||
|
|
||||
onMounted(() => { |
|
||||
const instance = getCurrentInstance() |
|
||||
nextTick(() => { |
|
||||
const viewerDom = canvasContainer.value |
|
||||
if (!viewerDom) { |
|
||||
system.showErrorDialog('viewerDom is null') |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
viewport = new Viewport(worldModel) |
|
||||
viewport.initThree(viewerDom) |
|
||||
|
|
||||
window['editor'] = instance |
|
||||
window['viewport'] = viewport |
|
||||
window['scene'] = viewport.scene |
|
||||
window['renderer'] = viewport.renderer |
|
||||
window['camera'] = viewport.camera |
|
||||
window['renderer'] = viewport.renderer |
|
||||
window['controls'] = viewport.controls |
|
||||
|
|
||||
viewerDom?.focus() |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
onBeforeUnmount(() => { |
|
||||
viewport.destroy() |
|
||||
|
|
||||
delete window['editor'] |
|
||||
delete window['viewport'] |
|
||||
delete window['scene'] |
|
||||
delete window['renderer'] |
|
||||
delete window['camera'] |
|
||||
delete window['renderer'] |
|
||||
delete window['controls'] |
|
||||
}) |
|
||||
</script> |
|
||||
Loading…
Reference in new issue