import _ from 'lodash' import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' import Stats from 'three/examples/jsm/libs/stats.module' import type WorldModel from '../manager/WorldModel' import $ from 'jquery' import { markRaw, reactive, watch } from 'vue' import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer' import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' import SceneHelp from './SceneHelp' import SelectManager from '../manager/SelectManager' import MouseMoveManager from '../manager/MouseMoveManager' import EntityManager from '../manager/EntityManager' import InteractionManager from '@/core/manager/InteractionManager' import { calcPositionUseSnap } from '@/core/ModelUtils' import StateManager from '@/core/manager/StateManager.ts' import Constract from '@/core/Constract.ts' import DragManager from '@/core/manager/DragManager.ts' import type { PropertySetter } from '@/core/base/PropertyTypes.ts' import LabelManager from '@/core/manager/LabelManager.ts' import type LineSegmentManager from '@/core/manager/LineSegmentManager.ts' import type { Object3DLike } from '@/types/ModelTypes.ts' import type InstanceMeshManager from '@/core/manager/InstanceMeshManager.ts' import ItemFindManager from '@/core/manager/ItemFindManager.ts' import { MapControls } from 'three/examples/jsm/controls/MapControls' import ModelManager from '@/core/script/ModelManager.ts' import RuntimeManager from '@/core/manager/RuntimeManager.ts' import EnvManager from '@/core/manager/EnvManager.ts' /** * 视窗对象 * 所有状态管理器,场景,控制器,摄像机,实体管理器, 都在这里可以取到 */ export default class Viewport { viewerDom: HTMLElement camera: THREE.Camera // THREE.OrthographicCamera renderer: THREE.WebGLRenderer statsControls: Stats controls: OrbitControls raycaster: THREE.Raycaster animationFrameId: any = null scene: SceneHelp selectManager = new SelectManager() mouseMoveManager = new MouseMoveManager() dragManager = new DragManager() labelManager = new LabelManager() entityManager = new EntityManager() itemFindManager = new ItemFindManager() interactionManager = new InteractionManager() modelManager = new ModelManager() runtimeManager = new RuntimeManager() envManager = new EnvManager() // 状态管理器 stateManager: StateManager tools: IControls[] = [ markRaw(this.selectManager), markRaw(this.mouseMoveManager), markRaw(this.dragManager), markRaw(this.labelManager), markRaw(this.entityManager), markRaw(this.itemFindManager), markRaw(this.interactionManager), markRaw(this.modelManager), markRaw(this.runtimeManager), markRaw(this.envManager) ] // 对象实例管理器 moduleName -> InstanceMeshManager meshManager: Map = new Map() // 线段实例管理器 moduleName -> LineSegmentManager lineSegmentManagerMap: Map = new Map() // 监听窗口大小变化 resizeObserver?: ResizeObserver // vue 的 watcher 管理器, 卸载时需要调用 watchList: (() => void)[] = [] css2DRenderer: CSS2DRenderer = new CSS2DRenderer() css3DRenderer: CSS3DRenderer = new CSS3DRenderer() //@ts-ignore state: ViewportState = reactive({ isReady: false, isUpdating: false, cursorMode: 'normal', selectedObject: undefined, selectedItem: undefined, selectedEntityId: undefined, selectedObjectSetter: undefined, multiSelectedObjects: [], multiSelectedItems: [], multiSelectedEntityIds: [], view3DMode: Constract.Mode2D, camera: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } }, mouse: { x: 0, y: 0 } }) constructor(sceneHelp: SceneHelp, viewerDom: HTMLElement, stateManager) { this.scene = sceneHelp this.viewerDom = viewerDom this.stateManager = stateManager } /** * 根据实体 ID 查找实体 * @param entityId */ find(entityId: string): ItemJson { return this.entityManager.findItemById(entityId) } /** * 获取或创建形状管理器 * @param typeName 点类型名称 * @param createFn 创建点管理器的函数 */ getOrCreateMeshManager(typeName: string, createFn: () => InstanceMeshManager): InstanceMeshManager { let meshManager = this.meshManager.get(typeName) if (!meshManager) { meshManager = createFn() if (meshManager) { this.meshManager.set(typeName, meshManager) } } return meshManager } /** * 获取或创建线段管理器 * @param typeName 线段类型名称 * @param createFn 创建线段管理器的函数 */ getOrCreateLineManager(typeName: string, createFn: () => LineSegmentManager): LineSegmentManager { let lineSegmentManager = this.lineSegmentManagerMap.get(typeName) if (!lineSegmentManager) { lineSegmentManager = createFn() if (lineSegmentManager) { this.lineSegmentManagerMap.set(typeName, lineSegmentManager) } } return lineSegmentManager } /** * 初始化 THREE 渲染器 */ async initThree(option: InitThreeOption) { console.log('viewport on catelogCode: ' + this.scene.catalogCode) const viewerDom = this.viewerDom // 状态管理器初始化 this.stateManager.init(this) // 渲染器 const renderer = new THREE.WebGLRenderer({ logarithmicDepthBuffer: true, antialias: true, alpha: true, precision: 'mediump', premultipliedAlpha: true, preserveDrawingBuffer: false, powerPreference: 'high-performance' }) renderer.debug.checkShaderErrors = true //@ts-ignore renderer.outputEncoding = THREE.SRGBColorSpace renderer.clearDepth() renderer.shadowMap.enabled = true renderer.toneMapping = THREE.ACESFilmicToneMapping renderer.setPixelRatio(Math.max(Math.ceil(window.devicePixelRatio), 1)) renderer.setViewport(0, 0, this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) renderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) viewerDom.appendChild(renderer.domElement) renderer.domElement.style.touchAction = 'none' // 防止重复添加 if (this.css2DRenderer.domElement.parentNode !== this.viewerDom) { this.css2DRenderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) this.css2DRenderer.domElement.setAttribute('id', 'astral-3d-preview-css2DRenderer') this.css2DRenderer.domElement.style.position = 'absolute' this.css2DRenderer.domElement.style.top = '0px' this.css2DRenderer.domElement.style.pointerEvents = 'none' this.viewerDom.appendChild(this.css2DRenderer.domElement) } // 防止重复添加 if (this.css3DRenderer.domElement.parentNode !== this.viewerDom) { this.css3DRenderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) this.css3DRenderer.domElement.setAttribute('id', 'astral-3d-preview-css3DRenderer') this.css3DRenderer.domElement.style.position = 'absolute' this.css3DRenderer.domElement.style.top = '0px' this.css3DRenderer.domElement.style.pointerEvents = 'none' this.viewerDom.appendChild(this.css3DRenderer.domElement) } this.renderer = renderer // 性能监控 const statsControls = new Stats() this.statsControls = statsControls statsControls.showPanel(0) statsControls.dom.style.position = 'absolute' statsControls.dom.style.top = '0' statsControls.dom.style.left = '0' statsControls.dom.style.zIndex = '1' viewerDom.parentElement.parentElement.appendChild(statsControls.dom) $(statsControls.dom).children().css('height', '28px') // 监听事件 this.watchList.push(watch(() => this.state.camera.position.y, (newVal) => { if (!this.state.isReady) { return } this.updateGridVisibility() })) // 监听窗口大小变化 if (this.resizeObserver) { this.resizeObserver.unobserve(this.viewerDom) } this.resizeObserver = new ResizeObserver(this.handleResize.bind(this)) this.resizeObserver.observe(this.viewerDom) // 初始化射线投射器 this.raycaster = new THREE.Raycaster() // 初始化所有常驻工具 for (const tool of this.tools) { tool.init(this) } // 创建正交摄像机 // this.initMode2DCamera() this.watchList.push(watch(() => this.state.view3DMode, (newVal) => { if (newVal === Constract.Mode3D) { this.initMode3DCamera() } else { this.initMode2DCamera() } }, { immediate: true })) this.animate() window['viewport'] = this window['stateManager'] = this.stateManager window['entityManager'] = this.entityManager window['renderer'] = this.renderer window['camera'] = this.camera window['renderer'] = this.renderer window['controls'] = this.controls this.state.isReady = true } /** * 初始化3D相机 */ initMode3DCamera() { if (this.camera) { this.scene.remove(this.camera) } if (this.controls) { this.controls.dispose() this.controls = null } // ============================ 创建透视相机 const viewerDom = this.viewerDom const cameraNew = new THREE.PerspectiveCamera( 25, viewerDom.clientWidth / viewerDom.clientHeight, 1, 2000 ) cameraNew.position.set(4, 2, -3) cameraNew.lookAt(0, 0, 0) this.camera = cameraNew this.scene.add(this.camera) // ============================ 创建控制器 const controls = new OrbitControls( this.camera, this.renderer?.domElement ) controls.enableDamping = false controls.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 controls.minDistance = 2 controls.maxDistance = 1000 controls.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // 下面这句话非常影响性能 // this.controls.addEventListener('change', ()=>{ // this.renderer.render(this.scene, this.camera); // }); this.controls = controls window['camera'] = this.camera window['controls'] = this.controls } /** * 初始化2D相机 */ initMode2DCamera() { if (this.camera) { this.scene.remove(this.camera) } if (this.controls) { this.controls.dispose() this.controls = null } // ============================ 创建2D正交相机 // 模拟俯视2D的模式, 操作也用2D模式 const viewerDom = this.viewerDom const cameraNew = new THREE.OrthographicCamera( viewerDom.clientWidth / -2, viewerDom.clientWidth / 2, viewerDom.clientHeight / 2, viewerDom.clientHeight / -2, 1, 500 ) this.camera = cameraNew this.scene.add(this.camera) // ============================ 创建控制器 const controlsNew = new MapControls(this.camera, this.renderer.domElement) controlsNew.enableRotate = false // 禁止旋转 controlsNew.enableZoom = true // 启用缩放 // controlsNew.zoomSpeed = 0.5 // 调整缩放速度 // controlsNew.panSpeed = 0.5 // 调整平移速度 controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN } controlsNew.addEventListener('change', () => { this.syncCameraState() }) this.controls = controlsNew window['camera'] = this.camera window['controls'] = this.controls if (this.camera instanceof THREE.OrthographicCamera) { this.camera.position.set(0, 100, 0) this.camera.lookAt(0, 0, 0) this.camera.zoom = 34 this.camera.updateProjectionMatrix() } this.syncCameraState() } offset = 0 /** * 动画循环 */ animate() { this.animationFrameId = requestAnimationFrame(this.animate.bind(this)) for (const lineSegmentManager of this.lineSegmentManagerMap.values()) { if (lineSegmentManager.needsUpdate) { lineSegmentManager.updateGeometry() } } this.statsControls?.update() this.renderer?.render(this.scene.scene, this.camera) this.css2DRenderer.render(this.scene.scene, this.camera) this.css3DRenderer.render(this.scene.scene, this.camera) // if (window['lineMaterial']) { // this.offset -= 0.002 // window['lineMaterial'].dashOffset = this.offset // } } /** * 同步相机状态到全局状态 */ syncCameraState() { if (this.camera) { const camera = this.camera if (this.camera instanceof THREE.OrthographicCamera) { this.state.camera.position.x = camera.position.x this.state.camera.position.y = (camera as THREE.OrthographicCamera).zoom // this.getEffectiveViewDistance() this.state.camera.position.z = camera.position.z } else { this.state.camera.position.x = camera.position.x this.state.camera.position.y = 5 this.state.camera.position.z = camera.position.z } } } /** * 摄像机追踪到指定位置 */ cameraToEntity(id: string) { const { tf } = this.entityManager.findItemById(id) // 移动正交相机去往目标点 if (this.camera instanceof THREE.OrthographicCamera) { const targetX = tf[0][0] const targetZ = tf[0][2] // this.controls.target.set(targetX, 0, targetZ) // this.camera.position.set(targetX, 60, targetZ) // y 可以固定一个值,比如 60 // this.camera.zoom = 34 // this.camera.updateProjectionMatrix() // this.controls.update() this.smoothMoveCameraTo(targetX, targetZ) } else if (this.camera instanceof THREE.PerspectiveCamera) { this.camera.position.set(tf[0][0], tf[1][1] + 10, tf[2][2]) this.camera.lookAt(tf[0][0], tf[1][1], tf[2][2]) } } /** * 动画移动相机 */ smoothMoveCameraTo(targetX: number, targetZ: number, duration = 300) { const orthCamera = this.camera as THREE.OrthographicCamera const start = performance.now() const startTarget = this.controls.target.clone() const startPosition = this.camera.position.clone() const startZoom = orthCamera.zoom const endZoom = 34 const endPosition = new THREE.Vector3(targetX, 60, targetZ) const endTarget = new THREE.Vector3(targetX, 0, targetZ) const animate = (now: number) => { const elapsed = now - start const t = Math.min(elapsed / duration, 1) orthCamera.position.copy(startPosition.clone().lerp(endPosition, t)) this.controls.target.copy(startTarget.clone().lerp(endTarget, t)) orthCamera.zoom = startZoom + (endZoom - startZoom) * t orthCamera.updateProjectionMatrix() this.controls.update() if (t < 1) { requestAnimationFrame(animate) } } requestAnimationFrame(animate) } 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) this.css2DRenderer.setSize(width, height) this.css3DRenderer.setSize(width, height) break } } beginViewUpdate() { this.state.isUpdating = true } endViewUpdate() { this.state.isUpdating = false } get worldModel(): WorldModel { return this.scene.worldModel } get gridHelper(): THREE.GridHelper { return this.scene.gridHelper } /** * 根据可视化范围更新网格的透明度 */ updateGridVisibility() { if (this.camera === undefined || !(this.camera instanceof THREE.OrthographicCamera)) { // 如果没有相机或相机不是透视相机,则不更新网格可见性 this.gridHelper.visible = true this.gridHelper.material.opacity = 1 return } const cameraDistance = this.state.camera.position.y const maxVisibleDistance = 8 // 网格完全不可见的最小距离 const fadeStartDistance = 35 // 开始淡出的最大距离 let opacity if (cameraDistance >= fadeStartDistance) { // 如果摄像机位置在淡出开始距离或更远,则网格完全可见 opacity = 1 } else if (cameraDistance <= maxVisibleDistance) { // 如果摄像机位置小于等于最大可见距离,则网格完全不可见 opacity = 0 } else { // 计算透明度,使用线性插值 opacity = 1 - (fadeStartDistance - cameraDistance) / (fadeStartDistance - maxVisibleDistance) } // 设置材质的透明度,并确保材质的transparent属性为true以支持透明 this.gridHelper.material.opacity = opacity this.gridHelper.material.transparent = true // 更新网格是否可见 this.gridHelper.visible = opacity > 0 } dispose() { this.state.isReady = false if (this.animationFrameId !== null) { cancelAnimationFrame(this.animationFrameId) this.animationFrameId = null } if (this.meshManager.size > 0) { this.meshManager.forEach((manager) => { if (manager.dispose) { manager.dispose() } }) this.meshManager.clear() } if (this.lineSegmentManagerMap.size > 0) { this.lineSegmentManagerMap.forEach((manager) => { if (manager.dispose) { manager.dispose() } }) this.lineSegmentManagerMap.clear() } if (this.tools) { for (const tool of this.tools) { if (tool.dispose) { tool.dispose() } } this.tools = [] } if (this.watchList) { _.forEach(this.watchList, (unWatchFn => { if (typeof unWatchFn === 'function') { unWatchFn() } })) this.watchList = [] } if (this.resizeObserver) { this.resizeObserver.unobserve(this.viewerDom) this.resizeObserver.disconnect() this.resizeObserver = undefined } 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 } if (this.stateManager) { this.stateManager.dispose() this.stateManager = null } if (this.controls) { this.controls.dispose() this.controls = null } delete window['viewport'] delete window['stateManager'] delete window['entityManager'] delete window['renderer'] delete window['camera'] delete window['renderer'] delete window['controls'] } getIntersects(point: THREE.Vector2) { const mouse = new THREE.Vector2() mouse.set((point.x * 2) - 1, -(point.y * 2) + 1) this.raycaster.setFromCamera(mouse, this.camera) return this.raycaster.intersectObjects([this.gridHelper], false) } /** * 获取鼠标所在的 x,y,z 位置。 * 鼠标坐标是相对于 canvas 元素 (renderer.domElement) 元素的 */ getClosestIntersection(e: MouseEvent) { const _point = new THREE.Vector2() _point.x = e.offsetX / this.renderer.domElement.offsetWidth _point.y = e.offsetY / this.renderer.domElement.offsetHeight const intersects = this.getIntersects(_point) if (intersects && intersects.length > 2) { const point = new THREE.Vector3(intersects[0].point.x, 0.1, intersects[1].point.z) return calcPositionUseSnap(e, point) } return null } } export interface ViewportState { /** * 是否准备完成 */ isReady: boolean /** * 鼠标模式 */ cursorMode: string // CursorMode, /** * 黄选的对象 */ selectedObject: Object3DLike | undefined selectedItem: ItemJson | undefined selectedEntityId: string | undefined selectedObjectSetter: PropertySetter | undefined /** * 红选的对象集 */ multiSelectedObjects: Object3DLike[] multiSelectedItems: ItemJson[] multiSelectedEntityIds: string[] view3DMode: string // Constract.Mode2D | Constract.Mode3D /** * 是否正在更新中 */ isUpdating: boolean /** * 相机状态 */ camera: { position: { x: number, y: number, z: number }, rotation: { x: number, y: number, z: number } } /** * 鼠标位置(归一化坐标) */ mouse: { /** * 鼠标在设计图上的坐标 */ x: number, z: number } }