Browse Source

WorldMode / Viewport / editor 设计模式重构

master
修宁 7 months ago
parent
commit
94dd13f284
  1. 5
      package.json
  2. 29
      pnpm-lock.yaml
  3. 35
      src/designer/Designer.ts
  4. 325
      src/designer/model2DEditor/ThreeJsEditor.vue
  5. 16
      src/designer/model2DEditor/tools/MouseMoveInspect.ts
  6. 4
      src/designer/model3DView/Model3DView.vue
  7. 5
      src/types/global.d.ts

5
package.json

@ -30,7 +30,6 @@
"pinia": "^3.0.1", "pinia": "^3.0.1",
"sortablejs": "1.15.6", "sortablejs": "1.15.6",
"split.js": "^1.6.4", "split.js": "^1.6.4",
"three": "^0.176.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@ -55,6 +54,8 @@
"typescript": "~5.8.0", "typescript": "~5.8.0",
"vite": "^6.2.4", "vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2", "vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8",
"three": "^0.176.0",
"camera-controls": "2.10.1"
} }
} }

29
pnpm-lock.yaml

@ -62,9 +62,6 @@ importers:
split.js: split.js:
specifier: ^1.6.4 specifier: ^1.6.4
version: 1.6.5 version: 1.6.5
three:
specifier: ^0.176.0
version: 0.176.0
vue: vue:
specifier: ^3.5.13 specifier: ^3.5.13
version: 3.5.14(typescript@5.8.3) version: 3.5.14(typescript@5.8.3)
@ -114,6 +111,9 @@ importers:
'@vue/tsconfig': '@vue/tsconfig':
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0(typescript@5.8.3)(vue@3.5.14(typescript@5.8.3)) version: 0.7.0(typescript@5.8.3)(vue@3.5.14(typescript@5.8.3))
camera-controls:
specifier: 2.10.1
version: 2.10.1(three@0.176.0)
npm-run-all2: npm-run-all2:
specifier: ^7.0.2 specifier: ^7.0.2
version: 7.0.2 version: 7.0.2
@ -123,6 +123,9 @@ importers:
rimraf: rimraf:
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1 version: 6.0.1
three:
specifier: ^0.176.0
version: 0.176.0
typescript: typescript:
specifier: ~5.8.0 specifier: ~5.8.0
version: 5.8.3 version: 5.8.3
@ -543,67 +546,56 @@ packages:
resolution: {integrity: sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==} resolution: {integrity: sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.41.0': '@rollup/rollup-linux-arm-musleabihf@4.41.0':
resolution: {integrity: sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==} resolution: {integrity: sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.41.0': '@rollup/rollup-linux-arm64-gnu@4.41.0':
resolution: {integrity: sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==} resolution: {integrity: sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.41.0': '@rollup/rollup-linux-arm64-musl@4.41.0':
resolution: {integrity: sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==} resolution: {integrity: sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.41.0': '@rollup/rollup-linux-loongarch64-gnu@4.41.0':
resolution: {integrity: sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==} resolution: {integrity: sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.41.0': '@rollup/rollup-linux-powerpc64le-gnu@4.41.0':
resolution: {integrity: sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==} resolution: {integrity: sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.41.0': '@rollup/rollup-linux-riscv64-gnu@4.41.0':
resolution: {integrity: sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==} resolution: {integrity: sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.41.0': '@rollup/rollup-linux-riscv64-musl@4.41.0':
resolution: {integrity: sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==} resolution: {integrity: sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.41.0': '@rollup/rollup-linux-s390x-gnu@4.41.0':
resolution: {integrity: sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==} resolution: {integrity: sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.41.0': '@rollup/rollup-linux-x64-gnu@4.41.0':
resolution: {integrity: sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==} resolution: {integrity: sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.41.0': '@rollup/rollup-linux-x64-musl@4.41.0':
resolution: {integrity: sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==} resolution: {integrity: sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.41.0': '@rollup/rollup-win32-arm64-msvc@4.41.0':
resolution: {integrity: sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==} resolution: {integrity: sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==}
@ -866,6 +858,11 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
camera-controls@2.10.1:
resolution: {integrity: sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==}
peerDependencies:
three: '>=0.126.1'
caniuse-lite@1.0.30001718: caniuse-lite@1.0.30001718:
resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==}
@ -2384,6 +2381,10 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
function-bind: 1.1.2 function-bind: 1.1.2
camera-controls@2.10.1(three@0.176.0):
dependencies:
three: 0.176.0
caniuse-lite@1.0.30001718: {} caniuse-lite@1.0.30001718: {}
codemirror@5.65.19: {} codemirror@5.65.19: {}

35
src/designer/Designer.ts

@ -1,35 +0,0 @@
import Example1 from './example1'
import { markRaw, reactive } from 'vue'
/**
*
*/
export default class Designer {
data: any = null
allLevels: any = null
currentFloor: string = null
editorState = reactive({
ready: false,
camera: {
position: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 }
}
})
constructor() {
this.init()
this.open()
}
init() {
window['designer'] = this
}
open() {
system.msg('打开成功')
this.data = markRaw(Example1)
this.allLevels = reactive(this.data.allLevels)
}
}

325
src/designer/model2DEditor/ThreeJsEditor.vue

@ -2,333 +2,48 @@
<div class="canvas-container" ref="canvasContainer" tabindex="1" /> <div class="canvas-container" ref="canvasContainer" tabindex="1" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as THREE from 'three' import { getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import $ from 'jquery' import Viewport from '@/designer/Viewport.ts'
import Stats from 'three/examples/jsm/libs/stats.module'
import {
ref,
onMounted,
nextTick,
reactive,
watch,
getCurrentInstance,
onUnmounted,
onBeforeUnmount,
defineExpose
} from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// DOM refs // DOM refs
const canvasContainer = ref(null) const canvasContainer = ref<HTMLElement>(null)
//
let resizeObserver
// Three.js // Three.js
let scene, camera, renderer, controls let viewport: Viewport
let statsControls, axesHelper, gridHelper
const state = designer.editorState
function initMode2DCamera() {
if (camera) {
scene.remove(camera)
}
// ============================
const viewerDom = canvasContainer.value
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
camera = cameraNew
scene.add(camera)
// ============================
const controlsNew = new OrbitControls(
camera,
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
controls = controlsNew
controlsNew.addEventListener('change', syncCameraState)
camera.updateProjectionMatrix()
syncCameraState()
}
function initThree() {
const viewerDom = canvasContainer.value
//
scene = new THREE.Scene()
scene.background = new THREE.Color(0xeeeeee)
//
renderer = new THREE.WebGLRenderer({
logarithmicDepthBuffer: true,
antialias: true,
alpha: true,
precision: 'mediump',
premultipliedAlpha: true,
preserveDrawingBuffer: false,
powerPreference: 'high-performance'
})
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)
//
initMode2DCamera()
// 线
axesHelper = new THREE.AxesHelper(3)
scene.add(axesHelper)
gridHelper = new THREE.GridHelper(500, 500)
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)
//
statsControls = new Stats()
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)
camera.position.z = 5
animate()
}
//
function animate() {
requestAnimationFrame(animate)
renderView()
}
function renderView() {
statsControls?.update()
renderer?.render(scene, camera)
}
onMounted(() => { onMounted(() => {
const editor = getCurrentInstance() const instance = getCurrentInstance()
nextTick(() => { nextTick(() => {
initThree()
const viewerDom = canvasContainer.value const viewerDom = canvasContainer.value
if (resizeObserver) { if (!viewerDom) {
resizeObserver.unobserve(viewerDom) system.showErrorDialog('viewerDom is null')
return
} }
resizeObserver = new ResizeObserver(handleResize)
resizeObserver.observe(viewerDom)
state.ready = true viewport = new Viewport(worldModel)
viewport.initThree(viewerDom)
window['editor'] = editor window['editor'] = instance
window['renderer'] = renderer window['viewport'] = viewport
window['scene'] = scene window['scene'] = viewport.scene
window['camera'] = camera window['renderer'] = viewport.renderer
window['renderer'] = renderer window['camera'] = viewport.camera
window['controls'] = controls window['renderer'] = viewport.renderer
window['controls'] = viewport.controls
viewerDom?.focus() viewerDom?.focus()
}) })
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
state.ready = false viewport.destroy()
cleanupThree()
const viewerDom = canvasContainer.value
if (resizeObserver) {
resizeObserver.unobserve(viewerDom)
}
delete window['editor'] delete window['editor']
delete window['renderer'] delete window['viewport']
delete window['scene'] delete window['scene']
delete window['renderer']
delete window['camera'] delete window['camera']
delete window['renderer'] delete window['renderer']
delete window['controls'] delete window['controls']
}) })
function handleResize(entries) {
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 (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = width / height
camera.updateProjectionMatrix()
} else if (camera instanceof THREE.OrthographicCamera) {
camera.left = width / -2
camera.right = width / 2
camera.top = height / 2
camera.bottom = height / -2
camera.updateProjectionMatrix()
}
renderer.setSize(width, height)
break
}
}
watch(() => state.camera.position.y, (newVal) => {
if (state.ready) {
updateGridVisibility()
}
}, { deep: true })
function cleanupThree() {
//
if (scene) {
scene.traverse((obj) => {
//
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 = []
}
if (statsControls) {
statsControls.dom.remove()
}
if (renderer) {
renderer.dispose()
renderer.forceContextLoss()
console.log('WebGL disposed, memory:', renderer.info.memory)
renderer.domElement = null
}
}
/**
* 根据可视化范围更新网格的透明度
*/
function updateGridVisibility() {
const cameraDistance = 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)
}
//
gridHelper.material.opacity = opacity
gridHelper.visible = opacity > 0
console.log('opacity', opacity)
}
/**
* 计算相机到目标的有效视距
*/
function getEffectiveViewDistance() {
if (!camera) {
return 10
}
const viewHeight = (camera.top - camera.bottom) / camera.zoom
// 使45fov
const referenceFOV = 45 //
return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2))
}
/**
* 同步相机状态到全局状态
*/
function syncCameraState() {
if (camera) {
state.camera.position.x = camera.position.x
state.camera.position.y = getEffectiveViewDistance()
state.camera.position.z = camera.position.z
}
}
</script> </script>

16
src/designer/model2DEditor/tools/MouseMoveInspect.ts

@ -0,0 +1,16 @@
import Editor from '@/designer/Editor.js'
import type Viewport from '@/designer/Viewport.ts'
/**
* designer.mousePos
*/
export default class MouseMoveInspect {
viewport: Viewport
constructor(viewport: Viewport) {
this.viewport = viewport
}
mouseMove(event) {
}
}

4
src/designer/model3DView/Model3DView.vue

@ -322,11 +322,9 @@ function initMode3DCamera() {
const viewerDom = canvasContainer.value const viewerDom = canvasContainer.value
// // ============================
const cameraNew = new THREE.PerspectiveCamera(25, viewerDom.clientWidth / viewerDom.clientHeight, 0.1, 2000) const cameraNew = new THREE.PerspectiveCamera(25, viewerDom.clientWidth / viewerDom.clientHeight, 0.1, 2000)
//
cameraNew.position.set(5, 5, 5) cameraNew.position.set(5, 5, 5)
//
cameraNew.lookAt(0, 0, 0) cameraNew.lookAt(0, 0, 0)
camera = cameraNew camera = cameraNew

5
src/types/global.d.ts

@ -1,11 +1,12 @@
import _ from 'lodash' import _ from 'lodash'
import $ from 'jquery' import $ from 'jquery'
import type System from '@/runtime/System' import type System from '@/runtime/System'
import type Designer from '@/designer/Designer' import type WorldModel from '@/designer/WorldModel'
declare global { declare global {
const $: $ const $: $
const _: _ const _: _
const system: System const system: System
const designer: Designer
const worldModel: WorldModel
} }
Loading…
Cancel
Save