xzw 1 tahun lalu
induk
melakukan
421d80c26a

File diff ditekan karena terlalu besar
+ 2790 - 0
service/PanoBoxFrame.js


+ 24 - 0
service/util/BoundingMesh.js

@@ -0,0 +1,24 @@
+//skybox
+import * as THREE from 'three'
+export default class BoundingMesh extends THREE.Mesh {
+    constructor(boundingBox, material, skyHeight = 50) {
+        //boundingBox = boundingBox.clone().expandByScalar(0.01) //许钟文改: 稍微放大了一丢丢的,避免和其他mesh冲突闪烁。尤其在飞入飞出时,地面闪烁一下,好像是和chunk冲突。过去是和marker冲突。
+        //大部分没有mesh的都是户外,所以放大些,作为天空盒背景.  另外地板因为可能破损,所以position向上提升使最低高度不变
+        boundingBox = boundingBox.clone().expandByVector(new THREE.Vector3(skyHeight, skyHeight, skyHeight))
+
+        var size = new THREE.Vector3()
+        boundingBox.getSize(size)
+        var geometry = new THREE.BoxGeometry(size.x, size.y, size.z)
+        //geometry.boundingBox = boundingBox; //xzw delete 较早:这句加上不对。因为后面算鼠标与skybox交点时 要用到的是boundingbox加上position才是真实skybox真实位置,所以boundingbox要居中的,而不应该是整体模型所在的位置。
+        geometry.computeBoundingBox() //需要重新计算对称的bounding, 在获取鼠标交点时需要。否则热点加不上
+
+        super(geometry, material)
+        var center = new THREE.Vector3()
+        boundingBox.getCenter(center)
+        center.y += skyHeight - 0.1
+
+        this.position.copy(center)
+        this.frustumCulled = !1
+        //flag && this.add(new THREE.WireframeHelper(this))
+    }
+}

+ 16 - 0
service/util/Chunk.js

@@ -0,0 +1,16 @@
+
+import * as THREE from 'three'
+
+//模型会分成多个chunk
+class Chunk extends THREE.Mesh {
+    constructor(meshObject) { 
+        var material = new THREE.MeshBasicMaterial({
+            side: THREE.DoubleSide,
+        })
+        super(meshObject.geometry)
+
+        this.isChunk = true
+    }
+}
+
+export default Chunk

+ 26 - 0
service/util/Chunks.js

@@ -0,0 +1,26 @@
+export default {
+    parseIdsFromChunkName(chunkName, floorRoomIds) {
+        floorRoomIds.floorId = this.parseFloor(chunkName)
+        floorRoomIds.roomId = this.parseRoom(chunkName)
+    },
+    parseFloor(chunkName) {
+        var floorInfo = chunkName.match(/_group([0-9]+)/)
+        if (!floorInfo) return 0
+        try {
+            return parseInt(floorInfo[1], 10)
+        } catch (msg) {
+            logger.warn('Non-int value "' + floorInfo[1] + '" for mesh group, defaulting to floor 0')
+            return 0
+        }
+    },
+    parseRoom(chunkName) {
+        var roomInfo = chunkName.match(/_sub([0-9]+)/)
+        if (!roomInfo) return -1
+        try {
+            return parseInt(roomInfo[1], 10)
+        } catch (msg) {
+            logger.warn('Non-int value "' + roomInfo[1] + '" for mesh subgroup, defaulting to subgroup 0')
+            return 0
+        }
+    }
+}

File diff ditekan karena terlalu besar
+ 47 - 0
service/util/Decompress.js


+ 107 - 0
service/util/Floor.js

@@ -0,0 +1,107 @@
+ 
+import * as THREE from 'three'
+
+
+class Floor extends THREE.Object3D {
+    constructor(model, floorIndex, name) {
+        super()
+        this.model = model
+        this.floorIndex = floorIndex
+        this.name = name || '楼层' + (floorIndex + 1)
+        this.panos = []
+        this.chunksDam = []
+        this.tiles = []  
+        this.center = null
+        this.boundingBox = new THREE.Box3()
+        this.size = null  
+        this.conservativeBoundingBox = new THREE.Box3()
+        this.debugColor = 16777215 * Math.random()
+        this.transition = null
+        this.entryArrow = []
+        this.views = []
+    }
+
+    get chunks() {
+        if (this.chunksDam.length) {
+            // Dam
+            return this.chunksDam
+        } else {
+            // 3dTiles
+            let chunks = []
+            this.tiles.forEach(tile => {
+                tile.traverse(obj => {
+                    if (obj.isChunk) {
+                        chunks.push(obj)
+                    }
+                })
+            })
+            return chunks
+        }
+    }
+
+
+    addChunk(chunk) {
+        //chunk.renderOrder = RenderOrder.ghostFloor
+        this.add(chunk)
+        this.chunksDam.push(chunk)
+        this.boundingBox.union(chunk.geometry.boundingBox)
+        var size = new THREE.Vector3()
+        this.boundingBox.getSize(size)
+        this.size = size 
+        chunk.floor = this
+    }
+     
+    addTile(tileContent) {
+        tileContent.floorIndex = this.floorIndex
+        this.tiles.push(tileContent)
+        this.add(tileContent)
+        tileContent.modified = ''
+
+        tileContent.traverse(obj => {
+            if (obj.isChunk) {
+                obj.setMode(this.model.mode, this.model.$app.core.get('Player').modeTran)
+                //obj.renderOrder = RenderOrder.ghostFloor
+                this.setMaterial(obj) // floor显隐判断
+                 
+                // todo 关于3dtiles的boundingBox和collider
+                this.boundingBox.union(obj.geometry.boundingBox)
+                var size = new THREE.Vector3()
+                this.boundingBox.getSize(size)
+                this.size = size
+                this.colliderBuilder && this.colliderBuilder.add(obj.geometry)
+            }
+        })
+    }
+
+    removeTile(tileContent) {
+        this.tiles = this.tiles.filter(tile => tile !== tileContent)
+        tileContent.traverse(obj => {
+            if (obj.isChunk) {
+                obj.geometry.dispose()
+                obj.material.dispose()
+                obj.material.uniforms.map.value && obj.material.uniforms.map.value.dispose()
+            }
+        })
+        this.remove(tileContent)
+        tileContent.modified = 'remove'
+    }
+
+    /**
+     * panos
+     */
+    addPano(pano) {
+        this.panos.push(pano)
+        //this.add(pano.skyboxMesh)
+        pano.marker && this.add(pano.marker)
+        var t = new THREE.Vector3(1, 1, 1),
+            i = new THREE.Box3().setFromCenterAndSize(pano.position, t)
+        this.boundingBox.union(i)
+    }
+    removePano(pano) {
+        var index = this.panos.indexOf(pano)
+        index > -1 && this.panos.splice(index, 1)
+    }
+  
+}
+
+export default Floor

+ 56 - 0
service/util/FloorCollection.js

@@ -0,0 +1,56 @@
+import * as THREE from 'three'
+import IndexedCollection from './IndexedCollection.js'
+import Floor from './Floor.js'
+
+//用于处理楼层
+class FloorCollection extends IndexedCollection {
+    constructor(model) {
+        super()
+        this.model = model
+        this.exploded = !1
+    }
+    add(floor) {
+        super.add(floor)
+        this.model.add(floor)
+    }
+    getIndex(floor) {
+        return floor.floorIndex
+    }
+    build() {
+        this.list.forEach(function (floor) {
+            floor.build()
+        })
+    }
+    nextFloor(e, t) {
+        return this.index[e.floorIndex + t] || null
+    }
+    getOrMakeFloor(floorIndex) {
+        var floor = this.index[floorIndex]
+        if (!floor) {
+            floor = new Floor(this.model, floorIndex)
+            this.add(floor)
+        }
+        return floor
+    }
+    hide() {
+        this.list.forEach(function (floor) {
+            floor.hide()
+        })
+    }
+    show() {
+        this.list.forEach(function (floor) {
+            floor.show()
+        })
+    }
+
+    getFloorAtPoint(e) {
+        for (var t = null, i = 1 / 0, n = 0; n < this.list.length; n++) {
+            var r = this.list[n],
+                o = r.distanceToPoint(e)
+            ;(!t || i > o) && ((i = o), (t = r))
+        }
+        return t
+    }
+}
+
+export default FloorCollection

+ 66 - 0
service/util/IndexedCollection.js

@@ -0,0 +1,66 @@
+import common from './common.js'
+
+class IndexedCollection {
+    constructor() {
+        this.list = []
+        this.index = {}
+        Object.defineProperty(this, 'length', {
+            get: function () {
+                return this.list.length
+            },
+        })
+
+        if (typeof this.getIndex != 'function') {
+            throw new Error('IndexedCollection.getIndex not implemented in subclass.')
+        }
+    }
+    forEach(e) {
+        this.list.forEach(e)
+    }
+    add(pano) {
+        this.list.push(pano)
+        this.index[this.getIndex(pano)] = pano
+    }
+    extend(e) {
+        for (var t = 0; t < e.length; t++) this.add(e[t])
+    }
+    get(panoId) {
+        return this.index[panoId]
+    }
+    first() {
+        return this.list[0]
+    }
+    last() {
+        return this.list[this.list.length - 1]
+    }
+    reIndex() {
+        this.index = {}
+        var e = this
+        this.forEach(function (t) {
+            e.index[e.getIndex(t)] = t
+        })
+    }
+    filter(e) {
+        var t = this.list.filter(e)
+        return this.reIndex(), t
+    }
+    reduce(e, t) {
+        return this.list.reduce(e, t)
+    }
+    sort(e) {
+        return this.list.sort(e)
+    }
+    indexOf(e) {
+        for (var t = 0; t < this.list.length; ++t) if (this.list[t] === e) return t
+        return -1
+    }
+
+    clone() {
+        //xzw add
+        var newobj = new this.constructor()
+        newobj.extend(this.list)
+        return newobj
+    }
+}
+
+export default IndexedCollection

+ 53 - 0
service/util/Model.js

@@ -0,0 +1,53 @@
+
+
+import * as THREE from 'three'
+import PanoramaCollection from './PanoramaCollection.js'
+import FloorCollection from './FloorCollection.js'
+
+export default class Model extends THREE.Object3D{
+    
+    constructor( ){
+        super()
+        this.panos = new PanoramaCollection(this)
+        this.floors = new FloorCollection(this)
+        this.boundingBox = new THREE.Box3
+        this.supportsTiles = true
+    }
+    
+    
+    build(){
+        
+         
+       
+        /* this.panos.forEach(function (pano) {
+            pano.build2()
+        })  */
+
+        this.floors.forEach(
+            function (floor) {
+                this.boundingBox.union(floor.boundingBox)
+            }.bind(this)
+        ) 
+         
+          
+        
+        var size = new THREE.Vector3()
+        var center = new THREE.Vector3()
+        this.boundingBox.getSize(size)
+        this.boundingBox.getCenter(center)
+
+        this.size = size
+        this.center = center   
+         
+        this.builded = true //xzw 
+        this.dispatchEvent({ type: 'builded' }) 
+         
+    }
+
+
+
+
+    addChunk(floorIndex, chunk) {
+        this.floors.getOrMakeFloor(floorIndex).addChunk(chunk) 
+    }
+}

+ 64 - 0
service/util/Panorama.js

@@ -0,0 +1,64 @@
+import * as THREE from 'three'
+import common from './common.js' 
+import Vectors from './Vectors.js'
+import math from './math.js'
+
+const rot90 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2)
+ 
+ 
+ 
+class Panorama extends THREE.EventDispatcher {
+    constructor(model, sweepuuid, sweepLocation) {
+        super()
+        this.id = sweepuuid 
+        this.panoType = sweepLocation.panoType //标记特殊  如 'assistant'校准热点时的相机协助点   '360view' 独立全景
+        this.appId = sweepLocation.index + 1
+         
+        
+        this.origin = sweepLocation.position.clone()
+        this.position = sweepLocation.position.clone()
+        this.quaternion = sweepLocation.quaternion.clone()
+ 
+        this.model = model 
+        this.floorPosition = sweepLocation.puck ? sweepLocation.puck.clone() : null
+        this.marker = null
+  
+        this.tiled = sweepLocation.tiled != void 0 ? sweepLocation.tiled : this.model.supportsTiles
+ 
+        var rot90qua = new THREE.Quaternion().multiplyQuaternions(this.quaternion, rot90) //改  为球目全景照片而改
+        let rot90Matrix = new THREE.Matrix4().compose(this.position, rot90qua, new THREE.Vector3(1, 1, 1)) //转90度后的
+
+        if (this.tiled) {
+            this.rot90Matrix = rot90Matrix // 给热点校准用   因为热点求点时所右乘的matrix必须是单张全景照片时用的转90度的matrix才行
+            this.matrixWorld = new THREE.Matrix4().compose(this.position, this.quaternion, new THREE.Vector3(1, 1, 1))
+        } else { 
+            this.quaternion = rot90qua
+            this.matrixWorld = rot90Matrix
+        } 
+ 
+    }
+
+    
+   /*  build1() {
+        this.floor = this.floor || this.model.floors.get(this.floorIndex) || this.raycastToFindFloor() || this.model.floors.list[0] //this.model.getFloorAtPoint(this.position)
+        this.floor.addPano(this)
+        this.floorPosition = this.floorPosition || this.raycastFloorPosition()
+        this.neighbourPanos = this.neighbourPanos || this.findNeighourPanos()
+        if (settings.colorMarkerByFloor && this.marker) {
+            this.marker.material.color.set(this.floor.debugColor)
+        }
+    }
+    build2() {
+        this.floorPosition = this.floorPosition || this.interpolateFloorPosition()
+        this.height = this.position.distanceTo(this.floorPosition) 
+        if (this.isAligned()) {
+            this.$app.config.view || this.addLabel()
+            setTimeout(() => {
+                addLabel && this.addLabel2()
+            }, 1)
+        }
+    } */
+    
+}
+
+export default Panorama

+ 38 - 0
service/util/PanoramaCollection.js

@@ -0,0 +1,38 @@
+import common from './common.js'
+import IndexedCollection from './IndexedCollection.js'
+import math from './math.js'
+ 
+
+class PanoramaCollection extends IndexedCollection {
+    constructor() {
+        super()
+        this.neighbourMap = {}
+        this.map = null
+        this.animatePanoId = null
+    }
+    getIndex(pano) {
+        return pano.id
+    }
+    find(e, t) {
+        var i = common.filterAll(this.list, e)
+        return 0 === i.length
+            ? null
+            : (t &&
+                  t.forEach(function (e) {
+                      i = common.stableSort(i, e)
+                  }),
+              i[0])
+    }
+    
+    lowestByScore(e, t, i) {
+        return this.findRankedByScore(0, e, t, i)
+    }
+    findRankedByScore(e, t, i, n) {
+        n && ((n.candidates = null), (n.pano = null)), e || (e = 0)
+        var r = common.sortByScore(this.list, t, i)
+        return !r || 0 === r.length || e >= r.length ? null : (n && ((n.candidates = r), (n.pano = r[e].item)), r[e].item)
+    }
+    
+}
+
+export default PanoramaCollection

+ 123 - 0
service/util/Process.js

@@ -0,0 +1,123 @@
+/*
+ * @Author: Rindy
+ * @Date: 2021-05-13 17:27:29
+ * @LastEditors: Rindy
+ * @LastEditTime: 2021-05-27 16:30:31
+ * @Description: Process
+ */
+
+import * as THREE from 'three'
+import logger from './logger.js'
+import settings from './settings.js'
+import Chunk from './Chunk.js'
+import Panorama from './Panorama.js' 
+
+export default { 
+    convertProtobufToSceneObject: function (  loaddata , sceneNum ) { 
+        function getMesh(chunk) {
+            var geometry = new THREE.BufferGeometry()
+            geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(chunk.vertices.xyz, 0, 3), 3))
+            chunk.vertices.uv.length > 0 && geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(chunk.vertices.uv, 0, 2), 2))
+            geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(chunk.faces.faces, 0, 1), 1))
+            geometry.applyMatrix4(matrix)
+            geometry.computeBoundingBox()
+
+            var meshUrl = settings.job + settings.format
+            
+            return new Chunk({
+                geometry: geometry,
+                textureName: chunk.material_name,
+                name: chunk.chunk_name, 
+                //meshUrl: app.resource.getViewImagesURL(meshUrl),  
+                meshUrl: `https://4dkk.4dage.com/scene_view_data/${sceneNum}/images/${meshUrl}?_=1`
+            }) 
+        }
+        if (0 == loaddata.chunk.length) {
+            logger.warn('No chunks in damfile...')
+            return null
+        }
+
+        var matrix = new THREE.Matrix4()
+        matrix.set(1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1)
+        //var materails = {};
+        return loaddata.chunk.map(getMesh)
+    },
+
+    //vision.modeldata的数据不能直接用,需要转换,方法如下:
+    visionModeldata: function (loaddata) { 
+        var modeldata = loaddata.sweepLocations
+            .map(
+                function (n, i) {
+                    return {
+                        uuid: n.uuid.toUTF8().replace(/-/g, ''),
+                        position: {
+                            x: n.pose.translation.x,
+                            y: n.pose.translation.y,
+                            z: n.pose.translation.z,
+                        },
+                        quaternion: {
+                            x: n.pose.rotation.x,
+                            y: n.pose.rotation.y,
+                            z: n.pose.rotation.z,
+                            w: n.pose.rotation.w,
+                        },
+                        puck: {
+                            x: n.puck.x,
+                            y: n.puck.y,
+                            z: n.puck.z,
+                        }, 
+                        alignmentType: n.alignment_type, 
+                        group: n.group,
+                        subgroup: n.subgroup,
+                        index: i, //add
+                    }
+                }.bind(this)
+            )
+            .map(
+                function (n) {
+                    n.position = this.convertVisionVector(n.position)
+                    n.quaternion = this.convertVisionQuaternion(n.quaternion)
+                    n.puck = this.convertVisionVector(n.puck)
+                    return n
+                }.bind(this)
+            ) 
+        return modeldata 
+    },
+
+    panos: function (modeldata, model   ) { 
+        model.panos.extend(
+            modeldata.map(
+                function (modeldataitem) {
+                    return new Panorama(model, modeldataitem.uuid, modeldataitem  )
+                }.bind(this)
+            ),
+            'id'
+        ) 
+        if (0 === model.panos.length) {
+            logger.warn('Model has no panos, turning off inside mode')
+        }
+        return model.panos
+    },
+
+    
+
+    //变换vision.modeldata里拍摄点的坐标
+    convertVisionVector: function (position) {
+        return new THREE.Vector3(position.x, position.z, -position.y)
+    },
+
+    //变换vision.modeldata里拍摄点的旋转角度quaternion
+    convertVisionQuaternion: function (quaternion) {
+        return new THREE.Quaternion(quaternion.x, quaternion.z, -quaternion.y, quaternion.w).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(90)))
+    },
+
+    //变换初始点的坐标
+    convertWorkshopVector: function (position) {
+        return new THREE.Vector3(-position.x, position.y, position.z)
+    },
+
+    //变换初始点的quaternion
+    convertWorkshopQuaternion: function (quaternion) {
+        return new THREE.Quaternion(-quaternion.x, quaternion.y, quaternion.z, -quaternion.w).multiply(new THREE.Quaternion(Math.sqrt(2) / 2, Math.sqrt(2) / 2, 0, 0))
+    },
+}

+ 12 - 0
service/util/Vectors.js

@@ -0,0 +1,12 @@
+
+import * as THREE from 'three'
+
+export default {
+    UP: new THREE.Vector3(0, 1, 0),
+    DOWN: new THREE.Vector3(0, -1, 0),
+    LEFT: new THREE.Vector3(-1, 0, 0),
+    RIGHT: new THREE.Vector3(1, 0, 0),
+    FORWARD: new THREE.Vector3(0, 0, -1),
+    BACK: new THREE.Vector3(0, 0, 1),
+    ZERO: new THREE.Vector3(0, 0, 0),
+}

+ 37 - 0
service/util/Viewmode.js

@@ -0,0 +1,37 @@
+var Viewmode = {
+    PANORAMA: 'panorama',
+    DOLLHOUSE: 'dollhouse',
+    FLOORPLAN: 'floorplan',
+    TRANSITIONING: 'transitioning'
+}
+
+Viewmode.toInt = function(mode) {
+    switch (mode) {
+        case this.PANORAMA:
+            return 1
+        case this.DOLLHOUSE:
+            return 2
+        case this.FLOORPLAN:
+            return 3
+        case this.TRANSITIONING:
+            return -1
+    }
+    throw new Error('未知模式: ' + c)
+}
+
+Viewmode.fromInt = function(mode) {
+    switch (mode) {
+        case '1':
+        case 1:
+            return this.PANORAMA
+        case '2':
+        case 2:
+            return this.DOLLHOUSE
+        case '3':
+        case 3:
+            return this.FLOORPLAN
+    }
+    throw new Error('未知模式: ' + c)
+}
+
+export default Viewmode

+ 717 - 0
service/util/common.js

@@ -0,0 +1,717 @@
+import math from './math.js' 
+import * as THREE from 'three'
+var common = {
+    valueFromHash(e, t) {
+        var i = new RegExp('[#&?]' + e + '=([^#&?]*)'),
+            n = i.exec(window.location.href)
+        if (!n) return t
+        var r = n[1]
+        return 'boolean' == typeof t ? 'true' === r || '1' === r : 'number' == typeof t ? parseFloat(r) : window.decodeURIComponent(r)
+    },
+    lowerMedian(e, t) {
+        if (0 === e.length) return null
+        ;(t = t || 2),
+            e.sort(function (e, t) {
+                return e - t
+            })
+        var i = Math.floor(e.length / t)
+        return e[i]
+    },
+    stableSort(e, t) {
+        return e
+            .map(function (e, t) {
+                return {
+                    value: e,
+                    index: t,
+                }
+            })
+            .sort(function (e, i) {
+                var n = t(e.value, i.value)
+                return 0 !== n ? n : e.index - i.index
+            })
+            .map(function (e) {
+                return e.value
+            })
+    },
+    sortByScore: function (list, request, rank) {
+        var i = request ? common.filterAll(list, request) : list
+        return 0 === i.length
+            ? []
+            : (i = i
+                  .map(function (e) {
+                      let scores = rank.map(function (f) {
+                          return f(e)
+                      }) //add
+                      return {
+                          item: e,
+                          scores,
+                          score: scores.reduce(function (t, i) {
+                              return t + i
+                          }, 0),
+                      }
+                  })
+                  .sort(function (e, t) {
+                      return t.score - e.score
+                  }))
+    },
+    filterAll(e, t) {
+        return e.filter(function (e) {
+            return t.every(function (t) {
+                return t(e)
+            })
+        })
+    }, 
+    getMixedSet: function (arr1, arr2) {
+        //交集
+        return arr1.filter(item => arr2.includes(item))
+    },
+    getUnionSet: function (arr1, arr2) {
+        //并集
+        return arr1.concat(arr2.filter(item => !arr1.includes(item)))
+    },
+    getDifferenceSet: function (arr1, arr2) {
+        //差集  不能识别重复的,如getDifferenceSet([1,2,2],[1,1,2]) 为空
+        var arr11 = arr1.filter(item => !arr2.includes(item))
+        var arr22 = arr2.filter(item => !arr1.includes(item))
+        return arr11.concat(arr22)
+    },
+    getDifferenceSetMuti: function (arr) {
+        //收集绝对没有重复的元素,也就是判断出现次数=1的
+        var set = []
+        arr.forEach(arr1 => {
+            arr1.forEach(item => {
+                var index = set.indexOf(item)
+                if (index > -1) {
+                    set.splice(index, 1)
+                } else {
+                    set.push(item)
+                }
+            })
+        })
+        return set
+    },
+    pushToGroupAuto: function (items, groups, recognizeFunction, judgeRelationFun) {
+        //自动分组。 items是将分到一起的组合。items.length = 1 or 2.
+        let isSame = (a, b) => {
+            return a == b || (recognizeFunction && recognizeFunction(a, b))
+        }
+        var atGroups = groups.filter(group =>
+            group.find(
+                item => (isSame(item, items[0]) || isSame(item, items[1])) && (!judgeRelationFun || judgeRelationFun(group)) //根据关系进一步判断是否应该一组
+            )
+        )
+        if (atGroups.length) {
+            //在不同组
+            //因为items是一组的,所以先都放入组1
+            items.forEach(item => {
+                if (!atGroups[0].includes(item)) atGroups[0].push(item)
+            })
+
+            if (atGroups.length > 1) {
+                //如果在不同组,说明这两个组需要合并
+                var combineGroup = []
+                combineGroup.relationships = [items.slice()]
+                atGroups.forEach(group => {
+                    let relationships = common.getUnionSet(combineGroup.relationships, group.relationships)
+                    combineGroup = common.getUnionSet(combineGroup, group)
+                    combineGroup.relationships = relationships
+                    groups.splice(groups.indexOf(group), 1)
+                })
+                groups.push(combineGroup)
+            } else {
+                atGroups[0].relationships.push(items.slice())
+            }
+        } else {
+            //直接加入为一组
+            items.relationships = [items.slice()]
+            groups.push(items)
+        }
+    },
+
+    disconnectGroup: function (pairs, groups, recognizeFunction) {
+        //将atGroup中的pairs关系解除,然后重新分组
+
+        let isSame = (a, b) => {
+            return a == b || (recognizeFunction && recognizeFunction(a, b))
+        }
+        let oldGroups = groups.slice()
+
+        pairs.forEach(items => {
+            let relationship
+            let atGroup = groups.find(group => {
+                let r = group.relationships.find(arr => items.every(e => arr.some(a => isSame(a, e))))
+                if (r) {
+                    relationship = r
+                    return true
+                }
+            }) //能找到relationships 有包含items的, 代表它们有绑定关系
+            if (!atGroup) return
+
+            //断开连接时,因为组内没有其他成员的连接信息,所以需要清除整组,并将剩余的一个个重新连接
+            groups.splice(groups.indexOf(atGroup), 1) //删除
+
+            atGroup.relationships.splice(atGroup.relationships.indexOf(relationship), 1)
+
+            let newGroups_ = [] //为了防止裂解的该组(因之前有judgeRelationFun但现在没传)混入其他组,先放一个空组合里
+            atGroup.relationships.forEach(pair => {
+                //然后再重新生成这两个和组的关系,各自分组
+                common.pushToGroupAuto(pair, newGroups_, recognizeFunction)
+            })
+            groups.push(...newGroups_)
+        })
+
+        let newGroups = groups.filter(e => !oldGroups.includes(e))
+
+        return { newGroups }
+    },
+
+    removeFromGroup: function (items, atGroup, groups, recognizeFunction) {
+        //将该组移除items中的所有元素,以及包含它的关系
+
+        let isSame = (a, b) => {
+            return a == b || (recognizeFunction && recognizeFunction(a, b))
+        }
+        let newRelations = atGroup.relationships.filter(arr => !arr.some(e => items.some(item => isSame(e, item))))
+
+        if (newRelations.length == atGroup.relationships) return
+
+        //断开连接时,因为组内没有其他成员的连接信息,所以需要清除整组,并将剩余的一个个重新连接
+        groups.splice(groups.indexOf(atGroup), 1) //删除
+
+        let newGroups = [] //为了防止裂解的该组(因之前有judgeRelationFun但现在没传)混入其他组,先放一个空组合里
+        atGroup.relationships.forEach(pair => {
+            //然后再重新生成这两个和组的关系,各自分组
+            common.pushToGroupAuto(pair, newGroups, recognizeFunction)
+        })
+        groups.push(...newGroups)
+        return { newGroups }
+    },
+}
+ 
+common.dataURLtoBlob = function (dataurl) {
+    //将base64转换blob
+    var arr = dataurl.split(','),
+        mime = arr[0].match(/:(.*?);/)[1],
+        bstr = atob(arr[1]),
+        n = bstr.length,
+        u8arr = new Uint8Array(n)
+    while (n--) {
+        u8arr[n] = bstr.charCodeAt(n)
+    }
+    return new Blob([u8arr], { type: mime })
+}
+common.dataURLtoFile = function (dataurl, filename) {
+    //将base64转换为文件
+    var arr = dataurl.split(','),
+        mime = arr[0].match(/:(.*?);/)[1],
+        bstr = atob(arr[1]),
+        n = bstr.length,
+        u8arr = new Uint8Array(n)
+    while (n--) {
+        u8arr[n] = bstr.charCodeAt(n)
+    }
+    return new File([u8arr], filename, { type: mime })
+}
+common.saveFile = function (data, filename, cb) {
+    var save_link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a')
+    save_link.href = data
+    save_link.download = filename
+
+    var event = document.createEvent('MouseEvents')
+    event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)
+    save_link.dispatchEvent(event)
+
+    cb && cb()
+}
+ 
+common.replaceAll = function (str, f, e) {
+    //f全部替换成e
+    var reg = new RegExp(f, 'g') //创建正则RegExp对象
+    return str.replace(reg, e)
+}
+common.randomWord = function (randomFlag, min, max) {
+    //随机字符串
+    var str = '',
+        range = min,
+        arr = [
+            '0',
+            '1',
+            '2',
+            '3',
+            '4',
+            '5',
+            '6',
+            '7',
+            '8',
+            '9',
+            'a',
+            'b',
+            'c',
+            'd',
+            'e',
+            'f',
+            'g',
+            'h',
+            'i',
+            'j',
+            'k',
+            'l',
+            'm',
+            'n',
+            'o',
+            'p',
+            'q',
+            'r',
+            's',
+            't',
+            'u',
+            'v',
+            'w',
+            'x',
+            'y',
+            'z',
+            'A',
+            'B',
+            'C',
+            'D',
+            'E',
+            'F',
+            'G',
+            'H',
+            'I',
+            'J',
+            'K',
+            'L',
+            'M',
+            'N',
+            'O',
+            'P',
+            'Q',
+            'R',
+            'S',
+            'T',
+            'U',
+            'V',
+            'W',
+            'X',
+            'Y',
+            'Z',
+        ]
+
+    if (randomFlag) {
+        // 随机长度
+        range = Math.round(Math.random() * (max - min)) + min
+    }
+    for (var i = 0; i < range; i++) {
+        var pos = Math.round(Math.random() * (arr.length - 1))
+        str += arr[pos]
+    }
+    return str
+}
+common.getRandomSid = function () {
+    //5-7位随机字符串 + 6位时间    为热点准备
+    var pre = common.randomWord(true, 5, 7)
+    var post = new Date().getTime() + ''
+    var len = post.length
+    post = post.substring(len - 8, len - 5) + post.substring(len - 3, len) //其实还是有可能重复的....
+    return pre + post
+}
+
+common.getTime = function (second) {
+    //秒
+    var str = '' //不支持大于60分钟的时间哟
+    var minute = parseInt(second / 60)
+    if (minute < 10) str += '0'
+    str += minute
+    second = parseInt(second % 60) + ''
+    if (second.length == 1) second = '0' + second
+    str = str + ':' + second
+    return str
+}
+;(common.CloneJson = function (data) {
+    var str = JSON.stringify(data)
+    return JSON.parse(str)
+}),
+    (common.CloneObject = function (copyObj, result, isSimpleCopy, simpleCopyList = []) {
+        //isSimpleCopy 只复制最外层
+        //复制json		result的可能:普通数字或字符串、普通数组、复杂对象
+
+        simpleCopyList.push(THREE.Object3D) //遇到simpleCopyList中的类直接使用不拷贝
+
+        if (!copyObj || typeof copyObj == 'number' || typeof copyObj == 'string' || copyObj instanceof Function || simpleCopyList.some(className => copyObj instanceof className)) {
+            return copyObj
+        }
+
+        result = result || {}
+        if (copyObj instanceof Array) {
+            return copyObj.map(e => {
+                return this.CloneObject(e)
+            })
+        } else {
+            if (copyObj.clone instanceof Function) {
+                //解决一部分
+                return copyObj.clone()
+            }
+        }
+        for (var key in copyObj) {
+            if (copyObj[key] instanceof Object && !isSimpleCopy) result[key] = this.CloneObject(copyObj[key])
+            else result[key] = copyObj[key]
+            //如果是函数类同基本数据,即复制引用
+        }
+        return result
+    }),
+    (common.CloneClassObject = function (copyObj) {
+        //复杂类对象
+        var newobj = new copyObj.constructor()
+        this.CopyClassObject(newobj, copyObj)
+
+        return newobj
+    }),
+    (common.CopyClassObject = function (targetObj, copyObj) {
+        //复杂类对象
+        for (let i in copyObj) {
+            if (i in copyObj.__proto__) break //到函数了跳出
+
+            targetObj[i] = this.CloneObject(copyObj[i], null)
+        }
+    }),
+    (common.ifSame = function (object1, object2) {
+        if (object1 == object2) return true
+        // 0 != undefined  , 0 == ''
+        else if (!object1 || !object2) return false
+        else if (object1.constructor != object2.constructor) {
+            return false
+        } else if (object1 instanceof Array) {
+            if (object1.length != object2.length) return false
+            var _object2 = object2.slice(0)
+
+            for (let i = 0; i < object1.length; i++) {
+                var u = _object2.find(e => ifSame(object1[i], e))
+                if (u == void 0 && !_object2.includes(u) && !object1.includes(u)) return false
+                else {
+                    let index = _object2.indexOf(u)
+                    _object2.splice(index, 1)
+                }
+            }
+
+            return true
+        } else if (object1.equals instanceof Function) {
+            //复杂数据仅支持这种,其他的可能卡住?
+
+            return object1.equals(object2)
+        } else if (typeof object1 == 'number' || typeof object1 == 'string') {
+            if (isNaN(object1) && isNaN(object2)) return true
+            else return object1 == object2
+        } else if (typeof object1 == 'object') {
+            var keys1 = Object.keys(object1)
+            var keys2 = Object.keys(object2)
+            if (!ifSame(keys1, keys2)) return false
+
+            for (let i in object1) {
+                var same = ifSame(object1[i], object2[i])
+                if (!same) return false
+            }
+            return true
+        } else {
+            console.log('isSame出现例外')
+        }
+    })
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+  
+common.intervalTool = {
+    //延时update,防止卡顿
+    list: [],
+
+    isWaiting: function (name, func, delayTime) {
+        let item = this.list.find(e => e.name == name)
+        if (!item) {
+            //如果没有该项, 则加入循环
+            let ifContinue = func()
+            item = { name }
+            this.list.push(item)
+            setTimeout(() => {
+                var a = this.list.indexOf(item)
+                this.list.splice(a, 1)
+                if (item.requestUpdate || ifContinue) this.isWaiting(name, func, delayTime) //循环
+            }, delayTime)
+        } else {
+            //如果有该项,说明现在请求下一次继续更新
+            /* if(delayTime == 0){//想立刻更新一次
+                func()
+            }else{ */
+            item.requestUpdate = true
+            //}
+        }
+    },
+}
+
+common.batchHandling = {
+    //分批处理
+
+    lists: [],
+
+    getSlice: function (name, items, { stopWhenAllUsed, minCount = 5, maxCount = 100, durBound1, durBound2, maxUseCount }) {
+        if (
+            items.length == 0 ||
+            ((maxUseCount = maxUseCount == void 0 ? common.getBestCount({ name, minCount, maxCount, durBound1, durBound2, ifLog: false }) : maxUseCount), !maxUseCount) //本次最多可以使用的个数
+        ) {
+            return { list: [] }
+        }
+
+        if (!this.lists[name]) this.lists[name] = { list: [] }
+        //更新列表项目,但不变原来的顺序
+        let list = this.lists[name].list.filter(a => items.some(item => a.item == item)) //去掉已经不在items里的项目
+        this.lists[name].list = list
+
+        items.forEach(item => {
+            //增加新的项目。
+            if (!list.some(a => a.item == item)) {
+                list.push({ item, count: 0 })
+            }
+        })
+        //至此,在后排的都是未使用的
+
+        let unUsed = list.filter(e => e.count == 0) //未使用的项目(count为0)优先
+        let result = []
+        unUsed.slice(0, maxUseCount).forEach(e => {
+            result.push(e.item)
+            e.count++
+        })
+        if (unUsed.length > maxUseCount) {
+            //还是剩有未使用的项目,等待下一次
+        } else {
+            //所有项目都能使用一次
+            if (!stopWhenAllUsed) {
+                //若不是全部使用就停止
+                let wholeCount = Math.min(items.length, maxUseCount)
+                let restCount = wholeCount - result.length //补齐
+                list.slice(0, restCount).forEach(e => {
+                    result.push(e.item)
+                    e.count++
+                })
+            }
+            list.forEach(e => e.count--) //复原,等待新的循环
+        }
+
+        return { list: result }
+    },
+
+    addSliceListen({ getList, callback, minCount, maxCount, durBound1, durBound2, maxHistory, player }) {
+        let unUpdate, lastUpdate
+        player.on('update', e => {
+            if (player.flying) return
+
+            let waitForUpdate = getList()
+
+            let stopWhenAllUsed = !player.lastFrameChanged
+            let standardUpdate = player.lastFrameChanged || !lastUpdate //相机变化或第一次
+            let list
+            if (standardUpdate) {
+                list = waitForUpdate
+                unUpdate = null
+            } else {
+                if (!unUpdate) {
+                    unUpdate = common.getDifferenceSet(waitForUpdate, lastUpdate)
+                    //unUpdate = unUpdate.filter(e => e.visible) //如飞出后最后一次更新之后,都隐藏了,隐藏的就不用更新了
+                }
+                list = unUpdate
+            }
+
+            let result = common.batchHandling.getSlice('ifVideoInsight', list, { stopWhenAllUsed, minCount, maxCount, durBound1: 3, durBound2: 13, maxHistory: 3 }) //iphonex稳定后大概在7-10。
+            let updateList = result.list
+            //updateList.length && console.log(updateList.map(e=>e.sid))
+            updateList.forEach(callback)
+
+            if (!standardUpdate) {
+                //相机停止变化后只更新还未更新的
+                unUpdate = common.getDifferenceSet(unUpdate, updateList)
+            }
+            lastUpdate = updateList
+        })
+    },
+}
+
+common.getBestCount = (function () {
+    let lastCount = {}
+    return function ({ name, minCount = 1, maxCount = 6, durBound1 = 1, durBound2 = 4, ifLog, maxHistory }) {
+        let timeStamp = performance.getEntriesByName('loop-start')
+        let count
+        if (timeStamp.length) {
+            let dur = performance.now() - timeStamp[timeStamp.length - 1].startTime
+            /*let k = -(maxCount - minCount) / (durBound2 - durBound1)
+            let m = maxCount - durBound1 * k
+            count = THREE.MathUtils.clamp(Math.round(k * dur + m), minCount, maxCount) //dur在iphoneX中静止有7,pc是2
+            */
+
+            count = Math.round(math.linearClamp(dur, durBound1, durBound2, maxCount, minCount))
+
+            if (maxHistory) {
+                if (!lastCount[name]) lastCount[name] = []
+                if (count == 0 && lastCount[name].length > maxHistory - 1 && !lastCount[name].some(e => e > 0)) {
+                    count = 1
+                }
+                lastCount[name].push(count)
+                if (lastCount[name].length > maxHistory) lastCount[name].splice(0, 1)
+            }
+
+            ifLog && console.log(name, count, ' ,dur:', dur.toFixed(3))
+        } else {
+            count = maxCount //  ?
+        }
+
+        //主要在手机端有效果。
+        return count
+    }
+})()
+
+common.timeMeasuring = {
+    reportTimings: false,
+
+    collection: {},
+
+    registerCollect(name, o) {
+        this.collection[name] = o
+        o.measures = []
+        o.sum = 0
+    },
+
+    addTimeMark: function (name, type, ifLog) {
+        let record = this.collection[name]
+        let now = performance.now()
+        let needRecord = record && (record.measures.length < record.minCount || now - record.lastAddTime > record.refreshTime) //间隔时间超过refreshTime重新收集
+
+        if (needRecord || this.reportTimings) {
+            if (type == 'end' && performance.getEntriesByName(name + '-start').length == 0) return
+
+            performance.mark(name + '-' + type)
+
+            if (type == 'end') {
+                let measure = performance.measure(name, name + '-start', name + '-end')
+                if (!measure) {
+                    //console.error('没找到measure',name) //可能是其他地方报错了没进行下去所以找不到
+                    return
+                }
+
+                if (ifLog) console.log(name, '耗时', measure.duration.toFixed(3))
+
+                if (needRecord) {
+                    if (record.measures.length >= record.minCount) {
+                        //先清空上一轮的
+                        record.measures = []
+                        record.sum = 0
+                    }
+
+                    record.measures.push(measure.duration)
+                    record.sum += measure.duration
+                    record.mean = record.sum / record.measures.length
+                    record.measures.sort((a, b) => a - b)
+                    record.median = record.measures[parseInt(record.measures.length / 2)]
+                    record.lastAddTime = now
+                    if (record.measures.length == record.minCount) {
+                        //console.log(record)
+                    }
+                }
+            }
+        }
+    },
+
+    report: function (timestamp) {
+        //原resolveTimings
+        //打印用时。   注:performance手机的精度只到整数位
+
+        if (!this.toggle) {
+            this.toggle = timestamp
+        }
+        let duration = timestamp - this.toggle
+        if (duration > 1000.0) {
+            if (this.reportTimings) {
+                let measures = performance.getEntriesByType('measure')
+
+                let names = new Set()
+                for (let measure of measures) {
+                    names.add(measure.name)
+                }
+
+                let groups = new Map()
+                for (let name of names) {
+                    groups.set(name, {
+                        measures: [],
+                        sum: 0,
+                        n: 0,
+                        min: Infinity,
+                        max: -Infinity,
+                    })
+                }
+
+                for (let measure of measures) {
+                    let group = groups.get(measure.name)
+                    group.measures.push(measure)
+                    group.sum += measure.duration
+                    group.n++
+                    group.min = Math.min(group.min, measure.duration)
+                    group.max = Math.max(group.max, measure.duration)
+                }
+
+                for (let [name, group] of groups) {
+                    group.mean = group.sum / group.n
+                    group.measures.sort((a, b) => a.duration - b.duration)
+
+                    if (group.n === 1) {
+                        group.median = group.measures[0].duration
+                    } else if (group.n > 1) {
+                        group.median = group.measures[parseInt(group.n / 2)].duration
+                    }
+                }
+
+                let cn = Array.from(names).reduce((a, i) => Math.max(a, i.length), 0) + 5
+                let cmin = 5
+                let cmed = 5
+                let cmax = 5
+                let csam = 4
+
+                let message =
+                    ` ${'NAME'.padEnd(cn)} |` +
+                    ` ${'MIN'.padStart(cmin)} |` +
+                    ` ${'MEDIAN'.padStart(cmed)} |` +
+                    ` ${'MAX'.padStart(cmax)} |` +
+                    ` ${'AVE'.padStart(cmax)} |` +
+                    ` ${'SAMPLES'.padStart(csam)} \n`
+                message += ` ${'-'.repeat(message.length)}\n`
+
+                names = Array.from(names).sort()
+                for (let name of names) {
+                    let group = groups.get(name)
+                    let min = group.min.toFixed(2)
+                    let median = group.median.toFixed(2)
+                    let max = group.max.toFixed(2)
+                    let n = group.n
+                    let ave = group.mean.toFixed(2) //add
+                    message +=
+                        ` ${name.padEnd(cn)} |` +
+                        ` ${min.padStart(cmin)} |` +
+                        ` ${median.padStart(cmed)} |` +
+                        ` ${max.padStart(cmax)} |` +
+                        ` ${ave.padStart(cmax)} |` +
+                        ` ${n.toString().padStart(csam)}\n`
+                }
+                message += `\n`
+                console.log(message)
+            }
+
+            performance.clearMarks()
+            performance.clearMeasures()
+            this.toggle = timestamp
+        }
+    },
+}
+
+ 
+
+export default common

+ 26 - 0
service/util/logger.js

@@ -0,0 +1,26 @@
+/**
+ * 日志输出
+ */
+export default {
+    info(...arg) {
+        console.log(...arg)
+    },
+    debug(...arg) {
+        console.debug(...arg)
+    },
+    error(...arg) {
+        console.error(...arg)
+    },
+    warn(...arg) {
+        console.warn(...arg)
+    },
+    time(s) {
+        console.time(s)
+    },
+    timeEnd(s) {
+        console.timeEnd(s)
+    },
+    message(s) {
+        alert(s)
+    },
+}

+ 808 - 0
service/util/math.js

@@ -0,0 +1,808 @@
+import * as THREE from 'three'
+
+var math = {
+    getBaseLog(x, y) {
+        //返回以 x 为底 y 的对数(即 logx y) .  Math.log 返回一个数的自然对数
+        return Math.log(y) / Math.log(x)
+    },
+    convertVisionVector: function (e) {
+        return new THREE.Vector3(e.x, e.z, -e.y)
+    },
+    invertVisionVector: function (e) {
+        //反转给算法部
+        return new THREE.Vector3(e.x, -e.z, e.y)
+    },
+    convertVisionQuaternion: function (e) {
+        return new THREE.Quaternion(e.x, e.z, -e.y, e.w).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(90)))
+    },
+    invertVisionQuaternion: function (e) {
+        //反转给算法部
+        var a = e.clone().multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(-90)))
+        return new THREE.Quaternion(a.x, -a.z, a.y, a.w)
+    },
+    convertWorkshopVector: function (e) {
+        return new THREE.Vector3(-e.x, e.y, e.z)
+    },
+    convertWorkshopQuaternion: function (e) {
+        return new THREE.Quaternion(-e.x, e.y, e.z, -e.w).multiply(new THREE.Quaternion(Math.sqrt(2) / 2, Math.sqrt(2) / 2, 0, 0))
+    },
+    convertWorkshopPanoramaQuaternion: function (e) {
+        return new THREE.Quaternion(e.x, -e.y, -e.z, e.w).normalize().multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(270)))
+    },
+    convertWorkshopOrthoZoom: function (e, dom) {
+        //xzw
+        return e === -1 ? -1 : e * (dom.clientHeight / dom.clientHeight)
+    },
+
+    getVec2Angle: function (dir1, dir2) {
+        return Math.acos(THREE.MathUtils.clamp(this.getVec2Cos(dir1, dir2), -1, 1))
+    },
+    getVec2Cos: function (dir1, dir2) {
+        return dir1.dot(dir2) / dir1.length() / dir2.length()
+    },
+
+    closeTo: function (a, b, num) {
+        if (num != void 0) return Math.abs(a - b) < num
+        return Math.abs(a - b) < 1e-6
+    },
+    toPrecision: function (e, t) {
+        //xzw change 保留小数
+        var f = function (e, t) {
+            var i = Math.pow(10, t)
+            return Math.round(e * i) / i
+        }
+        if (e instanceof Array) {
+            for (var s = 0; s < e.length; s++) {
+                e[s] = f(e[s], t)
+            }
+            return e
+        } else if (e instanceof Object) {
+            for (var s in e) {
+                e[s] = f(e[s], t)
+            }
+            return e
+        } else return f(e, t)
+    },
+    isEmptyQuaternion: function (e) {
+        return 0 === Math.abs(e.x) && 0 === Math.abs(e.y) && 0 === Math.abs(e.z) && 0 === Math.abs(e.w)
+    },
+    projectPositionToCanvas: function (e, t, i, domE) {
+        ;(i = i || new THREE.Vector3()), i.copy(e)
+        var r = 0.5 * domE.clientWidth,
+            o = 0.5 * domE.clientHeight
+        return i.project(t), (i.x = i.x * r + r), (i.y = -(i.y * o) + o), i
+    },
+    convertScreenPositionToNDC: function (e, t, i, domE) {
+        /* return i = i || new THREE.Vector2,
+        i.x = e / window.innerWidth * 2 - 1,
+        i.y = 2 * -(t / window.innerHeight) + 1,
+        i
+		 */
+
+        return (i = i || new n.Vector2()), (i.x = (e / domE.clientWidth) * 2 - 1), (i.y = 2 * -(t / domE.clientHeight) + 1), i
+    },
+
+    handelPadding: (function () {
+        //去除player左边和上面的宽高,因为pc的player左上有其他element  许钟文
+
+        var pads = new Map() //记录下来避免反复计算
+
+        return function (x, y, domE) {
+            let pad
+
+            let padInfo = pads.get(domE)
+            if (padInfo) {
+                if (domE.clientWidth == padInfo.width && domE.clientHeight == padInfo.height) {
+                    pad = padInfo.pad
+                }
+            }
+            if (!pad) {
+                pad = {
+                    x: this.getOffset('left', domE),
+                    y: this.getOffset('top', domE),
+                }
+
+                pads.set(domE, {
+                    width: domE.clientWidth,
+                    height: domE.clientHeight,
+                    pad,
+                })
+            }
+
+            return {
+                x: x - pad.x,
+                y: y - pad.y,
+            }
+        }
+    })(),
+
+    getOffset: function (type, element, parent) {
+        //获取元素的边距 许钟文
+        var offset = type == 'left' ? element.offsetLeft : element.offsetTop
+        if (!parent) parent = document.body
+        while ((element = element.offsetParent)) {
+            if (element == parent) break
+            offset += type == 'left' ? element.offsetLeft : element.offsetTop
+        }
+        return offset
+    },
+
+    constrainedTurn: function (e) {
+        var t = e % (2 * Math.PI)
+        return (t = t > Math.PI ? (t -= 2 * Math.PI) : t < -Math.PI ? (t += 2 * Math.PI) : t)
+    },
+    getFOVDotThreshold: function (e) {
+        return Math.cos(THREE.MathUtils.degToRad(e / 2))
+    },
+    transform2DForwardVectorByCubeFace: function (e, t, i, n) {
+        switch (e) {
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_X:
+                i.set(1, t.y, t.x)
+                break
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
+                i.set(-1, t.y, -t.x)
+                break
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
+                i.set(-t.x, 1, -t.y)
+                break
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
+                i.set(-t.x, -1, t.y)
+                break
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
+                i.set(-t.x, t.y, 1)
+                break
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
+                i.set(t.x, t.y, -1)
+        }
+        n && i.normalize()
+    },
+
+    getFootPoint: function (oldPos, p1, p2, restricInline) {
+        //找oldPos在线段p1, p2上的垂足
+        /* if(isWorld){//输出全局坐标 需要考虑meshGroup.position
+			p1 = p1.clone();
+			p2 = p2.clone();
+			p1.y += mainDesign.meshGroup.position.y;
+			p2.y += mainDesign.meshGroup.position.y;
+		} */
+        var op1 = oldPos.clone().sub(p1)
+        var p1p2 = p1.clone().sub(p2)
+        var p1p2Len = p1p2.length()
+        var leftLen = op1.dot(p1p2) / p1p2Len
+        var pos = p1.clone().add(p1p2.multiplyScalar(leftLen / p1p2Len))
+
+        if (restricInline && pos.clone().sub(p1).dot(pos.clone().sub(p2)) > 0) {
+            //foot不在线段上
+            if (pos.distanceTo(p1) < pos.distanceTo(p2)) pos = p1.clone()
+            else pos = p2.clone()
+        }
+
+        return pos
+    },
+
+    /**
+     * 计算多边形的重心
+     * @param {*} points
+     */
+    getCenterOfGravityPoint: function (mPoints) {
+        var area = 0.0 //多边形面积
+        var Gx = 0.0,
+            Gy = 0.0 // 重心的x、y
+
+        for (var i = 1; i <= mPoints.length; i++) {
+            var ix = mPoints[i % mPoints.length].x
+            var iy = mPoints[i % mPoints.length].y
+            var nx = mPoints[i - 1].x
+            var ny = mPoints[i - 1].y
+            var temp = (ix * ny - iy * nx) / 2.0
+            area += temp
+            Gx += (temp * (ix + nx)) / 3.0
+            Gy += (temp * (iy + ny)) / 3.0
+        }
+        Gx = Gx / area
+        Gy = Gy / area
+        return { x: Gx, y: Gy }
+    },
+
+    getBound: function (ring) {
+        var bound = new THREE.Box2()
+        for (var j = 0, len = ring.length; j < len; j++) {
+            bound.expandByPoint(ring[j])
+        }
+        return bound
+    },
+
+    isPointInArea: function (ring, point, ifAtLine) {
+        //判断点是否在某个环内
+        var bound = this.getBound(ring)
+        if (point.x < bound.min.x || point.x > bound.max.x || point.y < bound.min.y || point.y > bound.max.y) return false
+
+        var inside = false
+        var x = point.x,
+            y = point.y
+
+        for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
+            var xi = ring[i].x,
+                yi = ring[i].y
+            var xj = ring[j].x,
+                yj = ring[j].y
+
+            if (
+                (xi - x) * (yj - y) == (xi - x) * (yi - y) &&
+                x >= Math.min(xi, xj) &&
+                x <= Math.max(xi, xj) && //xzw add
+                y >= Math.min(yi, yj) &&
+                y <= Math.max(yi, yj)
+            ) {
+                return !!ifAtLine //在线段上,则判断为…… (默认在外)
+            }
+
+            if (yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) {
+                inside = !inside
+            }
+        }
+
+        return inside
+    },
+
+    getArea: function (ring) {
+        //求面积  顺时针为正  来自three shape
+        for (var t = ring.length, i = 0, n = t - 1, r = 0; r < t; n = r++) i += ring[n].x * ring[r].y - ring[r].x * ring[n].y
+        return -0.5 * i
+    },
+    isInBetween: function (a, b, c, precision) {
+        // 如果b几乎等于a或c,返回false.为了避免浮点运行时两值几乎相等,但存在相差0.00000...0001的这种情况出现使用下面方式进行避免
+
+        /* if (Math.abs(a - b) < 0.000001 || Math.abs(b - c) < 0.000001) {
+            return false;
+        } 
+ 
+        return (a <= b && b <= c) || (c <= b && b <= a);*/
+
+        //更改:如果b和a或c中一个接近 就算在a和c之间
+        return (a <= b && b <= c) || (c <= b && b <= a) || this.closeTo(a, b, precision) || this.closeTo(b, c, precision)
+    },
+
+    ifPointAtLineBound: function (point, linePoints, precision) {
+        //待验证  横线和竖线比较特殊
+        return math.isInBetween(linePoints[0].x, point.x, linePoints[1].x, precision) && math.isInBetween(linePoints[0].y, point.y, linePoints[1].y, precision)
+    },
+
+    isLineIntersect: function (line1, line2, notSegment) {
+        //线段和线段是否有交点.  notSegment代表是直线而不是线段
+        var a1 = line1[1].y - line1[0].y
+        var b1 = line1[0].x - line1[1].x
+        var c1 = a1 * line1[0].x + b1 * line1[0].y
+        //转换成一般式: Ax+By = C
+        var a2 = line2[1].y - line2[0].y
+        var b2 = line2[0].x - line2[1].x
+        var c2 = a2 * line2[0].x + b2 * line2[0].y
+        // 计算交点
+        var d = a1 * b2 - a2 * b1
+
+        // 当d==0时,两线平行
+        if (d == 0) {
+            return false
+        } else {
+            var x = (b2 * c1 - b1 * c2) / d
+            var y = (a1 * c2 - a2 * c1) / d
+
+            // 检测交点是否在两条线段上
+            /* if (notSegment || (isInBetween(line1[0].x, x, line1[1].x) || isInBetween(line1[0].y, y, line1[1].y)) &&
+			(isInBetween(line2[0].x, x, line2[1].x) || isInBetween(line2[0].y, y, line2[1].y))) {
+			return {x,y};
+		  } */
+            if (notSegment || (math.ifPointAtLineBound({ x, y }, line1) && math.ifPointAtLineBound({ x, y }, line2))) {
+                return { x, y }
+            }
+        }
+    },
+
+    getNormal: function (line2d) {
+        //获取二维法向量 方向向内
+        var x, y //要求的向量
+        //line2d的向量
+        var x1 = line2d.points[1].x - line2d.points[0].x
+        var y1 = line2d.points[1].y - line2d.points[0].y
+        //假设法向量的x或y固定为1或-1
+        if (y1 != 0) {
+            x = 1
+            y = -(x1 * x) / y1
+        } else if (x1 != 0) {
+            //y如果为0,正常情况x不会是0
+            y = 1
+            x = -(y1 * y) / x1
+        } else {
+            console.log('两个点一样')
+            return null
+        }
+
+        //判断方向里或者外:
+        var vNormal = new THREE.Vector3(x, 0, y)
+        var vLine = new THREE.Vector3(x1, 0, y1)
+        var vDir = vNormal.cross(vLine)
+        if (vDir.y > 0) {
+            x *= -1
+            y *= -1
+        }
+        return new THREE.Vector2(x, y).normalize()
+    },
+
+    getQuaBetween2Vector: function (oriVec, newVec, upVec) {
+        //获取从oriVec旋转到newVec可以应用的quaternion
+        var angle = oriVec.angleTo(newVec)
+        var axis = oriVec.clone().cross(newVec).normalize() //两个up之间
+        if (axis.length() == 0) {
+            //当夹角为180 或 0 度时,得到的axis为(0,0,0),故使用备用的指定upVec
+            return new THREE.Quaternion().setFromAxisAngle(upVec, angle)
+        }
+        return new THREE.Quaternion().setFromAxisAngle(axis, angle)
+    },
+
+    getScaleForConstantSize: (function () {
+        //获得规定二维大小的mesh的scale值。可以避免因camera的projection造成的mesh视觉大小改变。  来源:tag.updateDisc
+        var w
+        var i = new THREE.Vector3(),
+            o = new THREE.Vector3(),
+            l = new THREE.Vector3(),
+            c = new THREE.Vector3(),
+            h = new THREE.Vector3()
+        return function (op = {}) {
+            if (op.width2d) w = op.width2d
+            //如果恒定二维宽度
+            else {
+                var currentDis
+                if (op.camera.type == 'OrthographicCamera') {
+                    //floorplan要直接使用activeControl.camera,主要用到projectionMatrix
+                    currentDis = (op.camera.right - op.camera.left) / op.camera.zoom / 3
+                } else {
+                    currentDis = op.position.distanceTo(op.camera.position) //dollhouse要直接使用player.camera, 因为activeControl.camera没有更新matrixWorld
+                }
+
+                if ((op.nearBound == void 0 && op.farBound != void 0) || (op.nearBound != void 0 && op.farBound == void 0)) {
+                    //仅限制最大或最小的话,不判断像素大小,直接限制mesh的scale
+                    //这个判断也可以写到getScaleForConstantSize里,可以更严谨控制像素宽度,这里只简单计算大小
+                    let scale
+                    if (op.farBound == void 0 && currentDis < op.nearBound) {
+                        scale = (op.scale * currentDis) / op.nearBound
+                    } else if (op.nearBound == void 0 && currentDis > op.farBound) {
+                        scale = (op.scale * currentDis) / op.farBound
+                    } else {
+                        scale = op.scale
+                    }
+                    return scale
+                }
+
+                w = op.maxSize - (op.maxSize - op.minSize) * THREE.MathUtils.smoothstep(currentDis, op.nearBound, op.farBound)
+                //maxSize : mesh要表现的最大像素宽度;   nearBound: 最近距离,若比nearBound近,则使用maxSize
+            }
+            i.copy(op.position).project(op.camera), //tag中心在屏幕上的二维坐标
+                o.set(op.dom.clientWidth / 2, op.dom.clientHeight / 2, 1).multiply(i), //转化成px   -w/2 到 w/2的范围
+                l.set(w / 2, 0, 0).add(o), //加上tag宽度的一半
+                c.set(2 / op.dom.clientWidth, 2 / op.dom.clientHeight, 1).multiply(l), //再转回  -1 到 1的范围
+                h.copy(c).unproject(op.camera) //再转成三维坐标,求得tag边缘的位置
+            var g = h.distanceTo(op.position) //就能得到tag的三维半径
+
+            return g
+        }
+    })(),
+
+    //W , H, left, top分别是rect的宽、高、左、上
+    getCrossPointAtRect: function (p1, aim, W, H, left, top) {
+        //求射线p1-aim在rect边界上的交点,其中aim在rect范围内,p1则不一定(交点在aim这边的延长线上)
+
+        var x, y, borderX
+        var r = (aim.x - p1.x) / (aim.y - p1.y) //根据相似三角形原理先求出这个比值
+        var getX = function (y) {
+            return r * (y - p1.y) + p1.x
+        }
+        var getY = function (x) {
+            return (1 / r) * (x - p1.x) + p1.y
+        }
+        if (aim.x >= p1.x) {
+            borderX = W + left
+        } else {
+            borderX = left
+        }
+        x = borderX
+        y = getY(x)
+        if (y < top || y > top + H) {
+            if (y < top) {
+                y = top
+            } else {
+                y = top + H
+            }
+            x = getX(y)
+        }
+        return new THREE.Vector2(x, y)
+    },
+    getDirFromUV: function (uv) {
+        //获取dir   反向计算 - -  二维转三维比较麻烦
+        var dirB //所求 单位向量
+
+        uv.x %= 1
+        if (uv.x < 0) uv.x += 1 //调整为0-1
+
+        var y = Math.cos(uv.y * Math.PI) //uv中纵向可以直接确定y,  根据上面getUVfromDir的反向计算
+
+        var angle = 2 * Math.PI * uv.x - Math.PI //x/z代表的是角度
+
+        var axisX, axisZ //axis为1代表是正,-1是负数
+        if (-Math.PI <= angle && angle < 0) {
+            axisX = -1 //下半圆
+        } else {
+            axisX = 1 //上半圆
+        }
+        if (-Math.PI / 2 <= angle && angle < Math.PI / 2) {
+            axisZ = 1 //右半圆
+        } else {
+            axisZ = -1 //左半圆
+        }
+
+        var XDivideZ = Math.tan(angle)
+        var z = Math.sqrt((1 - y * y) / (1 + XDivideZ * XDivideZ))
+        var x = XDivideZ * z
+
+        if (z * axisZ < 0) {
+            //异号
+            z *= -1
+            x *= -1
+            if (x * axisX < 0) {
+                //      console.log("wrong!!!!!??????????")
+            }
+        }
+
+        x *= -1 //计算完成后这里不能漏掉 *= -1
+        dirB = new THREE.Vector3(x, y, z)
+
+        //理想状态下x和z和anotherDir相同
+        return dirB
+    },
+
+    getUVfromDir: function (dir) {
+        //获取UV  同shader里的计算
+        var dir = dir.clone()
+        dir.x *= -1 //计算前这里不能漏掉 *= -1  见shader
+        var tx = Math.atan2(dir.x, dir.z) / (Math.PI * 2.0) + 0.5 //atan2(y,x) 返回从 X 轴正向逆时针旋转到点 (x,y) 时经过的角度。区间是-PI 到 PI 之间的值
+        var ty = Math.acos(dir.y) / Math.PI
+        return { x: tx, y: ty }
+
+        //理想状态下tx相同
+    },
+    crossRight: function (vec3, matrix) {
+        //向量右乘矩阵,不能用向量的applyMatrix4(左乘)
+        var e = matrix.elements
+        var v = new THREE.Vector3()
+        v.x = e[0] * vec3.x + e[1] * vec3.y + e[2] * vec3.z + e[3]
+        v.y = e[4] * vec3.x + e[5] * vec3.y + e[6] * vec3.z + e[7]
+        v.z = e[8] * vec3.x + e[9] * vec3.y + e[10] * vec3.z + e[11]
+        //v.w不要
+        return v
+    },
+    getNormalDir: function (point, supportsTiles, currentPano) {
+        //获取A单位法线
+        /* console.log("lookVector:")
+         console.log(objects.player.cameraControls.activeControl.lookVector) */
+
+        var dir = point.clone().sub(currentPano.position) //OA
+        /* console.log("A的dir(无matrix转化):")
+         console.log(dir.clone().normalize()); */
+
+        if (supportsTiles) {
+            var matrixWorld = currentPano.rot90Matrix.clone() //因为热点求点时所右乘的matrix必须是单张全景照片时用的转90度的matrix才行
+        } else {
+            var matrixWorld = currentPano.matrixWorld.clone()
+        }
+        dir = this.crossRight(dir, matrixWorld) //右乘matrixWorld 得matrix转化的向量
+        dir.normalize()
+        /* var b = player.currentPano.skyboxMesh.matrixWorld.clone().getInverse(player.currentPano.skyboxMesh.matrixWorld)
+         console.log(crossRight(dir,b).normalize()) */
+
+        return dir
+    },
+    getDirByLonLat: function (lon, lat) {
+        var dir = new THREE.Vector3()
+        var phi = THREE.MathUtils.degToRad(90 - lat)
+        var theta = THREE.MathUtils.degToRad(lon)
+        dir.x = Math.sin(phi) * Math.cos(theta)
+        dir.y = Math.cos(phi)
+        dir.z = Math.sin(phi) * Math.sin(theta)
+        return dir
+    }, //0,0 => (1,0,0)     270=>(0,0,-1)
+
+    getLineIntersect2(o = {}) {
+        //得两条直线在其向量构成的面的法线方向的交点,投影在线上点的中点。
+        if (o.A != void 0) {
+            // Ap1为一条线,Bp2为一条线
+            var A = o.A
+            var B = o.B
+            var p1 = o.p1
+            var p2 = o.p2
+
+            var dir0 = o.dir0 || new THREE.Vector3().subVectors(p1, A)
+            var dir1 = o.dir1 || new THREE.Vector3().subVectors(p2, B)
+        }
+        if (A.equals(B)) return { pos3d: p1.clone() }
+
+        //寻找两个向量所在的面
+        let normal = dir0.clone().cross(dir1) //面的法线
+
+        //先把整体旋转到使在xz平面上,求完交点再转回来
+        var qua = math.getQuaBetween2Vector(normal, new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 1, 0))
+
+        let newPoints = [A, B, p1, p2].map(e => {
+            return e.clone().applyQuaternion(qua)
+        })
+
+        var pos2d = math.isLineIntersect(
+            [
+                { x: newPoints[0].x, y: newPoints[0].z },
+                { x: newPoints[2].x, y: newPoints[2].z },
+            ],
+            [
+                { x: newPoints[1].x, y: newPoints[1].z },
+                { x: newPoints[3].x, y: newPoints[3].z },
+            ],
+            true
+        )
+        var quaInverse = qua.clone().invert()
+
+        let pos3d = new THREE.Vector3(pos2d.x, 0, pos2d.y)
+        let pos3d1 = pos3d.clone().setY(newPoints[0].y)
+        let pos3d2 = pos3d.clone().setY(newPoints[1].y)
+
+        pos3d1.applyQuaternion(quaInverse)
+        pos3d2.applyQuaternion(quaInverse)
+
+        pos3d = new THREE.Vector3().addVectors(pos3d1, pos3d2).multiplyScalar(0.5)
+        return { pos3d, mid1: pos3d1, mid2: pos3d2 }
+    },
+
+    getLineIntersect(o) {
+        //两条三维直线相交 //取两线最短线段中心点  并且不能超出起点
+        o = o || {}
+        if (o.A != void 0) {
+            // Ap1为一条线,Bp2为一条线
+            var A = o.A
+            var B = o.B
+            var p1 = o.p1
+            var p2 = o.p2
+        }
+
+        if (A.equals(B)) return { pos3d: p1.clone() }
+
+        /* console.log("v1:")
+        console.log(A.clone().sub(p1).normalize())
+        console.log("v2:")
+        console.log(B.clone().sub(p2).normalize())
+         */
+
+        //调试热点夹角
+        var line1 = p1.clone().sub(A).normalize()
+        var line2 = p2.clone().sub(B).normalize()
+        var angle = line1.angleTo(line2)
+        //var pano = player.model.panos.index[player.posGets.list[1]]
+        //console.log('真实两线夹角: ', THREE.MathUtils.radToDeg(angle)) /*+ "旧夹角min: "+getAngle(pano.recentAngleScore)+"("+pano.recentAngleScore+")" */
+
+        //----------
+
+        var compute = function () {
+            var pos3d
+            var ux = p1.x - A.x
+            var uy = p1.y - A.y
+            var uz = p1.z - A.z
+
+            var vx = p2.x - B.x
+            var vy = p2.y - B.y
+            var vz = p2.z - B.z
+
+            var wx = A.x - B.x
+            var wy = A.y - B.y
+            var wz = A.z - B.z
+
+            var a = ux * ux + uy * uy + uz * uz
+            //u*u
+            var b = ux * vx + uy * vy + uz * vz
+            //u*v
+            var c = vx * vx + vy * vy + vz * vz
+            //v*v
+            var d = ux * wx + uy * wy + uz * wz
+            //u*w
+            var e = vx * wx + vy * wy + vz * wz
+            //v*w
+            var dt = a * c - b * b
+
+            var sd = dt
+            var td = dt
+
+            var sn = 0.0
+            //sn = be-cd
+            var tn = 0.0
+            //tn = ae-bd
+
+            var behind = function (index) {
+                //在后方交点的话,直接其中一个点    不用两posget点中心点是因为可能从不同方位 距离很大
+                pos3d = (index == 1 ? p1 : p2).clone()
+                //console.log(pos3d , ' 在后方交点,使用点' + index)
+            }.bind(this)
+
+            if (math.closeTo(dt, 0.0)) {
+                //两直线平行
+                sn = 0.0
+                //在s上指定取s0
+                sd = 1.0
+                //防止计算时除0错误
+                tn = e
+                //按(公式3)求tc
+                td = c
+            } else {
+                sn = b * e - c * d
+                tn = a * e - b * d
+                if (sn < 0.0) {
+                    //最近点在s起点以外,同平行条件
+                    behind(1)
+                    return { pos3d, behind: true }
+                    sn = 0.0
+                    tn = e
+                    td = c
+                } else if (sn > sd) {
+                    //超出终点不限制
+                    /* //最近点在s终点以外(即sc>1,则取sc=1)
+                            sn = sd;
+                            tn = e + b; //按(公式3)计算
+                            td = c; */
+                }
+            }
+
+            if (tn < 0.0) {
+                //最近点在t起点以外
+                behind(2)
+                return { pos3d, behind: true }
+                tn = 0.0
+                if (-d < 0.0) sn = 0.0
+                //按(公式2)计算,如果等号右边小于0,则sc也小于零,取sc=0
+                else if (-d > a) sn = sd
+                //按(公式2)计算,如果sc大于1,取sc=1
+                else {
+                    sn = -d
+                    sd = a
+                }
+            }
+            /*  else if (tn > td){   //超出终点不限制 
+                    tn = td;
+                    if ((-d + b) < 0.0)
+                        sn = 0.0;
+                    else if ((-d + b) > a)
+                        sn = sd;
+                    else
+                    {
+                        sn = (-d + b);
+                        sd = a;
+                    }
+                } */
+
+            var sc = 0.0
+            var tc = 0.0
+
+            if (math.closeTo(sn, 0.0)) sc = 0.0
+            else sc = sn / sd
+            if (math.closeTo(tn, 0.0)) tc = 0.0
+            else tc = tn / td
+            //两个最近点
+            var mid1 = new THREE.Vector3(A.x + sc * ux, A.y + sc * uy, A.z + sc * uz)
+            var mid2 = new THREE.Vector3(B.x + tc * vx, B.y + tc * vy, B.z + tc * vz)
+
+            /* console.log("v11:")
+            console.log(A.clone().sub(mid1).normalize())
+            console.log("v22:")
+            console.log(B.clone().sub(mid2).normalize()) */
+
+            //console.log('另一个结果', math.getLineIntersect2(o)) //结果一样的,只是没有限制后方交点
+
+            let r = { pos3d: mid1.clone().add(mid2).multiplyScalar(0.5), mid1, mid2 }
+            return r
+        }
+
+        return compute()
+
+        //https://blog.csdn.net/u011511587/article/details/52063663  三维空间两直线/线段最短距离、线段计算算法
+    },
+
+    getShapeGeo: function (points, holes) {
+        //获取任意形状(多边形或弧形)的形状面  //quadraticCurveTo() 这是弧形的含函数
+        var shape = new THREE.Shape()
+        shape.moveTo(points[0].x, points[0].y)
+        for (var i = 1, len = points.length; i < len; i++) {
+            shape.lineTo(points[i].x, points[i].y)
+        }
+
+        /* var holePath = new THREE.Path()
+				.moveTo( 20, 10 )
+				.absarc( 10, 10, 10, 0, Math.PI * 2, true ) 
+			arcShape.holes.push( holePath );
+		 */
+        if (holes) {
+            //挖空
+            holes.forEach(points => {
+                var holePath = new THREE.Path()
+                holePath.moveTo(points[0].x, points[0].y)
+                for (var i = 1, len = points.length; i < len; i++) {
+                    holePath.lineTo(points[i].x, points[i].y)
+                }
+                shape.holes.push(holePath)
+            })
+        }
+        var geometry = new THREE.ShapeBufferGeometry(shape) //ShapeGeometry
+        return geometry
+    },
+    getUnPosPlaneGeo: (function () {
+        //获取还没有赋值位置的plane geometry
+        var e = new Uint16Array([0, 1, 2, 0, 2, 3]),
+            //	, t = new Float32Array([-.5, -.5, 0, .5, -.5, 0, .5, .5, 0, -.5, .5, 0])
+            i = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
+            g = new THREE.BufferGeometry()
+        g.setIndex(new THREE.BufferAttribute(e, 1)),
+            //g.setAttribute("position", new n.BufferAttribute(t, 3)),
+            g.setAttribute('uv', new THREE.BufferAttribute(i, 2))
+        return function () {
+            return g
+        }
+    })(),
+    getPlaneGeo: function (A, B, C, D) {
+        var geo = this.getUnPosPlaneGeo().clone()
+        var pos = new Float32Array([A.x, A.y, A.z, B.x, B.y, B.z, C.x, C.y, C.z, D.x, D.y, D.z])
+        geo.setAttribute('position', new THREE.BufferAttribute(pos, 3))
+        geo.computeVertexNormals()
+        geo.computeBoundingSphere() //for raycaster
+        return geo
+    },
+    drawPlane: function (A, B, C, D, material) {
+        var wall = new THREE.Mesh(this.getPlaneGeo(A, B, C, D), material)
+        return wall
+    },
+    movePlane: function (mesh, A, B, C, D) {
+        var pos = new Float32Array([A.x, A.y, A.z, B.x, B.y, B.z, C.x, C.y, C.z, D.x, D.y, D.z])
+        mesh.geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3))
+        mesh.geometry.computeBoundingSphere() //for checkIntersect
+    },
+
+    getAngle(vec1, vec2, axis) {
+        var angle = vec1.angleTo(vec2)
+        var axis_ = vec1.clone().cross(vec2)
+        if (axis_[axis] < 0) {
+            angle *= -1
+        }
+        return angle
+    },
+
+    linearClamp(value, x1, x2, y1, y2) {
+        //x为bound.min, bound.max
+        value = THREE.MathUtils.clamp(value, x1, x2)
+        return y1 + ((y2 - y1) * (value - x1)) / (x2 - x1)
+    },
+
+    isInsideFrustum(bounding, camera) {
+        // bounding是否在视野范围内有可见部分(视野就是一个锥状box)
+        let frustumMatrix = new THREE.Matrix4()
+        frustumMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
+
+        let frustum = new THREE.Frustum()
+        frustum.setFromProjectionMatrix(frustumMatrix)
+
+        if (bounding instanceof THREE.Sphere) {
+            return frustum.intersectsSphere(bounding)
+        } else {
+            return frustum.intersectsBox(bounding)
+        }
+    },
+
+    getStandardYaw(yaw1, yaw2) {
+        //使yaw1过渡到yaw2时朝角度差小的那边走。如果差距大于半个圆,就要反个方向转(把大的那个数字减去360度)
+        if (Math.abs(yaw1 - yaw2) > Math.PI) {
+            yaw1 > yaw2 ? (yaw1 -= Math.PI * 2) : (yaw2 -= Math.PI * 2)
+        }
+        return [yaw1, yaw2]
+    },
+}
+
+export default math

+ 15 - 0
service/util/settings.js

@@ -0,0 +1,15 @@
+//import browser from '@sdk/utils/browser'
+import common from './common.js'   
+
+var settings = { 
+ 
+     
+    job: 'dacf7dfa24ae47fab8fcebfe4dc41ab9',
+    preTexture: '_50k_texture_jpg_high1',
+    format: '_50k.dam',
+     
+    modelBoundsPadding: 5,
+     
+}
+
+export default settings