import _ from 'lodash' import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' import EsDragControls from './model2DEditor/EsDragControls' import Stats from 'three/examples/jsm/libs/stats.module' import type WorldModel from '@/model/WorldModel.ts' import $ from 'jquery' import { reactive, watch } from 'vue' import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts' import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer' import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' import { getAllItemTypes } from '@/runtime/DefineItemType.ts' import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' import type Toolbox from '@/model/itemType/Toolbox.ts' import { calcPositionUseSnap } from '@/model/ModelUtils.ts' import SelectInspect from '@/designer/model2DEditor/tools/SelectInspect.ts' import MouseMoveInspect from '@/designer/model2DEditor/tools/MouseMoveInspect.ts' /** * 编辑器对象 * 这是非双向绑定的设计器对象,不记录状态,只记录全局使用到的对象,(实体类使用) */ export default class Viewport { viewerDom: HTMLElement scene: THREE.Scene camera: THREE.OrthographicCamera renderer: THREE.WebGLRenderer axesHelper: THREE.GridHelper gridHelper: THREE.GridHelper statsControls: Stats controls: OrbitControls worldModel: WorldModel raycaster: THREE.Raycaster dragControl: EsDragControls animationFrameId: any = null //搭配 state.cursorMode = xxx 之后, currentTool.start(第一个参数) 使用 toolStartObject: THREE.Object3D | null = null currentTool: Toolbox | null = null tools: ITool[] = [ new MouseMoveInspect(), new SelectInspect() ] toolbox: Record = {} /** * 监听窗口大小变化 */ resizeObserver?: ResizeObserver /** * vue 的 watcher */ watchList: (() => void)[] = [] css2DRenderer: CSS2DRenderer = new CSS2DRenderer() css3DRenderer: CSS3DRenderer = new CSS3DRenderer() //@ts-ignore state: ViewportState = reactive({ currentFloor: '', isReady: false, cursorMode: 'normal', selectedObject: null, camera: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } }, mouse: { x: 0, y: 0 } }) constructor(worldModel: WorldModel) { this.worldModel = worldModel } dispatchSignal(signal: string, data?: any) { // console.log('signal', signal, data) } /** * 初始化 THREE 渲染器 */ initThree(viewerDom: HTMLElement, floor: string) { console.log('viewport on floor', floor) this.state.currentFloor = floor this.viewerDom = viewerDom const rect = viewerDom.getBoundingClientRect() this.worldModel.registerViewport(this) // 场景 const scene = this.worldModel.getSceneByFloor(this, 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' }) 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 // 创建正交摄像机 this.initMode2DCamera() // 注册拖拽组件 this.dragControl = new EsDragControls(this) // 辅助线 const gridOption = this.worldModel.gridOption const axesHelper = new THREE.GridHelper(gridOption.axesSize, gridOption.axesDivisions) axesHelper.material.color.setHex(gridOption.axesColor) axesHelper.material.linewidth = 2 axesHelper.material.opacity = gridOption.gridOpacity axesHelper.material.transparent = true if (!gridOption.axesEnabled) { axesHelper.visible = false } // @ts-ignore axesHelper.material.vertexColors = false this.axesHelper = axesHelper this.scene.add(this.axesHelper) const gridHelper = new THREE.GridHelper(gridOption.gridSize, gridOption.gridDivisions) gridHelper.material.color.setHex(gridOption.gridColor) gridHelper.material.opacity = gridOption.gridOpacity gridHelper.material.transparent = true // @ts-ignore gridHelper.material.vertexColors = false if (!gridOption.gridEnabled) { gridHelper.visible = false } this.gridHelper = gridHelper this.scene.add(this.gridHelper) // 光照 const ambientLight = new THREE.AmbientLight(0xffffff, 0.8) 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 = '0' statsControls.dom.style.left = '0' viewerDom.parentElement.parentElement.appendChild(statsControls.dom) $(statsControls.dom).children().css('height', '28px') this.animate() // 监听事件 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) } // 触发所有物品类型的 afterAddViewport 方法 _.forEach(getAllItemTypes(), (itemType: ItemTypeDefineOption) => { itemType.clazz.afterAddViewport(this) }) 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.addEventListener('change', this.syncCameraState.bind(this)) controlsNew.panSpeed = 1 controlsNew.keyPanSpeed = 20 // normal 7 controlsNew.minDistance = 0.1 controlsNew.maxDistance = 1000 this.controls = controlsNew this.camera.updateProjectionMatrix() this.syncCameraState() } offset = 0 /** * 动画循环 */ animate() { this.animationFrameId = requestAnimationFrame(this.animate.bind(this)) this.renderView() this.offset -= 0.002 window['lineMaterial'].dashOffset = this.offset } /** * 渲染视图 */ renderView() { this.statsControls?.update() this.renderer?.render(this.scene, this.camera) this.css2DRenderer.render(this.scene, this.camera) this.css3DRenderer.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) this.css2DRenderer.setSize(width, height) this.css3DRenderer.setSize(width, height) break } } /** * 根据可视化范围更新网格的透明度 */ updateGridVisibility() { const cameraDistance = this.state.camera.position.y const maxVisibleDistance = 60 // 网格完全可见的最大距离 const fadeStartDistance = 15 // 开始淡出的距离 // 计算透明度(0~1) let opacity = 0.8 if (cameraDistance > fadeStartDistance) { opacity = 0.8 - Math.min((cameraDistance - fadeStartDistance) / (maxVisibleDistance - fadeStartDistance) * 0.8, 0.8) } // 修改网格材质透明度 this.gridHelper.material.opacity = opacity this.gridHelper.visible = opacity > 0 } destroy() { this.state.isReady = false if (this.animationFrameId !== null) { cancelAnimationFrame(this.animationFrameId) this.animationFrameId = null } if (this.watchList) { _.forEach(this.watchList, (unWatchFn => { if (typeof unWatchFn === 'function') { unWatchFn() } })) this.watchList = [] } if (this.tools) { for (const tool of this.tools) { if (tool.destory) { tool.destory() } } this.tools = [] } 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 } } 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 { /** * 当前楼层 */ currentFloor: string /** * 是否准备完成 */ isReady: boolean /** * 鼠标模式 */ cursorMode: CursorMode, /** * 选中的对象 */ selectedObject: THREE.Object3D | null /** * 相机状态 */ camera: { position: { x: number, y: number, z: number }, rotation: { x: number, y: number, z: number } } /** * 鼠标位置(归一化坐标) */ mouse: { /** * 鼠标在设计图上的坐标 */ x: number, z: number } }