import * as THREE from "../../../libs/three.js/build/three.module.js"; import SplitScreen from "../../utils/SplitScreen" import {BuildingBox} from "./BuildingBox" import Common from "../../utils/Common.js"; import {Images360} from '../Images360/Images360' const minFloorHeight = 0.5 const ifDrawDatasetBound = true //显示一下数据集的tightBound线框 var SiteModel = { entities:[], //所有实体 buildings:[], //所有建筑父集 meshGroup: new THREE.Object3D, init: function(){ viewer.scene.scene.add(this.meshGroup) this.meshGroup.name = 'siteModel' this.SplitScreen = SplitScreen this.createHeightPull(); if(Potree.settings.isTest && ifDrawDatasetBound){ viewer.on('allLoaded',()=>{ viewer.scene.pointclouds.forEach(pointcloud=>{ let boxPoints = pointcloud.getUnrotBoundPoint(); let boundingBox = new BuildingBox({ name: '数据集tightBound_'+pointcloud.dataset_id, points: boxPoints, buildType : 'dataset', zMax: pointcloud.bound.max.z, zMin: pointcloud.bound.min.z, ifDraw:true }) this.meshGroup.add(boundingBox) //boundingBox.markers.forEach(e=>e.visible = false) }) }) } if(Potree.settings.isOfficial){ let lastPos = new THREE.Vector3 let lastEntity viewer.addEventListener('camera_changed', e => { if(!this.entities.length || this.editing) return Common.intervalTool.isWaiting('sitemodelCameraInterval', ()=>{ //延时update,防止卡顿 let currPos = viewer.scene.getActiveCamera().position if(!currPos.equals(lastPos)){ lastPos.copy(currPos) let entity; if(Potree.settings.displayMode == 'showPanos'){ entity = this.entities.find(e=>e.panos.includes(viewer.images360.currentPano)) if(!entity)console.log('没找到entity') } if(!entity){ entity = this.pointInWhichEntity(currPos, 'room'); } if(lastEntity != entity ){ console.log('buildingChange', entity) entity && Potree.sdk.scene.emit('buildingChange', entity.polygon) lastEntity = entity } return true } }, 1000) }) } }, enter:function(){ Potree.Log('sitemodel enter') this.clear() //确保全部清空 this.editing = true let mapViewport = viewer.mapViewer.viewports[0] SplitScreen.splitScreen4Views({siteModel:true/* , viewports:[{name:'Top',viewport : mapViewport }] */}) viewer.viewports.forEach(e=>{ if(e.name != 'mapViewport'){ e.layersAdd('siteModelMapUnvisi') } if(e.name == 'Right' || e.name == 'Back'){ e.layersAdd('siteModeSideVisi') } }) viewer.images360.panos.forEach(pano=>{ viewer.setObjectLayers(pano.marker, 'siteModelMapUnvisi' ) }) mapViewport.layersAdd('siteModeOnlyMapVisi') //只有mapViewport能看到marker }, leave:function(){ Potree.Log('sitemodel leave') let mapViewport = viewer.mapViewer.viewports[0] SplitScreen.recoverFrom4Views() viewer.images360.panos.forEach(pano=>{ viewer.setObjectLayers(pano.marker, 'mapObjects' ) }) mapViewport.layersRemove('siteModeOnlyMapVisi') this.clear() this.editing = false } , /* startSetSiteModel:function(pos, type){//开始创建空间模型(非编辑状态的,不绘制) if(this.editing)return //编辑中不允许重新创建 this.clear() }, */ addFloor:function(parent, dirType, sid, name){//dirType:'top'|'bottom'在上方建还是下方。如果建筑中没有楼层,默认在基底建一个 let buildType = 'floor' let zMin, zMax if(parent.buildChildren.length == 0){ zMin = parent.zMin zMax = zMin + Potree.config.siteModel.floorHeightDefault }else{ if(dirType == 'bottom'){ //var btm = Common.find(parent.buildChildren,null,[e=>e.zMin]) var btm = parent.buildChildren[0] zMax = btm.zMin zMin = zMax - Potree.config.siteModel.floorHeightDefault }else{ //var top = Common.find(parent.buildChildren,null,[e=>e.zMax]) var top = parent.buildChildren[parent.buildChildren.length - 1] zMin = top.zMax zMax = zMin + Potree.config.siteModel.floorHeightDefault } } let prop = { buildType, //name : Potree.config.siteModel.names[buildType], zMin, zMax, buildParent:parent, sid, name, ifDraw:true } var floor = new BuildingBox(prop); /* parent.buildChildren.push(floor) this.meshGroup.add(floor); this.entities.push(floor) */ floor.update() this.addEntity(floor,parent) this.selectEntity(floor) this.updateBuildingZ(parent) return floor }, startInsertion:function(buildType, parent, sid, name, callback, cancelFun){ let zMin, zMax if(buildType == 'hole' || buildType == 'room'){ zMin = parent.zMin zMax = parent.zMax }else if(buildType == 'building'){ parent = null zMin = viewer.bound.boundingBox.min.z zMax = viewer.bound.boundingBox.min.z } let minMarkers = 3 let mapViewport = viewer.mapViewer.viewports[0] let entity if(buildType == 'hole'){ entity = parent.addHole() this.selectEntity(parent) entity.select() console.log('挖洞 ',entity.uuid) }else{ let prop = { buildType, //name : Potree.config.siteModel.names[buildType],//'building', zMin, zMax, buildParent:parent, sid, name, ifDraw:true } entity = new BuildingBox(prop); this.selectEntity(entity) } entity.isNew = true let timer; let endDragFun = (e) => { if (e.button == THREE.MOUSE.LEFT ) { var marker = entity.addMarker({point:entity.points[entity.points.length - 1].clone()}) //entity.editStateChange(true) //重新激活reticule状态 entity.continueDrag(marker, e) } else if (e.button === THREE.MOUSE.RIGHT ) { if(e.pressDistance < Potree.config.clickMaxDragDis )end(e);//非拖拽的话 else entity.continueDrag(null, e/* .drag.object */) } }; let end = (e={}) => {//确定、结束 if(!e.finish && entity.markers.length<=minMarkers){//右键 当个数不够时取消 //重新开始画 entity.reDraw(1) viewer.updateVisible(entity.markers[0],'unMove',false); var f = ()=>{ viewer.updateVisible(entity.markers[0],'unMove',true); entity.removeEventListener('dragChange',f) } entity.addEventListener('dragChange',f) console.log('waitcontinue') entity.continueDrag(entity.markers[0], e) return } viewer.removeEventListener('cancel_insertions', Exit); //entity.removeEventListener('unselect', Exit); clearTimeout(timer) entity.editStateChange(false) if (!e.finish && entity.markers.length > 3) { entity.removeMarker(entity.points.length - 1); entity.addHoverEvent() if(buildType == 'room'){ this.fitPullBox() } entity.isNew = false entity.addMidMarkers() pressExit && viewer.inputHandler.removeEventListener('keydown', pressExit); callback && callback(entity) }else{ this.removeEntity(entity) //直接删除没画好的,比较简单。这样就不用担心旧的continueDrag仍旧触发了 } return entity }; let Exit = (e)=>{ //entity.removeEventListener('unselect', Exit); if(viewer.inputHandler.drag){//还未触发drop的话 viewer.inputHandler.drag.object.dispatchEvent({ type: 'drop', drag: viewer.inputHandler.drag, viewer: viewer, pressDistance:0, button : THREE.MOUSE.RIGHT }); viewer.inputHandler.drag = null }else{ end({finish:true, remove:e.remove}) //未结束时添加新的polygon时会触发 } viewer.inputHandler.drag = null } viewer.dispatchEvent({ type: 'cancel_insertions' //取消之前的 }); viewer.addEventListener('cancel_insertions', Exit); //entity.addEventListener('unselect', Exit); //这个太难了,创建时也会被取消选中的 let pressExit if(!Potree.settings.isOfficial){ pressExit = (e)=>{ if(e.keyCode == 27){//Esc Exit() } } viewer.inputHandler.addEventListener('keydown', pressExit) } var marker = entity.addMarker({point:new THREE.Vector3(0, 0, 0)}) viewer.inputHandler.startDragging(marker , {dragViewport:mapViewport, endDragFun, notPressMouse:true} ); //notPressMouse代表不是通过按下鼠标来拖拽. dragViewport指定了只能在地图上拖拽 viewer.updateVisible(marker,'unMove',false);//这时候的位置是假的(0,0,0)所以先不可见 var f = ()=>{ viewer.updateVisible(marker,'unMove',true); entity.removeEventListener('dragChange',f) } entity.addEventListener('dragChange',f) if(buildType!='hole'){ this.addEntity(entity, parent) } return entity; }, createFromData:function( buildType, parent ,sid, name, points=[], holes=[], zMin, zMax, initial,panos,flagPano){ if(buildType != 'building' && buildType != 'floor' && buildType != 'room' ) return { if(initial){//初始数据错的,要自己建(只有一个building和floor) 原posIsLonlat var bound = viewer.bound.boundingBox points = [ new THREE.Vector3(bound.min.x, bound.min.y,0), new THREE.Vector3(bound.max.x, bound.min.y,0), new THREE.Vector3(bound.max.x, bound.max.y,0), new THREE.Vector3(bound.min.x, bound.max.y,0), ] zMin = bound.min.z zMax = bound.max.z /* points = points.map(e=>{ return viewer.transform.lonlatToLocal.forward(e) }) */ }else{//相对于初始数据集的模型内坐标 points = points.map(e=> this.transform(e, 'fromDataset')) } } if(buildType == 'building' ){ zMax = zMin //强制变得一样,作为基底。如果有必要,保存时再算真实的zMax。目前zMin没有保存所以数据是错的,会直接根据floor计算 } { let getPano = (id)=>{ return viewer.images360.panos.find(pano=>pano.id == id) } panos = panos ? panos.map(e=>getPano(e)) : []; flagPano = flagPano != void 0 ? getPano(flagPano) : null ; //最中心的pano 或者 最靠近该实体的pano(当panos为空时) if(!this.editing && buildType == 'floor' && !flagPano){//没有的话可能是自动添加的floor,直接用parent的吧 panos = parent.panos; flagPano = parent.flagPano; } } let prop = { buildType, points, name, sid, zMin, zMax, buildParent:parent, ifDraw:this.editing || Potree.settings.drawEntityData, panos,flagPano } let entity = new BuildingBox(prop) SiteModel.addEntity(entity, parent ) if(this.editing){ if(buildType == 'building'|| buildType == 'room'){ entity.addMidMarkers() } } holes.forEach(points =>{ let ps = points.map(e=> this.transform(e, 'fromDataset')) let hole = entity.addHole(ps) this.editing && hole.addMidMarkers() }) if(buildType == 'floor'){ this.updateBuildingZ(parent) } return entity }, transform:function(pos, type){ if(type == 'toDataset'){ let point = Potree.Utils.datasetPosTransform({ toDataset: true, position: pos.clone(), datasetId: Potree.datasetData[0].id }) return new THREE.Vector2().copy(point) }else{ let position = new THREE.Vector3().copy(pos).setZ(0) return Potree.Utils.datasetPosTransform({ fromDataset: true, position, datasetId: Potree.datasetData[0].id }) } }, addEntity:function(entity, parent){ this.meshGroup.add(entity); this.entities.push(entity) if(entity.buildType == 'building'){ this.buildings.push(entity) }else{ parent.buildChildren.push(entity) } if(entity.buildType == 'room'){ entity.addEventListener('marker_dropped',()=>{ this.fitPullBox() }) }else if(entity.buildType == 'floor'){ parent.dispatchEvent({type:'addFloor'}) } console.log('添加实体:', entity.buildType, entity.sid, entity.uuid) }, removeEntity : function(entity){ if(!this.entities.includes(entity))return if(this.selected == entity){ this.height_pull_box.visible = false this.selectEntity(null) } if(entity.buildType == 'building'){ var index = this.buildings.indexOf(entity); if(index>-1){ this.buildings.splice(index,1) } }else{ var index = entity.buildParent.buildChildren.indexOf(entity); if(index>-1){ entity.buildParent.buildChildren.splice(index,1) } } var index = this.entities.indexOf(entity); if(index>-1){ this.entities.splice(index,1) } entity.dispose() entity.dispatchEvent({type:'delete'}) console.log('删除实体:', entity.buildType, entity.sid) }, updateBuildingZ:function(building){ building.buildChildren = building.buildChildren.sort((e,a)=>e.zMin-a.zMin)//从低到高排序 building.zMin = building.zMax = building.buildChildren[0].zMin //基底高度 //building.zMax = building.buildChildren[building.buildChildren.length-1].zMax if(this.editing) building.update({dontUpdateChildren:true}) }, selectEntity : function(entity){ if(this.selected == entity || entity && entity.buildType == 'hole')return //this.buildings.forEach(e=>e.unselect()) this.selected && this.selected.unselect() this.height_pull_box.visible = false if(entity){ entity.select() } this.selected = entity if(entity && (entity.buildType == 'floor' || entity.buildType == 'room' )){ this.height_pull_box.visible = true this.fitPullBox() } }, /* selectFloor:function(floor){ this.buildings.forEach(e=>e.unselect()) floor.select() this.selected = floor this.height_pull_box.visible = true this.fitPullBox() }, selectBuilding:function(building){ this.buildings.forEach(e=>e.unselect()) building.select() } selectRoom:function(room){ this.buildings.forEach(e=>e.unselect()) room.select() } */ fitPullBox: function(){ //自适应拖拽楼层的pullMesh let bound = new THREE.Box3(); bound.expandByObject(this.selected.box) let center = bound.getCenter(new THREE.Vector3() ) let size = bound.getSize(new THREE.Vector3() ) this.height_pull_box.scale.copy(size) this.height_pull_box.position.copy(center) }, changeZ:function(entity, dirType, value){ // floor or room 修改zMin or zMax let max, min //limit if(entity.buildType == 'floor'){//楼层 let index = entity.buildParent.buildChildren.indexOf(this.selected) if(dirType == 'zMax'){ let upper = entity.buildParent.buildChildren[index+1]; entity.zMax = value min = entity.zMin + minFloorHeight if(entity.zMax < min){ entity.zMax = min }else{ if(upper){ max = upper.zMax - minFloorHeight; if(entity.zMax > max){ entity.zMax = max } } } if(upper){ upper.zMin = entity.zMax upper.update() upper.dispatchEvent({type:'changeHeight'}) } }else{ let lower = entity.buildParent.buildChildren[index-1]; entity.zMin = value max = entity.zMax - minFloorHeight if(entity.zMin > max){ entity.zMin = max }else{ if(lower){ min = lower.zMin + minFloorHeight; if(entity.zMin < min){ entity.zMin = min } } } if(lower){ lower.zMax = entity.zMin lower.update() lower.dispatchEvent({type:'changeHeight'}) } if(index == 0)this.updateBuildingZ(this.selected.buildParent) } }else if(entity.buildType == 'room'){//房间 //按照navvis的是不一定限制在当前楼层,只要高度不超过当前楼层即可。 let maxHeight = entity.buildParent.zMax - entity.buildParent.zMin if(dirType == 'zMax'){ min = entity.zMin + minFloorHeight max = entity.zMin + maxHeight entity.zMax = THREE.Math.clamp(value, min, max); }else{ min = entity.zMax - maxHeight max = entity.zMax - minFloorHeight entity.zMin = THREE.Math.clamp(value, min, max); } } entity.update() entity.dispatchEvent({type:'changeHeight'}) //this.selected.emit('update') this.fitPullBox() }, createHeightPull:function(){ //拖拽楼层的bounding box let boxGeo = new THREE.BoxBufferGeometry( 1, 1, 1/4 ) let boxMat = new THREE.MeshBasicMaterial({ color:"#F00", opacity:0 , transparent:true, depthTest:false }) let height_pull_box_up = new THREE.Mesh(boxGeo,boxMat) let height_pull_box_down = new THREE.Mesh(boxGeo,boxMat) height_pull_box_up.name = 'height_pull_box_up'; height_pull_box_down.name = 'height_pull_box_down'; this.height_pull_box = new THREE.Object3D(); this.height_pull_box.name = 'height_pull_box' this.height_pull_box.add(height_pull_box_up) this.height_pull_box.add(height_pull_box_down) this.height_pull_box.visible = false this.meshGroup.add(this.height_pull_box) height_pull_box_up.position.set(0,0,3/8) height_pull_box_down.position.set(0,0,-3/8) viewer.setObjectLayers(this.height_pull_box, 'siteModeSideVisi' ) let mouseover = (e)=>{ viewer.dispatchEvent({ type : "CursorChange", action : "add", name:"siteModelFloorDrag" }) } let mouseleave = (e)=>{ viewer.dispatchEvent({ type : "CursorChange", action : "remove", name:"siteModelFloorDrag" }) } let firstZ, firstIntersect; let drag = (e)=>{ var intersectPoint = e.intersectPoint.orthoIntersect //不要点云的intersect,只要orthocamera算出的平面intersect if(firstIntersect != void 0){ let moveZ = intersectPoint.z - firstIntersect if(this.selected.buildType == 'floor'){//楼层 //限制高度不能超过上下 if(e.target == height_pull_box_up){ if(firstZ == void 0)firstZ = this.selected.zMax this.changeZ(this.selected, 'zMax', firstZ + moveZ) }else{ if(firstZ == void 0)firstZ = this.selected.zMin this.changeZ(this.selected, 'zMin', firstZ + moveZ) } }else if(this.selected.buildType == 'room'){//房屋 if(e.target == height_pull_box_up){ if(firstZ == void 0)firstZ = this.selected.zMax this.changeZ(this.selected, 'zMax', firstZ + moveZ) }else{ if(firstZ == void 0)firstZ = this.selected.zMin this.changeZ(this.selected, 'zMin', firstZ + moveZ) } } }else{ firstIntersect = intersectPoint.z } } let drop = (e)=>{ firstZ = firstIntersect = null } height_pull_box_up.addEventListener('mousemove',mouseover) height_pull_box_down.addEventListener('mousemove',mouseover) height_pull_box_up.addEventListener('mouseleave',mouseleave) height_pull_box_down.addEventListener('mouseleave',mouseleave) height_pull_box_up.addEventListener('drag',drag) height_pull_box_down.addEventListener('drag',drag) height_pull_box_up.addEventListener('drop',drop) height_pull_box_down.addEventListener('drop',drop) }, pointInWhichEntity(position, buildType, ifIgnoreHole){//返回第一个符合标准的实体,buildType是要找的建筑类型 //由于房间可能在building外,所以房间要另外单独识别。 let lastResult; //最接近的上一层结果,如果没有result返回这个 let result let traverse = (parent)=>{ if(parent.ifContainsPoint(position)){ lastResult = parent if(parent.buildType == buildType){ return parent }else{ for(let i=0,len=parent.buildChildren.length; ie.buildType == 'room'); result = rooms.find(e=>e.ifContainsPoint(position)) }*/ //虽然房间可以画到上级之外,但是为了方便起见,假定房间绝对在楼层之内。找不到的话要调整空间模型了。 return result || lastResult } , findPanos: function(){ viewer.images360.panos.forEach(pano=>{ let result = this.pointInWhichEntity(pano.position, 'room'); {//get panos for every entities let entity = result while(entity){ entity.panos.push(pano); entity = entity.buildParent } } }) {//search center pano this.entities.forEach(entity=>{ let bound = entity.getBound(); let center = bound.getCenter(new THREE.Vector3) let request = [] let rank = [ Images360.scoreFunctions.distanceSquared({position: center}) ] let panos = entity.panos && entity.panos.length ? entity.panos : viewer.images360.panos let r = Common.sortByScore(panos, request, rank);//entity没有panos的话,就扩大到所有panos if(r && r.length){ entity.flagPano = r[0].item }else{ console.error('no flagPano??') } }) } } , findEntityForDataset:function(){ var entities = this.entities.filter(e=>e.buildType == 'room' || e.buildType == 'floor' && e.buildChildren.length == 0) entities.length && viewer.scene.pointclouds.forEach(pointcloud=>{ let volumes = [] entities.forEach(entity=>{ let volume = entity.intersectPointcloudVolume(pointcloud) volumes.push({entity, volume}) }) volumes.sort((a,b)=>{ return b.volume-a.volume }) console.log(volumes) pointcloud.belongToEntity = volumes[0].entity; //如果约等于0怎么办??? }) /* 只需要考虑 floor 和 room, 因为building的只有一个基底没高度 floor 和 room 在空间中没有完全的从属关系,因为room可以超出floor之外。所以直接混在一起来查找,但要排除有房间的楼层。 直接计算实体和点云的重叠体积 ,重叠体积大的最小实体将会拥有该点云。 */ } , clear:function(){//清空 /* entities:[], //所有实体 buildings:[], //所有建筑父集 meshGroup: new THREE.Object3D, */ this.selectEntity(null) let length = this.buildings.length; for(let i=0;i e.sid == id) let aimPano if (!entity) { return console.error('没找到entity ') } if (isNearBy && entity.panos.length) { let position = viewer.scene.getActiveCamera().position let request = [] let rank = [Images360.scoreFunctions.distanceSquared({ position })] let r = Common.sortByScore(entity.panos, request, rank) aimPano = r[0].item } else { if (!entity.flagPano) { return console.error('没找到flagPano') } aimPano = entity.flagPano } viewer.images360.flyToPano(aimPano) }, } /* 规则 层级: type 中文名 改动范围 其他 BUILDING 建筑 xy mesh由自己的基底以及所有floor的组成。如果删除所有floor,就剩一个平面。故而zMin == zMax FLOOR 楼层 z(xy未解锁) 点击楼层时房间也会显示,而建筑的其他楼层不显示线框,会显示面. 拖拽高度实际是拖拽楼层间的分界线,楼层之间不会有缝隙 ROOM 房间 xyz(xy可加锁) 可能超出楼层外,因为楼层拖拽时房间没变。所以建筑不一定包容房间。 CUSTOM 自定义(现作房间) xyz ( xy范绘制时不能超出父级外轮廓,但父级编辑时可以进入子级轮廓。只要能解锁轮廓的都能切洞) floor输入高度数值的限制和拖拽是一样的,相当于调节其zMax, 且不能超出其上层的zMax。 navvis弊端: 空间模型不会随着数据集移动而移动 (可以做成跟随,但是如果一个建筑对应多个数据集,那只能跟序号在前的数据集走) 建筑点修改后,房间可能飘出建筑外的。 楼层高度修改后也是。 调整高度时,看不到相邻的楼层界限,导致拖不动时像bug。 调整高度时,侧面看有的重叠的部分比较高亮,感觉是冗余信息?有点乱 问题: 磨砂材质 没有阴影,可directionallight 加了呀 暂定一个数据集只属于一个实体(从最小的找起) https://testlaser.4dkankan.com/indoor/t-8KbK1JjubE/api/site_model 删点全删光了要删实体吗 */ export {SiteModel}