import * as THREE from "../../../../libs/three.js/build/three.module.js"; import math from "../../utils/math.js" import Common from '../../utils/Common.js' import {LineDraw, MeshDraw} from "../../utils/DrawUtil.js"; import {ExtendView} from "../../../viewer/ExtendView.js"; import Viewport from "../../viewer/Viewport.js"; import Sprite from "../../objects/Sprite.js"; import {transitions, easing, lerp} from '../../utils/transitions.js' import {TransformControls} from "../../objects/tool/TransformControls.js"; import SplitScreen from "../../utils/SplitScreen.js" import DepthBasicMaterial from "../../materials/DepthBasicMaterial.js"; let clickPanoToDisLink = false;//是否在编辑漫游点连接时,通过点击漫游点能断开连接 let images360, Alignment, SiteModel const texLoader = new THREE.TextureLoader() texLoader.crossOrigin = "anonymous" const rotQua = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1), Math.PI) const lineMats = {} const circleMats = {} const renderOrders = { circleSelected:3, circle:2, line:1, } const pointColor = { /* selected:"#c80", default:'#1ac' */ selected:"#c60", default:'#17c' } const opacitys = { default:1.3, selected: 2.0 } const cameraProps = [ { name : 'top', axis:["x","y"], direction : new THREE.Vector3(0,0,-1), //镜头朝向 openCount:0, }, { name : 'right', axis:["y","z"], direction : new THREE.Vector3(1,0,0), openCount:0, } ] class PanoEditor extends THREE.EventDispatcher{ constructor(){ super() this.panoGroup = [], //分组 this.viewports = {}, this.panoLink = {}, this.panoMeshs = new THREE.Object3D, this.lineMeshes = new THREE.Object3D this.views = {} this.cameras = {} this.orthoCamera = new THREE.OrthographicCamera(-100, 100, 100, 100, 0.01, 10000) this.orthoCamera.up.set(0,0,1) this.selectedPano; this.selectedGroup; this.operation; this.visiblePanos = [] } init(){ {//init lineMats lineMats.default = LineDraw.createFatLineMat({ color: '#eeeeee', lineWidth: 2, depthTest:false }) lineMats.hovered = LineDraw.createFatLineMat({ color: '#00c8af', lineWidth: 2, depthTest:false }) lineMats.selected = LineDraw.createFatLineMat({ color: '#00c8af', lineWidth: 3, depthTest:false }) } this.initViews() viewer.addEventListener('allLoaded',()=>{ images360 = viewer.images360 Alignment = viewer.modules.Alignment SiteModel = viewer.modules.SiteModel this.panoMeshs.name = 'panoMeshs' viewer.scene.scene.add(this.panoMeshs) this.lineMeshes.name = 'lineMeshes' viewer.scene.scene.add(this.lineMeshes) { this.transformControls = new TransformControls(viewer.mainViewport.camera, viewer.renderArea,{ dontHideWhenFaceCamera: true, rotFullCircle:true }); this.transformControls.setSize(1.5) viewer.scene.scene.add(this.transformControls) this.transformControls._gizmo.hideAxis = {translate:['x','y'], rotate:['x','y','e'] } this.transformControls.setRotateMethod(2) this.fakeMarkerForTran = new THREE.Mesh(new THREE.BoxBufferGeometry(0.3,0.3,0.3) , new THREE.MeshBasicMaterial({ color:"#FFFFFF", opacity:0.4, transparent:true, visible:false }));//一个看不见的mesh,只是为了让transformControls移动点云 viewer.scene.scene.add(this.fakeMarkerForTran) let afterMoveCircle = (type)=>{ if(type == 'position'){ let moveVec = new THREE.Vector3().subVectors(this.fakeMarkerForTran.position, this.fakeMarkerForTran.oldState.position) this.selectedClouds.forEach(cloud=>Alignment.translate(cloud, moveVec)) }else{ let center = this.selectedPano.position; let forward = new THREE.Vector3(0,1,0); let vec1 = forward.clone().applyQuaternion(this.fakeMarkerForTran.oldState.quaternion) let vec2 = forward.clone().applyQuaternion(this.fakeMarkerForTran.quaternion) let diffAngle = math.getAngle(vec1,vec2,'z') this.selectedClouds.forEach(cloud=>{ Alignment.rotateAround(center, cloud, null, diffAngle) }) } this.fakeMarkerForTran.oldState = { position: this.fakeMarkerForTran.position.clone(), quaternion: this.fakeMarkerForTran.quaternion.clone(), } Alignment.writeToHistory( this.selectedClouds ) } this.fakeMarkerForTran.addEventListener('position_changed', afterMoveCircle.bind(this,'position')) this.fakeMarkerForTran.addEventListener("rotation_changed", afterMoveCircle.bind(this,'rotation') ) this.transformControls.addEventListener('transform_end',()=>{ Alignment.prepareRecord = true }) Alignment.history.addEventListener('undo',()=>{ this.updateTranCtl() }) } this.initPanoLink() this.addPanoMesh() viewer.scene.pointclouds.forEach(e=>{ e.material.color = pointColor.default }) this.switchView('top') SiteModel.bus.addEventListener('initDataDone',()=>{ let floor = SiteModel.entities.find(e=>e.buildType == 'floor' && e.panos.length) //选择有漫游点的一层 if(!floor){ floor = 'all' //SiteModel.entities.find(e=>e.buildType == 'floor') console.log('没有一层有漫游点?!') } this.gotoFloor(floor) }) Alignment.bus.addEventListener('switchHandle', this.updateCursor.bind(this)) viewer.addEventListener('global_click',(e)=>{ if(e.button === THREE.MOUSE.RIGHT){//取消旋转和平移 console.log('right click',e) this.setLinkOperateState('addLink',false) this.setLinkOperateState('removeLink',false) }else if(this.clickToZoomInEnabled){ if(this.activeViewName == 'mainView'){ viewer.controls.zoomToLocation(e.mouse) }else{ this.zoomIn(e.intersect.orthoIntersect, e.pointer) } this.setZoomInState(false) } }) /* {//旋转时的辅助线--绕某个点旋转的版本 this.rotGuideLine = LineDraw.createLine([], {color:'#aaffee'}) this.rotGuideLine.visible = false this.rotGuideLine.name = 'rotGuideLine' this.rotGuideLine.renderOrder = renderOrders.line viewer.scene.scene.add(this.rotGuideLine) let startPoint Alignment.bus.addEventListener('rotateStart', (e)=>{ startPoint = e.startPoint }) Alignment.bus.addEventListener('rotate', (e)=>{ LineDraw.updateLine(this.rotGuideLine, [startPoint, e.endPoint] ) this.rotGuideLine.visible = true }) viewer.fpControls.addEventListener("end",(e)=>{ startPoint = null this.rotGuideLine.visible = false }) } */ {//连接时的辅助线 this.linkGuideLine = LineDraw.createLine([], {color:'#aaa', deshed:true, dashSize:0.1,gapSize:0.1,}) this.linkGuideLine.visible = false this.linkGuideLine.name = 'linkGuideLine' viewer.scene.scene.add(this.linkGuideLine) this.linkGuideLine.renderOrder = renderOrders.line let update = (e)=>{ if(this.operation != 'addLink' || this.activeViewName != 'top' && this.activeViewName != 'mainView' ||!this.selectedPano){ return this.linkGuideLine.visible = false } let endPos if(this.activeViewName == 'top' ){ endPos = e.intersect.orthoIntersect.clone().setZ(this.selectedPano.position.z) }else if(this.activeViewName == 'mainView' ){ if(!e.intersect.point)return endPos = e.intersect.point.position } LineDraw.updateLine(this.linkGuideLine, [this.selectedPano.position, endPos] ) this.linkGuideLine.visible = true } viewer.addEventListener('global_mousemove', (e)=>{ update(e) }) this.addEventListener('updateLinkGuideLine', update) //为何打开调试时移动很卡 } /* viewer.inputHandler.addEventListener('keydown', (e)=>{ if(e.event.key == "r" ){ this.setTranMode('rotate') }else if(e.event.key == "t"){ this.setTranMode('translate') } }) */ }) } setTranMode(mode){//rotate or translate this.tranMode = mode if(this.activeViewName == 'mainView'){ mode && this.transformControls.setMode(mode) this.updateTranCtl() }else{ Alignment.switchHandle(mode) } } updateTranCtl(){// 设置3D页面的transformControls相关 if(!this.tranMode || !this.selectedPano || this.activeViewName != 'mainView' ) { return this.transformControls.detach() }else if(this.checkIfAllLinked({group:this.selectedGroup})){ this.dispatchEvent('needToDisConnect') return this.transformControls.detach() } this.transformControls.attach(this.fakeMarkerForTran) let {position, quaternion} = this.getPanoPose(this.selectedPano); this.fakeMarkerForTran.position.copy(position) this.fakeMarkerForTran.quaternion.copy(quaternion) this.fakeMarkerForTran.oldState = { position: position.clone(), quaternion: quaternion.clone(), } } ////////////////////////////////// initViews(){ this.splitScreenTool = new SplitScreen this.targetPlane = viewer.mainViewport.targetPlane = new THREE.Plane() this.shiftTarget = viewer.mainViewport.shiftTarget = new THREE.Vector3 //project在targetPlane上的位置 for(let i=0;i<2;i++){ let prop = cameraProps[i]; let view = new ExtendView() this.views[prop.name] = view this.cameras[prop.name] = this.orthoCamera view.direction = prop.direction } this.views.mainView = viewer.mainViewport.view this.cameras.mainView = viewer.mainViewport.camera } switchView(name){//替换view和camera到mainViewport let view = this.views[name] let camera = this.cameras[name] let prop = cameraProps.find(e=>e.name == name) let {boundSize, center} = viewer.bound this.lastViewName = this.activeViewName this.activeViewName = name let lastView = this.views[this.lastViewName] let lastCamera = this.cameras[this.lastViewName] viewer.mainViewport.view = view viewer.mainViewport.camera = camera if(lastCamera)lastView.zoom = lastCamera.zoom this.targetPlane.setFromNormalAndCoplanarPoint( view.direction.clone(), center ) this.targetPlane.projectPoint(view.position, this.shiftTarget ) //target转换到过模型中心的平面,以保证镜头一定在模型外 view.position.copy(this.splitScreenTool.getPosOutOfModel(viewer.mainViewport)) if(view.zoom)camera.zoom = view.zoom//恢复上次的zoom viewer.updateScreenSize({forceUpdateSize:true})//更新camera aspect left等 this.updateCursor() if(name == 'mainView'){ viewer.mainViewport.alignment = null viewer.scene.pointclouds.forEach(e=>{ e.material.activeAttributeName = 'rgba' e.material.useFilterByNormal = false e.changePointOpacity(1,true) }) viewer.updateVisible(viewer.reticule, 'force', true) if(lastView){//2d->3d view.copy(lastView) let direction = view.direction let panos = images360.panos.filter(e=>e.circle.visible) let nearestPano = Common.sortByScore(panos , [], [(pano)=>{ let vec = new THREE.Vector3().subVectors(pano.position, view.position); return -vec.dot(direction); }], true); //console.log('最近',nearestPano ) if(nearestPano && nearestPano[0] ){ //尽量不变画面范围,使pano点保持原位,转换到mainView let halfHeight = lastCamera.top/lastCamera.zoom let dis = halfHeight / Math.tan( THREE.Math.degToRad(camera.fov/2)) view.position.add(direction.clone().multiplyScalar(-nearestPano[0].score - dis)) //console.log('getCloser', -nearestPano[0].score - dis) this.lastDisToPano = dis //记录一下 } } viewer.fpControls.lockKey = false }else{ if(this.lastViewName == 'mainView'){//3d->2d let direction = lastView.direction let panos = images360.panos.filter(e=>e.circle.visible) //尽量靠近画布中心,且距离相机较近 let nearestPano = Common.sortByScore(panos , [], [(pano)=>{ let vec = new THREE.Vector3().subVectors(pano.position, lastView.position); let dis = vec.dot(direction); return dis < 0 ? dis * 10 : - dis },(pano)=>{ let vec = new THREE.Vector3().subVectors(pano.position, lastView.position); let angle = vec.angleTo(direction) return - angle * 70 }], true); //目前还存在的问题就是不知selectedPano和最近点的取舍 //console.log('panos',nearestPano ) if(nearestPano && nearestPano[0] ){ //console.log('nearestPano',nearestPano[0].item.id ) let pos1 = nearestPano[0].item.position.clone() let pos2 = pos1.clone() let dis = new THREE.Vector3().subVectors(nearestPano[0].item.position, lastView.position).dot(direction) //-nearestPano[0].score //根据2d->3d的式子逆求zoom let halfHeight = Math.abs(dis) * Math.tan( THREE.Math.degToRad(lastCamera.fov/2)) camera.zoom = camera.top / halfHeight camera.updateProjectionMatrix() if(name == 'right'){//侧视图 view.direction = direction.clone().setZ(0) //水平方向设定为3d的方向 this.targetPlane.setFromNormalAndCoplanarPoint( view.direction.clone(), center ) this.targetPlane.projectPoint(view.position, this.shiftTarget ) //target转换到过模型中心的平面,以保证镜头一定在模型外 view.position.copy(this.splitScreenTool.getPosOutOfModel(viewer.mainViewport)) } view.applyToCamera(camera)//update pos1.project(lastCamera) pos2.project(camera) //目标是找到画面上最接近中心的一点(最好是漫游点,不然就是点云),让其在转换画面后在画面上的位置不变。万一找到的点不在屏幕中(比如当屏幕中没点云时),就默认让那个点移动到屏幕中央,也就是假设当前它pos1在屏幕中央位置。 // if(pos1.z>1){ console.warn('选取的点在相机背后了!?') } //如果最近点超出屏幕范围 (-1,1), 最好将其拉到边缘,甚至居中 。这样屏幕上就不会没有漫游点了 let bound = 0.9 pos1.x = THREE.Math.clamp(pos1.x, -bound, bound) pos1.y = THREE.Math.clamp(pos1.y, -bound, bound) let vecOnscreen = new THREE.Vector3().subVectors(pos1,pos2) let moveVec = Potree.Utils.getOrthoCameraMoveVec(vecOnscreen, camera ) //console.log('pos1', pos1) view.position.sub(moveVec) } }else{ if(prop.openCount == 0){//至多执行一次 this.viewportFitBound(name, boundSize, center) } } prop.openCount ++; viewer.scene.pointclouds.forEach(e=>{ e.material.activeAttributeName = 'color' e.material.useFilterByNormal = true //defines : use_filter_by_normal attenuated_opacity if(this.selectedPano && this.selectedClouds.includes(e) /* this.selectedPano.pointcloud == e */){ e.changePointOpacity(opacitys.selected,true) }else{ e.changePointOpacity(opacitys.default,true) } }) viewer.updateVisible(viewer.reticule, 'force', false) if(name == 'top') viewer.mainViewport.alignment = {rotate:true,translate:true}; if(name == 'right'){ viewer.mainViewport.alignment = {translate:true, rotateSide:true, translateVec:new THREE.Vector3(0,0,1)}; //只能上下移动 viewer.mainViewport.rotateSide = true }else{ viewer.mainViewport.rotateSide = false } viewer.fpControls.lockKey = true } this.updateTranCtl() this.setTranMode(this.tranMode) // update this.setZoomInState(false) //取消放大模式 this.updatePointLevels() } viewportFitBound(){ //使一个viewport聚焦在某个范围 if(viewer.mainViewport.resolution.x == 0 || viewer.mainViewport.resolution.y == 0){ return setTimeout(()=>{ this.viewportFitBound() },10) } this.gotoFloor(this.currentFloor, true, 0, null, true) } rotateSideCamera(angle){//侧视图绕模型中心水平旋转 this.splitScreenTool.rotateSideCamera(viewer.mainViewport, angle) } zoomIn(intersect, pointer){ let camera = viewer.mainViewport.camera let endZoom = 200 //this.orthoMoveFit(intersect, {endZoom:viewer.mainViewport.camera.zoom < aimZoom ? aimZoom : null} , 300) let startZoom = camera.zoom if(startZoom >= endZoom){return} viewer.mainViewport.view.zoomOrthoCamera(camera, endZoom, pointer, 300) } orthoMoveFit(pos, info, duration){ var margin = {x:200, y:230} this.splitScreenTool.viewportFitBound(viewer.mainViewport, info.bound, pos, duration, margin ) } setZoomInState(state, informinformBy2d){//是否点击后可放大 //if(state && this.activeViewName == 'mainView')return console.log('3D不可放大') this.clickToZoomInEnabled = !!state if(state){ viewer.dispatchEvent({type : "CursorChange", action : "add", name:"zoomInCloud"} ) }else{ viewer.dispatchEvent({type : "CursorChange", action : "remove", name:"zoomInCloud" }) } if(!state && !informinformBy2d){ this.dispatchEvent({type:'operationCancel', operation: 'zoomIn'}) } } gotoFloor(floor, force, duration = 600, informBy2d, fitBound){// 选择不同楼层, 切换点位显示。 'all'为全部显示 floor = floor || 'all' if(this.currentFloor == floor && !force)return if(this.currentFloor != floor){//如果楼层没变,不修改可视 //let pointclouds = viewer.findPointcloudsAtFloor(floor) let panos = floor == 'all' ? viewer.images360.panos : floor.panos viewer.images360.panos.forEach(pano=>{ let v = panos.includes(pano) this.switchPanoVisible(pano,v) }) } this.updateLinesVisible() //切换楼层时清空选择状态 if(this.selectedPano && floor != 'all' && !floor.panos.includes(this.selectedPano)){ this.selectedPano.circle.dispatchEvent('click') } if(this.selectedLine){ this.selectedLine.dispatchEvent('click') } let bound, center if(floor == 'all'){ bound = viewer.images360.bound.bounding center = viewer.images360.bound.center }else{ bound = this.getPanosBound(floor) center = bound.getCenter(new THREE.Vector3()) if(floor.panos.length == 0)console.log(floor.name, 'floor无漫游点' ) } if(this.activeViewName != 'mainView' ){ fitBound && this.orthoMoveFit(center, {bound}, duration) }else if(this.activeViewName == 'mainView'){ if(floor != 'all'){ //切换一下位置,因为原处点云会消失 viewer.scene.view.setView({position:center, duration }) } } this.currentFloor = floor if(!informBy2d){ this.dispatchEvent({type:'changeFloor', floor}) } } getPanosBound(floor){ if(!floor.panosBound){ if(floor.panos.length == 0){ floor.panosBound = viewer.images360.bound.bounding.clone() }else{ let minSize = new THREE.Vector3(5,5,5) let bound = math.getBoundByPoints(floor.panos.map(e=>e.position), minSize) floor.panosBound = bound.bounding } } return floor.panosBound } switchPanoVisible(pano, v, informBy2d){ pano.circle.visible = v viewer.updateVisible(pano, 'panoEditor', v) viewer.updateVisible(pano.pointcloud, 'panoEditor', v) if(v){ this.visiblePanos.includes(pano) || this.visiblePanos.push(pano) }else{ let index = this.visiblePanos.indexOf(pano); index>-1 && this.visiblePanos.splice(index,1) } if(informBy2d){ this.updateLinesVisible() } informBy2d || this.dispatchEvent({type:"switchPanoVisible", pano, v}) this.updatePointLevels() } updateLinesVisible(){ this.lineMeshes.children.forEach(line=>{ let names = line.name.split('-') var pano0 = images360.getPano(names[0]) var pano1 = images360.getPano(names[1]) line.visible = this.visiblePanos.includes(pano0) || this.visiblePanos.includes(pano1) }) } updateCursor(){ let cursor if(this.activeViewName == 'mainView' || !this.selectedPano){ cursor = null }else{ cursor = Alignment.handleState } if(cursor == 'rotate'){ viewer.dispatchEvent({ type : "CursorChange", action : "add", name:"rotatePointcloud" }) viewer.dispatchEvent({ type : "CursorChange", action : "remove", name:"movePointcloud" }) }else if(cursor == 'translate'){ viewer.dispatchEvent({ type : "CursorChange", action : "add", name:"movePointcloud" }) viewer.dispatchEvent({ type : "CursorChange", action : "remove", name:"rotatePointcloud" }) }else{ viewer.dispatchEvent({ type : "CursorChange", action : "remove", name:"movePointcloud" }) viewer.dispatchEvent({ type : "CursorChange", action : "remove", name:"rotatePointcloud" }) } //this.cursorState = cursor } setLinkOperateState(name, state, informinformBy2d){ if(state && name == this.operation || !state && name != this.operation)return let old = this.operation this.operation = state ? name : null if(this.operation == 'removeLink'){ if(this.selectedLine){ this.selectedLine.dispatchEvent('click')//删除 } if(this.selectedPano && clickPanoToDisLink){ this.selectedPano.circle.dispatchEvent('click')//删除 } } if(this.operation != 'addLink'){ this.linkGuideLine.visible = false } if(!state && !informinformBy2d){ this.dispatchEvent({type: "operationCancel", operation: old}) } if(this.operation == 'addLink'){ viewer.dispatchEvent({type : "CursorChange", action : "add", name:"connectPano"} ) }else{ viewer.dispatchEvent({type : "CursorChange", action : "remove", name:"connectPano"} ) } if(this.operation == 'removeLink'){ viewer.dispatchEvent({type : "CursorChange", action : "add", name:"disconnectPano"} ) }else{ viewer.dispatchEvent({type : "CursorChange", action : "remove", name:"disconnectPano"} ) } } ///////////////////////////////// initPanoLink(){ images360.panos.forEach((pano)=>{ this.panoLink[pano.id] = {} }) images360.panos.forEach((pano)=>{ pano.visibles.forEach(index=>{//visibles中存的是下标! this.linkChange(pano, images360.getPano(index,'index'), 'add') }) }) console.log('panoLink',this.panoLink) } groupChange(pano0, pano1, type){//修改group (type == 'remove'时,pano1可以为空) if(type == 'add'){ Common.pushToGroupAuto([pano0, pano1], this.panoGroup ) }else{ let atGroup = this.panoGroup.find(e=>e.includes(pano0) && (e.includes(pano1) || !pano1));//所在组 if(!atGroup){ if(pano1){ console.log('这两个pano原本就不在一个组', pano0.id, pano1.id) }else{ console.log('pano0不在任何组', pano0) } return } //断开连接时,因为组内没有其他成员的连接信息,所以需要清除整组,并将剩余的一个个重新连接 this.panoGroup.splice(this.panoGroup.indexOf(atGroup),1) //删除 atGroup.forEach(pano=>{//然后再重新生成这两个和组的关系,各自分组 if(pano == pano0 || pano == pano1)return for(let id in this.panoLink[pano.id]){ if(this.panoLink[pano.id][id]){ let pano_ = images360.getPano(id) Common.pushToGroupAuto([pano, pano_], this.panoGroup ) } } }) } } linkChange(pano0, pano1, type){//修改link (type == 'remove'时,pano1可以为空) let temp = [] if(type == 'add'){ if(!pano1)return console.error('不支持add时pano1为空') this.panoLink[pano0.id][pano1.id] = this.panoLink[pano0.id][pano1.id] || {} this.panoLink[pano1.id][pano0.id] = this.panoLink[pano1.id][pano0.id] || {} }else{ if(!pano1){ for(let id in this.panoLink[pano0.id]){ if(this.panoLink[pano0.id][id]){ this.panoLink[id][pano0.id] = false temp.push(id) } } this.panoLink[pano0.id] = {} //全部断连 }else{ this.panoLink[pano0.id][pano1.id] = false this.panoLink[pano1.id][pano0.id] = false } } if(!pano1){ //全部断连 temp.forEach(id=>{ this.lineChange(pano0, images360.getPano(id) , type) }) }else{ this.lineChange(pano0, pano1, type) } this.groupChange(pano0, pano1, type) //this.updateSelectGroup() this.selectPano(this.selectedPano, false,true) //更新选中点云显示 } lineChange(pano0, pano1, type){//修改line if(type == 'add'){ if(this.panoLink[pano0.id][pano1.id].line) return let line = LineDraw.createFatLine([pano0.position, pano1.position], {material:lineMats.default}) line.name = `${pano0.id}-${pano1.id}` line.renderOrder = line.pickOrder = renderOrders.line this.lineMeshes.add(line) this.panoLink[pano0.id][pano1.id].line = this.panoLink[pano1.id][pano0.id].line = line line.addEventListener('mouseover', ()=>{ if(this.clickToZoomInEnabled)return //if(this.activeViewName == 'mainView')return if(this.selectedLine != line)line.material = lineMats.hovered viewer.dispatchEvent({ type : "CursorChange", action : "add", name:"hoverLine" }); }); line.addEventListener('mouseleave', ()=>{ if(this.clickToZoomInEnabled)return //if(this.activeViewName == 'mainView')return if(this.selectedLine != line)line.material = lineMats.default viewer.dispatchEvent({ type : "CursorChange", action : "remove", name:"hoverLine" }); }); line.addEventListener('click', (e)=>{ if(this.clickToZoomInEnabled)return //if(this.activeViewName == 'mainView')return if(this.operation == 'removeLink'){ if(this.selectedLine == line) this.selectLine(null) return this.linkChange(pano0, pano1, 'remove') } this.selectLine(line) }) }else{ let line = this.lineMeshes.children.find(e=>e.name == `${pano0.id}-${pano1.id}` || e.name == `${pano1.id}-${pano0.id}` ) if(line){ this.lineMeshes.remove(line) line.geometry.dispose() } } } selectLine(line){ if(this.selectedLine == line)return if(this.selectedLine){ this.selectedLine.material = lineMats.default; } if(line){ line.material = lineMats.selected } this.selectedLine = line } addPanoMesh(){ let map = texLoader.load(Potree.resourcePath+'/textures/correct_n.png' ) /* circleMats.default_normal = new THREE.MeshBasicMaterial({ map, color: 0xffffff, transparent: true, depthTest: false, depthWrite: false, }) */ window.circleMats = circleMats circleMats.default_normal = new DepthBasicMaterial({ map, color: 0xffffff, transparent: true, /* depthTest: false, depthWrite: false, */ clipDistance : 5,//消失距离 occlusionDistance: 2,//变为backColor距离 useDepth:true, maxClipFactor: 0.6, backColor: 0x33ffdd, }) circleMats.default_rtk_on = circleMats.default_normal.clone(); circleMats.default_rtk_on.map = texLoader.load(Potree.resourcePath+'/textures/rtk-y-n.png' ) circleMats.default_rtk_off = circleMats.default_normal.clone(); circleMats.default_rtk_off.map = texLoader.load(Potree.resourcePath+'/textures/rtk-f-n.png' ) circleMats.selected_normal = circleMats.default_normal.clone(); circleMats.selected_normal.map = texLoader.load(Potree.resourcePath+'/textures/correct_s.png' ) circleMats.selected_normal.useDepth = false; circleMats.selected_rtk_on = circleMats.selected_normal.clone(); circleMats.selected_rtk_on.map = texLoader.load(Potree.resourcePath+'/textures/rtk-y-s.png' ) circleMats.selected_rtk_off = circleMats.selected_normal.clone(); circleMats.selected_rtk_off.map = texLoader.load(Potree.resourcePath+'/textures/rtk-f-s.png' ) circleMats.hovered_normal = circleMats.default_normal.clone(); circleMats.hovered_normal.color.set(0x00ff00) circleMats.hovered_normal.useDepth = false circleMats.hovered_rtk_on = circleMats.default_rtk_on.clone(); circleMats.hovered_rtk_on.color.set(0x00ff00) circleMats.hovered_rtk_on.useDepth = false circleMats.hovered_rtk_off = circleMats.default_rtk_off.clone(); circleMats.hovered_rtk_off.color.set(0x00ff00) circleMats.hovered_rtk_off.useDepth = false let setPos = (circle)=>{ circle.position.copy(circle.pano.position) for(let id in this.panoLink[circle.pano.id]){ let linkInfo = this.panoLink[circle.pano.id][id] if(linkInfo){ LineDraw.updateLine(linkInfo.line, [circle.pano.position, images360.getPano(id).position] ) } } circle.update() //update sprite Matrix } images360.panos.forEach(pano=>{ var circle = new Sprite({mat: circleMats['default' + '_'+ this.getPanoRtkState(pano) ] , sizeInfo:{ minSize : 50 , maxSize : 120, nearBound : 2, farBound : 10, }, renderOrder : renderOrders.circle, pickOrder: renderOrders.circle }) circle.pickDontCheckDis = true circle.name = 'panoCircle' circle.sid = pano.id circle.pano = pano; pano.circle = circle; this.panoMeshs.add(circle) setPos(circle) pano.addEventListener('rePos', setPos.bind(this,circle)) let drag = ()=>{ /* if(this.activeViewName == 'mainView' && this.tranMode == 'translate'){//如果3d页不禁止xy的话,这段打开 this.transformControls.dispatchEvent('dragging')//触发拖拽 return } */ if(this.tranMode != 'translate' || this.activeViewName == 'mainView')return this.selectPano(circle.pano) //为了方便拖拽点云,拖动circle就直接选中 viewer.inputHandler.drag.object = null //取消拖拽状态,否则不触发点云拖动 } circle.addEventListener('drag', drag) circle.addEventListener('mouseover', ()=>{ this.hoverPano(pano,true) }) circle.addEventListener('mouseleave', ()=>{ this.hoverPano(pano,false) }) circle.addEventListener('click', ()=>{ //if(this.activeViewName == 'mainView')return if(this.clickToZoomInEnabled)return if(clickPanoToDisLink && this.operation == 'removeLink'){ this.linkChange(pano, null, 'remove') //删除所有连接 } if(this.selectedPano == circle.pano) return this.selectPano(null) if(this.operation == 'addLink' && this.selectedPano){ this.linkChange(this.selectedPano, circle.pano, 'add') //this.setLinkOperateState('addLink',false) return } //if(this.operation == 'removeLink' && this.selectedPano){ //和选择中心点冲突 // this.linkChange(this.selectedPano, circle.pano, 'remove') // //this.setLinkOperateState('removeLink',false) // return // } this.selectPano(circle.pano) }) }) } hoverPano(pano, state){ if(this.clickToZoomInEnabled)return if(pano && state){ //在hover一个pano之前,一定会先取消已经hover的pano, 最多存在一个hovered的pano if(this.hoveredPano == pano)return if(this.hoveredPano){ this.hoverPano(this.hoveredPano,false) } this.hoveredPano = pano pano.hovered = true if(/* this.activeViewName == 'mainView' || */Alignment.handleState && this.selectedPano && this.selectedPano == pano)return if(this.operation != 'addLink' || !this.selectedPano || this.selectedPano == pano){ // this.selectedPano == pano? viewer.dispatchEvent({ type : "CursorChange", action : "add", name:"hoverPano" }); } if(this.selectedPano != pano) pano.circle.material = circleMats['hovered' + '_'+ this.getPanoRtkState(pano) ] }else if(pano && !state){//unhover if(this.hoveredPano != pano)return pano.hovered = false viewer.dispatchEvent({ type : "CursorChange", action : "remove", name:"hoverPano" }); if(this.selectedPano != pano) pano.circle.material = circleMats['default' + '_'+ this.getPanoRtkState(pano) ] this.hoveredPano = null; }else{//unhover any if(this.hoveredPano){ this.hoverPano(this.hoveredPano, false) } } } selectPano(pano, informinformBy2d, force){ if(this.selectedPano == pano && !force)return let lastSeletedPano = this.selectedPano if(this.selectedPano){ this.selectedPano.circle.material = circleMats['default' + '_'+ this.getPanoRtkState(this.selectedPano) ] this.selectedPano.circle.renderOrder = renderOrders.circle if(this.activeViewName == 'mainView'){ }else{ this.selectedClouds.forEach(e=>{ e.changePointOpacity(opacitys.default,true) e.material.color = pointColor.default; }) } } this.selectedPano = pano || null this.updateSelectGroup(); if(pano){ this.selectedPano.circle.material = circleMats['selected' + '_'+ this.getPanoRtkState(this.selectedPano) ] this.selectedPano.circle.renderOrder = this.selectedPano.circle.pickOrder = renderOrders.circleSelected //侧视图能显示在最前 if(this.activeViewName == 'mainView'){ }else{ this.selectedClouds.forEach(e=>{ e.changePointOpacity(opacitys.selected,true) e.material.color = pointColor.selected; }) } {//自动切换楼层 let atFloor = SiteModel.entities.find(e=>e.buildType == 'floor' && e.panos.includes(pano)) if(!atFloor){ atFloor = 'all' }else{ } this.gotoFloor(atFloor, false, 600 ) } } this.updateCursor() this.updateTranCtl() if(informinformBy2d){ if(this.selectedPano){ if(this.activeViewName == 'mainView'){ //平移,focus选中的pano let distance = this.lastDisToPano || 5; if(lastSeletedPano){ distance = viewer.mainViewport.camera.position.distanceTo(lastSeletedPano.position) } viewer.focusOnObject({ position:this.selectedPano.position}, 'point', null, {distance }) }else{ this.orthoMoveFit(this.selectedPano.position, {}, 500) } } }else{ this.dispatchEvent({type:'panoSelect', pano }) } } updatePointLevels(){ let percent = 1 if(this.activeViewName == 'mainView'){ //假设每个pointcloud所带的点个数大致相同,那么当可见点云个数越多,所能展示的level越低,否则因总个数超过budget的话密度会参差不齐。 let visiCount = viewer.scene.pointclouds.filter(e=>e.visible).length let maxCount = 70, minCount = 1, minPer = 0.4, maxPer = 1 percent = maxPer - ( maxPer - minPer) * THREE.Math.clamp((visiCount - minCount) / (maxCount - minCount),0,1) //dis2d越大,角度要越小 //pointcloud.changePointSize() //console.log('updatePointLevels', percent, visiCount) }else{ percent = null } Potree.settings.UserDensityPercent = percent viewer.setPointLevels() } getPanoRtkState(pano){ return pano.panosData.has_rtk ? pano.rtkState ? 'rtk_on' : 'rtk_off' : 'normal' } setPanoRtkState(pano,state){ pano.rtkState = state pano.circle.material = circleMats[(this.selectedPano == pano ? 'selected' : 'default') + '_'+ this.getPanoRtkState(pano) ] } updateSelectGroup(){//更新选中的组 this.selectedGroup = this.panoGroup.find(e=>e.includes(this.selectedPano)) if(this.selectedGroup){ this.selectedGroup = [this.selectedPano, ...this.selectedGroup.filter(e=>e != this.selectedPano)];//将选中的放第一个,便于旋转时绕其旋转。 } this.selectedClouds = this.selectedPano ? (this.selectedGroup || [this.selectedPano]).map(e=>e.pointcloud) : [] } checkIfCanSave(){//如果未全部相连,不能保存 for(let datasetId in Potree.settings.datasetsPanos ) { if(!this.checkIfAllLinked({datasetId})){ console.log('没有全部连通,不能保存。其中一个:', datasetId) return } } return true } checkIfAllLinked(o){//某个(or组所在的)数据集是否全部连通 let datasetId, group if(o.group){ group = o.group let pano = o.group[0] if(!pano)return //会有没有漫游点的点云来编辑吗 datasetId = pano.pointcloud.dataset_id }else if(o.datasetId){ datasetId = o.datasetId group = this.panoGroup.find(panos=>panos[0].pointcloud.dataset_id == datasetId ) if(!group)return //要找的数据集的pano全部都孤立了 } if(!datasetId)return let panos = Potree.settings.datasetsPanos[datasetId].panos return panos.length == group.length } getPanoPose(pano){ let pose = { position: pano.position.clone(), quaternion: new THREE.Quaternion().setFromRotationMatrix(pano.panoMatrix).premultiply(rotQua) , } return pose } exportSavingData(){//输出漫游点新的坐标和朝向、以及连接信息 let sweepLocations = {} for(let datasetId in Potree.settings.datasetsPanos ) { let {panos} = Potree.settings.datasetsPanos[datasetId] let data = panos.map(pano=>{ let visibles = [] for(let id in this.panoLink[pano.id]){ if(this.panoLink[pano.id][id]){ visibles.push(viewer.images360.getPano(id).index) } } let {position, quaternion} = this.getPanoPose(pano); return Object.assign({}, pano.panosData, { uuid: pano.uuid, /* pose:{ translation: dealData(pano.position.clone() ), rotation: dealData(new THREE.Quaternion().setFromRotationMatrix(pano.panoMatrix).premultiply(rotQua) ), }, */ pose : { translation : dealData(position), rotation : dealData(quaternion) }, visibles, use_rtk : !!pano.rtkState //subgroup: 0,group: 1, "id_view":.. }) }) sweepLocations[datasetId] = {sweepLocations:data} } /* this.lineMeshes.children.forEach(e=>{//从line中搜集连接信息,而不从linkInfo,这样visibles不会重复一次 let names = e.name.split('-') //是不是该转成数字 var pano0 = names[0] var pano1 = names[1] sweepLocations.find(s=>s.uuid == pano0).visibles.push(pano1) }) */ function dealData(value){ let v = math.toPrecision(value, 6) if(v instanceof THREE.Quaternion){ return {x:v.x, y:v.y, z:v.z, w:v.w} }else if(v instanceof THREE.Vector3){ return {x:v.x, y:v.y, z:v.z} } } console.log(sweepLocations) return sweepLocations } } /* 不同数据集之间不能连线 不同楼层可能也不能 如果楼层在不同建筑物怎么办? 楼层切换按钮只能在一个建筑内切换。 全部相连时不能移动和旋转 如果未全部相连,不能保存 */ export default new PanoEditor()