import * as THREE from 'three' import math from './util/math.js' import common from './util/common.js' import BoundingMesh from './util/BoundingMesh.js' import Vectors from './util/Vectors.js' const version = 'output' const convertTool = { getQuaByAim: function (aim, center=new THREE.Vector3, forward=new THREE.Vector3(0, 0, -1)){ //z朝上的坐标系是 forward = new THREE.Vector3(0, 1, 0) let qua1 = new THREE.Quaternion().setFromUnitVectors(forward, aim.clone().sub(center).normalize()) //或var _ = (new THREE.Matrix4).lookAt(pano.position, aim, new THREE.Vector3(0,1,0)); aimQua = (new THREE.Quaternion).setFromRotationMatrix(_); return qua1 }, } //----------------------复制以下内容--------------------------------- let player, skyBoxTight, meshGroup, modelBound = new THREE.Box3(), ray = new THREE.Raycaster(), groundPlane = new THREE.Plane(), groundY, safeBound, boundConfirmed, //安全区域应该在扣除每种类型柜子大概的长宽的一半 hue = 0, startTime, isLaser, boxesSolid = [] const MinBoxInitialScore = 0.68 //找不到匹配时,若box分数低于该值,不createSinglePano let standards = { cabinet: { widthNormal: { min: 0.55, max: 1.05 /* max: 0.65 */ }, //widthNormal是不计宽还是厚度的平均宽度 //个别场景如S9yepREK8Jl 宽0.8米 height: { min: 0.3, max: 2.5, standard: 2 }, closeRatio: 0.7, //数值越小越容易findRest。一般在墙上的位置不准要设置大些,扎堆放置的设置小些 }, fire: { widthNormal: { min: 0.12, max: 0.16 }, height: { min: 0.4, max: 0.58 }, widthSame: true, //长宽相等 closeRatio: 4, tinyXZ: true, //可以通过它近似确定地面高度 }, air: { widthNormal: { min: 0.35, max: 0.7 }, width: { min: 0.48, max: 0.75 }, //因为总是斜着放所以范围较大 thick: { min: 0.33, max: 0.5 }, height: { min: 1.2, max: 2.2, standard: 1.8 }, atWall: 0.8, closeRatio: 1.1, }, airSmart: { widthNormal: { min: 0.35, max: 0.7 }, width: { min: 0.48, max: 0.75 }, //因为总是斜着放所以范围较大 thick: { min: 0.33, max: 0.5 }, height: { min: 1.2, max: 2.2, standard: 1.8 }, atWall: 0.8, closeRatio: 1.1, }, 'air-hanging': { widthNormal: { min: 0.3, max: 1 }, width: { min: 0.8, max: 1.1 }, thick: { min: 0.2, max: 0.3 }, height: { min: 0.3, max: 0.5, standard: 0.4 }, //standard是通常出现的最高高度. 有这个值的在离地的时候直接使用该高度 bottom: { min: 0.8, max: 2.0 }, // 不绝对,大部分 atWall: 1, //在墙壁的可能性 closeRatio: 1.5, }, battery: { widthNormal: { min: 0.45, max: 1.35 }, width: { min: 0.7, max: 1.4 }, thick: { min: 0.35, max: 0.5 }, height: { min: 0.3, max: 2.5 }, //maxHeight //有的电池很小。考虑是否追加battery-little 并且限制大电池的长宽高比例 atWall: 0.9, closeRatio: 0.9, }, groundBar: { widthNormal: { min: 0.06, max: 0.4 }, width: { min: 0.3, max: 0.5 }, thick: { min: 0.05, max: 0.08 }, height: { min: 0.1, max: 0.2 }, bottom: { min: 1.2, max: 3 }, atWall: 1, closeRatio: 2, }, hlkcWindow: { widthNormal: { min: 0.08, max: 0.5 }, width: { min: 0.35, max: 0.5 }, thick: { min: 0.03, max: 0.06 }, height: { min: 0.35, max: 0.5 }, bottom: { min: 1.2, max: 3 }, atWall: 1, closeRatio: 2, }, electric: { widthNormal: { min: 0.2, max: 0.7 }, width: { min: 0.5, max: 0.65 }, thick: { min: 0.2, max: 0.3 }, height: { min: 0.5, max: 1 }, //maxHeight 4GqaqNdyjGf一米高 bottom: { min: 0.8, max: 1.8 }, atWall: 1, closeRatio: 2, }, monitor: { widthNormal: { min: 0.08, max: 0.11 }, height: { min: 0.1, max: 0.2, standard: 0.15 }, //maxHeight bottom: { min: 1.2, max: 3 }, atWall: 1, closeRatio: 4, tiny: true, //因为较小且无方向,所以近似一个点,用射线算出的位置比墙面还准,所以其最终位置可用于expandModelBound }, rowBigBox: { widthNormal: { min: 0.55, max: Infinity }, height: { min: 0.7, max: 2.4, standard: 2 }, }, /*"cabling-wall":{ }, "cabling-ceil":{ atCeil:1 } */ } /* const typeNames = { cabinet : 'cabinet', //标准机柜 air : 'air', //普通空调柜式 battery : 'battery', //蓄电池组 } */ const typeNames = { fire: 'extinguisher', //灭火器 monitor: 'surveillance_camera', //监控摄像机 放第一个,用于继续确定边界 hlkcWindow: 'hlkc', //馈线窗 groundBar: 'grounding_bar', //接地排 cabinet: 'equipment_cabinet', //标准机柜 battery: 'accumulator', //蓄电池组 /* ac : 'ac_switchboard', //交流配电柜 //这两种合并,因为差别太小了,见QlJau21WP8G的,在全景图两侧识别的竟然一个ac一个dc dc : 'dc_distribution', //直流配电设备 */ electric: ['ac_switchboard', 'dc_distribution'], air: 'sdkt', //普通空调柜式 airSmart: 'ventilation_installation', //智能通风设备 //cabling : 'cabling_rack', //单层走线架 因为在天花板的走线有点复杂,经常断开,无法确定方向所以放弃 } /* const typeNamesReverse = {} //为了方便访问 for(let i in typeNames){ typeNamesReverse[typeNames[i]] = i } */ let addLine = (origin, dir, len, color) => { return if (version != 'vision') return var line1 = LineDraw.createLine([origin, origin.clone().add(dir.clone().multiplyScalar(len || 1))], { color }) //console.log(origin.toArray(), dir.toArray()) meshGroup.add(line1) return line1 } let addLabel = (pos, text, { bgcolor, a } = {}) => { if (version != 'vision') return let shift = new THREE.Vector3(0, -0.2, 0) bgcolor = bgcolor ? new THREE.Color(bgcolor) : { r: 1, g: 1, b: 1 } //let endPos = new THREE.Vector3().addVectors(pos,shift); text instanceof Array || (text = [text]) let lineCount = Math.round(Math.random() * 6) + 1 let lines = [] while (lineCount-- > 0) { lines.push('|') } text = [...text, ...lines, 'o'] let textMesh = new TextSprite({ text, textColor: { r: 0, g: 0, b: 0, a: 1 }, backgroundColor: { r: bgcolor.r * 255, g: bgcolor.g * 255, b: bgcolor.b * 250, a: a || 0 }, textBorderColor: { r: bgcolor.r * 255, g: bgcolor.g * 255, b: bgcolor.b * 250, a: a || 0.9 }, textBorderThick: 2, margin: { x: 0, y: 0 }, borderRadius: 0, player: player, sizeInfo: { minSize: 90, maxSize: 300, nearBound: 1, farBound: 7 }, }) textMesh.position.copy(pos) //textMesh.scale.set(0.3, 0.3, 0.3) meshGroup.add(textMesh) textMesh.sprite.position.y += textMesh.sprite.scale.y * 0.4 return textMesh } let getBoxFinalPos = info => { //创建solidbox时的position let position let center = getBoxPos(info) if (standards[info.boxType].bottom) { //悬挂 position = center } else { position = center.clone().setY(groundY + info.size.y / 2) //使着地 } return position } const axises = [ new THREE.Vector3(-1, 1, -1), new THREE.Vector3(1, 1, -1), new THREE.Vector3(1, 1, 1), new THREE.Vector3(-1, 1, 1), new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, -1, -1), new THREE.Vector3(1, -1, 1), new THREE.Vector3(-1, -1, 1), ] let traverse = (info, fun) => { //忽略.infos的row 的信息 fun(info) info.list && info.list.forEach(a => traverse(a, fun)) info.mixedFrom && info.mixedFrom.forEach(a => traverse(a, fun)) } class Box { //结果 constructor(info) { //preDealBox(info) this.setFromInfo(info) this.buildFromData || (this.name = this.boxType + '-' + this.name) if (version == 'vision' && (this.buildFromData || boundConfirmed)) this.draw() boxesSolid.push(this) } setFromInfo(info) { for (let i in info) { this[i] = info[i] } /* let h = info.size.y let standardH = standards[info.boxType].height.standard if (h > standardH) { h = standardH + Math.log(1 + (h - standardH) / 2) //Math.log2: 以2为底的对数 ,Math.log:自然对数 info.size.y = h } */ this.position = this.buildFromData ? this.center : getBoxFinalPos(this) let bound = new THREE.Box3().setFromCenterAndSize(this.position, this.size) this.bound = bound } draw() { hue += 0.23 var color = new THREE.Color().setHSL(hue, 0.9, 0.85) this.boxHelper = new THREE.Box3Helper(this.bound, color) this.boxHelper.material.depthTest = false this.boxHelper.material.transparent = true this.boxHelper.renderOrder = 30 /* let { warnStr, exStr } = this warnStr && (exStr += `【${warnStr}】`) */ this.label = addLabel(this.position, /* exStr ? [this.name, exStr] : */ this.score ? [this.name, this.score.toFixed(1)] : this.name, { bgcolor: color }) /* //color = new THREE.Color('#fff') let box = new THREE.Mesh(new THREE.BoxBufferGeometry(), new THREE.MeshBasicMaterial({color, opacity:0.5, transparent:true})) box.position.copy(this.position) box.scale.copy(this.size) meshGroup.add(box) */ meshGroup.add(this.boxHelper) } dispose() { let index = boxesSolid.indexOf(this) if (index > -1) { boxesSolid.splice(index, 1) if (version == 'vision' && boundConfirmed) { this.label.sprite.material.opacity = 0.3 this.boxHelper.material.opacity = 0.2 } } } traversePair(fun) { traverse(this, fun) } getDirection() { //获得正面朝向。 需要全部box都创建完再调用 let xProp = this.xProp let dir //哪边pano多朝哪边 if (this.boxType == 'cabinet' && this.name.includes('row')) { if (this.infos.rowboxs.length > 1) { let k = this.infos.reduce((w, c) => { return w + c.k }, 0) xProp = k > 1 ? 'width' : 'thick' } else { //直接使用别的多box的row的方向 ,大多数都相同(会有例外,无所谓了) let rows = boxesSolid.filter(e => e.boxType == 'cabinet' && e.name.includes('row') && e.dirQua) rows.sort((a, b) => b.infos.rowboxs.length - a.infos.rowboxs.length) let box = rows[0] if (box) { return (this.dirQua = box.dirQua) } } } if (!xProp) { //根据房间的长宽 散落的cabinet。 fire monitor let { xWidthPossible, yWidthPossible } = getBoxDirProp(this, true) xProp = this.xProp /* if(Math.abs(xWidthPossible - yWidthPossible) < 0.3){ let size = new THREE.Vector3() safeBound.getSize(size) xWidthPossible += (size.x - size.z) * 0.5 if(xWidthPossible > yWidthPossible){ xProp = 'width' } } */ } if (xProp) { if (!this.panosDir) getPanosDir(this) if (xProp == 'width') { if (Math.abs(this.panosDir['z+']) < Math.abs(this.panosDir['z-'])) { //朝-z//也就是新坐标系的y //this.dirQua='下' dir = new THREE.Vector3(0, 1, 0) } else { //朝+z //this.dirQua='上' dir = new THREE.Vector3(0, -1, 0) } } else { if (Math.abs(this.panosDir['x+']) < Math.abs(this.panosDir['x-'])) { //朝-x //this.dirQua='右' dir = new THREE.Vector3(-1, 0, 0) } else { //朝+x //this.dirQua='左' dir = new THREE.Vector3(1, 0, 0) } } } //addLabel(this.position, this.dirQua) this.dirQua = convertTool.getQuaByAim(dir, new THREE.Vector3(), new THREE.Vector3(0, 1, 0)) return this.dirQua /*飞到俯视图查看(不旋转视图,x朝右,z朝下)。以下四个qua是四个墙壁每个墙壁上的dirQua。 _x: 0, _y: -0, _z: 1, _w: 0 _x: 0, _y: 0, _z: -0.707, _w: 0.707 _x: 0, _y: -0, _z: 0.707, _w: 0.707 x: 0, _y: 0, _z: 0, _w: 1 */ } toJson() { //转出的坐标系是z朝上的 let category = typeNames[this.boxType] if (category instanceof Array) { let scoreMap = new Map() category = category.slice(0) category.forEach(e => scoreMap.set(e, 0)) //初始化 //判断boxType: 寻找所使用的box总分最高的boxType let add = box => { if (!box) return let score = scoreMap.get(box.category) + box.score scoreMap.set(box.category, score) } this.traversePair(e => { add(e.box0) add(e.box1) }) category = category.sort((a, b) => { return scoreMap.get(b) - scoreMap.get(a) }) category = category[0] //最高分 } this.getDirection() let json = { points: axises.map(axis => math.invertVisionVector(new THREE.Vector3().addVectors(this.position, this.size.clone().multiply(axis).multiplyScalar(0.5))).toArray()), category, type: this.boxType, sid: this.name, quaternion: this.dirQua.toArray(), } return json } } // 2d坐标转3d坐标 let getDirByUV = (uv, pano) => { // 计算方向向量 let yaw = -uv.x * (Math.PI * 2) let pitch = Math.PI / 2 - uv.y * Math.PI let dir = new THREE.Vector3() dir.copy(Vectors.RIGHT).applyAxisAngle(Vectors.BACK, pitch).applyAxisAngle(Vectors.UP, yaw).applyQuaternion(pano.quaternion) return dir } let getCenterDir = box => { if (box.centerDir) return //假设不存在在box中间拍摄的情况,所以y不会横跨两边 let bbox = box.bbox2 let center = { x: getBbox2center(bbox[0], bbox[2]), y: (bbox[1] + bbox[3]) / 2 } box.bbox2CenterX = center.x let dir = getDirByUV(center, box.pano) box.centerDir = dir let centerTop = { x: center.x, y: bbox[1] } box.centerTopDir = getDirByUV(centerTop, box.pano) let centerBtm = { x: center.x, y: bbox[3] } box.centerBtmDir = getDirByUV(centerBtm, box.pano) let leftBtm = { x: bbox[0], y: bbox[3] } box.leftBtmDir = getDirByUV(leftBtm, box.pano) let rightBtm = { x: bbox[2], y: bbox[3] } box.rightBtmDir = getDirByUV(rightBtm, box.pano) } let getOtherPos = box => { if (!box.boxType) return let config = standards[box.boxType] if (!boundConfirmed) { if (!config.bottom) { ray.set(box.pano.position, box.centerBtmDir) box.btmPosPredict = ray.ray.intersectPlane(groundPlane, new THREE.Vector3()) //没有的话就在空中 (部分air-hanging也会有)。 fire的这个值会较大误差,因为groundY还不确定,但不影响,因只用它射线交点的位置。 if (box.btmPosPredict) { let dir2d = new THREE.Vector2(box.centerBtmDir.x, box.centerBtmDir.z).normalize() let { min, max } = standards[box.boxType].widthNormal min = min * 0.4 max = max * 0.4 let minA = Math.min(Math.abs(dir2d.x), Math.abs(dir2d.y)) const depth = math.linearClamp(minA, 0, 1, min, max) /* const depth = box.category == typeNames.cabinet ? 0.5 : 0.4 */ dir2d = dir2d.clone().multiplyScalar(depth) box.btmPosPredict.x += dir2d.x box.btmPosPredict.z += dir2d.y //addLabel(box.btmPosPredict,'b_'+box.category+"_"+box.sid, {bgcolor:'#6ff',a:0.1}) //box.btmPosPredict.clamp(safeBound.min, safeBound.max) } } return } if (!box.posAtWall && config.atWall > 0) { //console.log(box.sid, 'getPosWall') const shrink = config.thick ? config.thick.min : config.widthNormal.min ray.set(box.pano.position, box.centerDir) let o = ray.intersectObjects([skyBoxTight]) if (o[0]) box.posAtWall = new THREE.Vector3().addVectors(box.pano.position, box.centerDir.clone().multiplyScalar(o[0].distance - shrink)) //因墙壁不准确,所以还是尽量不用墙的位置 /* if(box.btmPos){ let wallRatio = 0.5; if(new THREE.Vector3().subVectors(box.btmPos, box.posAtWall).setY(0).length() > 1 )wallRatio = 0.2 //可能墙壁位置不准,靠后了 box.predictCenter = new THREE.Vector3().addVectors(box.btmPos.clone().multiplyScalar(1-wallRatio), box.posAtWall.clone().multiplyScalar(wallRatio)) //box.predictCenter = new THREE.Vector3().addVectors(box.posAtWall, box.btmPos).multiplyScalar(0.5) //也许能当中心点? 虽然y会低一些 addLabel(box.predictCenter, box.sid+'-preC') } */ } if (!box.btmPos) { getBoxBtm(box) } if (!box.topPos) { getBoxTop(box) } } let getUVs = (box, imageWidth, imageHeight) => { if (box.bbox2) return let uvs = [] if (!imageWidth) { imageWidth = global.boxFrame.datas[box.pano.id].imageWidth imageHeight = global.boxFrame.datas[box.pano.id].imageHeight } if (imageWidth != imageWidth || imageHeight != imageHeight) { console.log(imageWidth, imageHeight) } box.bbox2 = box.bbox.map((e, i) => { //(x1,y1,x2,y2) return i % 2 == 0 ? e / imageWidth /* + 0.25 */ : e / imageHeight }) } let getBoxBase = (box, imageWidth, imageHeight) => { getBoxType(box) getUVs(box, imageWidth, imageHeight) getCenterDir(box) getOtherPos(box) } let getBbox2Diff = (x1, x2) => { //获取x1-x2,如果x1在x2右边则为正 if (Math.abs(x1 - x2) < 0.5) return x1 - x2 else { if (x1 > x2) x1 -= 1 else x2 -= 1 return x1 - x2 } } let getBbox2center = (x1, x2) => { //找中间位置 if (Math.abs(x1 - x2) > 0.5) { //永远找小于180度的那一边 return (x1 + x2 + 1) / 2 //另外半边 } else { return (x1 + x2) / 2 } } let getBoxTop = info => { /* if(info.sid == 'pano0-11(mix4,8)'){ console.log(5) } */ if (info.box1) { let o2 = getIntersect2(info.box0.pano.position, info.box0.centerTopDir, info.box1.pano.position, info.box1.centerTopDir) info.topPos = o2.pos3d info.diffHeight = o2.mid2 ? o2.mid2.distanceTo(o2.mid1) : 1 if (info.box0.topPos && info.box1.topPos) { info.topPos.y = (info.box0.topPos.y + info.box1.topPos.y) / 2 //原先的不准 } } else { //取btm上方对应的位置 ( 因为和skybox的交点会因离墙远而偏上或偏下) let box = info.box0 || info let btm = box.btmPos if (!btm) { btm = getBoxBtm(info) } box.topPos = btm.clone() //xz同btm,要求y let xDelta = btm.x - box.pano.position.x let zDelta = btm.z - box.pano.position.z let yDelta //因为pano有旋转所以btm和top的xz其实是不一样的,所以会有误差。 故这里选择delta较大的 if (Math.abs(xDelta) < Math.abs(zDelta)) { yDelta = (zDelta * box.centerTopDir.y) / box.centerTopDir.z /* console.log('use z', box) if(Math.abs(xDelta)<0.1)console.error('!!!!!!!!!!!!!!!!!!!!!!! xDelta',xDelta, box.sid ) */ } else { yDelta = (xDelta * box.centerTopDir.y) / box.centerTopDir.x // console.log('use x', box) } box.topPos.y = yDelta + box.pano.position.y let minHeight = /* info.boxType ? standards[info.boxType].height.min : box.category == typeNames.air ? 0.5 : */ standards[getBoxType(box)].height.min let diffH = Math.max(box.topPos.y - btm.y, minHeight) box.topPos.y = btm.y + diffH info.topPos = box.topPos /* if (box.sid == 'pano2-1') { addLabel(box.topPos,'t_'+box.sid,{bgcolor:'#ff4399'}) addLine(box.pano.position,box.centerTopDir, 20) } */ } return info.topPos } let getBoxBtm = info => { if (info.box1) { let o2 = getIntersect2(info.box0.pano.position, info.box0.centerBtmDir, info.box1.pano.position, info.box1.centerBtmDir) info.btmPos = o2.pos3d //info.btmPos.y = (info.box0.btmPos.y + info.box1.btmPos.y)/2 //原先的不准 } else { let box = info.box0 || info if (!box.btmPos) { if (box.sid == 'pano0-7') { console.log(3) addLine(box.pano.position, box.centerBtmDir, 20) } if (!boundConfirmed) { return box.btmPosPredict } ray.set(box.pano.position, box.centerBtmDir) let o = ray.intersectObjects([skyBoxTight]) //如果skybound有问题,位置就会错 box.btmPosOri = o[0].point.clone() let depth //缩进 //let depth = Math.abs(o[0].face.normal.y) > 0.9 ? 0.4 : -0.4 getBoxType(box) let dir2d = new THREE.Vector2(box.centerBtmDir.x, box.centerBtmDir.z).normalize() if (standards[box.boxType].thick && standards[box.boxType].atWall /* box.boxType == 'battery' */) { //平贴于墙上,且厚度和宽度相差较大 //注:air-hanging主要用的是posAtWall let { min, max } = standards[box.boxType].widthNormal min = min * 0.3 max = max * 0.5 if (!box.xProp) getBoxDirProp(box) if (box.xProp == 'width') { depth = math.linearClamp(Math.abs(dir2d.x), 0, 1, min, max) } else { depth = math.linearClamp(Math.abs(dir2d.y), 0, 1, min, max) } } else { let w = standards[box.boxType].thick || standards[box.boxType].widthNormal let w0 = (w.min + w.max) / 2 let min = w0 * 0.5, max = w0 * 0.8 let minA = Math.min(Math.abs(box.centerBtmDir.x), Math.abs(box.centerBtmDir.z)) depth = math.linearClamp(minA, 0, 0.707, min /* 0.3 */, max /* 0.5 */) //在45度时需要最长的距离。主要针对cabinet } if (Math.abs(o[0].face.normal.y) < 0.9) { if (standards[box.boxType].atWall || o[0].point.y - groundY > 0.3) { //battery的识别框比较乱,有可能一个电池被识别出好几个,所以 depth *= -1 //at wall } } let dir2d1 = dir2d.clone().multiplyScalar(depth) box.btmPos = o[0].point.clone() box.btmPos.x += dir2d1.x box.btmPos.z += dir2d1.y //addLabel(box.btmPos,'b_'+box.sid,{bgcolor:'#ff4399'}) } info.btmPos = box.btmPos } return info.btmPos } let getIntersect = (pano0Pos, dir0, pano1Pos, dir1) => { let pos0 = new THREE.Vector3().addVectors(pano0Pos, dir0) let pos1 = new THREE.Vector3().addVectors(pano1Pos, dir1) /* var {pos3d, mid1, mid2, behind} = math.getLineIntersect({ A: pano0Pos.clone(), B: pano1Pos.clone(), p1: pos0, p2: pos1 }) */ //三维线若接近平行,算出的交点可能很近,不是实际应该无交点才对 var pos2d = math.isLineIntersect( [ { x: pano0Pos.x, y: pano0Pos.z }, { x: pos0.x, y: pos0.z }, ], [ { x: pano1Pos.x, y: pano1Pos.z }, { x: pos1.x, y: pos1.z }, ], true ) //优先考虑水平面方向的交点 if (pos2d) { let y0 = ((pos2d.x - pano0Pos.x) * dir0.y) / dir0.x + pano0Pos.y let y1 = ((pos2d.x - pano1Pos.x) * dir1.y) / dir1.x + pano1Pos.y //console.log(y1-y0) let pos3d = new THREE.Vector3(pos2d.x, (y0 + y1) / 2, pos2d.y) return { pos3d, diffHeight: Math.abs(y0 - y1) } //diffHeight越小越好 } } //究竟哪个比较准 - - 可能两个都判断? let getIntersect2 = (pano0Pos, dir0, pano1Pos, dir1) => { let pos0 = new THREE.Vector3().addVectors(pano0Pos, dir0) let pos1 = new THREE.Vector3().addVectors(pano1Pos, dir1) let o = math.getLineIntersect2({ A: pano0Pos.clone(), B: pano1Pos.clone(), p1: pos0, p2: pos1, dir0, dir1 }) //不用getLineIntersect,因为这个针对热点写的,当无交点时选用的点不是想要的 if (!o.pos3d) { console.error('getIntersect2 no result? ?') } return o } let getBoxPos = info => { let boxType = getBoxType(info) return ( (info.preDealRes && info.preDealRes.position) || info.center || (boxType && (standards[boxType].atWall > 0.5 && standards[boxType].bottom ? info.posAtWall : info.btmPos || info.btmPosPredict)) || info.posAtWall ) } let isType = (category, type) => { return type == category || typeNames[type] instanceof Array ? typeNames[type].includes(category) : typeNames[type] == category } let getBoxType = info => { if (info.boxType) return info.boxType let category = info.category || info.box0.category if (category == 'rowBigBox') info.boxType = 'rowBigBox' else { let type if (category == 'ac_switchboard') { console.log(1) } for (let i in typeNames) { /* if (i == category || typeNames[i] instanceof Array ? typeNames[i].includes(category) : typeNames[i] == category) { type = i break //type = typeNamesReverse[type] } */ if (isType(category, i)) { type = i break } } /* if(type == 'ac' || type == 'dc'){ type = 'electric' //合并 } */ info.boxType = type } return info.boxType } /* let getBoxType = info => { let type = info.category || info.box0.category if (type == 'air') { let btm = info.btmPos || info.btmPos //btmPosAtWall if (!btm) { btm = getBoxBtm(info) } if (!btm) return let center = info.posAtWall || (info.preDealRes && info.preDealRes.position) || info.center const s = standards['air-hanging'] if (btm.y - groundY > s.bottom.min) { let h0 = btm.y - groundY let h1 = (modelBound.max.y - center.y) / (modelBound.max.y - modelBound.min.y) let h2 = center.y - btm.y let score = h0 * 2 - h1 * 3 - h2 * 3 if (score > 0) { type = 'air-hanging' } //console.error( score, h0,h1,h2, info.sid||info.name) } //注意:如果air被遮住底部,露出的部分只有一点,还是有可能被识别成air-hanging。只能希望 //console.error( type, info.sid||info.name) } if (info.box0) { info.boxType = type //info.box1 && (info.box1.type = type) //因为box0和box1不一定匹配,所以不能直接赋值 } else { info.type = type } } */ let getPanosDir = (info, center) => { center = center || getBoxPos(info) let dirs = { 'x+': 0, 'x-': 0, 'z+': 0, 'z-': 0, got: false } let getDirs = () => { //靠墙的在它到墙之间是不会有漫游点的 if (dirs.got || !center) return player.model.panos.list.forEach(pano => { let dir = new THREE.Vector3().subVectors(pano.position, center) if (dir.x > 0) { dirs['x+'] += dir.x } else { dirs['x-'] += dir.x } if (dir.z > 0) { dirs['z+'] += dir.z } else { dirs['z-'] += dir.z } }) dirs.got = true } getDirs() if (info.panosDir) { console.error('already has dir') } info.panosDir = dirs return dirs } let getBoxDirProp = (info, force) => { //仅适用于方形单个房间房间,不可以是多边形、两个房间 let xProp, yProp if (info.name == 'pano10-8&pano12-6') { console.log(4) } //if (info.boxType == 'battery' || info.boxType == 'air-hanging' || info.boxType == 'air' || info.category == 'battery' || info.category == 'air') { if ((standards[info.boxType].atWall && standards[info.boxType].thick) || force) { //根据比例判断 /* let r1 = Math.abs((center.x - skyBoxTight.position.x) / (center.z - skyBoxTight.position.z)) let r2 = player.model.size.x / player.model.size.z if(!math.closeTo(r1,r2, 0.05)){ if (r1 { if (info.boxposes) { //是matchInfo info.boxposes.forEach(e => { xWidthPossible += e.xWidthPossible yWidthPossible += e.yWidthPossible }) } }) } } /* if(noX!=noZ){ if(noX)xWidthPossible } */ if (xWidthPossible > yWidthPossible) { ;(xProp = 'width'), (yProp = 'thick') //贴附x(横)墙 } else { ;(xProp = 'thick'), (yProp = 'width') //贴附y(竖)墙 } } xProp && ((info.xProp = xProp), (info.yProp = yProp)) return { xWidthPossible, yWidthPossible } } } let preDealBox = matchInfo => { if (matchInfo.preDealRes || !matchInfo.center) return matchInfo.preDealRes = {} matchInfo.boxType || getBoxType(matchInfo) let config = standards[matchInfo.boxType] let minWidth = config.widthNormal.min let needGetPose let dis = safeBound.distanceToPoint(matchInfo.center) if (matchInfo.name == 'pano2-5') { console.log(4) } if (dis > 0.3 && !config.tiny && !config.tinyXZ) { //Tmo1vLp9Q13: hlkcWindow超出才准确 //tiny的位置优先级高于bound,因为他们可以确定bound /* matchInfo.str && matchInfo.str.includes('outsideBound') */ //const shrink = minWidth * 0.85 //addLabel(matchInfo.center, '原') let finalPos = matchInfo.center.clone().clamp(safeBound.min, safeBound.max) matchInfo.preDealRes.position = finalPos //addLabel(finalPos, 'finalPos') getBoxType(matchInfo) needGetPose = true } let center = getBoxPos(matchInfo) if (needGetPose || !matchInfo.boxposes) { matchInfo.boxposes = [] ;[matchInfo.box0, matchInfo.box1].forEach(box => { box && matchInfo.boxposes.push(getBoxPoseByPos(box, center)) }) } //----------------- getBoxDirProp(matchInfo) } let getBoxPoseByPos = (box, centerPos, addDis = 0) => { //当得知box的大概位置时,求box在这个角度上的宽度、朝向 //在这个方向看的box的宽度 let config = standards[box.boxType] let angle = getBbox2Diff(box.bbox2[2], box.bbox2[0]) * Math.PI //角度的一半 let dis = new THREE.Vector3().subVectors(box.pano.position, centerPos).setY(0).length() + addDis let projectWidth = 2 * Math.tan(angle) * dis //投影宽度 (准确的投影宽度无法求得,只能近似) let camDir = /* new THREE.Vector2(centerPos.x-box.pano.position.x, centerPos.z-box.pano.position.z).normalize() */ box.centerDir.clone().setY(0).normalize() let camTangent = math.getNormal({ points: [ { x: 0, y: 0 }, /* camDir */ { x: camDir.x, y: camDir.z }, ], }) //视线切线方向 camTangent.x = Math.abs(camTangent.x) camTangent.y = Math.abs(camTangent.y) /* if (box.sid == 'pano4-6') { console.log(7) } */ const maxWidth = config.widthNormal.max const minWidth = config.widthNormal.min let minProjectWidth //= (camTangent.x + camTangent.y) * minWidth let maxProjectWidth //= (camTangent.x + camTangent.y) * maxWidth let maxX, maxY, minX, minY if (!standards[box.boxType].thick) { minProjectWidth = (camTangent.x + camTangent.y) * minWidth maxProjectWidth = (camTangent.x + camTangent.y) * maxWidth //该角度下该类型允许的最大投影距离 maxX = THREE.MathUtils.clamp((projectWidth - minWidth * camTangent.y) / camTangent.x, minWidth, maxWidth) //可得x的最大值(假设y为最小值) maxY = THREE.MathUtils.clamp((projectWidth - minWidth * camTangent.x) / camTangent.y, minWidth, maxWidth) //可得y的最大值(假设x为最小值) minX = THREE.MathUtils.clamp((projectWidth - maxWidth * camTangent.y) / camTangent.x, minWidth, maxWidth) //可得x的最小值(假设y为最大值) minY = THREE.MathUtils.clamp((projectWidth - maxWidth * camTangent.x) / camTangent.y, minWidth, maxWidth) //可得y的最小值(假设x为最大值) } else { const minThick_ = config.thick.min const minWidth_ = config.width.min const maxThick_ = config.thick.max const maxWidth_ = config.width.max let maxProjectWidth1 = camTangent.x * maxWidth_ + camTangent.y * maxThick_ let maxProjectWidth2 = camTangent.x * maxThick_ + camTangent.y * maxWidth_ let minProjectWidth1 = camTangent.x * minWidth_ + camTangent.y * minThick_ let minProjectWidth2 = camTangent.x * minThick_ + camTangent.y * minWidth_ minProjectWidth = Math.min(minProjectWidth1, minProjectWidth2) maxProjectWidth = Math.max(maxProjectWidth1, maxProjectWidth2) let a = (camTangent.x + camTangent.y) * maxWidth //console.log('diffaaaaaa',maxProjectWidth,a, box.sid) maxX = THREE.MathUtils.clamp((projectWidth - minThick_ * camTangent.y) / camTangent.x, minThick_, maxWidth_) //可得x的最大值(假设y为最小值) maxY = THREE.MathUtils.clamp((projectWidth - minThick_ * camTangent.x) / camTangent.y, minThick_, maxWidth_) //可得y的最大值(假设x为最小值) minX = THREE.MathUtils.clamp((projectWidth - maxWidth_ * camTangent.y) / camTangent.x, minThick_, maxWidth_) minY = THREE.MathUtils.clamp((projectWidth - maxWidth_ * camTangent.x) / camTangent.y, minThick_, maxWidth_) } /* let maxX = projectWidth / camTangent.x //可得x的最大值(假设y为0) let maxY = projectWidth / camTangent.y //可得y的最大值(假设x为0) */ //判断方向 let o = { box, projectWidth, camTangent, maxProjectWidth, minProjectWidth, dis, maxX, maxY, minX, minY } if (config.atWall > 0 /* isType(box.category,battery) || isType(box.category,air) */) { //为了获取朝向 o.xWidthPossible = -Math.abs(projectWidth - camTangent.x * maxWidth - camTangent.y * minWidth) o.yWidthPossible = -Math.abs(projectWidth - camTangent.x * minWidth - camTangent.y * maxWidth) //在接近45度时容易不准。另外如果被遮住一部分更会错,因此尽量不让被遮住的匹配 } return o } let getPoseScore = (boxposes, boxType) => { let score = 0 const minDis = 1.5 /* if (boxposes[0].box.sid == 'pano12-13' && boxposes[1].box.sid == 'pano8-6') { console.log(4) } */ boxposes.forEach(pose => { //pose.lowR = pose.dis < minDis ? Math.pow(THREE.MathUtils.smoothstep(pose.dis / minDis, 0, 1),2) : 1 //太近的话误差大 pose.lowR = pose.dis < minDis ? Math.pow(pose.dis / minDis, 1.4) : 1 //太近的话误差大 if (pose.projectWidth > pose.maxProjectWidth) { score += Math.pow((pose.projectWidth / pose.maxProjectWidth - 1) * pose.lowR, 2) * 500 //超过的话数字较大所以乘的数小一些 } else if (/* isSingle && */ pose.projectWidth < pose.minProjectWidth) { score += Math.pow((pose.minProjectWidth / pose.projectWidth - 1) * pose.lowR, 2) * 500 } let { min, max } = standards[boxType].widthNormal if ((standards[boxType].atWall == 1 && min < 0.3) || (boxposes.length == 2 && boxType == 'battery')) { let { min, max } = standards[boxType].widthNormal //let r = Math.max(0.001, (pose.projectWidth - min) / (max - min)) let diff = max - min let r = math.linearClamp(pose.projectWidth, min, min + diff * 0.5, 600, 0) //if(boxposes.length == 2 && boxType == 'battery'){//由于battery经常出现遮挡,导致方向出错,因此尽量不匹配projectWidth较短的,尽管这本有可能是对的 score += r //} /* if (min < 0.3 && r < 0.5) { //从贴近墙面的位置看侧面的话,容易被挡住,不准,尤其是电箱 score += ((max - min) / r / min) * 5 } */ } }) score = Math.min(score, 1300) //压低一点,因为得的宽度可能不准 if (boxposes.length == 2) { //每一个方向对应有四个方向(每个象限一个)看到的projectWidth应该接近。 //先把camTangent转化为第一个象限的 let camTangent0 = new THREE.Vector2(Math.abs(boxposes[0].camTangent.x), Math.abs(boxposes[0].camTangent.y)) let camTangent1 = new THREE.Vector2(Math.abs(boxposes[1].camTangent.x), Math.abs(boxposes[1].camTangent.y)) let a = camTangent0.dot(camTangent1) if (a > 0.8) { //WcLVXvmV9AU //0.9: 25度之内. 0.8: 36.8度之内 let diff = Math.abs(boxposes[0].projectWidth - boxposes[1].projectWidth) boxposes.score2 = a * diff * 1300 * boxposes[0].lowR * boxposes[1].lowR score += boxposes.score2 //console.warn('在同一个方向看到的projectWidth应该接近。 ', diff) } boxposes.camTangentCos = a } //要不要加上minX等的差距? score = Math.min(score, 1200) return -score } let getBoxSize = info => { if (info.boxType == 'groundBar') { console.log(1) } if (info.size) return //console.warn('开始算 ' + info.name) let exStr = '', warnStr = '' let x, y //求对角线的向量 x>0,y>0 //假设盒子的长宽为x,y (x>0,y>0),视线切线单位向量为(k,m),投影距离:x'k+y'm.(x'是正负x,y'是正负y) //由于盒子的对角线有四个可选方向,(类似四个象限) 则需要能使投影距离最长的一个对角线向量。 //如,当k<0,m>0时,要使xk+ym 最大,必有x<0,y>0. 故 x = -x', y = y', 故 投影距离:x'k+y'm = x(-k)+ym 。 //故无论km的符号如何,只要变为正数,再去联立方程即可得xy。(相当于切线转到第一象限) //注:但是因为无法获取准确的投影距离(角平分线左右两边的端点到角平分线的距离不相等,垂足也无法确定),所以所算的误差非常大。 if (info.name == 'pano6-13&pano4-13') { console.log(6) } let center = getBoxPos(info) let oriX, oriY if (info.predictSize) { ;(x = oriX = info.predictSize.x), (y = oriY = info.predictSize.y) } else { if (info.box1) { let x1 = info.boxposes[0].camTangent.x, x2 = info.boxposes[1].camTangent.x, y1 = info.boxposes[0].camTangent.y, y2 = info.boxposes[1].camTangent.y, w1 = /* info.boxposes[0].projectWidth, */ THREE.MathUtils.clamp(info.boxposes[0].projectWidth, info.boxposes[0].minProjectWidth, info.boxposes[0].maxProjectWidth * 1.1), //校准。如果projectwidth不准那算出来更不准. 但XswQxwmn2ZC的里侧电池是前者更准 w2 = /* info.boxposes[1].projectWidth */ THREE.MathUtils.clamp(info.boxposes[1].projectWidth, info.boxposes[1].minProjectWidth, info.boxposes[1].maxProjectWidth * 1.1) //如果识别到柜门上,(柜体被遮住了),整体中心就会在柜门上,且厚度小于真实值。 if (x1 == 0) { y = w1 x = (w2 - y2 * y) / x2 } else { //联立方程得: y = (w2 - (x2 / x1) * w1) / (y2 - (x2 / x1) * y1) x = (w1 - y1 * y) / x1 } //console.log('xy', { x, y }) ;(x < 0.3 || x > 1.4) && (exStr += ' x:' + math.toPrecision(x, 2)) ;(y < 0.3 || y > 1.4) && (exStr += ' y:' + math.toPrecision(y, 2)) if (y < 0 || x < 0) { //console.log('<0 ?????????') warnStr = x < 0 ? 'x<0!' : 'y<0!' } ;(oriX = x), (oriY = y) } else { //single pano data //将maxX maxY 限定在标准范围内 if (info.xProp) { let widthValue = standards[info.boxType].width let thickValue = standards[info.boxType].thick let maxX, maxY, minX, minY if (info.xProp == 'width') { /* x = oriX = THREE.MathUtils.clamp(info.boxposes[0].maxX, widthValue.min, widthValue.max) y = oriY = THREE.MathUtils.clamp(info.boxposes[0].maxY, thickValue.min, thickValue.max) */ maxX = THREE.MathUtils.clamp(info.boxposes[0].maxX, widthValue.min, widthValue.max) //AG0bi2fhb3 需要将得到的minX等这四项都clamp后再平均 maxY = THREE.MathUtils.clamp(info.boxposes[0].maxY, thickValue.min, thickValue.max) minX = THREE.MathUtils.clamp(info.boxposes[0].minX, widthValue.min, widthValue.max) minY = THREE.MathUtils.clamp(info.boxposes[0].minY, thickValue.min, thickValue.max) } else { /* x = oriX = THREE.MathUtils.clamp(info.boxposes[0].maxX, thickValue.min, thickValue.max) y = oriY = THREE.MathUtils.clamp(info.boxposes[0].maxY, widthValue.min, widthValue.max) */ maxX = THREE.MathUtils.clamp(info.boxposes[0].maxX, thickValue.min, thickValue.max) maxY = THREE.MathUtils.clamp(info.boxposes[0].maxY, widthValue.min, widthValue.max) minX = THREE.MathUtils.clamp(info.boxposes[0].minX, thickValue.min, thickValue.max) minY = THREE.MathUtils.clamp(info.boxposes[0].minY, widthValue.min, widthValue.max) } x = oriX = (maxX + minX) / 2 y = oriY = (maxY + minY) / 2 } else { //x = oriX = y = oriY = (min+max)/2 let standard = standards[info.boxType].widthNormal x = oriX = THREE.MathUtils.clamp(info.boxposes[0].maxX, standard.min, standard.max) y = oriY = THREE.MathUtils.clamp(info.boxposes[0].maxY, standard.min, standard.max) } } //按正常来说,得到的x,y都应>0,但是由于箱子会被遮挡,导致投影宽度比真实的小,算出的也不准,可能是负数 //所以手动将过小的宽度矫正 } /* if(info.name == "pano2-6"){ console.log(7) } */ let height if (standards[info.boxType].bottom) { //悬挂的 //挂式空调最好把长宽固定。 不过极少出错 //center.y -= 0.1 //很可能过高 height = standards[info.boxType].height.standard getBoxBtm(info) if (height) { let d = center.y - info.btmPos.y center.y -= THREE.MathUtils.clamp((d - height / 2) / 2, -0.1, 0.1) //如果中心点到底部的距离和height的一半不同,中心点移动差值的一半 } else { let btmY = info.btmPos.y /* let ys = [info.btmPos.y]//info.btmPos.y蛮不准的 if(info.box0){ ys.push(info.box0.btmPos.y) } if(info.box1){ ys.push(info.box1.btmPos.y) } btmY = ys.reduce((w,c)=>{return w+c},0) btmY/=ys.length //平均 */ height = (center.y - btmY) * 2 } } else { if (!info.topPos) getBoxTop(info) height = info.topPos.y - groundY } let o = restrictSize(x, height, y, info) ;(x = o.x), (height = o.y), (y = o.z) if (standards[info.boxType].widthSame) { x = y = (x + y) / 2 } info.size = new THREE.Vector3(x, height, y) info.sizeAdjust = Math.pow(Math.abs(x - oriX), 1.3) + Math.pow(Math.abs(y - oriY), 1.3) //计算得到的值和标准值之间的差距,可以反映该info的匹配分值 if (info.sizeAdjust) info.score = (info.score || 0) - Math.min(info.sizeAdjust * 100, 400) ;(info.size.oriX = oriX), (info.size.oriY = oriY) ;(info.exStr = exStr), (info.warnStr = warnStr) } let restrictSize = (x, y, z, info) => { let s let { xProp, yProp } = info if (xProp != void 0) { var { min, max } = standards[info.boxType][xProp] x = THREE.MathUtils.clamp(x, min, max) var { min, max } = standards[info.boxType][yProp] z = THREE.MathUtils.clamp(z, min, max) s = true } if (!s) { var { min, max } = standards[info.boxType].widthNormal x = THREE.MathUtils.clamp(x, min, max) z = THREE.MathUtils.clamp(z, min, max) } var { min, max } = standards[info.boxType].height y = THREE.MathUtils.clamp(y, min, max) return { x, y, z } } let getMixBox = (box0, box1) => { //重叠部分 let box = new THREE.Box2() box.min.set(Math.max(box0.min.x, box1.min.x), Math.max(box0.min.y, box1.min.y)) box.max.set(Math.min(box0.max.x, box1.max.x), Math.min(box0.max.y, box1.max.y)) return box } let getLeftRight = boxArr => { //获取pano的boxes中最左和最右的bbox.x let lefts = boxArr.map(e => e.bbox2[0]) let rights = boxArr.map(e => e.bbox2[2]) lefts.sort((a, b) => getBbox2Diff(a, b)) rights.sort((a, b) => getBbox2Diff(b, a)) let leftX = lefts[0] //最左 let rightX = rights[0] //最右 return { leftX, rightX, } } ;(global.searchCount1 = 0), (global.escapeCount1 = 0) let searchPair = (beginItem, group0_, group1_, parentPairs, resultPairs, evaluateFun, minScore) => { //配对结果个数为n!,其中n是每组的元素个数。注意当n=10时,已经有40320个,非常恐怖。 let pair = [], parentExit = !!parentPairs let removeParent = () => { //元结点裂变出多个,来装新的pair if (parentExit) { let i = resultPairs.indexOf(parentPairs) resultPairs.splice(i, 1) parentExit = false } } if (!parentPairs) { //首次 if (group0_.length == 0 || group1_.length == 0) return //保证第一个的个数<=第二个,否则第一组多出来的永远匹配不上 if (group0_.length > group1_.length) { let t = group0_ group0_ = group1_ group1_ = t } beginItem = group0_[0] let complex = Object.keys(global.boxFrame.datas).length if (complex < 10) evaluateFun = null else minScore = math.linearClamp(complex, 10, 80, -4000, -500) //console.log('searchPair length',group0_.length,group1_.length) } searchCount1++ for (let j = 0; j < group1_.length; j++) { pair = [beginItem, group1_[j]] //if(pair[0].sid == 'void' || pair[1].sid == 'void')continue let evaluate if (evaluateFun /* && !(pair[0].sid == 'void' || pair[1].sid == 'void') */) { evaluate = evaluateFun(pair[0], pair[1]) if (evaluate == void 0 || evaluate.score <= minScore) { //console.log('因为评估出匹配可能性低所以跳过',pair[0],pair[1],evaluate) escapeCount1++ continue } } let newPairs //用来存放该组pair if (parentPairs) { removeParent() newPairs = parentPairs.slice(0) //复制 newPairs.push(pair) } else { newPairs = [pair] //新的容器 } resultPairs.push(newPairs) let newGroup0 = group0_.slice(0) let newGroup1 = group1_.slice(0) let index = newGroup0.indexOf(pair[0]) newGroup0.splice(index, 1) index = newGroup1.indexOf(pair[1]) newGroup1.splice(index, 1) if (newGroup0.length > 0 && newGroup1.length > 0) { searchPair(newGroup0[0], newGroup0, newGroup1, newPairs, resultPairs, evaluateFun, minScore) } } } //如果第一个元素就和后面所有的都不匹配,就直接返回了怎么办? export default class PanoBoxFrame extends THREE.Group { constructor(player_, ifAnalyze, dataList) { super() this.clear() player = player_ player.model.add(this) global.boxFrame = this this.ifAnalyze = ifAnalyze this.wireframes = new THREE.Object3D() this.wireframes.name = 'wireframes' this.add(this.wireframes) this.matchScoreMap = {} this.bindEvents() meshGroup = new THREE.Object3D() meshGroup.name = 'testBox' this.add(meshGroup) this.compute(dataList) } async compute(dataList) { startTime = Date.now() this.datas = {} this.datasMixed = {} this.boxesSolid = boxesSolid /* let metadata = player.$app.store.getValue('metadata') //await player.$app.resource.metadata() isLaser = metadata.sceneFrom == 'laser' */ let compu = 0 let beginCompute = () => { //获取匹配分数 let getMatchScore = (box0, box1, { isSingle, center, onlyGet, dontCheckDis } = {}) => { let name0 = box0.sid + '&' + box1.sid let name1 = box1.sid + '&' + box0.sid let boxType = getBoxType(box0) let matchInfo0 = this.matchScoreMap[boxType][name0] let matchInfo1 = this.matchScoreMap[boxType][name1] let matchInfo = matchInfo0 || matchInfo1 if (onlyGet) return matchInfo let name if (!matchInfo) { name = name0 matchInfo = { name, box0, box1, center } this.matchScoreMap[boxType][name] = matchInfo } else { return matchInfo } if (name == 'pano6-9&pano8-4') { console.log(5) } getBoxBase(box0) getBoxBase(box1) let A = box0.pano.position.clone() let B = box1.pano.position.clone() let AB = new THREE.Vector3().subVectors(B, A) let AB2d = new THREE.Vector2(AB.x, AB.z).normalize() let AP12d = center ? new THREE.Vector2(center.x - A.x, center.z - A.z).normalize() : new THREE.Vector2(box0.centerDir.x, box0.centerDir.z).normalize() let BP22d = center ? new THREE.Vector2(center.x - B.x, center.z - B.z).normalize() : new THREE.Vector2(box1.centerDir.x, box1.centerDir.z).normalize() let angleA = Math.acos(AB2d.dot(AP12d)) let angleB = Math.PI - Math.acos(AB2d.dot(BP22d)) let score = 100, str = [] if (angleA + angleB > Math.PI + 0.2) { //无交点(比180大是因为中心角度有误差,所以给一定的容错) //console.log(`${panoId0}的第${box0.index}个与${panoId1}的第${box1.index}个因角度大于180度 不匹配`) return Object.assign(matchInfo, { score: -5000, str: ['angle>180'] }) } if (box0.type != box1.type) { return Object.assign(matchInfo, { score: -5000, str: ['typeNotSame'] }) } if (matchInfo.dirAngleXZ == void 0) { matchInfo.dirAngleXZ = THREE.MathUtils.radToDeg(Math.acos(AP12d.dot(BP22d))) //需要尽量接近90度算出来的交点会比较准 matchInfo.minAng = Math.min(180 - matchInfo.dirAngleXZ, matchInfo.dirAngleXZ) //角度小的getIntersect2容易算不准 if (isSingle) { let bestDisSquared = 2 //单个匹配单个,而非多个匹配多个(没有固定到两个漫游点),所以可以直接寻找最优角度 score += Math.sin(THREE.MathUtils.degToRad(matchInfo.dirAngleXZ)) * 300 score += matchInfo.dirAngleXZ //另外角度越大越不容易偏向一边 score -= Math.abs(getBoxPos(box0).distanceToSquared(box0.pano.position) - bestDisSquared) * 10 score -= Math.abs(getBoxPos(box1).distanceToSquared(box1.pano.position) - bestDisSquared) * 10 } } let shinkRatio = 1 let btmPos0 = box0.btmPos || box0.btmPosPredict //fire类型先用btmPosPredict,测定groundY后才有btmPos let btmPos1 = box1.btmPos || box1.btmPosPredict if (!dontCheckDis) { //let r = box0.boxType == 'air' ? 1 : box0.boxType == 'cabinet' ? 0.9 : 0.7 //随着宽度增加而降低 let r = THREE.MathUtils.clamp(0.8 / standards[boxType].widthNormal.max, 0.6, 2) //随着宽度增加而降低 UWrshepp0G5的fire if (!standards[boxType].bottom && btmPos0 && btmPos1) { //注:挂空调不应使用btmPosPredict let d = btmPos0.distanceToSquared(btmPos1) matchInfo.btmPosPreDis = d score -= d * 1300 * r * shinkRatio if (box1.topPos) { let a = box0.topPos.distanceToSquared(box1.topPos) matchInfo.topPosPreDis = a let u = a * 700 * r * shinkRatio let AP0 = new THREE.Vector2(btmPos0.x - A.x, btmPos0.z - A.z).lengthSq() let AP1 = new THREE.Vector2(btmPos1.x - B.x, btmPos1.z - B.z).lengthSq() if (AP0 < 0.4 || AP1 < 0.4) u *= 0.3 //太近 score -= u } } else if (box0.posAtWall && box1.posAtWall) { // let d = box0.posAtWall.distanceToSquared(box1.posAtWall) matchInfo.wallPosPreDis = d score -= d * 200 * r //墙面不准所以分低 ftMTQIrs79 d = box0.btmPosOri.distanceToSquared(box1.btmPosOri) //还是加一下 matchInfo.btmPosPreDis = d score -= d * 200 * r * shinkRatio /* let h0 = box0.topPos.y - box0.btmPos.y let h1 = box1.topPos.y - box1.btmPos.y score -= Math.abs(h0-h1) * 3000 * r * shinkRatio //高度差 倾斜角度大的不准 */ } } if (!matchInfo.center) { let o = getIntersect2(A, box0.centerDir, B, box1.centerDir) matchInfo.center = o.pos3d.clone() /* let o2 = math.getLineIntersect({ A, B, p1: A.clone().add(box0.centerDir), p2:B.clone().add(box1.centerDir) }) matchInfo.center2 = o2.pos3d.clone() */ /* if (name == "pano16-4&pano18-5") { addLine(A, box0.centerBtmDir, 10), addLine(B, box1.centerBtmDir, 10) } */ //验证是否漫游点到中心点的方向和centerDir一样 let dir0 = new THREE.Vector3().subVectors(o.pos3d, A).normalize() let dir1 = new THREE.Vector3().subVectors(o.pos3d, B).normalize() let sum = dir0.dot(box0.centerDir) + dir1.dot(box1.centerDir) let wrongDir = sum < 1.95 score -= (2 - sum) * 10000 if (wrongDir) { str.push('wrongDir') return Object.assign(matchInfo, { score: score - 5000, str }) } if (!dontCheckDis && !standards[boxType].bottom && box0.btmPos && box1.btmPos) { //墙壁位置不准所以不用 KK-ftMTQIrs79 let p0 = new THREE.Vector2(box0.btmPos.x, box0.btmPos.z) let p1 = new THREE.Vector2(box1.btmPos.x, box1.btmPos.z) let p = new THREE.Vector2(matchInfo.center.x, matchInfo.center.z) let dis = p0.distanceToSquared(p) + p1.distanceToSquared(p) let s = math.linearClamp(matchInfo.minAng, 0, 20, 0, 1) score -= dis * 1500 * s //如果距离较远就说明算的center误差大,不可信。可能有一个框不准确 matchInfo.centerDrift = dis } getBoxBtm(matchInfo) let cr //getIntersect2结果的权重 if (standards[box0.boxType].atWall == 1) { cr = math.linearClamp(matchInfo.minAng, 2, 15, 0, 1) //墙壁误差大,所以尽量完全依赖cIntersect。。。后期如果该墙壁校准了可以调整 } else { cr = math.linearClamp(matchInfo.minAng, 2, 20, 0, 0.4) } let predict0 = (!standards[boxType].bottom && box0.btmPos) || box0.posAtWall || matchInfo.topPos //在墙上的除非角度小,否则不考虑cIntersect let predict1 = (!standards[boxType].bottom && box1.btmPos) || box1.posAtWall || matchInfo.topPos //相对来说btmPos要比center准一点?因为center有在两个维度上的误差 const btmRatio = 0.5 let cIntersect = new THREE.Vector3().addVectors(matchInfo.center.clone().multiplyScalar(1 - btmRatio), matchInfo.btmPos.clone().multiplyScalar(btmRatio)).setY(o.pos3d.y) if (predict0 && predict1) { matchInfo.center = new THREE.Vector3() .addVectors( cIntersect.clone().multiplyScalar(cr), predict0 .clone() .add(predict1) .multiplyScalar((1 / 2) * (1 - cr)) ) .setY(o.pos3d.y) } else { matchInfo.center = cIntersect } matchInfo.cIntersect = cIntersect //addLabel(matchInfo.center, matchInfo.name + '-c') } getBoxType(matchInfo) /* if (matchInfo.boxType != box0.type || matchInfo.boxType != box1.type) { ;(score -= 1000), str.push('typeNotSame2') } */ { let vec0 = new THREE.Vector3().subVectors(box0.pano.position, matchInfo.center) let vec1 = new THREE.Vector3().subVectors(box1.pano.position, matchInfo.center) if (vec0.x * vec1.x > 0 && vec0.z * vec1.z > 0) { //同一个象限(center会偏向一侧) score -= 200 } } if (!safeBound.containsPoint(matchInfo.center)) { let dis = safeBound.distanceToPoint(matchInfo.center) //console.log(`${panoId0}的第${box0.index}个与${panoId1}的第${box1.index}个因中心点在bounding外不匹配`, center, 'dis: ' + dis) ;(score -= 1000 * dis * dis), str.push('outsideBound') Object.assign(matchInfo, { score, str, center: matchInfo.center, disToBound: dis }) if (dis > 0.5) return matchInfo } //检查宽度 let boxposes let checkWidth = () => { boxposes = [] ;[box0, box1].forEach(box => { let pose = getBoxPoseByPos(box, matchInfo.center) boxposes.push(pose) //如果超出标准,基本上这二者不匹配;但过小的话,有可能是被遮挡所以残缺,因此不予过滤 }) } checkWidth() score += getPoseScore(boxposes, boxType /* isSingle */) //根据投影信息预测的长度再得匹配分数 compu++ return Object.assign(matchInfo, { score: score, str, /* diffHalfHeight, */ boxposes }) } this.rows = {} /* let getchainNext = (left, end, chain, boxes) => { chain.push(left) if (left == end) return boxes.chains.push(chain) let nodes = boxes.relationships.filter(pair => pair.includes(left)) let rights = nodes.map(pair => pair.find(e => e != left)) rights = rights.filter(e => !chain.includes(e) && boxes.indexOf(e) > boxes.indexOf(left)) rights.forEach(right => { getchainNext(right, end, chain.slice(), boxes) }) } */ let getPanoBigRowBox = (panoBoxes, { reason = 'row' } = {}) => { //将一个pano中的所有boxes分组,识别哪些是一排的。也可用于识别融合 let pano = panoBoxes[0].pano const category = panoBoxes[0].category let type = category + '|' + reason this.rows[type] || (this.rows[type] = {}) if (this.rows[type][pano.id]) return this.rows[type][pano.id] let bigBoxes let bigBox = { sid: 'pano' + pano.id + (reason == 'mix' ? '-mix' : '-row'), pano, category: reason == 'mix' ? category : 'rowBigBox', boxType: reason == 'mix' ? panoBoxes[0].boxType : 'rowBigBox', } let rows = [] for (let i = 0; i < panoBoxes.length; i++) { let box0 = panoBoxes[i] getBoxBase(box0) let [left0, right0] = [box0.bbox2[0], box0.bbox2[2]] for (let j = i + 1; j < panoBoxes.length; j++) { let box1 = panoBoxes[j] getBoxBase(box1) if (box0.boxType != box0.boxType) continue //类型不同 let [left1, right1] = [box1.bbox2[0], box1.bbox2[2]] let d1 = getBbox2Diff(left1, right0), d2 = getBbox2Diff(left1, left0), d3 = getBbox2Diff(left0, right1) if (box0.sid == 'pano2-4' && box1.sid == 'pano2-5') { console.log(9) } const min = reason == 'mix' ? 0.004 : 0.003 //mix代表寻找分裂的重新融合到一起 if ((d1 <= min && d2 >= min) || (d3 <= min && d2 <= min)) { //边框交接 let atEdgeMight = left1 < 0.002 && right0 > 0.998 ? [left1, right0] : left0 < 0.002 && right1 > 0.998 ? [left0, right1] : null //有在全景图的边界的可能性 //if (reason == 'mix' && box0.category == typeNames.cabinet && !atEdgeMight) continue //柜子容易并排,尽量不融合 //再看,啥意思,没懂 if (reason == 'mix' && !atEdgeMight) continue //不在边缘 const { max } = standards[box0.boxType].widthNormal //standards[box0.btmPos ? category : 'air-hanging'].widthNormal const tolerate = max * max * (reason == 'mix' ? 0.7 : 1.8) //yDCiaTQvRYn:row不能低于1.5 let p0 = standards[box0.boxType].atWall == 1 ? box0.posAtWall : reason == 'mix' ? box0.btmPosOri || box0.btmPosPredict : box0.btmPos let p1 = standards[box1.boxType].atWall == 1 ? box1.posAtWall : reason == 'mix' ? box1.btmPosOri || box1.btmPosPredict : box1.btmPos //let p0 = box0.type == 'air-hanging' ? box0.posAtWall : reason == 'mix' ? box0.btmPosOri : box0.btmPos //let p1 = box1.type == 'air-hanging' ? box1.posAtWall : reason == 'mix' ? box1.btmPosOri : box1.btmPos let dis = p0.distanceToSquared(p1) if (reason == 'mix') { let allY = box0.bbox2[3] - box0.bbox2[1] + (box1.bbox2[3] - box1.bbox2[1]) //各自高度和 let wholeY = Math.max(box0.bbox2[3], box1.bbox2[3]) - Math.min(box0.bbox2[1], box1.bbox2[1]) //总跨越高度 let coverY = allY - wholeY //重合区域的y高度,可为负数 let disY = (1 - coverY / wholeY + (wholeY - coverY) * 3) * 4 * tolerate //既要考虑占比也要考虑差值 dis += disY //SGyhEzZNGP9案例:虽然是atEdge但并不应该融合,便通过disY来阻挡 ; MW6MEeCOy9Y:pano16-15 //console.log('disY',disY, box0.sid, box1.sid ) let atEdgePossib = atEdgeMight ? 0.002 / (atEdgeMight[0] + (1 - atEdgeMight[1])) : 0 // 两条线越接近越可能融合 atEdgePossib = Math.min(6, atEdgePossib) //原本计算得 min:1, max:Infinity dis -= atEdgePossib * tolerate //给点点优势 } /* if(box0.sid == "pano2-0"){ console.log('dis',dis,'tolerate',tolerate,[box0, box1],disY) } if (box0.sid == 'pano2-1' && box1.sid == 'pano2-4') { reason == 'mix' && console.log(dis, tolerate, box0.sid, box1.sid) }*/ if (dis < tolerate) { //reason == 'mix' && console.log('-------------------') common.pushToGroupAuto([box0, box1], rows) } } } } //一排箱子的角度范围不可超过180度,因为不可能站在箱子上拍,所以超过的话肯定有边缘的不在这一排中。 //可判断边缘箱子的是否角度偏大,一般中间的被遮挡所以偏小 rows.forEach(boxes => { //从左到右排序 boxes.sort((a, b) => { //但因有的box跨越到别的box区域,所以这个顺序不准确 return getBbox2Diff(a.bbox2CenterX, b.bbox2CenterX) }) }) //去除不在一条直线上的连接. 当bound超出后就断开 if (reason == 'row') { rows.slice(0).forEach(boxes => { if (boxes.length >= 2) { let removes = [], bound = new THREE.Box2(), size = new THREE.Vector2(), maxW = 0.6 for (let i = 0, j = boxes.length; i < j; i++) { let box = boxes[i] let pos2d = new THREE.Vector2(box.btmPos.x, box.btmPos.z) bound.expandByPoint(pos2d) bound.getSize(size) let min = Math.min(size.x, size.y) if (min > maxW) { removes.push([boxes[i], boxes[i - 1]]) bound = new THREE.Box2() bound.expandByPoint(pos2d) //console.log('removes', size) } //console.log('removes',k, box1.sid) } if (removes.length) { /* console.log( '去除错误row连接', removes.map(e => e.map(a => a.sid)) ) */ let { newGroups } = common.disconnectGroup(removes, rows) //if(newGroups.length>1){//分裂成多组了,重新计算 // console.log(newGroups) //} } } }) rows.forEach(boxes => { //从左到右重新排序 boxes.sort((a, b) => { //但因有的box跨越到别的box区域,所以这个顺序不准确 return getBbox2Diff(a.bbox2CenterX, b.bbox2CenterX) }) }) } rows.sort((a, b) => { return b.length - a.length }) //箱子数量从大到小排序 bigBoxes = rows.map((boxes, i) => { let { leftX, rightX } = getLeftRight(boxes) //最左 let topY = boxes.slice().sort((a, b) => a.bbox2[1] - b.bbox2[1])[0].bbox2[1] let btmY = boxes.slice().sort((a, b) => b.bbox2[3] - a.bbox2[3])[0].bbox2[3] let rowBigBox = Object.assign({}, bigBox, { boxes, bbox2: [leftX, topY, rightX, btmY], //整排的bbox left: boxes.find(e => e.bbox2[0] == leftX), right: boxes.find(e => e.bbox2[2] == rightX), }) let p0 = getBoxPos(rowBigBox.left) let p1 = getBoxPos(rowBigBox.right) let vec = new THREE.Vector2(p0.x - p1.x, p0.z - p1.z) rowBigBox.k = Math.abs(vec.x / vec.y) rowBigBox.predictLen = (rowBigBox.k > 1 ? Math.abs(vec.x) : Math.abs(vec.y)) + 0.6 //加入一个宽度 /* if(boxes.length <= boxes.relationships.length){//多条链(为了识别一个box嵌套多个的情况。不过后来在开头时处理了一部分) boxes.chains = [] getchainNext(left,right,[], boxes ) let aveAngle = (getBbox2Diff(left.bbox2[2], left.bbox2[0]) + getBbox2Diff(right.bbox2[2], right.bbox2[0]) ) / 2 -0.01 //首尾的angle平均数。但如果这两个不准那就导致整体出错了 let middleAngle = getBbox2Diff(right.bbox2[0], left.bbox2[2]) let counts = boxes.chains.map(e=>e.length) counts.sort((a,b)=>a-b) let min = counts[0],max = counts[counts.length-1] let r = [], cur = min; while(cur<=max){ r.push({cur, diff:Math.abs((middleAngle / (cur-2) - aveAngle)}) //加 0.01是因为增加边缘 cur++ } r.sort((a,b)=>a.diff-b.diff) rowBigBox.predictBoxCount = r[0].cur //--------- let goodCountChains = boxes.chains.filter(e=>e.length == rowBigBox.predictBoxCount) if(goodCountChains.length == 1) rowBigBox.bestChain = goodCountChains[0] else{ goodCountChains = goodCountChains.map((chain,i)=>{ let j = 1, diff=0 //中间的box的angle的方差 while(ja.diff-b.diff) rowBigBox.bestChain = goodCountChains[0].chain } console.log('getChains',boxes.chains, 'predictBoxCount',rowBigBox.predictBoxCount, r) } */ return rowBigBox }) if (reason != 'mix') { panoBoxes.forEach(box => { //加入单个的 if (!rows.some(row => row.includes(box))) { let boxBig = Object.assign({}, bigBox, { bbox2: box.bbox2, boxes: [box], left: box, right: box, }) bigBoxes.push(boxBig) } }) } //mix的之前的btm因pose错误而延伸了不对的depth所以不准 bigBoxes.forEach(bigBox => { bigBox.sid += '-' + bigBox.boxes.map(e => e.index).join(',') /* if (bigBox.sid == 'pano0-rowBigBox-1,0,2,4') { console.log(3) } */ if (reason == 'row') { //取平均值 if (bigBox.boxes[0].btmPos) { bigBox.btmPos = bigBox.boxes.reduce((w, c) => w.add(c.btmPos), new THREE.Vector3()).multiplyScalar(1 / bigBox.boxes.length) //addLabel(bigBox.btmPos,'b_'+bigBox.sid, {bgcolor:'#f93',a:0.4}) } if (bigBox.boxes[0].topPos) { bigBox.topPos = bigBox.boxes.reduce((w, c) => w.add(c.topPos), new THREE.Vector3()).multiplyScalar(1 / bigBox.boxes.length) } if (bigBox.boxes[0].posAtWall) { bigBox.posAtWall = bigBox.boxes.reduce((w, c) => w.add(c.posAtWall), new THREE.Vector3()).multiplyScalar(1 / bigBox.boxes.length) } } //mix的需要合并后计算才准确 }) this.rows[type][pano.id] = bigBoxes //当前pano的所有row return bigBoxes } /* let getPanoBoxAngleTrend = rowBox => { //顺时针方向该pano的box角度范围是越来越大还是越来越小 let diffs = [] let angles = rowBox.boxes.map(box => getBbox2Diff(box.bbox2[2], box.bbox2[0])) for (let i = 0, j = angles.length; i < j - 1; i++) { //得所有相邻之间的差 let a0 = angles[i], a1 = angles[i + 1] diffs.push(a1 - a0) } diffs.sort((a, b) => a - b) return diffs[Math.floor(diffs.length / 2)] //中位数 } */ /* let getBoxCount = (rowBigBox)=>{ return rowBigBox.predictBoxCount || rowBigBox.boxes.length } */ let getReverseInfo = (rowBigBox0, rowBigBox1) => { //两个row的方向对应 let reversed = false let lefts = [rowBigBox0.left, rowBigBox1.left] let rights = [rowBigBox0.right, rowBigBox1.right] let dis0 = lefts[0].btmPos.distanceToSquared(lefts[1].btmPos) let dis1 = rights[0].btmPos.distanceToSquared(rights[1].btmPos) let dis2 = lefts[0].btmPos.distanceToSquared(rights[1].btmPos) let dis3 = rights[0].btmPos.distanceToSquared(lefts[1].btmPos) let posLeft2, posRight2 if (dis0 + dis1 > dis2 + dis3) { //距离近的代表是同一端 reversed = true posLeft2 = new THREE.Vector3().addVectors(lefts[0].btmPos, rights[1].btmPos).multiplyScalar(0.5) posRight2 = new THREE.Vector3().addVectors(rights[0].btmPos, lefts[1].btmPos).multiplyScalar(0.5) } else { posLeft2 = new THREE.Vector3().addVectors(lefts[0].btmPos, lefts[1].btmPos).multiplyScalar(0.5) posRight2 = new THREE.Vector3().addVectors(rights[0].btmPos, rights[1].btmPos).multiplyScalar(0.5) } let vec = new THREE.Vector2(posLeft2.x - posRight2.x, posLeft2.z - posRight2.z) let k = Math.abs(vec.x / vec.y) //这个算斜率更准,但位置容易偏向一侧(可能用边缘的bbox算会好些?) return { reversed, k } } let searchByRow = (groups, datas) => { //先查找row,匹配row,再slice row的方法 this.matchScoreMap['rowBigBox'] = {} let rowInfos = [] let getRowMatchInfo = (rowBigBox0, rowBigBox1, ignoreCountMatch) => { //获取row间的匹配信息 //获取bigBox位置。由于一排的盒子比较长,中心方向误差大,所以采用先获取两边位置,再求中点的方法 //if (rowBigBox0.boxes.length != rowBigBox1.boxes.length && !ignoreCountMatch) return //太难了,不算不一样的情况了 let name = rowBigBox0.sid + '&' + rowBigBox1.sid if (name in rowInfos) { return rowInfos[name] } if (name == 'pano4-row-2,11,10,8,6&pano6-row-2,5,8,11') { console.log(3) } if (rowBigBox0.boxes.length != rowBigBox1.boxes.length && !ignoreCountMatch) return //if (getBoxCount(rowBigBox0) != getBoxCount(rowBigBox1) && getBoxCount(rowBigBox0) != 1 && getBoxCount(rowBigBox1) != 1)return /* if (rowBigBox0.sid == 'pano0-row-1,2,6' && rowBigBox1.sid == 'pano2-row-0,2,6') { console.log(4) } */ let rowInfo if (rowBigBox0.boxes.length > 1 && rowBigBox1.boxes.length > 1) { //多对多,可以求两端的位置 let lefts = [rowBigBox0.left, rowBigBox1.left] let rights = [rowBigBox0.right, rowBigBox1.right] let leftInfo let rightInfo let info2 = getReverseInfo(rowBigBox0, rowBigBox1) let len0 = rowBigBox0.predictLen, //长度应该接近 len1 = rowBigBox1.predictLen let overLen = Math.abs(len0 - len1) /* / (rowBigBox0.boxes.length + rowBigBox1.boxes.length) * 5 */ if (overLen > 1) { //console.warn('overLen> 1', overLen, rowBigBox0.sid, '和', rowBigBox1.sid) return done() } if (info2.reversed) { leftInfo = getMatchScore(lefts[0], rights[1], { isSingle: true }) rightInfo = getMatchScore(rights[0], lefts[1], { isSingle: true }) } else { leftInfo = getMatchScore(lefts[0], lefts[1], { isSingle: true }) rightInfo = getMatchScore(rights[0], rights[1], { isSingle: true }) } let posLeft = getBoxPos(leftInfo) let posRight = getBoxPos(rightInfo) if (!posLeft || !posRight || leftInfo.score < -4000 || rightInfo.score < -4000) { return done() //漫游点重合、>180度会导致此问题 } preDealBox(leftInfo) //getBoxSize(leftInfo) preDealBox(rightInfo) //getBoxSize(rightInfo) posLeft = getBoxPos(leftInfo) posRight = getBoxPos(rightInfo) //验证是否是垂直或水平 let vec = new THREE.Vector2(posLeft.x - posRight.x, posLeft.z - posRight.z) let k = Math.abs(vec.x / vec.y) if ((info2.k > 1 && k < 1) || (info2.k < 1 && k > 1)) { console.error('请检查!info2.k > 1 && k < 1 || info2.k < 1 && k > 1', rowBigBox0.sid, '和', rowBigBox1.sid) //绘制的方向错误,尺寸错误 return done() } let wrongK = 0 if ((rowBigBox0.k > 1 && rowBigBox1.k < 1) || (rowBigBox0.k < 1 && rowBigBox1.k > 1)) { wrongK = rowBigBox0.k / rowBigBox1.k if (wrongK < 1) wrongK = 1 / wrongK } /* let trend0 = getPanoBoxAngleTrend(rowBigBox0) let trend1 = getPanoBoxAngleTrend(rowBigBox1) let judgeReverse = () => { //这个方法有时不准 let disLeftSquared0 = new THREE.Vector2(posLeft.x - rowBigBox0.pano.position.x, posLeft.z - rowBigBox0.pano.position.z).lengthSq() let disRightSquared0 = new THREE.Vector2(posRight.x - rowBigBox0.pano.position.x, posRight.z - rowBigBox0.pano.position.z).lengthSq() let a = trend0 * (disLeftSquared0 - disRightSquared0) if (a < 0 && Math.abs(a) > 0.1) return true let posLeft2 = reversed ? posRight : posLeft, //反向过的对第二个漫游点来说左右是反的 posRight2 = reversed ? posLeft : posRight let disLeftSquared1 = new THREE.Vector2(posLeft2.x - rowBigBox1.pano.position.x, posLeft2.z - rowBigBox1.pano.position.z).lengthSq() let disRightSquared1 = new THREE.Vector2(posRight2.x - rowBigBox1.pano.position.x, posRight2.z - rowBigBox1.pano.position.z).lengthSq() let b = trend1 * (disLeftSquared1 - disRightSquared1) if (b < 0 && Math.abs(b) > 0.1) return true } if (leftInfo.score < -2000 || rightInfo.score < -2000 || judgeReverse()) { //反向试试 leftInfo = getMatchScore(lefts[0], rights[1], { isSingle: true }) rightInfo = getMatchScore(rights[0], lefts[1], { isSingle: true }) posLeft = getBoxPos(leftInfo) posRight = getBoxPos(rightInfo) reversed = true //rowBigBox1 反向了 } if (leftInfo.score < -2000 || rightInfo.score < -2000 || judgeReverse()) { return console.log('getCenter ;两个方向都不符合', rowBigBox0.sid, rowBigBox1.sid) } */ /*const maxK = Math.max(0.6 / Math.sqrt(rowBigBox0.boxes.length), 0.2) // 最大斜率 if (k < maxK && k > 1 / maxK) { return //console.log('放弃,斜率', k) } */ //横的话,按x从小到大,竖的按z从小到大 if ((k < 1 && posLeft.z > posRight.z) || (k > 1 && posLeft.x > posRight.x)) { let temp = posRight ;(posRight = posLeft), (posLeft = temp) } //addLabel(posLeft, 'left-' + rowBigBox0.pano.id + '&' + rowBigBox1.pano.id, { a: 0.1 }) //addLabel(posRight, 'right-' + rowBigBox0.pano.id + '&' + rowBigBox1.pano.id, { a: 0.1 }) /* if (rowBigBox0.pano.id + '&' + rowBigBox1.pano.id == '22&26') { console.log(777) } */ /* var line1 = LineDraw.createLine([posLeft, posRight]) meshGroup.add(line1) */ //根据btmPos矫正一下中心位置, 否则容易偏漫游点这一侧 let center = new THREE.Vector3().addVectors(posLeft, posRight).multiplyScalar(0.5) center .add(rowBigBox0.btmPos) .add(rowBigBox1.btmPos) .multiplyScalar(1 / 3) let axis = k > 1 ? 'z' : 'x' //posLeft[axis] = center[axis], posRight[axis] = center[axis] let match = getMatchScore(rowBigBox0, rowBigBox1, { isSingle: true, center }) //是否预先传送center ? //rowInfo.minAngs = [leftInfo.minAng , rightInfo.minAng] /* if (match.name == 'pano4-row-6,4,1,3&pano0-row-0,1,4,5') { console.log(8) } */ let sc = match.score - overLen * 1000 - wrongK * 100 + leftInfo.score + rightInfo.score if (sc < -4000) { //console.log('放弃,匹配分过低,可能不是一组', rowBigBox0.sid, '和', rowBigBox1.sid, sc) return done() } //console.log('getcenter', rowBigBox0.sid, '和', rowBigBox1.sid, overLen, match.score + overLen * 1000 + leftInfo.score + rightInfo.score) rowInfo = { rowBigBox0, rowBigBox1, match, k, posLeft, posRight, score: sc / 3 + 500, //700 + match.score*0.7 + (leftInfo.score + rightInfo.score)*0.3 , reversed: info2.reversed, } } else { if (rowBigBox0.boxes.length == 1 && rowBigBox1.boxes.length == 1) { rowInfo = getMatchScore(rowBigBox0.boxes[0], rowBigBox1.boxes[0]) //直接匹配box } else { //一对多。getMatchScore计算误差大(长度越长中心误差越大、宽度计算也误差大)所以再写点限制。直接使用btm来预测长度和位置似乎更准 let mulBoxRow = rowBigBox0.boxes.length > 1 ? rowBigBox0 : rowBigBox1 let singleBox = rowBigBox0.boxes.length == 1 ? rowBigBox0 : rowBigBox1 /* if(rowBigBox0.sid == "pano12-row-3" && rowBigBox1.sid == "pano0-row-3,1,0" ){ console.log(5) } */ rowInfo = getMatchScore(rowBigBox0, rowBigBox1, { dontCheckDis: true }) //一对多 也可以根据方向检查距离,如仅检查z rowInfo.k = mulBoxRow.k if (rowInfo.center) { rowInfo.center.add(getBoxPos(mulBoxRow)).multiplyScalar(0.5) } if (rowInfo.name == 'pano0-row-14&pano4-row-8,11') { addLabel(rowInfo.center, 'c') } rowInfo.predictSize = rowInfo.k > 1 ? { x: mulBoxRow.predictLen, y: 0.6 } : { y: mulBoxRow.predictLen, x: 0.6 } //单个的应该和多个的其中一端一样,且是离单个漫游点近的那端(也就是要走到箱子一端看不见其他箱子才行) let dis0 = getBoxPos(mulBoxRow.left).distanceToSquared(singleBox.pano.position) let dis1 = getBoxPos(mulBoxRow.right).distanceToSquared(singleBox.pano.position) let near = dis0 < dis1 ? mulBoxRow.left : mulBoxRow.right let p0 = getBoxPos(singleBox) let dis = getBoxPos(near).distanceToSquared(p0) rowInfo.score -= dis * 1000 let p1 = getBoxPos(mulBoxRow) let vec1 = new THREE.Vector2(p1.x - singleBox.pano.position.x, p1.z - singleBox.pano.position.z).normalize() //看向中心的方向 let vec2 = new THREE.Vector2(singleBox.centerDir.x, singleBox.centerDir.z).normalize() rowInfo.score += (vec1.dot(vec2) - 1) * 1000 //同一个方向是最好 } } function done(rowInfo) { rowInfo && rowInfos.push(rowInfo) rowInfos[name] = rowInfo } done(rowInfo) return rowInfo } let matchGroups = [] let allRelations = [] let getK = info => { let k if (info.left) { let vec = new THREE.Vector2(info.left.x - info.right.x, info.left.z - info.right.z) k = Math.abs(vec.x / vec.y) } else { k = Math.abs(Math.max(info.size.x, 0.6) / Math.max(info.size.z, 0.6)) } return k } let ignoreCountMatch = groups.filter(e => e.length > 1).length == 1 //是否不同数量box的row也能匹配 let minScore = boundConfirmed ? -2000 : -800 let match = searchType => { if (searchType == 'second') ignoreCountMatch = true for (let i = 0; i < groups.length - 1; i++) { let rowBigBoxes_0 = getPanoBigRowBox(groups[i]) let pano0 = groups[i][0].pano if (searchType == 'second') rowBigBoxes_0 = rowBigBoxes_0.filter(e => !matchGroups.some(u => u.includes(e))) for (let j = i + 1; j < groups.length; j++) { let rowBigBoxes_1 = getPanoBigRowBox(groups[j]) if (searchType == 'second') rowBigBoxes_1 = rowBigBoxes_1.filter(e => !matchGroups.some(u => u.includes(e))) let pano1 = groups[j][0].pano if (pano0.id == 54 && pano1.id == 56) { console.log(2) } let resultPairs = [] let evaluateFun = (row0, row1) => { return getRowMatchInfo(row0, row1, ignoreCountMatch) } searchPair(null /* bigBoxes_0[0] */, rowBigBoxes_0.slice(), rowBigBoxes_1.slice(), null, resultPairs, evaluateFun) resultPairs = resultPairs.map(pairs => { let infos = pairs.map(pair => (pair.some(e => e.sid == 'void') ? null : getRowMatchInfo(pair[0], pair[1], ignoreCountMatch))) //infos.sort((a,b)=>{return a.score-b.score}); let score = infos.reduce((s, e) => { return s + (e && e.score > minScore ? e.score : minScore / 2) //只考虑组成功的分数 }, 0) return { pairs, infos, score, name: pairs.map(pair => pair.map(item => item.sid).join(' & ')), } }) resultPairs.sort((a, b) => b.score - a.score) /* if (resultPairs[0].name[0].includes('pano8') && resultPairs[0].name[0].includes('pano0')) { console.log(111) } */ resultPairs[0] && resultPairs[0].pairs.forEach((pair, i) => { let info = resultPairs[0].infos[i] if (info && info.score > minScore) { allRelations.push(info) let items = pair.filter(e => e.sid != 'void') common.pushToGroupAuto(items, matchGroups, null, atGroup => { //需要朝向一致才行 if (!info.k) return true //(box识别的宽高识别不准所以不需要) let onePair = atGroup.relationships[0] let name = onePair[0].sid + '&' + onePair[1].sid if (!rowInfos[name].k) return true //不过不应该有这种情况,否则匹配不到一起才对 if ((rowInfos[name].k < 1 && info.k < 1) || (rowInfos[name].k > 1 && info.k > 1)) { return true } else { console.log('k不一致无法匹配', info, atGroup) } }) //根据目前的规则应该是有端点的和有端点的匹配,box和box匹配 } }) //console.log(resultPairs[0]) } } } match() ignoreCountMatch || match('second') //再次将剩余的匹配一下,这次允许个数不同的row匹配 //console.log('matchGroups', matchGroups) //识别出来的多组,可能有重复的,因为box个数不同所以才没到一组 //整理一下,每个组整理出一个info,同时重新检查一下,挑去每组中和其他成员非常不同的 let groupInfo = [] let getGroupInfo = group => { let left = new THREE.Vector3(), right = new THREE.Vector3(), pointsLen = 0 let bigBoxes = [] let info = {} group.relationships.forEach(pair => { let name = pair[0].sid + '&' + pair[1].sid let matchInfo = rowInfos[name] //this.matchScoreMap["rowBigBox"][name] || this.matchScoreMap["cabinet"][name]; if (matchInfo.posLeft) { left.add(matchInfo.posLeft), right.add(matchInfo.posRight), pointsLen++ } else { bigBoxes.push(matchInfo) preDealBox(matchInfo) getBoxSize(matchInfo) } }) let index = groupInfo.length if (pointsLen > 0) { left.multiplyScalar(1 / pointsLen) right.multiplyScalar(1 / pointsLen) //addLabel(left, 'Left' + index, { bgcolor: '#F00', a: 0.2 }) //addLabel(right, 'Right' + index, { bgcolor: '#F00', a: 0.2 }) let center = new THREE.Vector3().addVectors(left, right).multiplyScalar(0.5) //addLabel(center, 'center' + index, { bgcolor: '#F00', a: 0.3 }) ;(info.left = left), (info.right = right), (info.center = center) info.pointsLen = pointsLen } if (bigBoxes.length > 0) { let getAve = bigBoxes => { let center1 = new THREE.Vector3(), size = new THREE.Vector3() bigBoxes.forEach(box => { let center0 = getBoxPos(box) center1.add(center0) size.add(box.size) }) if (pointsLen > 0) { let size0 = new THREE.Vector3(Math.abs(left.x - right.x), size.y, Math.abs(left.z - right.z)) size.add(size0.multiplyScalar(pointsLen)).multiplyScalar(1 / (pointsLen + bigBoxes.length)) center1.add(info.center.clone().multiplyScalar(pointsLen)).multiplyScalar(1 / (pointsLen + bigBoxes.length)) } else { size.multiplyScalar(1 / bigBoxes.length) center1.multiplyScalar(1 / bigBoxes.length) } return { center1, size } } let getScores = (center, size) => { //获得相对于center,size的差别分数 bigBoxes.forEach(box => { box.sc = -box.center.distanceToSquared(center1) - size.distanceToSquared(box.size) * 0.5 }) } let { center1, size } = getAve(bigBoxes) //console.log(center1, size) getScores(center1, size) bigBoxes.sort((a, b) => b.sc - a.sc) let midItem = bigBoxes[Math.floor(bigBoxes.length / 2)] //中位数 getScores(midItem.center, midItem.size) const minScore = -8 let removes = bigBoxes.filter(e => { return e.sc < minScore }) if (removes.length) { let { newGroups } = common.disconnectGroup( removes.map(e => [e.box0, e.box1]), matchGroups ) console.log('去除错误数据', removes) if (newGroups.length > 1) { //分裂成多组了,重新计算 newGroups.forEach(e => { getGroupInfo(e) }) return } bigBoxes = bigBoxes.filter(e => { return e.sc >= minScore }) } if (bigBoxes.length) { let o = getAve(bigBoxes) //again ;(info.center = o.center1), (info.size = o.size) } } info.k = getK(info) info.bigBoxes = bigBoxes info.group = group groupInfo.push(info) } matchGroups.slice(0).forEach(group => { getGroupInfo(group) }) let getLength = c => { //获取bigbox长度 return c.size ? (c.k > 1 ? c.size.x : c.size.z) : c.k > 1 ? c.right.x - c.left.x : c.right.z - c.left.z + 0.6 } let getLeft = (group, k) => { let dirAxis = (k || group.k) > 1 ? 'x' : 'z' return group.left ? group.left[dirAxis] - 0.3 : group.center[dirAxis] - group.size[dirAxis] / 2 //left和right加减半个宽度 } let getRight = (group, k) => { let dirAxis = (k || group.k) > 1 ? 'x' : 'z' return group.right ? group.right[dirAxis] + 0.3 : group.center[dirAxis] + group.size[dirAxis] / 2 } //识别是否group之间有一样的, 去重 { let realGroups = [] let getAveWidth = (infos, len) => { //获取这些infos最合适的箱子平均宽度和个数 let boxCounts = [] infos.forEach(e => { boxCounts.push(...e.group.map(bigBox => bigBox.boxes.length)) }) boxCounts.sort((a, b) => a - b) let midCounts = [] let r0 = 0.3, r1 = 0.7 //取中间这部分的算最适合的个数,结果不一定是中位数 boxCounts.slice(Math.floor(boxCounts.length * r0), Math.floor(boxCounts.length * r1) + 1).forEach(c => { if (!midCounts.includes(c)) midCounts.push(c) }) let { min, max } = standards.cabinet.widthNormal let standardW = (min + max) / 2 let aveWs = midCounts.map(e => { return { aveW: len / e, count: e } }) aveWs.sort((a, b) => Math.abs(a.aveW - standardW) - Math.abs(b.aveW - standardW)) let aveW = aveWs[0].aveW let count = aveWs[0].count if (aveW > max || aveW < min) { let w = THREE.MathUtils.clamp(aveW, min, max) //console.warn(`box aveW宽度不太对,从${aveW}修改到${w}`) aveW = w } return { aveW, count } } let getBox2 = (center, len, thick, k) => { let box2 = new THREE.Box2() box2.expandByPoint(new THREE.Vector2(center.x, center.z)) let sizeVec = k > 1 ? new THREE.Vector2(len / 2, thick / 2) : new THREE.Vector2(thick / 2, len / 2) box2.expandByVector(sizeVec) return box2 } const standardW = 0.6 //两排之间最小距离 for (let m = 0; m < groupInfo.length - 1; m++) { let group0 = groupInfo[m] for (let n = m + 1; n < groupInfo.length; n++) { let group1 = groupInfo[n] if (group1.bigBoxes[0]?.name == 'pano4-row-4&pano6-row-5,1' && group0.bigBoxes[0]?.name == 'pano4-row-2,5&pano6-row-3') { console.log(4) } /* if (group0.k == 4.242560016595383 || group1.k == 0.8571428571428572) { console.log(2) } */ const maxR = 2.3 if (((group0.k > 1 && group1.k < 1) || (group0.k < 1 && group1.k > 1)) && getLength(group0) > 1.5 && getLength(group1) > 1.5) continue //如果是方块状的无视k //间距 let spaceAxis = (group0.k + group1.k) / 2 > 1 ? 'z' : 'x' let spaceDis = Math.abs(group0.center[spaceAxis] - group1.center[spaceAxis]) if (spaceDis > standardW * 1.5) continue let o0 = getAveWidth([group0], getLength(group0)) //因为有可能长度和箱子个数不匹配,所以需要得到限制后的宽度再比较 let o1 = getAveWidth([group1], getLength(group1)) let len0 = (group0.predictLen = o0.aveW * o0.count) let len1 = (group1.predictLen = o1.aveW * o1.count) const minR = 0.5 //不可限制太死,因为有的框个数识别少了,导致len短。但可通过重叠面积来判断 // if( len0 / len1 < minR || len0 / len1 > 1/minR) continue let area0 = (group0.area = len0 * o0.aveW) let area1 = (group1.area = len1 * o1.aveW) let getBoxMixArea = (expandRatio1, expandRatio2) => { let box0 = getBox2(group0.center, len0 + expandRatio1, o0.aveW + expandRatio2, group0.k) let box1 = getBox2(group1.center, len1 + expandRatio1, o1.aveW + expandRatio2, group1.k) let mixBox = getMixBox(box0, box1) //重叠部分 let s = mixBox.getSize(new THREE.Vector2()) return { box0, box1, areaMix: Math.max(0, s.x) * Math.max(0, s.y) } //可能是0 } let areaMixExpand = getBoxMixArea(0.1, 0.3).areaMix if (areaMixExpand / area0 < 0.65 && areaMixExpand / area1 < 0.65) continue //包含的可以通过 /*let areaMix = getBoxMixArea(0, 0).areaMix //实际重合面积 group0.contains = group0.contains || [] group1.contains = group1.contains || [] group0.contains.push({ group: group1, selfPercent: areaMix / area0, percent2: areaMix / area1, areaMix }) group1.contains.push({ group: group0, selfPercent: areaMix / area1, percent2: areaMix / area0, areaMix }) */ //console.log('两个合并', group0, group1) common.pushToGroupAuto([group0, group1], realGroups) //包含的直接合并吧 - - ,这样会使结果偏移,不过没办法了,多个重叠面积太难算了 } } //但没合并前样本数量少,包含关系可能有错 - - /*for(let m=0; m e.percent2 > 0.8) //所有包含的 contains.reduce } */ groupInfo.forEach(info => { //加入单个的 if (!realGroups.some(groups => groups.includes(info))) { realGroups.push([info]) } }) //console.log('realGroups', realGroups) //get boxes realGroups.forEach((infos, i) => { const sampleCount = infos.reduce((w, c) => { return (w += c.pointsLen || c.bigBoxes.length) }, 0) let k /* { //const k = infos.reduce((w, c) => (w += c.k), 0) / infos.length let ks = infos.map(e => e.k) ks.sort((a, b) => a - b) let min = ks[0], max = ks[ks.length - 1] if (min < 1 && max > 1) { //比较最小和最大,选取更极端的那个 let min_ = 1 / min if (min_ < max) k = max else k = min } else k = (min + max) / 2 } */ { //看>1的和<1的平均数哪个多用哪个 let ks = { '<1': { count: 0, sum: 0 }, '>1': { count: 0, sum: 0 } } infos.forEach(e => { if (e.k < 1) { ks['<1'].count++, (ks['<1'].sum += 1 / e.k) } else { ks['>1'].count++, (ks['>1'].sum += e.k) } }) ks['<1'].count && (ks['<1'].ave = ks['<1'].sum / ks['<1'].count) ks['>1'].count && (ks['>1'].ave = ks['>1'].sum / ks['>1'].count) if (ks['<1'].ave > ks['>1'].ave) { k = 1 / ks['<1'].ave } else { k = ks['>1'].ave } } let centerPos = infos .reduce((w, c) => { return w.add(c.center.clone().multiplyScalar(c.pointsLen || c.bigBoxes.length)) }, new THREE.Vector3()) .multiplyScalar(1 / sampleCount) //预得中心点 //获取左右端点(需要排除可能的误差,所以采用最靠近端点的三个点。但无法排除前三个点中万一含有包含box的、或者误差大的端点) let lefts = infos .map(e => getLeft(e, k)) .sort((a, b) => a - b) .filter(a => a < centerPos[k > 1 ? 'x' : 'z']) .slice(0, 3) let rights = infos .map(e => getRight(e, k)) .sort((a, b) => b - a) .filter(a => a > centerPos[k > 1 ? 'x' : 'z']) .slice(0, 3) let left = 0, right = 0 let c0 = ((lefts.length + 1) * lefts.length) / 2 lefts.forEach((e, i) => { //越靠近最外侧权重越高。 left += e * ((lefts.length - i) / c0) }) c0 = ((rights.length + 1) * rights.length) / 2 rights.forEach((e, i) => { right += e * ((rights.length - i) / c0) }) centerPos[k > 1 ? 'x' : 'z'] = (left + right) / 2 let len = right - left //加一点值是因为之前计算长度,用的是最外box的中心点,会少box一半宽度 let infos2 = infos.filter(e => { return !e.predictLen || e.predictLen / len > 0.7 }) if (infos2.length == 0) { infos2 = infos.sort((a, b) => b.predictLen - a.predictLen).slice(0, 1) } let { aveW, count } = getAveWidth(infos2, len) //长宽比中心点的误差更大,尤其是box类型的、或样本少的 //获取高度 let heights = [] { let pairs = [], heightss /* infos.forEach(e => { pairs.push(...e.group.relationships.filter(pair => pair[0].boxes.length == count && pair[1].boxes.length == count)) }) if (pairs.length) { heightss = pairs.map(pair => { let boxes = pair.map(e => { return e.boxes.slice() }) let match = rowInfos[pair[0].sid + '&' + pair[1].sid] let ifReverse = match.reversed if (match.reversed == void 0 && pair[0].boxes.length > 1 && pair[1].boxes.length > 1) { let { reversed } = getReverseInfo(pair[0], pair[1]) ifReverse = reversed } if (ifReverse) { boxes[1].reverse() } let heights1 = [] let topPoss = [] for (let i = 0; i < count; i++) { let match1 = getMatchScore(boxes[0][i], boxes[1][i], { onlyGet: true }) let topPos = match1 && match1.topPos if (!topPos) { topPos = getBoxTop({ box0: boxes[0][i], box1: boxes[1][i] }) } heights1.push(topPos.y - groundY) if (topPos.y - groundY < 0) { console.log('?') } topPoss.push(topPos) } if ((k < 1 && topPoss[0].z > topPoss[count - 1].z) || (k > 1 && topPoss[0].x > topPoss[count - 1].x)) { heights1.reverse() } return heights1 }) } else { */ let bigBoxes = [] infos.forEach(e => { bigBoxes.push(...e.group.filter(e => !bigBoxes.includes(e) && e.boxes.length == count)) }) heightss = bigBoxes.map(bigBox => { let topPoss = bigBox.boxes.map(box => { let a = { box0: box } getBoxTop(a) return a.topPos }) if ((k < 1 && topPoss[0].z > topPoss[count - 1].z) || (k > 1 && topPoss[0].x > topPoss[count - 1].x)) { topPoss.reverse() } let heights1 = topPoss.map(topPos => topPos.y - groundY) return heights1 }) //} heightss.forEach(arr => { for (let i = 0; i < count; i++) { heights[i] = (heights[i] || 0) + arr[i] } }) heights = heights.map(e => { let h = e / heightss.length return h }) //console.log('heightss',heightss, pairs, heights) } //拆分成小box let size = new THREE.Vector3(aveW, 2, aveW) let c = 0 infos.rowboxs = [] while (c < count) { let center if (k > 1) { let startX = centerPos.x - ((count - 1) / 2) * aveW center = new THREE.Vector3(startX + c * aveW, centerPos.y, centerPos.z) } else { let startZ = centerPos.z - ((count - 1) / 2) * aveW center = new THREE.Vector3(centerPos.x, centerPos.y, startZ + c * aveW) } let size1 = heights[c] ? size.clone().setY(heights[c]) : size //如1*3的是得不到height的 let box = new Box({ name: 'row' + i + '-' + c, center, size: size1, boxType: 'cabinet', infos }) c++ infos.rowboxs.push(box) } }) return realGroups.length > 0 } } let removeContain = arr => { //去除嵌套 let len = arr.length if (len < 2) return for (let i = 0; i < len - 1; i++) { let box0 = arr[i] getBoxBase(box0) box0.contains = box0.contains || [] for (let j = i + 1; j < len; j++) { let box1 = arr[j] /* if (box0.sid == 'pano2-1' && box1.sid == 'pano2-7') { console.log(6) } */ getBoxBase(box1) box1.contains = box1.contains || [] let d3 = Math.abs(box1.bbox2CenterX - box0.bbox2CenterX) //限制d3是因为在相差180度两端可能也符合 //d4 = Math.abs(box1.bbox2[3] - box0.bbox2[3]) if (d3 > 0.4 /* || d4 > 0.01 */) continue let d0 = getBbox2Diff(box1.bbox2[0], box0.bbox2[0]), d1 = getBbox2Diff(box0.bbox2[2], box1.bbox2[2]), w0 = getBbox2Diff(box0.bbox2[2], box0.bbox2[0]), w1 = getBbox2Diff(box1.bbox2[2], box1.bbox2[0]) let min = Math.min(0.005, Math.min(w0, w1) * 0.2) //如果本身w0,w1宽度就小,差距更要小 if ((d1 >= 0 && Math.abs(d0) < min) || (d0 >= 0 && Math.abs(d1) < min) || (d1 >= 0 && d0 >= 0)) { box0.contains.push(box1) } else if ((d0 <= 0 && Math.abs(d1) < min) || (d1 <= 0 && Math.abs(d0) < min) || (d1 <= 0 && d0 <= 0)) { box1.contains.push(box0) } } } let getWidthScore = (box, type) => { const addDis = 0.1 //因为用的是btm的pos,比中心点近了一些,所以加上一段距离 let o = getBoxPoseByPos(box, getBoxPos(box), addDis) let boxPjW = o.projectWidth let standardPjW = (o.maxProjectWidth + o.minProjectWidth) / 2 let s = type == 'out' ? boxPjW - standardPjW : standardPjW - boxPjW return -Math.pow(s, 2) * 10 } let minChildCount = arr[0].boxType == 'battery' ? 1 : 2 arr.slice().forEach(box => { if (box.contains.length >= minChildCount) { //假设不存在第二层嵌套, 假设每个只能被一个嵌套 //决定留大还是留小 //先只去掉包含两个以上的,且角度范围一致 //尽量保留内层,除非内层太小 let { leftX, rightX } = getLeftRight(box.contains) if (box.contains.length > 1 && (Math.abs(getBbox2Diff(box.bbox2[0], leftX)) > 0.005 || Math.abs(getBbox2Diff(box.bbox2[2], rightX)) > 0.005)) return //范围不一致 let remainChild = true if (box.contains.length == 1) { //cdi6xzNiNdS 电池包含单个 感觉一般都是留大 remainChild = false } else { let outScore = getWidthScore(box, 'out') let childrenScores = box.contains.map(e => getWidthScore(e, 'in')) let childAve = childrenScores.reduce((w, c) => w + c, 0) / childrenScores.length if (childAve < -4) { //let outScore = getWidthScore(box, 'out') remainChild = childAve > outScore } } if (!remainChild) { box.contains.forEach(e => { ;(e.state = '因被嵌套被删除'), (e.containBy = box) arr.splice(arr.indexOf(e), 1) console.log('因被嵌套被删除', ...box.contains) }) } else { box.state = '因嵌套其他被删除' console.log('因嵌套其他被删除', box) arr.splice(arr.indexOf(box), 1) } } }) } let waitFindRest = [] let Search = type => { console.error('开始search', type) let matchScoreMap = (this.matchScoreMap[type] = {}) let datas = {} let panoIds = [] for (let id in this.datas) { if (!this.datas[id]) continue datas[id] = this.datas[id].shapes.filter(e => isType(e.category, type)) datas[id].length && panoIds.push(id) } for (let id in this.datas) { //对data预处理 //(之后如果还出现不同类型重叠在一起的,需要先识别摘除下。 )4GqaqNdyjGf if (!this.datas[id]) continue removeContain(datas[id]) //去除线框中的嵌套,主要是一个嵌套两个的。案例:KK-1Zjm9Rbl47 if (datas[id].length) { //融合。很多box被一分为二了,基本都是在全景图左右边界处。 let bigBoxes = getPanoBigRowBox(datas[id], { reason: 'mix' }) bigBoxes.forEach(bigBox => { if (bigBox.boxes.length > 1) { bigBox.boxes.forEach(box => { ;(box.state = '被删除'), (box.mixTo = bigBox) let i = datas[id].indexOf(box) datas[id].splice(i, 1) if (version == 'vision') { i = this.datasMixed[id].shapes.findIndex(e => e.sid == box.sid) this.datasMixed[id].shapes.splice(i, 1) } }) //console.log('因融合而删除', bigBox.boxes) datas[id].push(bigBox) if (version == 'vision') { this.datasMixed[id].shapes.push(bigBox) } let shapes = this.datas[id].shapes bigBox.index = shapes.length > 1 ? shapes[shapes.length - 2].index + 1 : 0 { let a = bigBox.sid.split('mix-') bigBox.sid = a[0] + bigBox.index + '(mix' + a[1] + ')' //"pano20-mix-1,2" } } }) } } if (panoIds.length == 0) { if (standards[type].tinyXZ) { //fire 调试:nZrBdvRaDuC this.expandModelBound() } return } panoIds.sort((a, b) => { return datas[b].length - datas[a].length }) let groups = panoIds.map(e => datas[e]) //console.log('按box个数排序:', groups.slice()) let group0 = groups[0], len0 = group0.length if (groups.length == 1) { //只有一个全景里有数据 if (standards[type].tinyXZ) { //fire. 无法match以获取groundY, 就取消。 调试: eGhyf5QdVHA this.expandModelBound(type) } group0.forEach(e => createSinglePano(e)) return combines(type) } let finish = groups => { waitFindRest.push({ type, args: [groups] }) //等待最后检查遗漏 if (standards[type].tinyXZ) { //fire 调试:nZrBdvRaDuC this.confirmGroundY(type) } if (standards[type].tiny) { //monitor 调试:S9yepREK8Jl this.expandModelBound2(type) } } if (len0 == 1) { //最多的也只有一个box。此情况大部分是空调 panoIds.forEach(e => getBoxBase(datas[e][0])) let maxAngle //找出centerDir夹角最大的两个pano for (let i = 0; i < panoIds.length; i++) { let box0 = datas[panoIds[i]][0] for (let j = i + 1; j < panoIds.length; j++) { let box1 = datas[panoIds[j]][0] getMatchScore(box0, box1, { isSingle: true }) } } let list = Object.keys(matchScoreMap) list.sort((a, b) => { return matchScoreMap[b].score - matchScoreMap[a].score }) let match = matchScoreMap[list[0]] preDealBox(match) if (match.score > -100) { getBoxSize(match) if (match.score > 0 && match.sizeAdjust < 0.1) { new Box(matchScoreMap[list[0]]) //waitFindRest.push({ type, args: [groups] }) //等待最后检查遗漏 finish(groups) return } } //根据分数重排序,前两个已匹配的pano放在第一第二(reMatchLowScores会跳过),获得groups2 let panoIds2 = [] list.forEach(e => { let info = matchScoreMap[e] if (!panoIds2.includes(info.box0.pano.id)) panoIds2.push(info.box0.pano.id) if (!panoIds2.includes(info.box1.pano.id)) panoIds2.push(info.box1.pano.id) }) let groups2 = panoIds2.map(e => datas[e]) //继续match reMatchLowScores([match], groups2) finish(groups2) return } { //重新根据距离排序,挑选离所有box距离最近的两个pano (远的可能看不到box,或者得到的线框计算的位置不准。不过其实太近也不准-,-) let counts = {} groups.forEach(e => { e.forEach(a => getBoxBase(a)) counts[e.length] || (counts[e.length] = []) counts[e.length].push(e) }) groups = [] let atWall = standards[type].atWall let nums = Object.keys(counts) let bestDisSquared = 2 nums.reverse() nums.forEach(count => { let groups_ = counts[count] if (groups_.length > 1) { groups_.forEach(e => { e.disSc = e.reduce((w, c) => { let pos = getBoxPos(c) //let s = atWall ? Math.pow(Math.abs(c.centerDir.y), 3)*100 : 0 //在墙上的话尽量平视 //console.log(c.sid,s) return w + Math.abs(c.pano.position.distanceToSquared(pos) - bestDisSquared) //+ s }, 0) }) groups_.sort((a, b) => a.disSc - b.disSc) } groups.push(...groups_) }) //console.log('按距离和个数排序:', groups) group0 = groups[0] len0 = group0.length } if (type == 'cabinet') { //转化为分组 if (searchByRow(groups, datas)) { waitFindRest.push({ type, args: [groups, 0] }) //等待最后检查遗漏 return } } //零散匹配。 let match2Group = (group0, group1, { fake }) => { let len0 = group0.length, len1 = group1.length for (let i = 0; i < len0; i++) { //复杂度:n的平方次 for (let j = 0; j < len1; j++) { let box1 = group0[i] let box2 = group1[j] let result = getMatchScore(box1, box2) } } //寻找最佳配对 n!种组合(是否要限制个数多的情况?) 超过8个就很恐怖 //仅先查找选中的两个pano配对 let resultPairs = [] let newGroup0 = group0.slice(0) let newGroup1 = group1.slice(0) /* while (newGroup1.length < newGroup0.length) { newGroup1.push({ sid: 'void' }) //为了使排列正确,补个空,使左右两边个数相等,过后和void匹配的不会计算box } */ let evaluateFun = getMatchScore.bind(this) searchPair(/* group0[0] */ null, newGroup0, newGroup1, null, resultPairs, evaluateFun) /* console.log( 'resultPairs', resultPairs.map(pairs => pairs.map(pair => pair.map(item => item.sid).join(' & '))) ) */ resultPairs = resultPairs.map(pairs => { let infos = pairs.map(pair => matchScoreMap[pair[0].sid + '&' + pair[1].sid]) let score = infos.reduce((s, e) => { return s + (e ? e.score : 0) }, 0) let o = { infos, score, pairs, name: pairs.map(pair => pair.map(item => item.sid).join(' & ')), } return o }) //console.log('resultPairs', resultPairs.slice()) console.log( 'resultPairs按分数高低', resultPairs.sort((a, b) => b.score - a.score) ) //console.log('compu',compu) let minScore = boundConfirmed ? -2000 : -800 let noMatches = [] //和void匹配的,需要和其他pano的重新匹配 let mayHaventMatched = [] let lowScores = [] if (resultPairs[0]) { resultPairs[0].infos.forEach((info, i) => { if (!info) { noMatches.push(resultPairs[0].pairs[i].find(e => e.sid != 'void')) return //match with void } if (info.score < minScore || (standards[info.boxType].bottom && info.minAng < 5)) { lowScores.push(info) return } preDealBox(info) getBoxSize(info) if ((info.sizeAdjust && standards[info.boxType].tiny) || info.sizeAdjust > 0.2) { //或者识别下悬挂的且线的角度较小 lowScores.push(info) return } if (info.box0.category == typeNames.battery && info.size.x < 1 && info.size.z < 1) { //宽度较小 let vec0 = new THREE.Vector3().subVectors(info.box0.pano.position, getBoxPos(info)) let vec1 = new THREE.Vector3().subVectors(info.box1.pano.position, getBoxPos(info)) /* let k0 = Math.abs(vec0.x / vec0.z), k1 = Math.abs(vec1.x / vec1.z), maxR = 6 if(info.name == '') */ if (vec0.x * vec1.x > 0 && vec0.z * vec1.z > 0 /* || k0/k1 > maxR || k0/k1 < 1/maxR */) { //同一个象限 或 偏向一侧 lowScores.push(info) return } } info.fake = fake let box = new Box(info) }) } if (noMatches.length) { reMatchLowScores( noMatches.map(e => { return { box0: e } }), groups, { fake } ) } reMatchLowScores(lowScores, groups, { fake }) } {//KJ-iUrPonbhCQo 大的场景有很多柜子时,groups的前两个元素很可能相隔很远,导致匹配不到柜子。 let disScores = [], bestDisSquared = 2 for (let i = 0; i < groups.length - 1; i++) { let dis = groups[i][0].pano.position.distanceToSquared(groups[i+1][0].pano.position) let score = 1/Math.max(dis - bestDisSquared, 0.05) + (groups[i].length + groups[i+1].length) disScores.push() } for (let i = 0; i < groups.length - 1; i++) { match2Group(groups[i], groups[i + 1], { fake: i > 0 }) if (!standards[type].tinyXZ /* && !browser.urlHasValue('many') */ || groups[i + 1].length < 2) { break } } } finish(groups) } function blocked(box) { //该box是否被实体box遮挡 //WcLVXvmV9AU:pano2-2 和 pano12-4 ; 5yhlMduTHL8:pano2-10 /* if (box.sid == 'pano6-7') { console.log(1) addLine(box.pano.position, box.centerBtmDir, 20) } */ if (!standards[box.boxType].bottom) { //当底部每个方向都有遮挡物时,其位置很可能不准。但若是部分遮住,还是有可能识别对的 if (!box.blocked) { box.blocked = { centerBtmDir: null, leftBtmDir: null, rightBtmDir: null, } } for (let dir in box.blocked) { if (!box.blocked[dir]) { let block = boxesSolid.find(boxSolid => { ray.set(box.pano.position, box[dir]) let o = ray.ray.intersectsBox(boxSolid.bound) if (o) { // 遮挡 return true } }) if (block) { //若不存在,即都遮挡了,就算真的遮挡 box.blocked[dir] = block } else { box.blocked[dir] = false return false //找到一个方向无遮挡则可返回 } } } return true //暂定,若某个方向已得到遮挡了,就不计算。但若没有,就再搜一遍(如果会重复搜之后再增加一个stamp啥的,比如记录下当前所有solidBoxes的name,如果改变了就重新搜下) } else { //若中心方向已经有同类型的box,很可能它就是自身(因为挂墙的预测位置很不准,所以需要这一步。越tiny的越能通过此来筛选) let block = boxesSolid.find(boxSolid => { if (boxSolid.boxType != box.boxType) return ray.set(box.pano.position, box.centerDir) let o = ray.ray.intersectsBox(boxSolid.bound) if (o) { // 遮挡 return true } }) if (block) { box.blocked = block return true } } } //调试 O540aEVF3b7 jQUQlER160 n4z0yd5tQaF WcLVXvmV9AU 8czlwsbSe5 NlUM8yGve9 function findRest(groups /* ,startIndex=2 */) { //查找是否有遗漏。 //1 可能有距离较远的box不在头两个pano的附近导致被漏掉。(概率很小) //2 被剩余的 (包括低分匹配中放弃的,这种需要的距离识别度高) const tolerateWidth = { min: 0.1, max: 0.5 } let boxes = [], scores = new Map(), bestDisSquared = 2 groups.forEach(g => { boxes.push(...g) }) boxes = boxes.filter(box => !used(box)) boxes.forEach(box => { getBoxBase(box) let p1 = new THREE.Vector2(box.pano.position.x, box.pano.position.z) let p2 = new THREE.Vector2(box.btmPos.x, box.btmPos.z) let dis = p1.distanceToSquared(p2) let s1 = -Math.abs(dis - bestDisSquared) //将距离最佳的排前面,距离远的得到的位置不准确,也容易被遮 if (!box.pose) box.pose = getBoxPoseByPos(box, getBoxPos(box)) let s2 = getPoseScore([box.pose], box.boxType) + THREE.MathUtils.clamp(box.pose.projectWidth, box.pose.minProjectWidth, box.pose.maxProjectWidth) * 7 //projectWidth长的更安全 scores.set(box, { s1, s2, sum: s1 + s2 }) }) //console.log(boxes[0].boxType,scores) boxes = boxes.sort((a, b) => { return scores.get(b).sum - scores.get(a).sum }) //FXcq5PI9QGv //console.log(boxes) boxes.forEach(box => { if (box.sid == 'pano0-4') { console.log(3) } if (!used(box) && !blocked(box)) { //如果和现有的box的距离都很远,很可能是漏掉的 let near = boxesSolid.find(solidBox => { if (solidBox.name == 'air-pano16-7&pano14-13') { console.log(3) } if (solidBox.boxType != box.boxType && (solidBox.boxType == 'air-hanging' || box.boxType == 'air-hanging')) return //挂空调一般不会撞到地面上的 let p1 = getBoxPos(solidBox) let p2 = getBoxPos(box) let p1_ = new THREE.Vector2(p1.x, p1.z) let p2_ = new THREE.Vector2(p2.x, p2.z) let maxWidth = standards[box.boxType].widthNormal.max / 2 maxWidth = THREE.MathUtils.clamp(maxWidth, tolerateWidth.min, tolerateWidth.max) //因为场景精度存在较大误差,所以maxWidth不能过小,否则像灭火器摄像头都容易findRest多个 let dis = solidBox.bound.distanceToPoint(p2) //let r0 = solidBox.boxType == 'air' ? 2 : solidBox.boxType == 'battery' ? 1.1 : 1 //空调最不容易扎堆放置,所以范围设置广一些 let r0 = standards[box.boxType].closeRatio let r1 = math.linearClamp(box.pano.position.distanceTo(p2), 3, 20, 1, 5) //距离远的话识别、计算都会更不准确,给一定的容错. 远的尽量不findRest,即尽量>0 let ra = (solidBox.boxType == box.boxType ? 1 : 0.5) * r0 * r1 //数字越小限制越大 /* let a = maxWidth * maxWidth * ra - p1_.distanceToSquared(p2_) let b = -dis * dis * 0.7 let c = a + b*/ let c = maxWidth * ra - dis * 1.3 /* if (c > 0) { //太近 不创建 console.log(1) } */ return c > 0 }) if (!near) { reMatchLowScores([{ box0: box, log: 'findRest' }], groups, { startIndex: 0 }) } else { //console.log('find near', near.name, box.sid) } } }) } function used(box) { //是否已经使用过 let has = e => { return e.box0 == box || e.box1 == box } let traverse = e => { return ( has(e) || (e.list && e.list.some(a => traverse(a))) || (e.mixedFrom && e.mixedFrom.some(a => traverse(a))) || (e.infos && e.infos.some(u => u.group.some(r => r.boxes.some(b => b == box)))) ) //row } return boxesSolid.some(e => traverse(e)) } function reMatchLowScores(lowScores, groups, { startIndex = 2, fake } = {}) { let matched = [], tooLows = [] let minScore = boundConfirmed ? -2000 : -800 let isSameMatch = (match0, match1) => { //是否对应同一个box实体(不一定准),通过两两box之间是否都match来判断 //如果是相同pano但不同的box肯定不是对应同一个box实体 let ifWrong = (box0, box1) => { if (box0 == box1) return if (box0.pano == box1.pano) return true let match2 = getMatchScore(box0, box1, { isSingle: true, restMatch: true }) if (match2.score < minScore) return true } if (ifWrong(match0.box0, match1.box0) || ifWrong(match0.box1, match1.box1) || ifWrong(match0.box0, match1.box1) || ifWrong(match0.box1, match1.box0)) return return true } if (lowScores.length) { //console.warn(lowScores[0].log || (lowScores[0].box1 ? '低分重新匹配' : '剩余匹配'), lowScores[0].box0.boxType, ...lowScores) if (lowScores[0].box1) { lowScores.sort((a, b) => { //低分优先 return a.score - b.score }) } lowScores.forEach(info => { //info中的box0和box1分别向后寻找其他的配对。选择分数高的配对。 但box0和box1可能是错误配对,会导致找到了替代的也可能遗漏。 /* if (info.name == 'pano0-3&pano2-3') { console.log(1) } */ let box01 = info.box0 let box02 = info.box1 let bigGroup = [] box02 && bigGroup.push(info) let got for (let cur = startIndex; cur < groups.length; cur++) { let thirdGroup = groups[cur] let scores0 = [], scores1 = [] thirdGroup.forEach(box1 => { //if (matched.includes(box1.sid)) return if (used(box1)) return //会不会太严格? if (box1.pano != box01.pano && box1 != box02) { let r1 = getMatchScore(box01, box1, { isSingle: true, restMatch: true }) r1.score > minScore * 1.5 && scores0.push(r1) } if (box02 && box1.pano != box02.pano && box1 != box01) { let r2 = getMatchScore(box02, box1, { isSingle: true, restMatch: true }) r2.score > minScore * 1.5 && scores1.push(r2) } }) scores0.sort((a, b) => { return b.score - a.score }) scores1.sort((a, b) => { return b.score - a.score }) scores0[0] && bigGroup.push(scores0[0]) scores1[0] && bigGroup.push(scores1[0]) } bigGroup.sort((a, b) => { return b.score - a.score }) let goodList = bigGroup.slice(0, 10).map(e => { if (!getBoxPos(e)) return e preDealBox(e) getBoxSize(e) return e }) let goodList2 = goodList .sort((a, b) => { return b.score - a.score }) .slice(0, 3) if (goodList2.length == 0) { return fake || createSinglePano(box01, 0.6) //minScorePercent原因:剩余匹配时位置不太好的案例: AhMgXXjM15 } if (goodList2[0].score > minScore * 0.65) { goodList2 = goodList2.filter(e => e.score > minScore * 0.65) } else { /* goodList2 = [goodList2[0]] // 最高分已经过小 if (goodList2[0].score < -1500) { */ if (!box02) { if (info.log == 'findRest' || getBoxPos(box01).distanceTo(box01.pano.position) < 2.5) { //远距离不准,留到过后findRest fake || createSinglePano(box01) } return } //console.warn('分数过低,是否有匹配错误?', goodList2[0]) return tooLows.push(goodList2[0]) //} } if (goodList2.length) { //需要确认两两之间是配对的,也就是都对应同一个box let subGroups = [], boxes = [] for (let i = 0, len = goodList2.length; i < len; i++) { //向后选择队友 let match0 = goodList2[i] if (subGroups.some(e => e.includes(match0))) continue //被挑选了的没有选择权 let gr = [match0] for (let j = i + 1; j < len; j++) { let match1 = goodList2[j] if (isSameMatch(match0, match1)) { //可能不是同一个,所以需要检验 gr.push(match1) } } //if(gr.length>1){ subGroups.push(gr) //} } //console.log('lowScores subGroups', subGroups) subGroups.forEach(pair => { boxes.push(mixMatchBox(pair, lowScores[0].log, fake)) }) fake || combineBoxes(boxes) //很可能其实还是同一个,需要检验是否要融合 } }) //改为之后 findRest, 因为两者都single的可能性低 /* let judge = box => { if (!used(box)) { matched.push(box) createSinglePano(box) } } tooLows.forEach(e => { judge(e.box0) judge(e.box1) }) */ } } function mixMatchBox(list, log, fake) { let center = new THREE.Vector3(), size = new THREE.Vector3(), name, bound = new THREE.Box3() list.forEach(e => { let _bound = new THREE.Box3().setFromCenterAndSize(getBoxFinalPos(e), e.size) bound.union(_bound) }) //bound.getCenter(center) //这两种获得center的方法哪个准? bound.getSize(size) list.forEach(e => center.add(getBoxPos(e))) center.multiplyScalar(1 / list.length) let { xProp, yProp } = list.find(e => e.xProp) || {} if (xProp && list.find(e => e.xProp && e.xProp != xProp)) { //如果有不同的话 xProp = yProp = null } let prefix = log == 'findRest' ? 'rest:' : 'low:' let object = { name: prefix + list.map(e => e.name), boxType: list[0].boxType, center, size, list, xProp, yProp, } xProp || getBoxDirProp(object) let o = restrictSize(size.x, size.y, size.z, object) size.x = o.x size.y = o.y size.z = o.z object.fake = fake let box = new Box(object) //console.log('mixMatchBox', box) return box } function combineBoxes(boxes, typeCount) { //调试 tY4ot33f8vT UWrshepp0G5的一高一低的电箱 //判断这些实体boxes是否需要合并 主要用于重复识别(重叠面积较大) 电池还可能是拼接 const group = [] if (boxes.length > 1) { let boxType = boxes[0].boxType let { min, max } = standards[boxType].widthNormal for (let i = 0, len = boxes.length; i < len - 1; i++) { let box0 = boxes[i] if (box0.fake) continue for (let j = i + 1; j < len; j++) { let box1 = boxes[j] if (box1.fake) continue let bound = box0.bound.clone().union(box1.bound) let size = bound.getSize(new THREE.Vector3()) let intersect = box0.bound.intersectsBox(box1.bound) if (box0.boxType == 'electric') { console.log(1) } let minX = min, minZ = min, maxX = max, maxZ = max, maxY = standards[boxType].height.max if (box0.xProp && box1.xProp && box0.xProp == box1.xProp) { maxX = standards[boxType][box0.xProp].max maxZ = standards[boxType][box0.yProp].max /* minX = standards[boxType][box0.xProp].min minZ = standards[boxType][box0.yProp].min */ if (box0.boxType != box1.boxType) { //air & airSmart maxX = Math.max(maxX, standards[box1.boxType][box1.xProp].max) maxZ = Math.max(maxZ, standards[box1.boxType][box1.yProp].max) /* minX = Math.min(minX, standards[box1.boxType][box1.xProp].min) minZ = Math.min(minZ, standards[box1.boxType][box1.yProp].min) */ } } maxX = Math.max(maxX, box0.size.x, box1.size.x) //必须大于各自的size,否则无法去除本身就oversize的box中包含的 maxZ = Math.max(maxZ, box0.size.z, box1.size.z) maxY = Math.max(maxY, box0.size.y, box1.size.y) let r = intersect ? 1.5 : 1.3 /* / standards[box1.boxType].closeRatio */ //如果是没有交集,限制更大些 //若需要更精确的结果,可以getMixBox算出重叠面积,重叠少,且各自都不太小,就不合并。但考虑到电池边界很模糊,合并了也无大碍。 if (box0.boxType == 'battery') { r *= 1.3 //比较可能扎堆 } let maxDiff = 0.4, maxDiffX = maxDiff, maxDiffZ = maxDiff, rx = r, rz = r if (standards[box0.boxType].atWall == 1) { //在墙面上不可能叠放,所以厚度限制可放宽 let s = 4 if (box0.yProp == 'width') { maxDiffX *= s rx *= s } else if (box0.xProp == 'width') { maxDiffZ *= s rz *= s } } //如果某个点位含有类似和这俩相近的box: A9rCPzp2UD9两个fire合并了。好难写,算了 /* if(box0.boxType){ this.datasMixed rx = rz = 1.1 } */ if (size.x - maxX < maxDiffX && size.x < maxX * rx && size.z < maxZ * rz && size.z - maxZ < maxDiffZ && size.y < maxY * r && size.y - maxY < maxDiff) { //总size不会太大 common.pushToGroupAuto([box0, box1], group) } } } if (group.length) { //虽然如果三个以上可能会超出maxWidth。 不过3个的概率很低,且可以限制宽度 group.forEach(pair => { let boxTypes = [] let bound = new THREE.Box3() pair.forEach(e => { bound.union(e.bound) e.dispose() if (typeCount > 1) { //判断boxType: 寻找所使用的box总分最高的boxType let a = boxTypes.find(a => a.boxType == e.boxType) let score = 0 let add = box => { box && (score += box.score) } e.traversePair(e => { add(e.box0) add(e.box1) }) if (a) { a.score += score } else { boxTypes.push({ score, boxType: e.boxType }) } } }) if (typeCount > 1) { boxTypes.sort((a, b) => { return b.score - a.score }) boxType = boxTypes[0].boxType } let size = bound.getSize(new THREE.Vector3()) let center = bound.getCenter(new THREE.Vector3()) let { xProp, yProp } = pair.find(e => e.xProp) || {} if (xProp && pair.find(e => e.xProp && e.xProp != xProp)) { //如果有不同的话 xProp = yProp = null } let info = { name: 'mix:' + pair.map(e => ' ' + e.name), mixedFrom: pair, boxType, center, size, xProp, yProp, } xProp || getBoxDirProp(info) let o = restrictSize(size.x, size.y, size.z, info) size.x = o.x size.y = o.y size.z = o.z let box = new Box(info) console.error('混合', boxType, pair, box) }) } } } function combines(types) { //合并boxSolids . battery经常嵌套 if (!(types instanceof Array)) types = [types] let boxes = boxesSolid.filter(e => types.includes(e.boxType)) combineBoxes(boxes, types.length) } let createSinglePano = (box, minScorePercent = 1) => { //仅用一个pano中的data来创建。 悬挂于墙上的准确性依赖于墙的准确性。 if (box.score < MinBoxInitialScore) { //如Xszq2fv03b的电池pano8-0其实是纸箱、 WZQoMbNmNTu的pano14-0分数0.649 return console.error('取消createSinglePano: 线框识别分数低,可能错误', box.sid, box) } getBoxBase(box) let center = getBoxPos(box) if (safeBound.distanceToPoint(center) > 0) { return console.log('取消createSinglePano:超出safebound', box) //可能是错误的线框,如H7pg1tO9oeJ pano8-1 } let info = { name: box.sid, box0: box, center, topPos: box.topPos, btmPos: box.btmPos, } preDealBox(info) const minScore = -500 * minScorePercent //调试 3MnIWabM6ne Tmo1vLp9Q13 let a = getPoseScore(info.boxposes, box.boxType /* true */) if (!standards[box.boxType].bottom && box.btmPos) { a -= (box.btmPos.y - groundY) * 3000 //底部被遮住一部分(但是像vHC1GfkdKtD在户外,容易底部变高到bound外去怎么办) } let failed = a < minScore console.log('createSinglePano', failed ? '失败' : '成功', 'pose score:', a, box.sid, box) if (failed) return //addLabel(center, 'center', { a: 0.3 }) //info.topPos && addLabel(info.topPos, 'topPos', { a: 0.3 }) info.score = a getBoxSize(info) new Box(info) } //去除挨得很近的漫游点,因为两个接近的点match出的值误差很大 { this.removedDatas = {} let panoIds = Object.keys(this.datas).filter(id=>{ if(!player.model.panos.index[id]){//排除数据错误 this.removedDatas[id] = this.datas[id] delete this.datas[id] return } return true }) let len = panoIds.length for (let i = 0; i < len; i++) { let pano0 = player.model.panos.index[panoIds[i]] for (let j = i + 1; j < len; j++) { let pano1 = player.model.panos.index[panoIds[j]] if (pano0.position.distanceToSquared(pano1.position) < 0.01) { //离的很近。保留shape多的那个data let remove if (this.datas[panoIds[i]].shapes.length > this.datas[panoIds[j]].shapes.length) { remove = panoIds[j] } else { remove = panoIds[i] } console.log(`删除pano${remove}的data,因pano${panoIds[i]}和pano${panoIds[j]}很近`) this.removedDatas[remove] = this.datas[remove] delete this.datas[remove] } } } } //this.expandModelBound() // if (version == 'vision') this.datasMixed = common.CloneObject(this.datas, null, [player.model.panos.list[0].constructor]) if (version == 'vision') this.datasMixed = common.CloneObject(this.datas, null, undefined, data => { return data['category'] }) /* Search('cabinet') Search('air') Search('battery') */ for (let i in typeNames) { Search(i) } console.log('----FindRest----') waitFindRest.forEach(e => { findRest(...e.args) if (e.type != 'air' && e.type != 'airSmart' && e.type != 'cabinet') combines(e.type) }) combines(['air', 'airSmart']) //这两种合在一起combine,因为太像了容易识别出多个 nZrBdvRaDuC console.log('cost:', Date.now() - startTime, 'ms, boxSolid:', this.boxesSolid.map(e=>e.position), '共'+this.boxesSolid.length+'个') } /* let getSid = (function(){ let sid = 0 return function(){ return sid++ } })() */ let done = () => { for (let panoId in this.datas) { if (!this.datas[panoId]) continue this.datas[panoId].shapes = this.datas[panoId].shapes.map((shape, i) => { return Object.assign( { sid: 'pano' + panoId + '-' + i, category: shape.category, //提前 便于调试 pano: player.model.panos.index[panoId], index: i, }, shape ) }) } if (this.ifAnalyze) { this.panoBound = new THREE.Box3() player.model.chunks.forEach(e => { modelBound.union(e.geometry.boundingBox) //注:不用model.boundingBox是 因为union了pano的position的 }) //针对部分模型错误,只有底面的,union一下pano.position let minY = Infinity, minYs = [] let panos = player.model.panos.list.filter(e => e.isAligned()) panos.forEach(e => { let bound = new THREE.Box3().setFromCenterAndSize(e.position, new THREE.Vector3(0.1, 0.1, 0.1)) modelBound.union(bound) this.panoBound.union(bound) minY = Math.min(e.floorPosition.y, minY) //avePanoFY += e.floorPosition.y minYs.push(e.floorPosition.y) }) groundY = modelBound.min.y minYs.sort((a, b) => { return a - b }) //console.log(minYs) let midFloorY = minYs[Math.floor(minYs.length / 2)] console.error('minY', minY, 'midFloorY', midFloorY, '原groundY', groundY) this.minY = minY //部分模型底部高度错误 /* if (minY > groundY) { console.error('minY > groundY', minY, groundY) groundY = modelBound.min.y = midFloorY //案例nZrBdvRaDuC } else { if (groundY - minY > 0.05) console.warn('minY', minY, 'groundY', groundY) groundY = modelBound.min.y = midFloorY , document.title += ' new' //修改以后未必更好所以暂时不修改 变更好的:eGhyf5QdVHA } */ groundY = modelBound.min.y = midFloorY //这个y可能不准。需要通过fire的btmPos.y来确定 safeBound = this.safeBound = modelBound groundPlane.setFromNormalAndCoplanarPoint(new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, groundY, 0)) beginCompute() } if (version == 'vision') this.load(player.currentPano.id) } async function load(panoId) { let data = await http.post('/service/scene/sceneMarkShape/getInfo', { num: player.$app.config.num, imagePath: panoId + '.jpg' }) // console.error(data) if (!data.data || !data.success) return (this.datas[panoId] = null) this.datas[panoId] = data.data if (Object.keys(this.datas).length == panosCount && !player.model.panos.list.some(e => e.isAligned() && !(panoId in this.datas))) { done() } } async function loadAll() { let data = await http.post('/service/shapes/sceneMarkShape/getInfos', { num: player.$app.config.num }) if (!data.data || !data.success) return if (browser.urlHasValue('ext')) { //利用算好的toJson的数据展示框 //console.log('box8',data.data) data.data.box8.dataSet.boxes.forEach(e => { let point1 = math.convertVisionVector(new THREE.Vector3().fromArray(e.points[0])) let point2 = math.convertVisionVector(new THREE.Vector3().fromArray(e.points[1])) let point3 = math.convertVisionVector(new THREE.Vector3().fromArray(e.points[2])) let point4 = math.convertVisionVector(new THREE.Vector3().fromArray(e.points[4])) let size = new THREE.Vector3(point1.distanceTo(point2), point1.y - point4.y, point3.distanceTo(point2)) let center = new THREE.Vector3().addVectors(point3, point4).multiplyScalar(0.5) //对角线中点 //可能需要考虑上rotation,现暂时不需要 let box = new Box({ buildFromData: true, center, size, boxType: e.type, name: e.sid, }) }) this.ifAnalyze = false } let box4 = Array.isArray(data.data) ? data.data : data.data.box4 // 兼容旧数据 box4.forEach(e => { let panoId = e.imagePath.split('.jpg')[0] this.datas[panoId] = e }) done() } let panosCount = 0 if (!dataList) { /* player.model.panos.list.forEach(e => { if (!e.isAligned()) return panosCount++ load.bind(this)(e.id) }) */ loadAll.bind(this)() //测试环境 } else { //when version == 'output' dataList.forEach(e => { let panoId = e.imagePath.split('.jpg')[0] this.datas[panoId] = e }) done() } } expandModelBound() { //有的模型太窄,容易将一排的柜体当做墙壁。所以可以根据box位置扩展bound. (如果因为点位太少,导致内部的电池远超模型范围,就不管了。R7xZsmm9FsG) const maxDis0 = 1, //最终位置不会超过这个距离 maxDis1 = 3 //搜寻范围。不可扩展太宽,否则不准确的框会飘很远,甚至多画多个box,如R7xZsmm9FsG let newBound = modelBound.clone() let list = [] for (let panoId in this.datas) { if (!this.datas[panoId]) continue let { imageWidth, imageHeight } = this.datas[panoId] this.datas[panoId].shapes.forEach(box => { getBoxBase(box, imageWidth, imageHeight) if (box.sid == 'pano2-10') { console.log(4) } if (box.btmPosPredict) { let far = box.pano.position.distanceToSquared(box.btmPosPredict) if (far > 20) return //太远不准 let dis = modelBound.distanceToPoint(box.btmPosPredict) if (dis > 0 && dis < maxDis1) { //maxDis1用来防air-hanging和一些错误的框 list.push({ box, dis }) } } }) } list.sort((a, b) => a.dis - b.dis) //let mid = list[Math.floor(list.length/2)] let mid = list.length // /2 for (let i = 0; i < mid; i++) { let box = list[i].box let pos = box.btmPosPredict if (list[i].dis > maxDis0) { let p1 = pos.clone().clamp(modelBound.min, modelBound.max) let vec = new THREE.Vector3().subVectors(pos, p1).normalize().multiplyScalar(maxDis0) pos = new THREE.Vector3().addVectors(p1, vec) } let marginBound = new THREE.Box3().setFromCenterAndSize(pos, new THREE.Vector3(0.2, 0, 0.2)) newBound.union(marginBound) } skyBoxTight = new BoundingMesh( newBound, new THREE.MeshBasicMaterial({ side: THREE.DoubleSide, wireframe: true, transparent: true, opacity: 0.05, }), 0 ) //skyBoxTight.visible = false skyBoxTight.updateMatrixWorld() //不update的话raycaster是错的 meshGroup.add(skyBoxTight) this.skyBoxTight = skyBoxTight this.safeBound = this.safeBoundFirstVer = safeBound = newBound boundConfirmed = true console.log('bound1:', this.safeBound.min.toArray(), this.safeBound.max.toArray()) } expandModelBound2(type) { let material = skyBoxTight.material skyBoxTight.geometry.dispose() boxesSolid.forEach(e => { if (e.boxType != type) return this.safeBound.union(e.bound) }) skyBoxTight = new BoundingMesh(this.safeBound, material, 0) //skyBoxTight.visible = false skyBoxTight.updateMatrixWorld() //不update的话raycaster是错的 meshGroup.add(skyBoxTight) this.skyBoxTight = skyBoxTight console.log('bound2:', this.safeBound.min.toArray(), this.safeBound.max.toArray()) } /* adjustModelBound(type){ if(standards[type].atWall != 1)return //必须是挂墙的 this.lastSafeBound = this.safeBound.clone() let list = [] let add = e => { if (e.btmPos && e.box1 && e.minAng>10 ) { //有 box1才能确定是getIntersect得到的pos list.includes(e) || e.push(e) } } this.boxesSolid.forEach((solidBox)=>{ if(solidBox.boxType != type)return solidBox.traversePair(add) }) let wallSides = {'x+': 0, 'x-': 0, 'z+': 0, 'z-': 0} let getBoxSide = (e)=>{ for(let i in e.panosDir){ if(e.panosDir[i] != 0){ wallSides[i] += 1 } } } if(list.length){ this.safeBound = this.panoBound.clone() list.forEach((e)=>{ this.safeBound.union(e.bound)//还是说使用cIntersect来expand? getBoxSide() }) } skyBoxTight = new BoundingMesh(this.safeBound, material, 0) //skyBoxTight.visible = false skyBoxTight.updateMatrixWorld() //不update的话raycaster是错的 meshGroup.add(skyBoxTight) this.skyBoxTight = skyBoxTight //主要目的,针对那些模型比room大得多的情况,比如多了门外的部分,造成墙面不准。 //monitor的可以完全决定位置。其他的只能收缩(但是不能收缩超过panos和第一次expand的那些btm的bound里) //太麻烦了,且考虑到即使墙面完全准确也可能反倒把它变的不准的风险。 } */ /* 最低点使用fire的吗 MW6MEeCOy9Y 只有最近三个点和fire接近 3MnIWabM6ne fire combine了 ov5NTEImzhW fire多个 n4z0yd5tQaF fire多两个错位离谱的。。 位置和显示的不一样。。 5BaiXkK6Ag1 fire算的不太准 低了0.1 样本数仅两个 */ confirmGroundY(type) { //利用fire来确定地面高度(会稍低于地板,但box框不打滑且xz更准,估计因box框比box大)。调试:3MnIWabM6ne up9PPZkx1px 4GqaqNdyjGfs GFbQi1LiSij if (boundConfirmed) return let btmYs = [], pairs = [], needCount = 5, maxDis = 3, btmY = 0 let add = e => { if (e.btmPos && e.box1 /* && e.score >= minScore */) { //有 box1才能确定是getIntersect得到的pos pairs.push(e) } } boxesSolid.forEach(e => { if (e.boxType != type) return e.traversePair(add) }) let usePairs = pairs if (pairs.length > needCount) { //调试:Y8czF2Z3h9m let disMap = new Map() pairs.forEach(e => { disMap.set(e, Math.max(e.box0.pano.position.distanceTo(e.box0.btmPosPredict), e.box1.pano.position.distanceTo(e.box1.btmPosPredict))) }) pairs.sort((a, b) => { return disMap.get(a) - disMap.get(b) }) //距离从近到远 . 远处的高度可能偏离严重,就不管了,而且框也不一定准 usePairs = pairs.slice(0, needCount) for (let i = needCount; i < pairs.length; i++) { if (disMap.get(pairs[i]) < maxDis) { usePairs.push(pairs[i]) } } } usePairs.forEach(e => { //有 box1才能确定是getIntersect得到的pos let btmY_ = e.btmPos.y const width = e.size.x / 2 let h0 = width / Math.tan(Math.acos(-e.box0.centerBtmDir.y)) //|centerBtmDir.y| 即俯视角度的cos let h1 = width / Math.tan(Math.acos(-e.box1.centerBtmDir.y)) let h = Math.min(h0, h1) //选个小的吧,因浅的会更快接触到中心,虽然交点是两条射线最近点 不一定在fire中心 //如果毫无误差,且centerBtmDir.y相同,只要两条线centerBtmDir的xz相同,交点就是和地面的交点,而只要不同,交点就是和地面的交点,而只要不同,交点必然是在fire中心之下。随着minAng和centerBtmDir.y 交点在这中间变化 let r = math.linearClamp(e.minAng, 0, 90, 0, 1) btmY_ += h * r btmYs.push(btmY_) btmY += btmY_ }) btmY /= btmYs.length if (btmYs.length) { console.error( 'confirmGroundY', btmY, /* '样本数', btmYs.length, */ btmYs, usePairs.map(e => e.name), pairs ) /* if(btmYs.length == 1 && pairs[0].minAngle<12 && ){ } */ groundY = this.safeBound.min.y = this.panoBound.min.y = btmY // = -1.1 groundPlane.setFromNormalAndCoplanarPoint(new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, groundY, 0)) } let { min, max } = standards[type].height boxesSolid.slice().forEach(e => { if (e.boxType != type) return if (e.fake) { e.dispose() return } let topY = e.topPos ? e.topPos.y : e.list.reduce((w, c) => { return w + c.topPos.y }, 0) / e.list.length e.size.y = THREE.MathUtils.clamp(topY - groundY, min, max) e.setFromInfo(e) if (version == 'vision') e.draw() }) this.panoBound this.expandModelBound() } bindEvents() { if (version != 'vision') return player.on(PlayerEvents.FlyingStarted, e => { //if(e.mode == 'dollhouse')return // 点位跳转前清除已有线框 this.traverse(obj => { if (obj.isMesh) { obj.geometry.dispose() obj.material.dispose() } }) this.wireframes.clear() this.currentId = null }) player.on(PlayerEvents.FlyingEnded, () => { // 点位跳转后加载线框 if (player.mode != Viewmode.PANORAMA) return // 只有PANORAMA模式下需要加载 this.load(player.currentPano.id) }) if (this.ifAnalyze) { setTimeout(() => { { let btn = document.createElement('button') btn.innerHTML = '点击切换box显示' btn.onclick = () => { this.boxesSolid.forEach(e => ((e.boxHelper.visible = !e.boxHelper.visible), (e.label.visible = !e.label.visible))) } document.querySelector('#app').appendChild(btn) btn.id = 'boxWire' btn.style.position = 'fixed' btn.style['z-index'] = '100' btn.style.background = '#e00472' btn.style.padding = '10px' btn.style.bottom = '80px' } { let btn = document.createElement('button') btn.innerHTML = '点击切换矩形框显示' btn.onclick = () => { this.wireframes.visible = !this.wireframes.visible } document.querySelector('#app').appendChild(btn) btn.id = 'wireframes' btn.style.position = 'fixed' btn.style['z-index'] = '100' btn.style.background = '#419aff' btn.style.padding = '10px' btn.style.bottom = '130px' } }, 1000) } } /** * 加载点位标记数据 * @param {*} panoId */ load(panoId) { let data = this.datasMixed[panoId] || this.datas[panoId] if (!data) { if (!(panoId in this.datas)) setTimeout(() => { this.load(panoId) }, 100) //否则无数据 return } if (player.currentPano.id != panoId || player.flying || this.currentId == panoId) return // 防止连续跳转点位时,clear后才load好上一点位的数据,导致出现之前的标记 this.currentId = panoId let { shapes, imageHeight, imageWidth } = data //data.data let allShapes = shapes.slice() shapes.forEach(e => { if (e.boxes) allShapes.push(...e.boxes) }) let fireIndex = 0 allShapes.forEach(shape => { // 填充色和线框色 // let { fill_color, line_color } = shape getUVs(shape) getCenterDir(shape, player.currentPano) let { fill_color, color = [56, 56, 255] } = shape let line_color = [...color, 255] if (!fill_color) fill_color = [255, 255, 255, 0] if (!line_color) line_color = [255, 0, 0, 255] if (shape.boxes) { line_color = [20, 205, 255, 255] } let pos = getBoxPos(shape) let dis = pos ? shape.pano.position.distanceTo(getBoxPos(shape)) : 1 let labelShift = (shape.boxType == 'fire' ? 0 : -0.2) / dis let name = shape.category + '-' + shape.sid //if(shape.score < MinBoxInitialScore){ name = [name, 'sc: ' + math.toPrecision(shape.score, 3)] //} this.showSignalFrom2d( name, shape.bbox2, imageWidth, imageHeight, { fill: { color: new THREE.Color().setRGB(fill_color[0] / 255, fill_color[1] / 255, fill_color[2] / 255), opacity: fill_color[3] / 255, }, line: { color: new THREE.Color().setRGB(line_color[0] / 255, line_color[1] / 255, line_color[2] / 255), opacity: shape.category == 'cabling_rack' ? 0.4 : line_color[3] / 255, //走线架太绕,扰乱视线 }, }, shape.centerDir, labelShift, shape.state == '被删除' ) }) // }) // .catch(err => console.log(`点位${panoId}无标记数据或数据出错:`, err)) } /** * 根据坐标标记全景图 * * 存在的问题:如果要准确复现全景图上的线框,上下边框会变为弧形。而按顶点连直线的话,180度以上会出bug。 * 解决方式:目前150度以下只画出4个顶点然后连直线,150度以上准确画出全景图线框。 */ showSignalFrom2d(name, rect, w, h, options, centerDir, labelShift, removed) { // 目前rect给的是矩形对角的两个点坐标,将它扩展成四个顶点 let cornerArr = [new THREE.Vector2(rect[0], rect[1]), new THREE.Vector2(rect[2], rect[1]), new THREE.Vector2(rect[2], rect[3]), new THREE.Vector2(rect[0], rect[3])] // 根据四个顶点,填充中间点 let pointArr = [] for (let i = 0; i < cornerArr.length; i++) { let corner1 = cornerArr[i] let corner2 = cornerArr[(i + 1) % cornerArr.length] pointArr.push(corner1) /* // 横向角度超过150度时,3d中边框的弧线已经不太明显,准确画出全景图线框 if ((rect[2] - rect[0]) / w < 5 / 12 && i % 2 == 0) continue const vec = [corner2[0] - corner1[0], corner2[1] - corner1[1]] let length = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]) let num = length / 150 for (let j = 1; j <= num; j++) { pointArr.push([corner1[0] + (vec[0] / num) * j, corner1[1] + (vec[1] / num) * j]) } */ } //pointArr.push(cornerArr[0], cornerArr[2], cornerArr[1], cornerArr[3]) //对角线 let points = [] pointArr.forEach(uv => { let dir = getDirByUV(uv, player.currentPano) // points.push(dir.sub(centerVec)) // 计算其他点相对于中点的坐标,方便旋转平移等 points.push(dir) }) // 线框 const lineGeometry = new THREE.BufferGeometry().setFromPoints(points) const lineMaterial = new THREE.LineBasicMaterial({ color: options.line.color, opacity: options.line.opacity, transparent: true, depthTest: false }) const wireframe = new THREE.LineLoop(lineGeometry, lineMaterial) // wireframe.position.copy(centerVec) // 将中点作为线框坐标 wireframe.renderOrder = 100 // 填充颜色 const fillGeometry = lineGeometry.clone().setIndex(new THREE.BufferAttribute(new Uint16Array([0, 1, 3, 2, 3, 1]), 1)) const fillMaterial = new THREE.MeshBasicMaterial({ color: options.fill.color, opacity: options.fill.opacity, transparent: true, side: THREE.DoubleSide, depthTest: false }) const plane = new THREE.Mesh(fillGeometry, fillMaterial) plane.renderOrder = wireframe.renderOrder - 1 wireframe.add(plane) // 名称 const textMesh = new TextSprite({ text: name, backgroundColor: { r: options.line.color.r * 255, g: options.line.color.g * 255, b: options.line.color.b * 255, a: options.line.opacity * 0.5 }, textColor: { r: 255, g: 255, b: 255, a: options.line.opacity * 1.1 }, borderRadius: 15, renderOrder: wireframe.renderOrder + 1, player: player, }) //const shift = new THREE.Vector3(0, labelShift , 0) textMesh.position.copy(centerDir /* .clone().add(shift).normalize() */) textMesh.lookAt(0, 0, 0) // 看向相机 textMesh.scale.set(0.12, 0.12, 0.12) /* let line = addLine(centerDir, shift, null, options.line.color) line.material.opacity = options.line.opacity */ let group = new THREE.Group() group.position.copy(player.currentPano.position) group.add(wireframe) group.add(textMesh) //group.add(line) this.wireframes.add(group) if (removed) { textMesh.sprite.material.opacity = 0.4 lineMaterial.opacity *= 0.6 lineMaterial.color.set('#efe') } } clear() { //清除上一次的结果 ;(skyBoxTight = null), (meshGroup = null), (modelBound = new THREE.Box3()), (groundY = null), (safeBound = null), (boundConfirmed = null), (hue = 0), (boxesSolid = []) } }