From dc410da6847da99d0f987834e20b3eb94c7b1258 Mon Sep 17 00:00:00 2001 From: Siddharth Prakashan <46198889+SiddharthPrakashan@users.noreply.github.com> Date: Sun, 18 Jun 2023 00:59:23 +0530 Subject: [PATCH] Allow initializing Annotation uuid as constructor parameter --- resources/page_template/libs/potree/potree.js | 28082 ++++++++-------- 1 file changed, 14042 insertions(+), 14040 deletions(-) diff --git a/resources/page_template/libs/potree/potree.js b/resources/page_template/libs/potree/potree.js index 709679d4..76f09b23 100644 --- a/resources/page_template/libs/potree/potree.js +++ b/resources/page_template/libs/potree/potree.js @@ -55670,7 +55670,7 @@ this._title = args.title || 'No Title'; this._description = args.description || ''; this.offset = new Vector3(); - this.uuid = MathUtils.generateUUID(); + this.uuid = args.uuid || MathUtils.generateUUID(); if (!args.position) { this.position = null; @@ -64312,2628 +64312,1383 @@ void main() { //Potree.workerPool = new Potree.WorkerPool(); - function createPointcloudData(pointcloud) { - - let material = pointcloud.material; - - let ranges = []; - - for(let [name, value] of material.ranges){ - ranges.push({ - name: name, - value: value, - }); - } - - if(typeof material.elevationRange[0] === "number"){ - ranges.push({ - name: "elevationRange", - value: material.elevationRange, - }); - } - if(typeof material.intensityRange[0] === "number"){ - ranges.push({ - name: "intensityRange", - value: material.intensityRange, - }); - } - - let pointSizeTypeName = Object.entries(Potree.PointSizeType).find(e => e[1] === material.pointSizeType)[0]; - - let jsonMaterial = { - activeAttributeName: material.activeAttributeName, - ranges: ranges, - size: material.size, - minSize: material.minSize, - pointSizeType: pointSizeTypeName, - matcap: material.matcap, - }; - - const pcdata = { - name: pointcloud.name, - url: pointcloud.pcoGeometry.url, - position: pointcloud.position.toArray(), - rotation: pointcloud.rotation.toArray(), - scale: pointcloud.scale.toArray(), - material: jsonMaterial, - }; - - return pcdata; - } - - function createProfileData(profile){ - const data = { - uuid: profile.uuid, - name: profile.name, - points: profile.points.map(p => p.toArray()), - height: profile.height, - width: profile.width, - }; - - return data; - } - - function createVolumeData(volume){ - const data = { - uuid: volume.uuid, - type: volume.constructor.name, - name: volume.name, - position: volume.position.toArray(), - rotation: volume.rotation.toArray(), - scale: volume.scale.toArray(), - visible: volume.visible, - clip: volume.clip, - }; - - return data; - } - - function createCameraAnimationData(animation){ - - const controlPoints = animation.controlPoints.map( cp => { - const cpdata = { - position: cp.position.toArray(), - target: cp.target.toArray(), - }; - - return cpdata; - }); - - const data = { - uuid: animation.uuid, - name: animation.name, - duration: animation.duration, - t: animation.t, - curveType: animation.curveType, - visible: animation.visible, - controlPoints: controlPoints, - }; - - return data; - } - - function createMeasurementData(measurement){ - - const data = { - uuid: measurement.uuid, - name: measurement.name, - points: measurement.points.map(p => p.position.toArray()), - showDistances: measurement.showDistances, - showCoordinates: measurement.showCoordinates, - showArea: measurement.showArea, - closed: measurement.closed, - showAngles: measurement.showAngles, - showHeight: measurement.showHeight, - showCircle: measurement.showCircle, - showAzimuth: measurement.showAzimuth, - showEdges: measurement.showEdges, - color: measurement.color.toArray(), - }; - - return data; - } - - function createOrientedImagesData(images){ - const data = { - cameraParamsPath: images.cameraParamsPath, - imageParamsPath: images.imageParamsPath, - }; - - return data; - } - - function createGeopackageData(geopackage){ - const data = { - path: geopackage.path, - }; - - return data; - } - - function createAnnotationData(annotation){ - - const data = { - uuid: annotation.uuid, - title: annotation.title.toString(), - description: annotation.description, - position: annotation.position.toArray(), - offset: annotation.offset.toArray(), - children: [], - }; - - if(annotation.cameraPosition){ - data.cameraPosition = annotation.cameraPosition.toArray(); - } - - if(annotation.cameraTarget){ - data.cameraTarget = annotation.cameraTarget.toArray(); - } - - if(typeof annotation.radius !== "undefined"){ - data.radius = annotation.radius; - } - - return data; - } - - function createAnnotationsData(viewer){ - - const map = new Map(); - - viewer.scene.annotations.traverseDescendants(a => { - const aData = createAnnotationData(a); - - map.set(a, aData); - }); - - for(const [annotation, data] of map){ - for(const child of annotation.children){ - const childData = map.get(child); - data.children.push(childData); - } - } - - const annotations = viewer.scene.annotations.children.map(a => map.get(a)); - - return annotations; - } - - function createSettingsData(viewer){ - return { - pointBudget: viewer.getPointBudget(), - fov: viewer.getFOV(), - edlEnabled: viewer.getEDLEnabled(), - edlRadius: viewer.getEDLRadius(), - edlStrength: viewer.getEDLStrength(), - background: viewer.getBackground(), - minNodeSize: viewer.getMinNodeSize(), - showBoundingBoxes: viewer.getShowBoundingBox(), - }; - } - - function createSceneContentData(viewer){ - - const data = []; - - const potreeObjects = []; - - viewer.scene.scene.traverse(node => { - if(node.potree){ - potreeObjects.push(node); - } - }); - - for(const object of potreeObjects){ - - if(object.potree.file){ - const saveObject = { - file: object.potree.file, - }; - - data.push(saveObject); - } - - - } - - - return data; - } - - function createViewData(viewer){ - const view = viewer.scene.view; - - const data = { - position: view.position.toArray(), - target: view.getPivot().toArray(), - }; - - return data; - } - - function createClassificationData(viewer){ - const classifications = viewer.classifications; - - const data = classifications; - - return data; - } - - function saveProject(viewer) { - - const scene = viewer.scene; - - const data = { - type: "Potree", - version: 1.7, - settings: createSettingsData(viewer), - view: createViewData(viewer), - classification: createClassificationData(viewer), - pointclouds: scene.pointclouds.map(createPointcloudData), - measurements: scene.measurements.map(createMeasurementData), - volumes: scene.volumes.map(createVolumeData), - cameraAnimations: scene.cameraAnimations.map(createCameraAnimationData), - profiles: scene.profiles.map(createProfileData), - annotations: createAnnotationsData(viewer), - orientedImages: scene.orientedImages.map(createOrientedImagesData), - geopackages: scene.geopackages.map(createGeopackageData), - // objects: createSceneContentData(viewer), - }; - - return data; - } - - class ControlPoint{ - - constructor(){ - this.position = new Vector3(0, 0, 0); - this.target = new Vector3(0, 0, 0); - this.positionHandle = null; - this.targetHandle = null; - } - - }; - - - - class CameraAnimation extends EventDispatcher{ - - constructor(viewer){ - super(); - - this.viewer = viewer; - - this.selectedElement = null; - - this.controlPoints = []; - - this.uuid = MathUtils.generateUUID(); - - this.node = new Object3D(); - this.node.name = "camera animation"; - this.viewer.scene.scene.add(this.node); - - this.frustum = this.createFrustum(); - this.node.add(this.frustum); - - this.name = "Camera Animation"; - this.duration = 5; - this.t = 0; - // "centripetal", "chordal", "catmullrom" - this.curveType = "centripetal"; - this.visible = true; - - this.createUpdateHook(); - this.createPath(); - } - - static defaultFromView(viewer){ - const animation = new CameraAnimation(viewer); - - const camera = viewer.scene.getActiveCamera(); - const target = viewer.scene.view.getPivot(); - - const cpCenter = new Vector3( - 0.3 * camera.position.x + 0.7 * target.x, - 0.3 * camera.position.y + 0.7 * target.y, - 0.3 * camera.position.z + 0.7 * target.z, - ); - - const targetCenter = new Vector3( - 0.05 * camera.position.x + 0.95 * target.x, - 0.05 * camera.position.y + 0.95 * target.y, - 0.05 * camera.position.z + 0.95 * target.z, - ); - - const r = camera.position.distanceTo(target) * 0.3; - - //const dir = target.clone().sub(camera.position).normalize(); - const angle = Utils.computeAzimuth(camera.position, target); - - const n = 5; - for(let i = 0; i < n; i++){ - let u = 1.5 * Math.PI * (i / n) + angle; - - const dx = r * Math.cos(u); - const dy = r * Math.sin(u); - - const cpPos = [ - cpCenter.x + dx, - cpCenter.y + dy, - cpCenter.z, - ]; - - const targetPos = [ - targetCenter.x + dx * 0.1, - targetCenter.y + dy * 0.1, - targetCenter.z, - ]; - - const cp = animation.createControlPoint(); - cp.position.set(...cpPos); - cp.target.set(...targetPos); - } - - return animation; - } - - createUpdateHook(){ - const viewer = this.viewer; - - viewer.addEventListener("update", () => { - - const camera = viewer.scene.getActiveCamera(); - const {width, height} = viewer.renderer.getSize(new Vector2()); - - this.node.visible = this.visible; - - for(const cp of this.controlPoints){ - - { // position - const projected = cp.position.clone().project(camera); - - const visible = this.visible && (projected.z < 1 && projected.z > -1); - - if(visible){ - const x = width * (projected.x * 0.5 + 0.5); - const y = height - height * (projected.y * 0.5 + 0.5); - - cp.positionHandle.svg.style.left = x - cp.positionHandle.svg.clientWidth / 2; - cp.positionHandle.svg.style.top = y - cp.positionHandle.svg.clientHeight / 2; - cp.positionHandle.svg.style.display = ""; - }else { - cp.positionHandle.svg.style.display = "none"; - } - } - - { // target - const projected = cp.target.clone().project(camera); - - const visible = this.visible && (projected.z < 1 && projected.z > -1); - - if(visible){ - const x = width * (projected.x * 0.5 + 0.5); - const y = height - height * (projected.y * 0.5 + 0.5); - - cp.targetHandle.svg.style.left = x - cp.targetHandle.svg.clientWidth / 2; - cp.targetHandle.svg.style.top = y - cp.targetHandle.svg.clientHeight / 2; - cp.targetHandle.svg.style.display = ""; - }else { - cp.targetHandle.svg.style.display = "none"; - } - } - - } - - this.line.material.resolution.set(width, height); - - this.updatePath(); - - { // frustum - const frame = this.at(this.t); - const frustum = this.frustum; - - frustum.position.copy(frame.position); - frustum.lookAt(...frame.target.toArray()); - frustum.scale.set(20, 20, 20); - - frustum.material.resolution.set(width, height); - } - - }); - } - - createControlPoint(index){ - - if(index === undefined){ - index = this.controlPoints.length; - } - - const cp = new ControlPoint(); - - - if(this.controlPoints.length >= 2 && index === 0){ - const cp1 = this.controlPoints[0]; - const cp2 = this.controlPoints[1]; - - const dir = cp1.position.clone().sub(cp2.position).multiplyScalar(0.5); - cp.position.copy(cp1.position).add(dir); - - const tDir = cp1.target.clone().sub(cp2.target).multiplyScalar(0.5); - cp.target.copy(cp1.target).add(tDir); - }else if(this.controlPoints.length >= 2 && index === this.controlPoints.length){ - const cp1 = this.controlPoints[this.controlPoints.length - 2]; - const cp2 = this.controlPoints[this.controlPoints.length - 1]; - - const dir = cp2.position.clone().sub(cp1.position).multiplyScalar(0.5); - cp.position.copy(cp1.position).add(dir); - - const tDir = cp2.target.clone().sub(cp1.target).multiplyScalar(0.5); - cp.target.copy(cp2.target).add(tDir); - }else if(this.controlPoints.length >= 2){ - const cp1 = this.controlPoints[index - 1]; - const cp2 = this.controlPoints[index]; - - cp.position.copy(cp1.position.clone().add(cp2.position).multiplyScalar(0.5)); - cp.target.copy(cp1.target.clone().add(cp2.target).multiplyScalar(0.5)); - } - - // cp.position.copy(viewer.scene.view.position); - // cp.target.copy(viewer.scene.view.getPivot()); - - cp.positionHandle = this.createHandle(cp.position); - cp.targetHandle = this.createHandle(cp.target); - - this.controlPoints.splice(index, 0, cp); - - this.dispatchEvent({ - type: "controlpoint_added", - controlpoint: cp, - }); - - return cp; - } - - removeControlPoint(cp){ - this.controlPoints = this.controlPoints.filter(_cp => _cp !== cp); - - this.dispatchEvent({ - type: "controlpoint_removed", - controlpoint: cp, - }); - - cp.positionHandle.svg.remove(); - cp.targetHandle.svg.remove(); - - // TODO destroy cp - } - - createPath(){ - - { // position - const geometry = new LineGeometry(); - - let material = new LineMaterial({ - color: 0x00ff00, - dashSize: 5, - gapSize: 2, - linewidth: 2, - resolution: new Vector2(1000, 1000), - }); - - const line = new Line2(geometry, material); - - this.line = line; - this.node.add(line); - } - - { // target - const geometry = new LineGeometry(); - - let material = new LineMaterial({ - color: 0x0000ff, - dashSize: 5, - gapSize: 2, - linewidth: 2, - resolution: new Vector2(1000, 1000), - }); - - const line = new Line2(geometry, material); - - this.targetLine = line; - this.node.add(line); - } - } - - createFrustum(){ - - const f = 0.3; - - const positions = [ - 0, 0, 0, - -f, -f, +1, - - 0, 0, 0, - f, -f, +1, - - 0, 0, 0, - f, f, +1, - - 0, 0, 0, - -f, f, +1, - - -f, -f, +1, - f, -f, +1, - - f, -f, +1, - f, f, +1, - - f, f, +1, - -f, f, +1, - - -f, f, +1, - -f, -f, +1, - ]; - - const geometry = new LineGeometry(); - - geometry.setPositions(positions); - geometry.verticesNeedUpdate = true; - geometry.computeBoundingSphere(); - - let material = new LineMaterial({ - color: 0xff0000, - linewidth: 2, - resolution: new Vector2(1000, 1000), - }); - - const line = new Line2(geometry, material); - line.computeLineDistances(); - - return line; - } - - updatePath(){ - - { // positions - const positions = this.controlPoints.map(cp => cp.position); - const first = positions[0]; - - const curve = new CatmullRomCurve3(positions); - curve.curveType = this.curveType; - - const n = 100; - - const curvePositions = []; - for(let k = 0; k <= n; k++){ - const t = k / n; - - const position = curve.getPoint(t).sub(first); - - curvePositions.push(position.x, position.y, position.z); - } - - this.line.geometry.setPositions(curvePositions); - this.line.geometry.verticesNeedUpdate = true; - this.line.geometry.computeBoundingSphere(); - this.line.position.copy(first); - this.line.computeLineDistances(); - - this.cameraCurve = curve; - } - - { // targets - const positions = this.controlPoints.map(cp => cp.target); - const first = positions[0]; - - const curve = new CatmullRomCurve3(positions); - curve.curveType = this.curveType; - - const n = 100; - - const curvePositions = []; - for(let k = 0; k <= n; k++){ - const t = k / n; - - const position = curve.getPoint(t).sub(first); - - curvePositions.push(position.x, position.y, position.z); - } - - this.targetLine.geometry.setPositions(curvePositions); - this.targetLine.geometry.verticesNeedUpdate = true; - this.targetLine.geometry.computeBoundingSphere(); - this.targetLine.position.copy(first); - this.targetLine.computeLineDistances(); - - this.targetCurve = curve; - } - } - - at(t){ - - if(t > 1){ - t = 1; - }else if(t < 0){ - t = 0; - } - - const camPos = this.cameraCurve.getPointAt(t); - const target = this.targetCurve.getPointAt(t); - - const frame = { - position: camPos, - target: target, - }; - - return frame; - } - - set(t){ - this.t = t; - } - - createHandle(vector){ - - const svgns = "http://www.w3.org/2000/svg"; - const svg = document.createElementNS(svgns, "svg"); - - svg.setAttribute("width", "2em"); - svg.setAttribute("height", "2em"); - svg.setAttribute("position", "absolute"); - - svg.style.left = "50px"; - svg.style.top = "50px"; - svg.style.position = "absolute"; - svg.style.zIndex = "10000"; - - const circle = document.createElementNS(svgns, 'circle'); - circle.setAttributeNS(null, 'cx', "1em"); - circle.setAttributeNS(null, 'cy', "1em"); - circle.setAttributeNS(null, 'r', "0.5em"); - circle.setAttributeNS(null, 'style', 'fill: red; stroke: black; stroke-width: 0.2em;' ); - svg.appendChild(circle); - - - const element = this.viewer.renderer.domElement.parentElement; - element.appendChild(svg); - - - const startDrag = (evt) => { - this.selectedElement = svg; - - document.addEventListener("mousemove", drag); - }; - - const endDrag = (evt) => { - this.selectedElement = null; - - document.removeEventListener("mousemove", drag); - }; - - const drag = (evt) => { - if (this.selectedElement) { - evt.preventDefault(); - - const rect = viewer.renderer.domElement.getBoundingClientRect(); - - const x = evt.clientX - rect.x; - const y = evt.clientY - rect.y; - - const {width, height} = this.viewer.renderer.getSize(new Vector2()); - const camera = this.viewer.scene.getActiveCamera(); - //const cp = this.controlPoints.find(cp => cp.handle.svg === svg); - const projected = vector.clone().project(camera); - - projected.x = ((x / width) - 0.5) / 0.5; - projected.y = (-(y - height) / height - 0.5) / 0.5; - - const unprojected = projected.clone().unproject(camera); - vector.set(unprojected.x, unprojected.y, unprojected.z); - - - } - }; - - svg.addEventListener('mousedown', startDrag); - svg.addEventListener('mouseup', endDrag); - - const handle = { - svg: svg, - }; - - return handle; - } - - setVisible(visible){ - this.node.visible = visible; - - const display = visible ? "" : "none"; - - for(const cp of this.controlPoints){ - cp.positionHandle.svg.style.display = display; - cp.targetHandle.svg.style.display = display; - } - - this.visible = visible; - } - - setDuration(duration){ - this.duration = duration; - } - - getDuration(duration){ - return this.duration; - } - - play(){ - - const tStart = performance.now(); - const duration = this.duration; - - const originalyVisible = this.visible; - this.setVisible(false); - - const onUpdate = (delta) => { - - let tNow = performance.now(); - let elapsed = (tNow - tStart) / 1000; - let t = elapsed / duration; - - this.set(t); - - const frame = this.at(t); - - viewer.scene.view.position.copy(frame.position); - viewer.scene.view.lookAt(frame.target); - - - if(t > 1){ - this.setVisible(originalyVisible); - - this.viewer.removeEventListener("update", onUpdate); - } - - }; - - this.viewer.addEventListener("update", onUpdate); - - } - - } - - function loadPointCloud(viewer, data){ - - let loadMaterial = (target) => { - - if(data.material){ - - if(data.material.activeAttributeName != null){ - target.activeAttributeName = data.material.activeAttributeName; - } - - if(data.material.ranges != null){ - for(let range of data.material.ranges){ - - if(range.name === "elevationRange"){ - target.elevationRange = range.value; - }else if(range.name === "intensityRange"){ - target.intensityRange = range.value; - }else { - target.setRange(range.name, range.value); - } - - } - } - - if(data.material.size != null){ - target.size = data.material.size; - } - - if(data.material.minSize != null){ - target.minSize = data.material.minSize; - } - - if(data.material.pointSizeType != null){ - target.pointSizeType = PointSizeType[data.material.pointSizeType]; - } - - if(data.material.matcap != null){ - target.matcap = data.material.matcap; - } - - }else if(data.activeAttributeName != null){ - target.activeAttributeName = data.activeAttributeName; - }else { - // no material data - } - - }; - - const promise = new Promise((resolve) => { - - const names = viewer.scene.pointclouds.map(p => p.name); - const alreadyExists = names.includes(data.name); - - if(alreadyExists){ - resolve(); - return; - } - - Potree.loadPointCloud(data.url, data.name, (e) => { - const {pointcloud} = e; - - pointcloud.position.set(...data.position); - pointcloud.rotation.set(...data.rotation); - pointcloud.scale.set(...data.scale); - - loadMaterial(pointcloud.material); - - viewer.scene.addPointCloud(pointcloud); - - resolve(pointcloud); - }); - }); - - return promise; - } - - function loadMeasurement(viewer, data){ - - const duplicate = viewer.scene.measurements.find(measure => measure.uuid === data.uuid); - if(duplicate){ - return; - } - - const measure = new Measure(); - - measure.uuid = data.uuid; - measure.name = data.name; - measure.showDistances = data.showDistances; - measure.showCoordinates = data.showCoordinates; - measure.showArea = data.showArea; - measure.closed = data.closed; - measure.showAngles = data.showAngles; - measure.showHeight = data.showHeight; - measure.showCircle = data.showCircle; - measure.showAzimuth = data.showAzimuth; - measure.showEdges = data.showEdges; - // color - - for(const point of data.points){ - const pos = new Vector3(...point); - measure.addMarker(pos); - } - - viewer.scene.addMeasurement(measure); - - } - - function loadVolume(viewer, data){ - - const duplicate = viewer.scene.volumes.find(volume => volume.uuid === data.uuid); - if(duplicate){ - return; - } - - let volume = new Potree[data.type]; - - volume.uuid = data.uuid; - volume.name = data.name; - volume.position.set(...data.position); - volume.rotation.set(...data.rotation); - volume.scale.set(...data.scale); - volume.visible = data.visible; - volume.clip = data.clip; - - viewer.scene.addVolume(volume); - } - - function loadCameraAnimation(viewer, data){ - - const duplicate = viewer.scene.cameraAnimations.find(a => a.uuid === data.uuid); - if(duplicate){ - return; - } - - const animation = new CameraAnimation(viewer); - - animation.uuid = data.uuid; - animation.name = data.name; - animation.duration = data.duration; - animation.t = data.t; - animation.curveType = data.curveType; - animation.visible = data.visible; - animation.controlPoints = []; - - for(const cpdata of data.controlPoints){ - const cp = animation.createControlPoint(); - - cp.position.set(...cpdata.position); - cp.target.set(...cpdata.target); - } - - viewer.scene.addCameraAnimation(animation); - } - - function loadOrientedImages(viewer, images){ - - const {cameraParamsPath, imageParamsPath} = images; - - const duplicate = viewer.scene.orientedImages.find(i => i.imageParamsPath === imageParamsPath); - if(duplicate){ - return; - } - - Potree.OrientedImageLoader.load(cameraParamsPath, imageParamsPath, viewer).then( images => { - viewer.scene.addOrientedImages(images); - }); - - } - - function loadGeopackage(viewer, geopackage){ - - const path = geopackage.path; - - const duplicate = viewer.scene.geopackages.find(i => i.path === path); - if(duplicate){ - return; - } - - const projection = viewer.getProjection(); - - proj4.defs("WGS84", "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"); - proj4.defs("pointcloud", projection); - const transform = proj4("WGS84", "pointcloud"); - const params = { - transform: transform, - }; - - Potree.GeoPackageLoader.loadUrl(path, params).then(data => { - viewer.scene.addGeopackage(data); - }); - - - } - - function loadSettings(viewer, data){ - if(!data){ - return; - } - - viewer.setPointBudget(data.pointBudget); - viewer.setFOV(data.fov); - viewer.setEDLEnabled(data.edlEnabled); - viewer.setEDLRadius(data.edlRadius); - viewer.setEDLStrength(data.edlStrength); - viewer.setBackground(data.background); - viewer.setMinNodeSize(data.minNodeSize); - viewer.setShowBoundingBox(data.showBoundingBoxes); - } - - function loadView(viewer, view){ - viewer.scene.view.position.set(...view.position); - viewer.scene.view.lookAt(...view.target); - } - - function loadAnnotationItem(item){ - - const annotation = new Annotation({ - position: item.position, - title: item.title, - cameraPosition: item.cameraPosition, - cameraTarget: item.cameraTarget, - }); - - - annotation.description = item.description; - annotation.uuid = item.uuid; - - if(item.offset){ - annotation.offset.set(...item.offset); - } - - return annotation; - } - - function loadAnnotations(viewer, data){ - - if(!data){ - return; - } - - const findDuplicate = (item) => { - - let duplicate = null; - - viewer.scene.annotations.traverse( a => { - if(a.uuid === item.uuid){ - duplicate = a; - } - }); - - return duplicate; - }; - - const traverse = (item, parent) => { - - const duplicate = findDuplicate(item); - if(duplicate){ - return; - } - - const annotation = loadAnnotationItem(item); - - for(const childItem of item.children){ - traverse(childItem, annotation); - } - - parent.add(annotation); - - }; - - for(const item of data){ - traverse(item, viewer.scene.annotations); - } - - } - - function loadProfile(viewer, data){ - - const {name, points} = data; - - const duplicate = viewer.scene.profiles.find(profile => profile.uuid === data.uuid); - if(duplicate){ - return; - } - - let profile = new Potree.Profile(); - profile.name = name; - profile.uuid = data.uuid; - - profile.setWidth(data.width); - - for(const point of points){ - profile.addMarker(new Vector3(...point)); - } - - viewer.scene.addProfile(profile); - } - - function loadClassification(viewer, data){ - if(!data){ - return; - } - - const classifications = data; - - viewer.setClassifications(classifications); - } - - async function loadProject(viewer, data){ - - if(data.type !== "Potree"){ - console.error("not a valid Potree project"); - return; - } - - loadSettings(viewer, data.settings); - - loadView(viewer, data.view); - - const pointcloudPromises = []; - for(const pointcloud of data.pointclouds){ - const promise = loadPointCloud(viewer, pointcloud); - pointcloudPromises.push(promise); - } - - for(const measure of data.measurements){ - loadMeasurement(viewer, measure); - } - - for(const volume of data.volumes){ - loadVolume(viewer, volume); - } - - for(const animation of data.cameraAnimations){ - loadCameraAnimation(viewer, animation); - } - - for(const profile of data.profiles){ - loadProfile(viewer, profile); - } - - if(data.orientedImages){ - for(const images of data.orientedImages){ - loadOrientedImages(viewer, images); - } - } - - loadAnnotations(viewer, data.annotations); - - loadClassification(viewer, data.classification); - - // need to load at least one point cloud that defines the scene projection, - // before we can load stuff in other projections such as geopackages - //await Promise.any(pointcloudPromises); // (not yet supported) - Utils.waitAny(pointcloudPromises).then( () => { - if(data.geopackages){ - for(const geopackage of data.geopackages){ - loadGeopackage(viewer, geopackage); - } - } - }); - - await Promise.all(pointcloudPromises); + function createPointcloudData(pointcloud) { + + let material = pointcloud.material; + + let ranges = []; + + for(let [name, value] of material.ranges){ + ranges.push({ + name: name, + value: value, + }); + } + + if(typeof material.elevationRange[0] === "number"){ + ranges.push({ + name: "elevationRange", + value: material.elevationRange, + }); + } + if(typeof material.intensityRange[0] === "number"){ + ranges.push({ + name: "intensityRange", + value: material.intensityRange, + }); + } + + let pointSizeTypeName = Object.entries(Potree.PointSizeType).find(e => e[1] === material.pointSizeType)[0]; + + let jsonMaterial = { + activeAttributeName: material.activeAttributeName, + ranges: ranges, + size: material.size, + minSize: material.minSize, + pointSizeType: pointSizeTypeName, + matcap: material.matcap, + }; + + const pcdata = { + name: pointcloud.name, + url: pointcloud.pcoGeometry.url, + position: pointcloud.position.toArray(), + rotation: pointcloud.rotation.toArray(), + scale: pointcloud.scale.toArray(), + material: jsonMaterial, + }; + + return pcdata; } - // - // Algorithm by Christian Boucheny - // shader code taken and adapted from CloudCompare - // - // see - // https://github.com/cloudcompare/trunk/tree/master/plugins/qEDL/shaders/EDL - // http://www.kitware.com/source/home/post/9 - // https://tel.archives-ouvertes.fr/tel-00438464/document p. 115+ (french) + function createProfileData(profile){ + const data = { + uuid: profile.uuid, + name: profile.name, + points: profile.points.map(p => p.toArray()), + height: profile.height, + width: profile.width, + }; - class EyeDomeLightingMaterial extends RawShaderMaterial{ + return data; + } - constructor(parameters = {}){ - super(); + function createVolumeData(volume){ + const data = { + uuid: volume.uuid, + type: volume.constructor.name, + name: volume.name, + position: volume.position.toArray(), + rotation: volume.rotation.toArray(), + scale: volume.scale.toArray(), + visible: volume.visible, + clip: volume.clip, + }; - let uniforms = { - screenWidth: { type: 'f', value: 0 }, - screenHeight: { type: 'f', value: 0 }, - edlStrength: { type: 'f', value: 1.0 }, - uNear: { type: 'f', value: 1.0 }, - uFar: { type: 'f', value: 1.0 }, - radius: { type: 'f', value: 1.0 }, - neighbours: { type: '2fv', value: [] }, - depthMap: { type: 't', value: null }, - uEDLColor: { type: 't', value: null }, - uEDLDepth: { type: 't', value: null }, - opacity: { type: 'f', value: 1.0 }, - uProj: { type: "Matrix4fv", value: [] }, + return data; + } + + function createCameraAnimationData(animation){ + + const controlPoints = animation.controlPoints.map( cp => { + const cpdata = { + position: cp.position.toArray(), + target: cp.target.toArray(), }; - this.setValues({ - uniforms: uniforms, - vertexShader: this.getDefines() + Shaders['edl.vs'], - fragmentShader: this.getDefines() + Shaders['edl.fs'], - lights: false - }); + return cpdata; + }); - this.neighbourCount = 8; - } + const data = { + uuid: animation.uuid, + name: animation.name, + duration: animation.duration, + t: animation.t, + curveType: animation.curveType, + visible: animation.visible, + controlPoints: controlPoints, + }; - getDefines() { - let defines = ''; + return data; + } - defines += '#define NEIGHBOUR_COUNT ' + this.neighbourCount + '\n'; + function createMeasurementData(measurement){ + + const data = { + uuid: measurement.uuid, + name: measurement.name, + points: measurement.points.map(p => p.position.toArray()), + showDistances: measurement.showDistances, + showCoordinates: measurement.showCoordinates, + showArea: measurement.showArea, + closed: measurement.closed, + showAngles: measurement.showAngles, + showHeight: measurement.showHeight, + showCircle: measurement.showCircle, + showAzimuth: measurement.showAzimuth, + showEdges: measurement.showEdges, + color: measurement.color.toArray(), + }; - return defines; - } + return data; + } - updateShaderSource() { + function createOrientedImagesData(images){ + const data = { + cameraParamsPath: images.cameraParamsPath, + imageParamsPath: images.imageParamsPath, + }; - let vs = this.getDefines() + Shaders['edl.vs']; - let fs = this.getDefines() + Shaders['edl.fs']; + return data; + } - this.setValues({ - vertexShader: vs, - fragmentShader: fs - }); + function createGeopackageData(geopackage){ + const data = { + path: geopackage.path, + }; - this.uniforms.neighbours.value = this.neighbours; + return data; + } - this.needsUpdate = true; + function createAnnotationData(annotation){ + + const data = { + uuid: annotation.uuid, + title: annotation.title.toString(), + description: annotation.description, + position: annotation.position.toArray(), + offset: annotation.offset.toArray(), + children: [], + }; + + if(annotation.cameraPosition){ + data.cameraPosition = annotation.cameraPosition.toArray(); } - get neighbourCount(){ - return this._neighbourCount; + if(annotation.cameraTarget){ + data.cameraTarget = annotation.cameraTarget.toArray(); } - set neighbourCount(value){ - if (this._neighbourCount !== value) { - this._neighbourCount = value; - this.neighbours = new Float32Array(this._neighbourCount * 2); - for (let c = 0; c < this._neighbourCount; c++) { - this.neighbours[2 * c + 0] = Math.cos(2 * c * Math.PI / this._neighbourCount); - this.neighbours[2 * c + 1] = Math.sin(2 * c * Math.PI / this._neighbourCount); - } + if(typeof annotation.radius !== "undefined"){ + data.radius = annotation.radius; + } - this.updateShaderSource(); + return data; + } + + function createAnnotationsData(viewer){ + + const map = new Map(); + + viewer.scene.annotations.traverseDescendants(a => { + const aData = createAnnotationData(a); + + map.set(a, aData); + }); + + for(const [annotation, data] of map){ + for(const child of annotation.children){ + const childData = map.get(child); + data.children.push(childData); } } - + const annotations = viewer.scene.annotations.children.map(a => map.get(a)); + + return annotations; } - class NormalizationEDLMaterial extends RawShaderMaterial{ + function createSettingsData(viewer){ + return { + pointBudget: viewer.getPointBudget(), + fov: viewer.getFOV(), + edlEnabled: viewer.getEDLEnabled(), + edlRadius: viewer.getEDLRadius(), + edlStrength: viewer.getEDLStrength(), + background: viewer.getBackground(), + minNodeSize: viewer.getMinNodeSize(), + showBoundingBoxes: viewer.getShowBoundingBox(), + }; + } - constructor(parameters = {}){ - super(); + function createSceneContentData(viewer){ - let uniforms = { - screenWidth: { type: 'f', value: 0 }, - screenHeight: { type: 'f', value: 0 }, - edlStrength: { type: 'f', value: 1.0 }, - radius: { type: 'f', value: 1.0 }, - neighbours: { type: '2fv', value: [] }, - uEDLMap: { type: 't', value: null }, - uDepthMap: { type: 't', value: null }, - uWeightMap: { type: 't', value: null }, - }; + const data = []; - this.setValues({ - uniforms: uniforms, - vertexShader: this.getDefines() + Shaders['normalize.vs'], - fragmentShader: this.getDefines() + Shaders['normalize_and_edl.fs'], - }); + const potreeObjects = []; - this.neighbourCount = 8; - } + viewer.scene.scene.traverse(node => { + if(node.potree){ + potreeObjects.push(node); + } + }); - getDefines() { - let defines = ''; + for(const object of potreeObjects){ + + if(object.potree.file){ + const saveObject = { + file: object.potree.file, + }; + + data.push(saveObject); + } - defines += '#define NEIGHBOUR_COUNT ' + this.neighbourCount + '\n'; - return defines; } - updateShaderSource() { - let vs = this.getDefines() + Shaders['normalize.vs']; - let fs = this.getDefines() + Shaders['normalize_and_edl.fs']; + return data; + } - this.setValues({ - vertexShader: vs, - fragmentShader: fs - }); + function createViewData(viewer){ + const view = viewer.scene.view; - this.uniforms.neighbours.value = this.neighbours; + const data = { + position: view.position.toArray(), + target: view.getPivot().toArray(), + }; - this.needsUpdate = true; - } + return data; + } - get neighbourCount(){ - return this._neighbourCount; - } + function createClassificationData(viewer){ + const classifications = viewer.classifications; - set neighbourCount(value){ - if (this._neighbourCount !== value) { - this._neighbourCount = value; - this.neighbours = new Float32Array(this._neighbourCount * 2); - for (let c = 0; c < this._neighbourCount; c++) { - this.neighbours[2 * c + 0] = Math.cos(2 * c * Math.PI / this._neighbourCount); - this.neighbours[2 * c + 1] = Math.sin(2 * c * Math.PI / this._neighbourCount); - } + const data = classifications; - this.updateShaderSource(); - } - } - + return data; } - class NormalizationMaterial extends RawShaderMaterial{ + function saveProject(viewer) { + + const scene = viewer.scene; + + const data = { + type: "Potree", + version: 1.7, + settings: createSettingsData(viewer), + view: createViewData(viewer), + classification: createClassificationData(viewer), + pointclouds: scene.pointclouds.map(createPointcloudData), + measurements: scene.measurements.map(createMeasurementData), + volumes: scene.volumes.map(createVolumeData), + cameraAnimations: scene.cameraAnimations.map(createCameraAnimationData), + profiles: scene.profiles.map(createProfileData), + annotations: createAnnotationsData(viewer), + orientedImages: scene.orientedImages.map(createOrientedImagesData), + geopackages: scene.geopackages.map(createGeopackageData), + // objects: createSceneContentData(viewer), + }; - constructor(parameters = {}){ - super(); + return data; + } - let uniforms = { - uDepthMap: { type: 't', value: null }, - uWeightMap: { type: 't', value: null }, - }; + class ControlPoint{ - this.setValues({ - uniforms: uniforms, - vertexShader: this.getDefines() + Shaders['normalize.vs'], - fragmentShader: this.getDefines() + Shaders['normalize.fs'], - }); + constructor(){ + this.position = new Vector3(0, 0, 0); + this.target = new Vector3(0, 0, 0); + this.positionHandle = null; + this.targetHandle = null; } - getDefines() { - let defines = ''; + }; - return defines; - } - updateShaderSource() { - let vs = this.getDefines() + Shaders['normalize.vs']; - let fs = this.getDefines() + Shaders['normalize.fs']; + class CameraAnimation extends EventDispatcher{ - this.setValues({ - vertexShader: vs, - fragmentShader: fs - }); + constructor(viewer){ + super(); + + this.viewer = viewer; - this.needsUpdate = true; - } + this.selectedElement = null; - } + this.controlPoints = []; - /** - * laslaz code taken and adapted from plas.io js-laslaz - * http://plas.io/ - * https://github.com/verma/plasio - * - * Thanks to Uday Verma and Howard Butler - * - */ + this.uuid = MathUtils.generateUUID(); - class LasLazLoader { + this.node = new Object3D(); + this.node.name = "camera animation"; + this.viewer.scene.scene.add(this.node); - constructor (version, extension) { - if (typeof (version) === 'string') { - this.version = new Version(version); - } else { - this.version = version; - } + this.frustum = this.createFrustum(); + this.node.add(this.frustum); - this.extension = extension; + this.name = "Camera Animation"; + this.duration = 5; + this.t = 0; + // "centripetal", "chordal", "catmullrom" + this.curveType = "centripetal"; + this.visible = true; + + this.createUpdateHook(); + this.createPath(); } - static progressCB () { + static defaultFromView(viewer){ + const animation = new CameraAnimation(viewer); - } + const camera = viewer.scene.getActiveCamera(); + const target = viewer.scene.view.getPivot(); - load (node) { - if (node.loaded) { - return; - } + const cpCenter = new Vector3( + 0.3 * camera.position.x + 0.7 * target.x, + 0.3 * camera.position.y + 0.7 * target.y, + 0.3 * camera.position.z + 0.7 * target.z, + ); - let url = node.getURL(); + const targetCenter = new Vector3( + 0.05 * camera.position.x + 0.95 * target.x, + 0.05 * camera.position.y + 0.95 * target.y, + 0.05 * camera.position.z + 0.95 * target.z, + ); - if (this.version.equalOrHigher('1.4')) { - url += `.${this.extension}`; - } + const r = camera.position.distanceTo(target) * 0.3; - let xhr = XHRFactory.createXMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.overrideMimeType('text/plain; charset=x-user-defined'); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 200 || xhr.status === 0) { - let buffer = xhr.response; - this.parse(node, buffer); - } else { - console.log('Failed to load file! HTTP status: ' + xhr.status + ', file: ' + url); - } - } - }; + //const dir = target.clone().sub(camera.position).normalize(); + const angle = Utils.computeAzimuth(camera.position, target); - xhr.send(null); - } + const n = 5; + for(let i = 0; i < n; i++){ + let u = 1.5 * Math.PI * (i / n) + angle; - async parse(node, buffer){ - let lf = new LASFile(buffer); - let handler = new LasLazBatcher(node); + const dx = r * Math.cos(u); + const dy = r * Math.sin(u); - try{ - await lf.open(); - lf.isOpen = true; - }catch(e){ - console.log("failed to open file. :("); + const cpPos = [ + cpCenter.x + dx, + cpCenter.y + dy, + cpCenter.z, + ]; - return; + const targetPos = [ + targetCenter.x + dx * 0.1, + targetCenter.y + dy * 0.1, + targetCenter.z, + ]; + + const cp = animation.createControlPoint(); + cp.position.set(...cpPos); + cp.target.set(...targetPos); } - let header = await lf.getHeader(); + return animation; + } - let skip = 1; - let totalRead = 0; - let totalToRead = (skip <= 1 ? header.pointsCount : header.pointsCount / skip); + createUpdateHook(){ + const viewer = this.viewer; - let hasMoreData = true; + viewer.addEventListener("update", () => { - while(hasMoreData){ - let data = await lf.readData(1000 * 1000, 0, skip); + const camera = viewer.scene.getActiveCamera(); + const {width, height} = viewer.renderer.getSize(new Vector2()); - handler.push(new LASDecoder(data.buffer, - header.pointsFormatId, - header.pointsStructSize, - data.count, - header.scale, - header.offset, - header.mins, header.maxs)); + this.node.visible = this.visible; - totalRead += data.count; - LasLazLoader.progressCB(totalRead / totalToRead); + for(const cp of this.controlPoints){ + + { // position + const projected = cp.position.clone().project(camera); - hasMoreData = data.hasMoreData; - } + const visible = this.visible && (projected.z < 1 && projected.z > -1); - header.totalRead = totalRead; - header.versionAsString = lf.versionAsString; - header.isCompressed = lf.isCompressed; + if(visible){ + const x = width * (projected.x * 0.5 + 0.5); + const y = height - height * (projected.y * 0.5 + 0.5); - LasLazLoader.progressCB(1); + cp.positionHandle.svg.style.left = x - cp.positionHandle.svg.clientWidth / 2; + cp.positionHandle.svg.style.top = y - cp.positionHandle.svg.clientHeight / 2; + cp.positionHandle.svg.style.display = ""; + }else { + cp.positionHandle.svg.style.display = "none"; + } + } - try{ - await lf.close(); + { // target + const projected = cp.target.clone().project(camera); - lf.isOpen = false; - }catch(e){ - console.error("failed to close las/laz file!!!"); - - throw e; - } - } + const visible = this.visible && (projected.z < 1 && projected.z > -1); - handle (node, url) { + if(visible){ + const x = width * (projected.x * 0.5 + 0.5); + const y = height - height * (projected.y * 0.5 + 0.5); - } - }; + cp.targetHandle.svg.style.left = x - cp.targetHandle.svg.clientWidth / 2; + cp.targetHandle.svg.style.top = y - cp.targetHandle.svg.clientHeight / 2; + cp.targetHandle.svg.style.display = ""; + }else { + cp.targetHandle.svg.style.display = "none"; + } + } - class LasLazBatcher{ + } - constructor (node) { - this.node = node; - } + this.line.material.resolution.set(width, height); - push (lasBuffer) { - const workerPath = Potree.scriptPath + '/workers/LASDecoderWorker.js'; - const worker = Potree.workerPool.getWorker(workerPath); - const node = this.node; - const pointAttributes = node.pcoGeometry.pointAttributes; + this.updatePath(); - worker.onmessage = (e) => { - let geometry = new BufferGeometry(); - let numPoints = lasBuffer.pointsCount; + { // frustum + const frame = this.at(this.t); + const frustum = this.frustum; - let positions = new Float32Array(e.data.position); - let colors = new Uint8Array(e.data.color); - let intensities = new Float32Array(e.data.intensity); - let classifications = new Uint8Array(e.data.classification); - let returnNumbers = new Uint8Array(e.data.returnNumber); - let numberOfReturns = new Uint8Array(e.data.numberOfReturns); - let pointSourceIDs = new Uint16Array(e.data.pointSourceID); - let indices = new Uint8Array(e.data.indices); + frustum.position.copy(frame.position); + frustum.lookAt(...frame.target.toArray()); + frustum.scale.set(20, 20, 20); - geometry.setAttribute('position', new BufferAttribute(positions, 3)); - geometry.setAttribute('color', new BufferAttribute(colors, 4, true)); - geometry.setAttribute('intensity', new BufferAttribute(intensities, 1)); - geometry.setAttribute('classification', new BufferAttribute(classifications, 1)); - geometry.setAttribute('return number', new BufferAttribute(returnNumbers, 1)); - geometry.setAttribute('number of returns', new BufferAttribute(numberOfReturns, 1)); - geometry.setAttribute('source id', new BufferAttribute(pointSourceIDs, 1)); - geometry.setAttribute('indices', new BufferAttribute(indices, 4)); - geometry.attributes.indices.normalized = true; + frustum.material.resolution.set(width, height); + } - for(const key in e.data.ranges){ - const range = e.data.ranges[key]; + }); + } - const attribute = pointAttributes.attributes.find(a => a.name === key); - attribute.range[0] = Math.min(attribute.range[0], range[0]); - attribute.range[1] = Math.max(attribute.range[1], range[1]); - } + createControlPoint(index){ - let tightBoundingBox = new Box3( - new Vector3().fromArray(e.data.tightBoundingBox.min), - new Vector3().fromArray(e.data.tightBoundingBox.max) - ); + if(index === undefined){ + index = this.controlPoints.length; + } - geometry.boundingBox = this.node.boundingBox; - this.node.tightBoundingBox = tightBoundingBox; + const cp = new ControlPoint(); - this.node.geometry = geometry; - this.node.numPoints = numPoints; - this.node.loaded = true; - this.node.loading = false; - Potree.numNodesLoading--; - this.node.mean = new Vector3(...e.data.mean); - Potree.workerPool.returnWorker(workerPath, worker); - }; + if(this.controlPoints.length >= 2 && index === 0){ + const cp1 = this.controlPoints[0]; + const cp2 = this.controlPoints[1]; - let message = { - buffer: lasBuffer.arrayb, - numPoints: lasBuffer.pointsCount, - pointSize: lasBuffer.pointSize, - pointFormatID: 2, - scale: lasBuffer.scale, - offset: lasBuffer.offset, - mins: lasBuffer.mins, - maxs: lasBuffer.maxs - }; - worker.postMessage(message, [message.buffer]); - }; - } + const dir = cp1.position.clone().sub(cp2.position).multiplyScalar(0.5); + cp.position.copy(cp1.position).add(dir); - class BinaryLoader{ + const tDir = cp1.target.clone().sub(cp2.target).multiplyScalar(0.5); + cp.target.copy(cp1.target).add(tDir); + }else if(this.controlPoints.length >= 2 && index === this.controlPoints.length){ + const cp1 = this.controlPoints[this.controlPoints.length - 2]; + const cp2 = this.controlPoints[this.controlPoints.length - 1]; - constructor(version, boundingBox, scale){ - if (typeof (version) === 'string') { - this.version = new Version(version); - } else { - this.version = version; + const dir = cp2.position.clone().sub(cp1.position).multiplyScalar(0.5); + cp.position.copy(cp1.position).add(dir); + + const tDir = cp2.target.clone().sub(cp1.target).multiplyScalar(0.5); + cp.target.copy(cp2.target).add(tDir); + }else if(this.controlPoints.length >= 2){ + const cp1 = this.controlPoints[index - 1]; + const cp2 = this.controlPoints[index]; + + cp.position.copy(cp1.position.clone().add(cp2.position).multiplyScalar(0.5)); + cp.target.copy(cp1.target.clone().add(cp2.target).multiplyScalar(0.5)); } - this.boundingBox = boundingBox; - this.scale = scale; + // cp.position.copy(viewer.scene.view.position); + // cp.target.copy(viewer.scene.view.getPivot()); + + cp.positionHandle = this.createHandle(cp.position); + cp.targetHandle = this.createHandle(cp.target); + + this.controlPoints.splice(index, 0, cp); + + this.dispatchEvent({ + type: "controlpoint_added", + controlpoint: cp, + }); + + return cp; } - load(node){ - if (node.loaded) { - return; - } + removeControlPoint(cp){ + this.controlPoints = this.controlPoints.filter(_cp => _cp !== cp); - let url = node.getURL(); + this.dispatchEvent({ + type: "controlpoint_removed", + controlpoint: cp, + }); - if (this.version.equalOrHigher('1.4')) { - url += '.bin'; - } + cp.positionHandle.svg.remove(); + cp.targetHandle.svg.remove(); - let xhr = XHRFactory.createXMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.overrideMimeType('text/plain; charset=x-user-defined'); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if((xhr.status === 200 || xhr.status === 0) && xhr.response !== null){ - let buffer = xhr.response; - this.parse(node, buffer); - } else { - //console.error(`Failed to load file! HTTP status: ${xhr.status}, file: ${url}`); - throw new Error(`Failed to load file! HTTP status: ${xhr.status}, file: ${url}`); - } - } - }; - - try { - xhr.send(null); - } catch (e) { - console.log('fehler beim laden der punktwolke: ' + e); + // TODO destroy cp + } + + createPath(){ + + { // position + const geometry = new LineGeometry(); + + let material = new LineMaterial({ + color: 0x00ff00, + dashSize: 5, + gapSize: 2, + linewidth: 2, + resolution: new Vector2(1000, 1000), + }); + + const line = new Line2(geometry, material); + + this.line = line; + this.node.add(line); } - }; - parse(node, buffer){ - let pointAttributes = node.pcoGeometry.pointAttributes; - let numPoints = buffer.byteLength / node.pcoGeometry.pointAttributes.byteSize; + { // target + const geometry = new LineGeometry(); - if (this.version.upTo('1.5')) { - node.numPoints = numPoints; + let material = new LineMaterial({ + color: 0x0000ff, + dashSize: 5, + gapSize: 2, + linewidth: 2, + resolution: new Vector2(1000, 1000), + }); + + const line = new Line2(geometry, material); + + this.targetLine = line; + this.node.add(line); } + } - let workerPath = Potree.scriptPath + '/workers/BinaryDecoderWorker.js'; - let worker = Potree.workerPool.getWorker(workerPath); + createFrustum(){ - worker.onmessage = function (e) { + const f = 0.3; - let data = e.data; - let buffers = data.attributeBuffers; - let tightBoundingBox = new Box3( - new Vector3().fromArray(data.tightBoundingBox.min), - new Vector3().fromArray(data.tightBoundingBox.max) - ); + const positions = [ + 0, 0, 0, + -f, -f, +1, - Potree.workerPool.returnWorker(workerPath, worker); + 0, 0, 0, + f, -f, +1, - let geometry = new BufferGeometry(); + 0, 0, 0, + f, f, +1, - for(let property in buffers){ - let buffer = buffers[property].buffer; - let batchAttribute = buffers[property].attribute; + 0, 0, 0, + -f, f, +1, - if (property === "POSITION_CARTESIAN") { - geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3)); - } else if (property === "rgba") { - geometry.setAttribute("rgba", new BufferAttribute(new Uint8Array(buffer), 4, true)); - } else if (property === "NORMAL_SPHEREMAPPED") { - geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); - } else if (property === "NORMAL_OCT16") { - geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); - } else if (property === "NORMAL") { - geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); - } else if (property === "INDICES") { - let bufferAttribute = new BufferAttribute(new Uint8Array(buffer), 4); - bufferAttribute.normalized = true; - geometry.setAttribute('indices', bufferAttribute); - } else if (property === "SPACING") { - let bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1); - geometry.setAttribute('spacing', bufferAttribute); - } else { - const bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1); + -f, -f, +1, + f, -f, +1, - bufferAttribute.potree = { - offset: buffers[property].offset, - scale: buffers[property].scale, - preciseBuffer: buffers[property].preciseBuffer, - range: batchAttribute.range, - }; + f, -f, +1, + f, f, +1, - geometry.setAttribute(property, bufferAttribute); + f, f, +1, + -f, f, +1, - const attribute = pointAttributes.attributes.find(a => a.name === batchAttribute.name); - attribute.range[0] = Math.min(attribute.range[0], batchAttribute.range[0]); - attribute.range[1] = Math.max(attribute.range[1], batchAttribute.range[1]); + -f, f, +1, + -f, -f, +1, + ]; - if(node.getLevel() === 0){ - attribute.initialRange = batchAttribute.range; - } + const geometry = new LineGeometry(); - } + geometry.setPositions(positions); + geometry.verticesNeedUpdate = true; + geometry.computeBoundingSphere(); + + let material = new LineMaterial({ + color: 0xff0000, + linewidth: 2, + resolution: new Vector2(1000, 1000), + }); + + const line = new Line2(geometry, material); + line.computeLineDistances(); + + return line; + } + + updatePath(){ + + { // positions + const positions = this.controlPoints.map(cp => cp.position); + const first = positions[0]; + + const curve = new CatmullRomCurve3(positions); + curve.curveType = this.curveType; + + const n = 100; + + const curvePositions = []; + for(let k = 0; k <= n; k++){ + const t = k / n; + + const position = curve.getPoint(t).sub(first); + + curvePositions.push(position.x, position.y, position.z); } - tightBoundingBox.max.sub(tightBoundingBox.min); - tightBoundingBox.min.set(0, 0, 0); + this.line.geometry.setPositions(curvePositions); + this.line.geometry.verticesNeedUpdate = true; + this.line.geometry.computeBoundingSphere(); + this.line.position.copy(first); + this.line.computeLineDistances(); - let numPoints = e.data.buffer.byteLength / pointAttributes.byteSize; - - node.numPoints = numPoints; - node.geometry = geometry; - node.mean = new Vector3(...data.mean); - node.tightBoundingBox = tightBoundingBox; - node.loaded = true; - node.loading = false; - node.estimatedSpacing = data.estimatedSpacing; - Potree.numNodesLoading--; - }; + this.cameraCurve = curve; + } - let message = { - buffer: buffer, - pointAttributes: pointAttributes, - version: this.version.version, - min: [ node.boundingBox.min.x, node.boundingBox.min.y, node.boundingBox.min.z ], - offset: [node.pcoGeometry.offset.x, node.pcoGeometry.offset.y, node.pcoGeometry.offset.z], - scale: this.scale, - spacing: node.spacing, - hasChildren: node.hasChildren, - name: node.name - }; - worker.postMessage(message, [message.buffer]); - }; + { // targets + const positions = this.controlPoints.map(cp => cp.target); + const first = positions[0]; - - } + const curve = new CatmullRomCurve3(positions); + curve.curveType = this.curveType; - function parseAttributes(cloudjs){ + const n = 100; - let version = new Version(cloudjs.version); + const curvePositions = []; + for(let k = 0; k <= n; k++){ + const t = k / n; - const replacements = { - "COLOR_PACKED": "rgba", - "RGBA": "rgba", - "INTENSITY": "intensity", - "CLASSIFICATION": "classification", - "GPS_TIME": "gps-time", - }; + const position = curve.getPoint(t).sub(first); - const replaceOldNames = (old) => { - if(replacements[old]){ - return replacements[old]; - }else { - return old; + curvePositions.push(position.x, position.y, position.z); + } + + this.targetLine.geometry.setPositions(curvePositions); + this.targetLine.geometry.verticesNeedUpdate = true; + this.targetLine.geometry.computeBoundingSphere(); + this.targetLine.position.copy(first); + this.targetLine.computeLineDistances(); + + this.targetCurve = curve; } - }; + } - const pointAttributes = []; - if(version.upTo('1.7')){ + at(t){ - for(let attributeName of cloudjs.pointAttributes){ - const oldAttribute = PointAttribute[attributeName]; + if(t > 1){ + t = 1; + }else if(t < 0){ + t = 0; + } - const attribute = { - name: oldAttribute.name, - size: oldAttribute.byteSize, - elements: oldAttribute.numElements, - elementSize: oldAttribute.byteSize / oldAttribute.numElements, - type: oldAttribute.type.name, - description: "", - }; + const camPos = this.cameraCurve.getPointAt(t); + const target = this.targetCurve.getPointAt(t); - pointAttributes.push(attribute); - } + const frame = { + position: camPos, + target: target, + }; - }else { - pointAttributes.push(...cloudjs.pointAttributes); + return frame; } + set(t){ + this.t = t; + } - { - const attributes = new PointAttributes(); + createHandle(vector){ + + const svgns = "http://www.w3.org/2000/svg"; + const svg = document.createElementNS(svgns, "svg"); - const typeConversion = { - int8: PointAttributeTypes.DATA_TYPE_INT8, - int16: PointAttributeTypes.DATA_TYPE_INT16, - int32: PointAttributeTypes.DATA_TYPE_INT32, - int64: PointAttributeTypes.DATA_TYPE_INT64, - uint8: PointAttributeTypes.DATA_TYPE_UINT8, - uint16: PointAttributeTypes.DATA_TYPE_UINT16, - uint32: PointAttributeTypes.DATA_TYPE_UINT32, - uint64: PointAttributeTypes.DATA_TYPE_UINT64, - double: PointAttributeTypes.DATA_TYPE_DOUBLE, - float: PointAttributeTypes.DATA_TYPE_FLOAT, + svg.setAttribute("width", "2em"); + svg.setAttribute("height", "2em"); + svg.setAttribute("position", "absolute"); + + svg.style.left = "50px"; + svg.style.top = "50px"; + svg.style.position = "absolute"; + svg.style.zIndex = "10000"; + + const circle = document.createElementNS(svgns, 'circle'); + circle.setAttributeNS(null, 'cx', "1em"); + circle.setAttributeNS(null, 'cy', "1em"); + circle.setAttributeNS(null, 'r', "0.5em"); + circle.setAttributeNS(null, 'style', 'fill: red; stroke: black; stroke-width: 0.2em;' ); + svg.appendChild(circle); + + + const element = this.viewer.renderer.domElement.parentElement; + element.appendChild(svg); + + + const startDrag = (evt) => { + this.selectedElement = svg; + + document.addEventListener("mousemove", drag); }; - for(const jsAttribute of pointAttributes){ - const name = replaceOldNames(jsAttribute.name); - const type = typeConversion[jsAttribute.type]; - const numElements = jsAttribute.elements; - const description = jsAttribute.description; + const endDrag = (evt) => { + this.selectedElement = null; - const attribute = new PointAttribute(name, type, numElements); + document.removeEventListener("mousemove", drag); + }; - attributes.add(attribute); - } + const drag = (evt) => { + if (this.selectedElement) { + evt.preventDefault(); + + const rect = viewer.renderer.domElement.getBoundingClientRect(); + + const x = evt.clientX - rect.x; + const y = evt.clientY - rect.y; + + const {width, height} = this.viewer.renderer.getSize(new Vector2()); + const camera = this.viewer.scene.getActiveCamera(); + //const cp = this.controlPoints.find(cp => cp.handle.svg === svg); + const projected = vector.clone().project(camera); + + projected.x = ((x / width) - 0.5) / 0.5; + projected.y = (-(y - height) / height - 0.5) / 0.5; + + const unprojected = projected.clone().unproject(camera); + vector.set(unprojected.x, unprojected.y, unprojected.z); - { - // check if it has normals - let hasNormals = - pointAttributes.find(a => a.name === "NormalX") !== undefined && - pointAttributes.find(a => a.name === "NormalY") !== undefined && - pointAttributes.find(a => a.name === "NormalZ") !== undefined; - if(hasNormals){ - let vector = { - name: "NORMAL", - attributes: ["NormalX", "NormalY", "NormalZ"], - }; - attributes.addVector(vector); } + }; + + svg.addEventListener('mousedown', startDrag); + svg.addEventListener('mouseup', endDrag); + + const handle = { + svg: svg, + }; + + return handle; + } + + setVisible(visible){ + this.node.visible = visible; + + const display = visible ? "" : "none"; + + for(const cp of this.controlPoints){ + cp.positionHandle.svg.style.display = display; + cp.targetHandle.svg.style.display = display; } - return attributes; + this.visible = visible; } - } + setDuration(duration){ + this.duration = duration; + } - function lasLazAttributes(fMno){ - const attributes = new PointAttributes(); + getDuration(duration){ + return this.duration; + } - attributes.add(PointAttribute.POSITION_CARTESIAN); - attributes.add(new PointAttribute("rgba", PointAttributeTypes.DATA_TYPE_UINT8, 4)); - attributes.add(new PointAttribute("intensity", PointAttributeTypes.DATA_TYPE_UINT16, 1)); - attributes.add(new PointAttribute("classification", PointAttributeTypes.DATA_TYPE_UINT8, 1)); - attributes.add(new PointAttribute("gps-time", PointAttributeTypes.DATA_TYPE_DOUBLE, 1)); - attributes.add(new PointAttribute("number of returns", PointAttributeTypes.DATA_TYPE_UINT8, 1)); - attributes.add(new PointAttribute("return number", PointAttributeTypes.DATA_TYPE_UINT8, 1)); - attributes.add(new PointAttribute("source id", PointAttributeTypes.DATA_TYPE_UINT16, 1)); - //attributes.add(new PointAttribute("pointSourceID", PointAttributeTypes.DATA_TYPE_INT8, 4)); + play(){ + const tStart = performance.now(); + const duration = this.duration; - return attributes; - } + const originalyVisible = this.visible; + this.setVisible(false); - class POCLoader { + const onUpdate = (delta) => { - static load(url, callback){ - try { - let pco = new PointCloudOctreeGeometry(); - pco.url = url; - let xhr = XHRFactory.createXMLHttpRequest(); - xhr.open('GET', url, true); + let tNow = performance.now(); + let elapsed = (tNow - tStart) / 1000; + let t = elapsed / duration; - xhr.onreadystatechange = function () { - if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 0)) { - let fMno = JSON.parse(xhr.responseText); + this.set(t); - let version = new Version(fMno.version); + const frame = this.at(t); - // assume octreeDir is absolute if it starts with http - if (fMno.octreeDir.indexOf('http') === 0) { - pco.octreeDir = fMno.octreeDir; - } else { - pco.octreeDir = url + '/../' + fMno.octreeDir; - } + viewer.scene.view.position.copy(frame.position); + viewer.scene.view.lookAt(frame.target); - pco.spacing = fMno.spacing; - pco.hierarchyStepSize = fMno.hierarchyStepSize; - pco.pointAttributes = fMno.pointAttributes; + if(t > 1){ + this.setVisible(originalyVisible); - let min = new Vector3(fMno.boundingBox.lx, fMno.boundingBox.ly, fMno.boundingBox.lz); - let max = new Vector3(fMno.boundingBox.ux, fMno.boundingBox.uy, fMno.boundingBox.uz); - let boundingBox = new Box3(min, max); - let tightBoundingBox = boundingBox.clone(); + this.viewer.removeEventListener("update", onUpdate); + } - if (fMno.tightBoundingBox) { - tightBoundingBox.min.copy(new Vector3(fMno.tightBoundingBox.lx, fMno.tightBoundingBox.ly, fMno.tightBoundingBox.lz)); - tightBoundingBox.max.copy(new Vector3(fMno.tightBoundingBox.ux, fMno.tightBoundingBox.uy, fMno.tightBoundingBox.uz)); - } + }; - let offset = min.clone(); + this.viewer.addEventListener("update", onUpdate); - boundingBox.min.sub(offset); - boundingBox.max.sub(offset); + } - tightBoundingBox.min.sub(offset); - tightBoundingBox.max.sub(offset); + } - pco.projection = fMno.projection; - pco.boundingBox = boundingBox; - pco.tightBoundingBox = tightBoundingBox; - pco.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); - pco.tightBoundingSphere = tightBoundingBox.getBoundingSphere(new Sphere()); - pco.offset = offset; - if (fMno.pointAttributes === 'LAS') { - pco.loader = new LasLazLoader(fMno.version, "las"); - pco.pointAttributes = lasLazAttributes(fMno); - } else if (fMno.pointAttributes === 'LAZ') { - pco.loader = new LasLazLoader(fMno.version, "laz"); - pco.pointAttributes = lasLazAttributes(fMno); - } else { - pco.loader = new BinaryLoader(fMno.version, boundingBox, fMno.scale); - pco.pointAttributes = parseAttributes(fMno); - } + function loadPointCloud(viewer, data){ - let nodes = {}; + let loadMaterial = (target) => { - { // load root - let name = 'r'; + if(data.material){ - let root = new PointCloudOctreeGeometryNode(name, pco, boundingBox); - root.level = 0; - root.hasChildren = true; - root.spacing = pco.spacing; - if (version.upTo('1.5')) { - root.numPoints = fMno.hierarchy[0][1]; - } else { - root.numPoints = 0; - } - pco.root = root; - pco.root.load(); - nodes[name] = root; - } + if(data.material.activeAttributeName != null){ + target.activeAttributeName = data.material.activeAttributeName; + } - // load remaining hierarchy - if (version.upTo('1.4')) { - for (let i = 1; i < fMno.hierarchy.length; i++) { - let name = fMno.hierarchy[i][0]; - let numPoints = fMno.hierarchy[i][1]; - let index = parseInt(name.charAt(name.length - 1)); - let parentName = name.substring(0, name.length - 1); - let parentNode = nodes[parentName]; - let level = name.length - 1; - //let boundingBox = POCLoader.createChildAABB(parentNode.boundingBox, index); - let boundingBox = Utils.createChildAABB(parentNode.boundingBox, index); + if(data.material.ranges != null){ + for(let range of data.material.ranges){ - let node = new PointCloudOctreeGeometryNode(name, pco, boundingBox); - node.level = level; - node.numPoints = numPoints; - node.spacing = pco.spacing / Math.pow(2, level); - parentNode.addChild(node); - nodes[name] = node; - } + if(range.name === "elevationRange"){ + target.elevationRange = range.value; + }else if(range.name === "intensityRange"){ + target.intensityRange = range.value; + }else { + target.setRange(range.name, range.value); } - pco.nodes = nodes; - - callback(pco); } - }; + } - xhr.send(null); - } catch (e) { - console.log("loading failed: '" + url + "'"); - console.log(e); + if(data.material.size != null){ + target.size = data.material.size; + } - callback(); - } - } + if(data.material.minSize != null){ + target.minSize = data.material.minSize; + } - loadPointAttributes(mno){ - let fpa = mno.pointAttributes; - let pa = new PointAttributes(); + if(data.material.pointSizeType != null){ + target.pointSizeType = PointSizeType[data.material.pointSizeType]; + } - for (let i = 0; i < fpa.length; i++) { - let pointAttribute = PointAttribute[fpa[i]]; - pa.add(pointAttribute); + if(data.material.matcap != null){ + target.matcap = data.material.matcap; + } + + }else if(data.activeAttributeName != null){ + target.activeAttributeName = data.activeAttributeName; + }else { + // no material data } - return pa; - } + }; - createChildAABB(aabb, index){ - let min = aabb.min.clone(); - let max = aabb.max.clone(); - let size = new Vector3().subVectors(max, min); + const promise = new Promise((resolve) => { - if ((index & 0b0001) > 0) { - min.z += size.z / 2; - } else { - max.z -= size.z / 2; - } + const names = viewer.scene.pointclouds.map(p => p.name); + const alreadyExists = names.includes(data.name); - if ((index & 0b0010) > 0) { - min.y += size.y / 2; - } else { - max.y -= size.y / 2; + if(alreadyExists){ + resolve(); + return; } - if ((index & 0b0100) > 0) { - min.x += size.x / 2; - } else { - max.x -= size.x / 2; - } + Potree.loadPointCloud(data.url, data.name, (e) => { + const {pointcloud} = e; - return new Box3(min, max); - } - } + pointcloud.position.set(...data.position); + pointcloud.rotation.set(...data.rotation); + pointcloud.scale.set(...data.scale); - class OctreeGeometry{ - - constructor(){ - this.url = null; - this.spacing = 0; - this.boundingBox = null; - this.root = null; - this.pointAttributes = null; - this.loader = null; - } - - }; - - class OctreeGeometryNode{ - - constructor(name, octreeGeometry, boundingBox){ - this.id = OctreeGeometryNode.IDCount++; - this.name = name; - this.index = parseInt(name.charAt(name.length - 1)); - this.octreeGeometry = octreeGeometry; - this.boundingBox = boundingBox; - this.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); - this.children = {}; - this.numPoints = 0; - this.level = null; - this.oneTimeDisposeHandlers = []; - } - - isGeometryNode(){ - return true; - } - - getLevel(){ - return this.level; - } - - isTreeNode(){ - return false; - } - - isLoaded(){ - return this.loaded; - } - - getBoundingSphere(){ - return this.boundingSphere; - } - - getBoundingBox(){ - return this.boundingBox; - } - - getChildren(){ - let children = []; - - for (let i = 0; i < 8; i++) { - if (this.children[i]) { - children.push(this.children[i]); - } - } - - return children; - } - - getBoundingBox(){ - return this.boundingBox; - } - - load(){ - - if (Potree.numNodesLoading >= Potree.maxNodesLoading) { - return; - } - - this.octreeGeometry.loader.load(this); - } - - getNumPoints(){ - return this.numPoints; - } - - dispose(){ - if (this.geometry && this.parent != null) { - this.geometry.dispose(); - this.geometry = null; - this.loaded = false; - - // this.dispatchEvent( { type: 'dispose' } ); - for (let i = 0; i < this.oneTimeDisposeHandlers.length; i++) { - let handler = this.oneTimeDisposeHandlers[i]; - handler(); - } - this.oneTimeDisposeHandlers = []; - } - } - - }; - - OctreeGeometryNode.IDCount = 0; + loadMaterial(pointcloud.material); - // let loadedNodes = new Set(); + viewer.scene.addPointCloud(pointcloud); - class NodeLoader{ + resolve(pointcloud); + }); + }); - constructor(url){ - this.url = url; - } + return promise; + } - async load(node){ + function loadMeasurement(viewer, data){ - if(node.loaded || node.loading){ - return; - } + const duplicate = viewer.scene.measurements.find(measure => measure.uuid === data.uuid); + if(duplicate){ + return; + } - node.loading = true; - Potree.numNodesLoading++; + const measure = new Measure(); - // console.log(node.name, node.numPoints); + measure.uuid = data.uuid; + measure.name = data.name; + measure.showDistances = data.showDistances; + measure.showCoordinates = data.showCoordinates; + measure.showArea = data.showArea; + measure.closed = data.closed; + measure.showAngles = data.showAngles; + measure.showHeight = data.showHeight; + measure.showCircle = data.showCircle; + measure.showAzimuth = data.showAzimuth; + measure.showEdges = data.showEdges; + // color - // if(loadedNodes.has(node.name)){ - // // debugger; - // } - // loadedNodes.add(node.name); + for(const point of data.points){ + const pos = new Vector3(...point); + measure.addMarker(pos); + } - try{ - if(node.nodeType === 2){ - await this.loadHierarchy(node); - } + viewer.scene.addMeasurement(measure); - let {byteOffset, byteSize} = node; + } + function loadVolume(viewer, data){ - let urlOctree = `${this.url}/../octree.bin`; + const duplicate = viewer.scene.volumes.find(volume => volume.uuid === data.uuid); + if(duplicate){ + return; + } - let first = byteOffset; - let last = byteOffset + byteSize - 1n; + let volume = new Potree[data.type]; - let buffer; + volume.uuid = data.uuid; + volume.name = data.name; + volume.position.set(...data.position); + volume.rotation.set(...data.rotation); + volume.scale.set(...data.scale); + volume.visible = data.visible; + volume.clip = data.clip; - if(byteSize === 0n){ - buffer = new ArrayBuffer(0); - console.warn(`loaded node with 0 bytes: ${node.name}`); - }else { - let response = await fetch(urlOctree, { - headers: { - 'content-type': 'multipart/byteranges', - 'Range': `bytes=${first}-${last}`, - }, - }); + viewer.scene.addVolume(volume); + } - buffer = await response.arrayBuffer(); - } + function loadCameraAnimation(viewer, data){ - let workerPath; - if(this.metadata.encoding === "BROTLI"){ - workerPath = Potree.scriptPath + '/workers/2.0/DecoderWorker_brotli.js'; - }else { - workerPath = Potree.scriptPath + '/workers/2.0/DecoderWorker.js'; - } + const duplicate = viewer.scene.cameraAnimations.find(a => a.uuid === data.uuid); + if(duplicate){ + return; + } - let worker = Potree.workerPool.getWorker(workerPath); + const animation = new CameraAnimation(viewer); - worker.onmessage = function (e) { + animation.uuid = data.uuid; + animation.name = data.name; + animation.duration = data.duration; + animation.t = data.t; + animation.curveType = data.curveType; + animation.visible = data.visible; + animation.controlPoints = []; - let data = e.data; - let buffers = data.attributeBuffers; + for(const cpdata of data.controlPoints){ + const cp = animation.createControlPoint(); - Potree.workerPool.returnWorker(workerPath, worker); + cp.position.set(...cpdata.position); + cp.target.set(...cpdata.target); + } - let geometry = new BufferGeometry(); - - for(let property in buffers){ + viewer.scene.addCameraAnimation(animation); + } - let buffer = buffers[property].buffer; + function loadOrientedImages(viewer, images){ - if(property === "position"){ - geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3)); - }else if(property === "rgba"){ - geometry.setAttribute('rgba', new BufferAttribute(new Uint8Array(buffer), 4, true)); - }else if(property === "NORMAL"){ - //geometry.setAttribute('rgba', new THREE.BufferAttribute(new Uint8Array(buffer), 4, true)); - geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); - }else if (property === "INDICES") { - let bufferAttribute = new BufferAttribute(new Uint8Array(buffer), 4); - bufferAttribute.normalized = true; - geometry.setAttribute('indices', bufferAttribute); - }else { - const bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1); + const {cameraParamsPath, imageParamsPath} = images; - let batchAttribute = buffers[property].attribute; - bufferAttribute.potree = { - offset: buffers[property].offset, - scale: buffers[property].scale, - preciseBuffer: buffers[property].preciseBuffer, - range: batchAttribute.range, - }; + const duplicate = viewer.scene.orientedImages.find(i => i.imageParamsPath === imageParamsPath); + if(duplicate){ + return; + } - geometry.setAttribute(property, bufferAttribute); - } + Potree.OrientedImageLoader.load(cameraParamsPath, imageParamsPath, viewer).then( images => { + viewer.scene.addOrientedImages(images); + }); - } - // indices ?? + } - node.density = data.density; - node.geometry = geometry; - node.loaded = true; - node.loading = false; - Potree.numNodesLoading--; - }; + function loadGeopackage(viewer, geopackage){ - let pointAttributes = node.octreeGeometry.pointAttributes; - let scale = node.octreeGeometry.scale; + const path = geopackage.path; - let box = node.boundingBox; - let min = node.octreeGeometry.offset.clone().add(box.min); - let size = box.max.clone().sub(box.min); - let max = min.clone().add(size); - let numPoints = node.numPoints; + const duplicate = viewer.scene.geopackages.find(i => i.path === path); + if(duplicate){ + return; + } - let offset = node.octreeGeometry.loader.offset; + const projection = viewer.getProjection(); - let message = { - name: node.name, - buffer: buffer, - pointAttributes: pointAttributes, - scale: scale, - min: min, - max: max, - size: size, - offset: offset, - numPoints: numPoints - }; + proj4.defs("WGS84", "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"); + proj4.defs("pointcloud", projection); + const transform = proj4("WGS84", "pointcloud"); + const params = { + transform: transform, + }; - worker.postMessage(message, [message.buffer]); - }catch(e){ - node.loaded = false; - node.loading = false; - Potree.numNodesLoading--; + Potree.GeoPackageLoader.loadUrl(path, params).then(data => { + viewer.scene.addGeopackage(data); + }); + - console.log(`failed to load ${node.name}`); - console.log(e); - console.log(`trying again!`); - } + } + + function loadSettings(viewer, data){ + if(!data){ + return; } - parseHierarchy(node, buffer){ + viewer.setPointBudget(data.pointBudget); + viewer.setFOV(data.fov); + viewer.setEDLEnabled(data.edlEnabled); + viewer.setEDLRadius(data.edlRadius); + viewer.setEDLStrength(data.edlStrength); + viewer.setBackground(data.background); + viewer.setMinNodeSize(data.minNodeSize); + viewer.setShowBoundingBox(data.showBoundingBoxes); + } - let view = new DataView(buffer); - let tStart = performance.now(); + function loadView(viewer, view){ + viewer.scene.view.position.set(...view.position); + viewer.scene.view.lookAt(...view.target); + } - let bytesPerNode = 22; - let numNodes = buffer.byteLength / bytesPerNode; + function loadAnnotationItem(item){ - let octree = node.octreeGeometry; - // let nodes = [node]; - let nodes = new Array(numNodes); - nodes[0] = node; - let nodePos = 1; + const annotation = new Annotation({ + position: item.position, + title: item.title, + cameraPosition: item.cameraPosition, + cameraTarget: item.cameraTarget, + }); - for(let i = 0; i < numNodes; i++){ - let current = nodes[i]; - let type = view.getUint8(i * bytesPerNode + 0); - let childMask = view.getUint8(i * bytesPerNode + 1); - let numPoints = view.getUint32(i * bytesPerNode + 2, true); - let byteOffset = view.getBigInt64(i * bytesPerNode + 6, true); - let byteSize = view.getBigInt64(i * bytesPerNode + 14, true); + annotation.description = item.description; + annotation.uuid = item.uuid; - // if(byteSize === 0n){ - // // debugger; - // } + if(item.offset){ + annotation.offset.set(...item.offset); + } + return annotation; + } - if(current.nodeType === 2){ - // replace proxy with real node - current.byteOffset = byteOffset; - current.byteSize = byteSize; - current.numPoints = numPoints; - }else if(type === 2){ - // load proxy - current.hierarchyByteOffset = byteOffset; - current.hierarchyByteSize = byteSize; - current.numPoints = numPoints; - }else { - // load real node - current.byteOffset = byteOffset; - current.byteSize = byteSize; - current.numPoints = numPoints; - } - - current.nodeType = type; + function loadAnnotations(viewer, data){ - if(current.nodeType === 2){ - continue; - } + if(!data){ + return; + } - for(let childIndex = 0; childIndex < 8; childIndex++){ - let childExists = ((1 << childIndex) & childMask) !== 0; + const findDuplicate = (item) => { - if(!childExists){ - continue; - } + let duplicate = null; - let childName = current.name + childIndex; + viewer.scene.annotations.traverse( a => { + if(a.uuid === item.uuid){ + duplicate = a; + } + }); - let childAABB = createChildAABB(current.boundingBox, childIndex); - let child = new OctreeGeometryNode(childName, octree, childAABB); - child.name = childName; - child.spacing = current.spacing / 2; - child.level = current.level + 1; + return duplicate; + }; - current.children[childIndex] = child; - child.parent = current; + const traverse = (item, parent) => { - // nodes.push(child); - nodes[nodePos] = child; - nodePos++; - } + const duplicate = findDuplicate(item); + if(duplicate){ + return; + } - // if((i % 500) === 0){ - // yield; - // } + const annotation = loadAnnotationItem(item); + + for(const childItem of item.children){ + traverse(childItem, annotation); } - let duration = (performance.now() - tStart); + parent.add(annotation); - // if(duration > 20){ - // let msg = `duration: ${duration}ms, numNodes: ${numNodes}`; - // console.log(msg); - // } + }; + + for(const item of data){ + traverse(item, viewer.scene.annotations); } - async loadHierarchy(node){ + } - let {hierarchyByteOffset, hierarchyByteSize} = node; - let hierarchyPath = `${this.url}/../hierarchy.bin`; - - let first = hierarchyByteOffset; - let last = first + hierarchyByteSize - 1n; + function loadProfile(viewer, data){ + + const {name, points} = data; - let response = await fetch(hierarchyPath, { - headers: { - 'content-type': 'multipart/byteranges', - 'Range': `bytes=${first}-${last}`, - }, - }); + const duplicate = viewer.scene.profiles.find(profile => profile.uuid === data.uuid); + if(duplicate){ + return; + } + let profile = new Potree.Profile(); + profile.name = name; + profile.uuid = data.uuid; + profile.setWidth(data.width); - let buffer = await response.arrayBuffer(); - - this.parseHierarchy(node, buffer); + for(const point of points){ + profile.addMarker(new Vector3(...point)); + } + + viewer.scene.addProfile(profile); + } - // let promise = new Promise((resolve) => { - // let generator = this.parseHierarchy(node, buffer); + function loadClassification(viewer, data){ + if(!data){ + return; + } - // let repeatUntilDone = () => { - // let result = generator.next(); + const classifications = data; - // if(result.done){ - // resolve(); - // }else{ - // requestAnimationFrame(repeatUntilDone); - // } - // }; - - // repeatUntilDone(); - // }); + viewer.setClassifications(classifications); + } - // await promise; + async function loadProject(viewer, data){ - + if(data.type !== "Potree"){ + console.error("not a valid Potree project"); + return; + } + loadSettings(viewer, data.settings); + loadView(viewer, data.view); + const pointcloudPromises = []; + for(const pointcloud of data.pointclouds){ + const promise = loadPointCloud(viewer, pointcloud); + pointcloudPromises.push(promise); } - } + for(const measure of data.measurements){ + loadMeasurement(viewer, measure); + } - let tmpVec3 = new Vector3(); - function createChildAABB(aabb, index){ - let min = aabb.min.clone(); - let max = aabb.max.clone(); - let size = tmpVec3.subVectors(max, min); + for(const volume of data.volumes){ + loadVolume(viewer, volume); + } - if ((index & 0b0001) > 0) { - min.z += size.z / 2; - } else { - max.z -= size.z / 2; + for(const animation of data.cameraAnimations){ + loadCameraAnimation(viewer, animation); } - if ((index & 0b0010) > 0) { - min.y += size.y / 2; - } else { - max.y -= size.y / 2; + for(const profile of data.profiles){ + loadProfile(viewer, profile); } - - if ((index & 0b0100) > 0) { - min.x += size.x / 2; - } else { - max.x -= size.x / 2; + + if(data.orientedImages){ + for(const images of data.orientedImages){ + loadOrientedImages(viewer, images); + } } - return new Box3(min, max); - } + loadAnnotations(viewer, data.annotations); - let typenameTypeattributeMap = { - "double": PointAttributeTypes.DATA_TYPE_DOUBLE, - "float": PointAttributeTypes.DATA_TYPE_FLOAT, - "int8": PointAttributeTypes.DATA_TYPE_INT8, - "uint8": PointAttributeTypes.DATA_TYPE_UINT8, - "int16": PointAttributeTypes.DATA_TYPE_INT16, - "uint16": PointAttributeTypes.DATA_TYPE_UINT16, - "int32": PointAttributeTypes.DATA_TYPE_INT32, - "uint32": PointAttributeTypes.DATA_TYPE_UINT32, - "int64": PointAttributeTypes.DATA_TYPE_INT64, - "uint64": PointAttributeTypes.DATA_TYPE_UINT64, - }; + loadClassification(viewer, data.classification); - class OctreeLoader{ + // need to load at least one point cloud that defines the scene projection, + // before we can load stuff in other projections such as geopackages + //await Promise.any(pointcloudPromises); // (not yet supported) + Utils.waitAny(pointcloudPromises).then( () => { + if(data.geopackages){ + for(const geopackage of data.geopackages){ + loadGeopackage(viewer, geopackage); + } + } + }); - static parseAttributes(jsonAttributes){ + await Promise.all(pointcloudPromises); + } - let attributes = new PointAttributes(); + // + // Algorithm by Christian Boucheny + // shader code taken and adapted from CloudCompare + // + // see + // https://github.com/cloudcompare/trunk/tree/master/plugins/qEDL/shaders/EDL + // http://www.kitware.com/source/home/post/9 + // https://tel.archives-ouvertes.fr/tel-00438464/document p. 115+ (french) - let replacements = { - "rgb": "rgba", + class EyeDomeLightingMaterial extends RawShaderMaterial{ + + constructor(parameters = {}){ + super(); + + let uniforms = { + screenWidth: { type: 'f', value: 0 }, + screenHeight: { type: 'f', value: 0 }, + edlStrength: { type: 'f', value: 1.0 }, + uNear: { type: 'f', value: 1.0 }, + uFar: { type: 'f', value: 1.0 }, + radius: { type: 'f', value: 1.0 }, + neighbours: { type: '2fv', value: [] }, + depthMap: { type: 't', value: null }, + uEDLColor: { type: 't', value: null }, + uEDLDepth: { type: 't', value: null }, + opacity: { type: 'f', value: 1.0 }, + uProj: { type: "Matrix4fv", value: [] }, }; - for (const jsonAttribute of jsonAttributes) { - let {name, description, size, numElements, elementSize, min, max} = jsonAttribute; + this.setValues({ + uniforms: uniforms, + vertexShader: this.getDefines() + Shaders['edl.vs'], + fragmentShader: this.getDefines() + Shaders['edl.fs'], + lights: false + }); - let type = typenameTypeattributeMap[jsonAttribute.type]; + this.neighbourCount = 8; + } - let potreeAttributeName = replacements[name] ? replacements[name] : name; + getDefines() { + let defines = ''; - let attribute = new PointAttribute(potreeAttributeName, type, numElements); + defines += '#define NEIGHBOUR_COUNT ' + this.neighbourCount + '\n'; - if(numElements === 1){ - attribute.range = [min[0], max[0]]; - }else { - attribute.range = [min, max]; - } + return defines; + } - if (name === "gps-time") { // HACK: Guard against bad gpsTime range in metadata, see potree/potree#909 - if (attribute.range[0] === attribute.range[1]) { - attribute.range[1] += 1; - } - } + updateShaderSource() { - attribute.initialRange = attribute.range; + let vs = this.getDefines() + Shaders['edl.vs']; + let fs = this.getDefines() + Shaders['edl.fs']; - attributes.add(attribute); - } + this.setValues({ + vertexShader: vs, + fragmentShader: fs + }); - { - // check if it has normals - let hasNormals = - attributes.attributes.find(a => a.name === "NormalX") !== undefined && - attributes.attributes.find(a => a.name === "NormalY") !== undefined && - attributes.attributes.find(a => a.name === "NormalZ") !== undefined; + this.uniforms.neighbours.value = this.neighbours; - if(hasNormals){ - let vector = { - name: "NORMAL", - attributes: ["NormalX", "NormalY", "NormalZ"], - }; - attributes.addVector(vector); - } - } + this.needsUpdate = true; + } - return attributes; + get neighbourCount(){ + return this._neighbourCount; } - static async load(url){ + set neighbourCount(value){ + if (this._neighbourCount !== value) { + this._neighbourCount = value; + this.neighbours = new Float32Array(this._neighbourCount * 2); + for (let c = 0; c < this._neighbourCount; c++) { + this.neighbours[2 * c + 0] = Math.cos(2 * c * Math.PI / this._neighbourCount); + this.neighbours[2 * c + 1] = Math.sin(2 * c * Math.PI / this._neighbourCount); + } - let response = await fetch(url); - let metadata = await response.json(); + this.updateShaderSource(); + } + } - let attributes = OctreeLoader.parseAttributes(metadata.attributes); + + } - let loader = new NodeLoader(url); - loader.metadata = metadata; - loader.attributes = attributes; - loader.scale = metadata.scale; - loader.offset = metadata.offset; + class NormalizationEDLMaterial extends RawShaderMaterial{ - let octree = new OctreeGeometry(); - octree.url = url; - octree.spacing = metadata.spacing; - octree.scale = metadata.scale; + constructor(parameters = {}){ + super(); - // let aPosition = metadata.attributes.find(a => a.name === "position"); - // octree + let uniforms = { + screenWidth: { type: 'f', value: 0 }, + screenHeight: { type: 'f', value: 0 }, + edlStrength: { type: 'f', value: 1.0 }, + radius: { type: 'f', value: 1.0 }, + neighbours: { type: '2fv', value: [] }, + uEDLMap: { type: 't', value: null }, + uDepthMap: { type: 't', value: null }, + uWeightMap: { type: 't', value: null }, + }; - let min = new Vector3(...metadata.boundingBox.min); - let max = new Vector3(...metadata.boundingBox.max); - let boundingBox = new Box3(min, max); + this.setValues({ + uniforms: uniforms, + vertexShader: this.getDefines() + Shaders['normalize.vs'], + fragmentShader: this.getDefines() + Shaders['normalize_and_edl.fs'], + }); - let offset = min.clone(); - boundingBox.min.sub(offset); - boundingBox.max.sub(offset); + this.neighbourCount = 8; + } - octree.projection = metadata.projection; - octree.boundingBox = boundingBox; - octree.tightBoundingBox = boundingBox.clone(); - octree.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); - octree.tightBoundingSphere = boundingBox.getBoundingSphere(new Sphere()); - octree.offset = offset; - octree.pointAttributes = OctreeLoader.parseAttributes(metadata.attributes); - octree.loader = loader; + getDefines() { + let defines = ''; - let root = new OctreeGeometryNode("r", octree, boundingBox); - root.level = 0; - root.nodeType = 2; - root.hierarchyByteOffset = 0n; - root.hierarchyByteSize = BigInt(metadata.hierarchy.firstChunkSize); - root.hasChildren = false; - root.spacing = octree.spacing; - root.byteOffset = 0; + defines += '#define NEIGHBOUR_COUNT ' + this.neighbourCount + '\n'; - octree.root = root; + return defines; + } - loader.load(root); + updateShaderSource() { - let result = { - geometry: octree, - }; + let vs = this.getDefines() + Shaders['normalize.vs']; + let fs = this.getDefines() + Shaders['normalize_and_edl.fs']; - return result; + this.setValues({ + vertexShader: vs, + fragmentShader: fs + }); - } + this.uniforms.neighbours.value = this.neighbours; - }; + this.needsUpdate = true; + } - /** - * @author Connor Manning - */ + get neighbourCount(){ + return this._neighbourCount; + } - class EptLoader { - static async load(file, callback) { - - let response = await fetch(file); - let json = await response.json(); - - let url = file.substr(0, file.lastIndexOf('ept.json')); - let geometry = new Potree.PointCloudEptGeometry(url, json); - let root = new Potree.PointCloudEptGeometryNode(geometry); - - geometry.root = root; - geometry.root.load(); - - callback(geometry); - } - }; - - class EptBinaryLoader { - extension() { - return '.bin'; - } - - workerPath() { - return Potree.scriptPath + '/workers/EptBinaryDecoderWorker.js'; - } - - load(node) { - if (node.loaded) return; - - let url = node.url() + this.extension(); - - let xhr = XHRFactory.createXMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.overrideMimeType('text/plain; charset=x-user-defined'); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - let buffer = xhr.response; - this.parse(node, buffer); - } else { - console.log('Failed ' + url + ': ' + xhr.status); - } + set neighbourCount(value){ + if (this._neighbourCount !== value) { + this._neighbourCount = value; + this.neighbours = new Float32Array(this._neighbourCount * 2); + for (let c = 0; c < this._neighbourCount; c++) { + this.neighbours[2 * c + 0] = Math.cos(2 * c * Math.PI / this._neighbourCount); + this.neighbours[2 * c + 1] = Math.sin(2 * c * Math.PI / this._neighbourCount); } - }; - try { - xhr.send(null); - } - catch (e) { - console.log('Failed request: ' + e); + this.updateShaderSource(); } } + + } - parse(node, buffer) { - let workerPath = this.workerPath(); - let worker = Potree.workerPool.getWorker(workerPath); - - worker.onmessage = function(e) { - let g = new BufferGeometry(); - let numPoints = e.data.numPoints; + class NormalizationMaterial extends RawShaderMaterial{ - let position = new Float32Array(e.data.position); - g.setAttribute('position', new BufferAttribute(position, 3)); + constructor(parameters = {}){ + super(); - let indices = new Uint8Array(e.data.indices); - g.setAttribute('indices', new BufferAttribute(indices, 4)); + let uniforms = { + uDepthMap: { type: 't', value: null }, + uWeightMap: { type: 't', value: null }, + }; - if (e.data.color) { - let color = new Uint8Array(e.data.color); - g.setAttribute('color', new BufferAttribute(color, 4, true)); - } - if (e.data.intensity) { - let intensity = new Float32Array(e.data.intensity); - g.setAttribute('intensity', - new BufferAttribute(intensity, 1)); - } - if (e.data.classification) { - let classification = new Uint8Array(e.data.classification); - g.setAttribute('classification', - new BufferAttribute(classification, 1)); - } - if (e.data.returnNumber) { - let returnNumber = new Uint8Array(e.data.returnNumber); - g.setAttribute('return number', - new BufferAttribute(returnNumber, 1)); - } - if (e.data.numberOfReturns) { - let numberOfReturns = new Uint8Array(e.data.numberOfReturns); - g.setAttribute('number of returns', - new BufferAttribute(numberOfReturns, 1)); - } - if (e.data.pointSourceId) { - let pointSourceId = new Uint16Array(e.data.pointSourceId); - g.setAttribute('source id', - new BufferAttribute(pointSourceId, 1)); - } + this.setValues({ + uniforms: uniforms, + vertexShader: this.getDefines() + Shaders['normalize.vs'], + fragmentShader: this.getDefines() + Shaders['normalize.fs'], + }); + } - g.attributes.indices.normalized = true; + getDefines() { + let defines = ''; - let tightBoundingBox = new Box3( - new Vector3().fromArray(e.data.tightBoundingBox.min), - new Vector3().fromArray(e.data.tightBoundingBox.max) - ); + return defines; + } - node.doneLoading( - g, - tightBoundingBox, - numPoints, - new Vector3(...e.data.mean)); + updateShaderSource() { - Potree.workerPool.returnWorker(workerPath, worker); - }; + let vs = this.getDefines() + Shaders['normalize.vs']; + let fs = this.getDefines() + Shaders['normalize.fs']; - let toArray = (v) => [v.x, v.y, v.z]; - let message = { - buffer: buffer, - schema: node.ept.schema, - scale: node.ept.eptScale, - offset: node.ept.eptOffset, - mins: toArray(node.key.b.min) - }; + this.setValues({ + vertexShader: vs, + fragmentShader: fs + }); - worker.postMessage(message, [message.buffer]); + this.needsUpdate = true; } - }; + + } /** * laslaz code taken and adapted from plas.io js-laslaz - * http://plas.io/ - * https://github.com/verma/plasio + * http://plas.io/ + * https://github.com/verma/plasio * * Thanks to Uday Verma and Howard Butler * */ - class EptLaszipLoader { - load(node) { - if (node.loaded) return; + class LasLazLoader { - let url = node.url() + '.laz'; + constructor (version, extension) { + if (typeof (version) === 'string') { + this.version = new Version(version); + } else { + this.version = version; + } + + this.extension = extension; + } + + static progressCB () { + + } + + load (node) { + if (node.loaded) { + return; + } + + let url = node.getURL(); + + if (this.version.equalOrHigher('1.4')) { + url += `.${this.extension}`; + } let xhr = XHRFactory.createXMLHttpRequest(); xhr.open('GET', url, true); @@ -66941,11 +65696,11 @@ void main() { xhr.overrideMimeType('text/plain; charset=x-user-defined'); xhr.onreadystatechange = () => { if (xhr.readyState === 4) { - if (xhr.status === 200) { + if (xhr.status === 200 || xhr.status === 0) { let buffer = xhr.response; this.parse(node, buffer); } else { - console.log('Failed ' + url + ': ' + xhr.status); + console.log('Failed to load file! HTTP status: ' + xhr.status + ', file: ' + url); } } }; @@ -66955,3378 +65710,5264 @@ void main() { async parse(node, buffer){ let lf = new LASFile(buffer); - let handler = new EptLazBatcher(node); + let handler = new LasLazBatcher(node); try{ - await lf.open(); + await lf.open(); + lf.isOpen = true; + }catch(e){ + console.log("failed to open file. :("); - lf.isOpen = true; + return; + } - const header = await lf.getHeader(); + let header = await lf.getHeader(); - { - let i = 0; + let skip = 1; + let totalRead = 0; + let totalToRead = (skip <= 1 ? header.pointsCount : header.pointsCount / skip); - let toArray = (v) => [v.x, v.y, v.z]; - let mins = toArray(node.key.b.min); - let maxs = toArray(node.key.b.max); + let hasMoreData = true; - let hasMoreData = true; + while(hasMoreData){ + let data = await lf.readData(1000 * 1000, 0, skip); - while(hasMoreData){ - const data = await lf.readData(1000000, 0, 1); + handler.push(new LASDecoder(data.buffer, + header.pointsFormatId, + header.pointsStructSize, + data.count, + header.scale, + header.offset, + header.mins, header.maxs)); - let d = new LASDecoder( - data.buffer, - header.pointsFormatId, - header.pointsStructSize, - data.count, - header.scale, - header.offset, - mins, - maxs); + totalRead += data.count; + LasLazLoader.progressCB(totalRead / totalToRead); - d.extraBytes = header.extraBytes; - d.pointsFormatId = header.pointsFormatId; - handler.push(d); + hasMoreData = data.hasMoreData; + } - i += data.count; + header.totalRead = totalRead; + header.versionAsString = lf.versionAsString; + header.isCompressed = lf.isCompressed; - hasMoreData = data.hasMoreData; - } + LasLazLoader.progressCB(1); - header.totalRead = i; - header.versionAsString = lf.versionAsString; - header.isCompressed = lf.isCompressed; + try{ + await lf.close(); - await lf.close(); + lf.isOpen = false; + }catch(e){ + console.error("failed to close las/laz file!!!"); + + throw e; + } + } - lf.isOpen = false; - } + handle (node, url) { - }catch(err){ - console.error('Error reading LAZ:', err); - - if (lf.isOpen) { - await lf.close(); - - lf.isOpen = false; - } - - throw err; - } } }; - class EptLazBatcher { - constructor(node) { this.node = node; } + class LasLazBatcher{ - push(las) { - let workerPath = Potree.scriptPath + - '/workers/EptLaszipDecoderWorker.js'; - let worker = Potree.workerPool.getWorker(workerPath); + constructor (node) { + this.node = node; + } + + push (lasBuffer) { + const workerPath = Potree.scriptPath + '/workers/LASDecoderWorker.js'; + const worker = Potree.workerPool.getWorker(workerPath); + const node = this.node; + const pointAttributes = node.pcoGeometry.pointAttributes; worker.onmessage = (e) => { - let g = new BufferGeometry(); - let numPoints = las.pointsCount; + let geometry = new BufferGeometry(); + let numPoints = lasBuffer.pointsCount; let positions = new Float32Array(e.data.position); let colors = new Uint8Array(e.data.color); - let intensities = new Float32Array(e.data.intensity); let classifications = new Uint8Array(e.data.classification); let returnNumbers = new Uint8Array(e.data.returnNumber); let numberOfReturns = new Uint8Array(e.data.numberOfReturns); let pointSourceIDs = new Uint16Array(e.data.pointSourceID); let indices = new Uint8Array(e.data.indices); - let gpsTime = new Float32Array(e.data.gpsTime); - g.setAttribute('position', - new BufferAttribute(positions, 3)); - g.setAttribute('rgba', - new BufferAttribute(colors, 4, true)); - g.setAttribute('intensity', - new BufferAttribute(intensities, 1)); - g.setAttribute('classification', - new BufferAttribute(classifications, 1)); - g.setAttribute('return number', - new BufferAttribute(returnNumbers, 1)); - g.setAttribute('number of returns', - new BufferAttribute(numberOfReturns, 1)); - g.setAttribute('source id', - new BufferAttribute(pointSourceIDs, 1)); - g.setAttribute('indices', - new BufferAttribute(indices, 4)); - g.setAttribute('gpsTime', - new BufferAttribute(gpsTime, 1)); - this.node.gpsTime = e.data.gpsMeta; + geometry.setAttribute('position', new BufferAttribute(positions, 3)); + geometry.setAttribute('color', new BufferAttribute(colors, 4, true)); + geometry.setAttribute('intensity', new BufferAttribute(intensities, 1)); + geometry.setAttribute('classification', new BufferAttribute(classifications, 1)); + geometry.setAttribute('return number', new BufferAttribute(returnNumbers, 1)); + geometry.setAttribute('number of returns', new BufferAttribute(numberOfReturns, 1)); + geometry.setAttribute('source id', new BufferAttribute(pointSourceIDs, 1)); + geometry.setAttribute('indices', new BufferAttribute(indices, 4)); + geometry.attributes.indices.normalized = true; - g.attributes.indices.normalized = true; + for(const key in e.data.ranges){ + const range = e.data.ranges[key]; + + const attribute = pointAttributes.attributes.find(a => a.name === key); + attribute.range[0] = Math.min(attribute.range[0], range[0]); + attribute.range[1] = Math.max(attribute.range[1], range[1]); + } let tightBoundingBox = new Box3( new Vector3().fromArray(e.data.tightBoundingBox.min), new Vector3().fromArray(e.data.tightBoundingBox.max) ); - this.node.doneLoading( - g, - tightBoundingBox, - numPoints, - new Vector3(...e.data.mean)); + geometry.boundingBox = this.node.boundingBox; + this.node.tightBoundingBox = tightBoundingBox; + + this.node.geometry = geometry; + this.node.numPoints = numPoints; + this.node.loaded = true; + this.node.loading = false; + Potree.numNodesLoading--; + this.node.mean = new Vector3(...e.data.mean); Potree.workerPool.returnWorker(workerPath, worker); }; let message = { - buffer: las.arrayb, - numPoints: las.pointsCount, - pointSize: las.pointSize, - pointFormatID: las.pointsFormatId, - scale: las.scale, - offset: las.offset, - mins: las.mins, - maxs: las.maxs + buffer: lasBuffer.arrayb, + numPoints: lasBuffer.pointsCount, + pointSize: lasBuffer.pointSize, + pointFormatID: 2, + scale: lasBuffer.scale, + offset: lasBuffer.offset, + mins: lasBuffer.mins, + maxs: lasBuffer.maxs }; - worker.postMessage(message, [message.buffer]); }; - }; - - class EptZstandardLoader extends EptBinaryLoader { - extension() { - return '.zst'; - } + } - workerPath() { - return Potree.scriptPath + '/workers/EptZstandardDecoderWorker.js'; - } - }; + class BinaryLoader{ - class ShapefileLoader{ + constructor(version, boundingBox, scale){ + if (typeof (version) === 'string') { + this.version = new Version(version); + } else { + this.version = version; + } - constructor(){ - this.transform = null; + this.boundingBox = boundingBox; + this.scale = scale; } - async load(path){ + load(node){ + if (node.loaded) { + return; + } - const matLine = new LineMaterial( { - color: 0xff0000, - linewidth: 3, // in pixels - resolution: new Vector2(1000, 1000), - dashed: false - } ); + let url = node.getURL(); - const features = await this.loadShapefileFeatures(path); - const node = new Object3D(); - - for(const feature of features){ - const fnode = this.featureToSceneNode(feature, matLine); - node.add(fnode); + if (this.version.equalOrHigher('1.4')) { + url += '.bin'; } - let setResolution = (x, y) => { - matLine.resolution.set(x, y); + let xhr = XHRFactory.createXMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if((xhr.status === 200 || xhr.status === 0) && xhr.response !== null){ + let buffer = xhr.response; + this.parse(node, buffer); + } else { + //console.error(`Failed to load file! HTTP status: ${xhr.status}, file: ${url}`); + throw new Error(`Failed to load file! HTTP status: ${xhr.status}, file: ${url}`); + } + } }; + + try { + xhr.send(null); + } catch (e) { + console.log('fehler beim laden der punktwolke: ' + e); + } + }; - const result = { - features: features, - node: node, - setResolution: setResolution, - }; + parse(node, buffer){ + let pointAttributes = node.pcoGeometry.pointAttributes; + let numPoints = buffer.byteLength / node.pcoGeometry.pointAttributes.byteSize; - return result; - } + if (this.version.upTo('1.5')) { + node.numPoints = numPoints; + } - featureToSceneNode(feature, matLine){ - let geometry = feature.geometry; - - let color = new Color(1, 1, 1); + let workerPath = Potree.scriptPath + '/workers/BinaryDecoderWorker.js'; + let worker = Potree.workerPool.getWorker(workerPath); - let transform = this.transform; - if(transform === null){ - transform = {forward: (v) => v}; - } - - if(feature.geometry.type === "Point"){ - let sg = new SphereGeometry(1, 18, 18); - let sm = new MeshNormalMaterial(); - let s = new Mesh(sg, sm); - - let [long, lat] = geometry.coordinates; - let pos = transform.forward([long, lat]); - - s.position.set(...pos, 20); - - s.scale.set(10, 10, 10); - - return s; - }else if(geometry.type === "LineString"){ - let coordinates = []; - - let min = new Vector3(Infinity, Infinity, Infinity); - for(let i = 0; i < geometry.coordinates.length; i++){ - let [long, lat] = geometry.coordinates[i]; - let pos = transform.forward([long, lat]); - - min.x = Math.min(min.x, pos[0]); - min.y = Math.min(min.y, pos[1]); - min.z = Math.min(min.z, 20); - - coordinates.push(...pos, 20); - if(i > 0 && i < geometry.coordinates.length - 1){ - coordinates.push(...pos, 20); - } - } - - for(let i = 0; i < coordinates.length; i += 3){ - coordinates[i+0] -= min.x; - coordinates[i+1] -= min.y; - coordinates[i+2] -= min.z; - } - - const lineGeometry = new LineGeometry(); - lineGeometry.setPositions( coordinates ); + worker.onmessage = function (e) { - const line = new Line2( lineGeometry, matLine ); - line.computeLineDistances(); - line.scale.set( 1, 1, 1 ); - line.position.copy(min); - - return line; - }else if(geometry.type === "Polygon"){ - for(let pc of geometry.coordinates){ - let coordinates = []; - - let min = new Vector3(Infinity, Infinity, Infinity); - for(let i = 0; i < pc.length; i++){ - let [long, lat] = pc[i]; - let pos = transform.forward([long, lat]); - - min.x = Math.min(min.x, pos[0]); - min.y = Math.min(min.y, pos[1]); - min.z = Math.min(min.z, 20); - - coordinates.push(...pos, 20); - if(i > 0 && i < pc.length - 1){ - coordinates.push(...pos, 20); - } - } - - for(let i = 0; i < coordinates.length; i += 3){ - coordinates[i+0] -= min.x; - coordinates[i+1] -= min.y; - coordinates[i+2] -= min.z; - } + let data = e.data; + let buffers = data.attributeBuffers; + let tightBoundingBox = new Box3( + new Vector3().fromArray(data.tightBoundingBox.min), + new Vector3().fromArray(data.tightBoundingBox.max) + ); - const lineGeometry = new LineGeometry(); - lineGeometry.setPositions( coordinates ); + Potree.workerPool.returnWorker(workerPath, worker); - const line = new Line2( lineGeometry, matLine ); - line.computeLineDistances(); - line.scale.set( 1, 1, 1 ); - line.position.copy(min); - - return line; - } - }else { - console.log("unhandled feature: ", feature); - } - } + let geometry = new BufferGeometry(); - async loadShapefileFeatures(file){ - let features = []; + for(let property in buffers){ + let buffer = buffers[property].buffer; + let batchAttribute = buffers[property].attribute; - let source = await shapefile.open(file); + if (property === "POSITION_CARTESIAN") { + geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3)); + } else if (property === "rgba") { + geometry.setAttribute("rgba", new BufferAttribute(new Uint8Array(buffer), 4, true)); + } else if (property === "NORMAL_SPHEREMAPPED") { + geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); + } else if (property === "NORMAL_OCT16") { + geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); + } else if (property === "NORMAL") { + geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); + } else if (property === "INDICES") { + let bufferAttribute = new BufferAttribute(new Uint8Array(buffer), 4); + bufferAttribute.normalized = true; + geometry.setAttribute('indices', bufferAttribute); + } else if (property === "SPACING") { + let bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1); + geometry.setAttribute('spacing', bufferAttribute); + } else { + const bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1); - while(true){ - let result = await source.read(); + bufferAttribute.potree = { + offset: buffers[property].offset, + scale: buffers[property].scale, + preciseBuffer: buffers[property].preciseBuffer, + range: batchAttribute.range, + }; - if (result.done) { - break; - } + geometry.setAttribute(property, bufferAttribute); - if (result.value && result.value.type === 'Feature' && result.value.geometry !== undefined) { - features.push(result.value); - } - } + const attribute = pointAttributes.attributes.find(a => a.name === batchAttribute.name); + attribute.range[0] = Math.min(attribute.range[0], batchAttribute.range[0]); + attribute.range[1] = Math.max(attribute.range[1], batchAttribute.range[1]); - return features; - } + if(node.getLevel() === 0){ + attribute.initialRange = batchAttribute.range; + } - }; + } + } - const defaultColors = { - "landuse": [0.5, 0.5, 0.5], - "natural": [0.0, 1.0, 0.0], - "places": [1.0, 0.0, 1.0], - "points": [0.0, 1.0, 1.0], - "roads": [1.0, 1.0, 0.0], - "waterways": [0.0, 0.0, 1.0], - "default": [0.9, 0.6, 0.1], - }; + tightBoundingBox.max.sub(tightBoundingBox.min); + tightBoundingBox.min.set(0, 0, 0); - function getColor(feature){ - let color = defaultColors[feature]; + let numPoints = e.data.buffer.byteLength / pointAttributes.byteSize; + + node.numPoints = numPoints; + node.geometry = geometry; + node.mean = new Vector3(...data.mean); + node.tightBoundingBox = tightBoundingBox; + node.loaded = true; + node.loading = false; + node.estimatedSpacing = data.estimatedSpacing; + Potree.numNodesLoading--; + }; - if(!color){ - color = defaultColors["default"]; - } + let message = { + buffer: buffer, + pointAttributes: pointAttributes, + version: this.version.version, + min: [ node.boundingBox.min.x, node.boundingBox.min.y, node.boundingBox.min.z ], + offset: [node.pcoGeometry.offset.x, node.pcoGeometry.offset.y, node.pcoGeometry.offset.z], + scale: this.scale, + spacing: node.spacing, + hasChildren: node.hasChildren, + name: node.name + }; + worker.postMessage(message, [message.buffer]); + }; - return color; + } - class Geopackage$1{ - constructor(){ - this.path = null; - this.node = null; - } - }; - - class GeoPackageLoader{ + function parseAttributes(cloudjs){ - constructor(){ + let version = new Version(cloudjs.version); - } + const replacements = { + "COLOR_PACKED": "rgba", + "RGBA": "rgba", + "INTENSITY": "intensity", + "CLASSIFICATION": "classification", + "GPS_TIME": "gps-time", + }; - static async loadUrl(url, params){ + const replaceOldNames = (old) => { + if(replacements[old]){ + return replacements[old]; + }else { + return old; + } + }; - await Promise.all([ - Utils.loadScript(`${Potree.scriptPath}/lazylibs/geopackage/geopackage.js`), - Utils.loadScript(`${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.js`), - ]); + const pointAttributes = []; + if(version.upTo('1.7')){ - const result = await fetch(url); - const buffer = await result.arrayBuffer(); + for(let attributeName of cloudjs.pointAttributes){ + const oldAttribute = PointAttribute[attributeName]; - params = params || {}; + const attribute = { + name: oldAttribute.name, + size: oldAttribute.byteSize, + elements: oldAttribute.numElements, + elementSize: oldAttribute.byteSize / oldAttribute.numElements, + type: oldAttribute.type.name, + description: "", + }; - params.source = url; + pointAttributes.push(attribute); + } - return GeoPackageLoader.loadBuffer(buffer, params); + }else { + pointAttributes.push(...cloudjs.pointAttributes); } - static async loadBuffer(buffer, params){ - await Promise.all([ - Utils.loadScript(`${Potree.scriptPath}/lazylibs/geopackage/geopackage.js`), - Utils.loadScript(`${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.js`), - ]); + { + const attributes = new PointAttributes(); - params = params || {}; + const typeConversion = { + int8: PointAttributeTypes.DATA_TYPE_INT8, + int16: PointAttributeTypes.DATA_TYPE_INT16, + int32: PointAttributeTypes.DATA_TYPE_INT32, + int64: PointAttributeTypes.DATA_TYPE_INT64, + uint8: PointAttributeTypes.DATA_TYPE_UINT8, + uint16: PointAttributeTypes.DATA_TYPE_UINT16, + uint32: PointAttributeTypes.DATA_TYPE_UINT32, + uint64: PointAttributeTypes.DATA_TYPE_UINT64, + double: PointAttributeTypes.DATA_TYPE_DOUBLE, + float: PointAttributeTypes.DATA_TYPE_FLOAT, + }; - const resolver = async (resolve) => { - - let transform = params.transform; - if(!transform){ - transform = {forward: (arg) => arg}; - } + for(const jsAttribute of pointAttributes){ + const name = replaceOldNames(jsAttribute.name); + const type = typeConversion[jsAttribute.type]; + const numElements = jsAttribute.elements; + const description = jsAttribute.description; - const wasmPath = `${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.wasm`; - const SQL = await initSqlJs({ locateFile: filename => wasmPath}); + const attribute = new PointAttribute(name, type, numElements); - const u8 = new Uint8Array(buffer); + attributes.add(attribute); + } - const data = await geopackage.open(u8); - window.data = data; + { + // check if it has normals + let hasNormals = + pointAttributes.find(a => a.name === "NormalX") !== undefined && + pointAttributes.find(a => a.name === "NormalY") !== undefined && + pointAttributes.find(a => a.name === "NormalZ") !== undefined; - const geopackageNode = new Object3D(); - geopackageNode.name = params.source; - geopackageNode.potree = { - source: params.source, - }; + if(hasNormals){ + let vector = { + name: "NORMAL", + attributes: ["NormalX", "NormalY", "NormalZ"], + }; + attributes.addVector(vector); + } + } - const geo = new Geopackage$1(); - geo.path = params.source; - geo.node = geopackageNode; + return attributes; + } - const tables = data.getTables(); + } - for(const table of tables.features){ - const dao = data.getFeatureDao(table); + function lasLazAttributes(fMno){ + const attributes = new PointAttributes(); - let boundingBox = dao.getBoundingBox(); - boundingBox = boundingBox.projectBoundingBox(dao.projection, 'EPSG:4326'); - const geoJson = data.queryForGeoJSONFeaturesInTable(table, boundingBox); + attributes.add(PointAttribute.POSITION_CARTESIAN); + attributes.add(new PointAttribute("rgba", PointAttributeTypes.DATA_TYPE_UINT8, 4)); + attributes.add(new PointAttribute("intensity", PointAttributeTypes.DATA_TYPE_UINT16, 1)); + attributes.add(new PointAttribute("classification", PointAttributeTypes.DATA_TYPE_UINT8, 1)); + attributes.add(new PointAttribute("gps-time", PointAttributeTypes.DATA_TYPE_DOUBLE, 1)); + attributes.add(new PointAttribute("number of returns", PointAttributeTypes.DATA_TYPE_UINT8, 1)); + attributes.add(new PointAttribute("return number", PointAttributeTypes.DATA_TYPE_UINT8, 1)); + attributes.add(new PointAttribute("source id", PointAttributeTypes.DATA_TYPE_UINT16, 1)); + //attributes.add(new PointAttribute("pointSourceID", PointAttributeTypes.DATA_TYPE_INT8, 4)); - const matLine = new LineMaterial( { - color: new Color().setRGB(...getColor(table)), - linewidth: 2, - resolution: new Vector2(1000, 1000), + + return attributes; + } + + class POCLoader { + + static load(url, callback){ + try { + let pco = new PointCloudOctreeGeometry(); + pco.url = url; + let xhr = XHRFactory.createXMLHttpRequest(); + xhr.open('GET', url, true); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 0)) { + let fMno = JSON.parse(xhr.responseText); + + let version = new Version(fMno.version); + + // assume octreeDir is absolute if it starts with http + if (fMno.octreeDir.indexOf('http') === 0) { + pco.octreeDir = fMno.octreeDir; + } else { + pco.octreeDir = url + '/../' + fMno.octreeDir; + } + + pco.spacing = fMno.spacing; + pco.hierarchyStepSize = fMno.hierarchyStepSize; + + pco.pointAttributes = fMno.pointAttributes; + + let min = new Vector3(fMno.boundingBox.lx, fMno.boundingBox.ly, fMno.boundingBox.lz); + let max = new Vector3(fMno.boundingBox.ux, fMno.boundingBox.uy, fMno.boundingBox.uz); + let boundingBox = new Box3(min, max); + let tightBoundingBox = boundingBox.clone(); + + if (fMno.tightBoundingBox) { + tightBoundingBox.min.copy(new Vector3(fMno.tightBoundingBox.lx, fMno.tightBoundingBox.ly, fMno.tightBoundingBox.lz)); + tightBoundingBox.max.copy(new Vector3(fMno.tightBoundingBox.ux, fMno.tightBoundingBox.uy, fMno.tightBoundingBox.uz)); + } + + let offset = min.clone(); + + boundingBox.min.sub(offset); + boundingBox.max.sub(offset); + + tightBoundingBox.min.sub(offset); + tightBoundingBox.max.sub(offset); + + pco.projection = fMno.projection; + pco.boundingBox = boundingBox; + pco.tightBoundingBox = tightBoundingBox; + pco.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); + pco.tightBoundingSphere = tightBoundingBox.getBoundingSphere(new Sphere()); + pco.offset = offset; + if (fMno.pointAttributes === 'LAS') { + pco.loader = new LasLazLoader(fMno.version, "las"); + pco.pointAttributes = lasLazAttributes(fMno); + } else if (fMno.pointAttributes === 'LAZ') { + pco.loader = new LasLazLoader(fMno.version, "laz"); + pco.pointAttributes = lasLazAttributes(fMno); + } else { + pco.loader = new BinaryLoader(fMno.version, boundingBox, fMno.scale); + pco.pointAttributes = parseAttributes(fMno); + } + + let nodes = {}; + + { // load root + let name = 'r'; + + let root = new PointCloudOctreeGeometryNode(name, pco, boundingBox); + root.level = 0; + root.hasChildren = true; + root.spacing = pco.spacing; + if (version.upTo('1.5')) { + root.numPoints = fMno.hierarchy[0][1]; + } else { + root.numPoints = 0; + } + pco.root = root; + pco.root.load(); + nodes[name] = root; + } + + // load remaining hierarchy + if (version.upTo('1.4')) { + for (let i = 1; i < fMno.hierarchy.length; i++) { + let name = fMno.hierarchy[i][0]; + let numPoints = fMno.hierarchy[i][1]; + let index = parseInt(name.charAt(name.length - 1)); + let parentName = name.substring(0, name.length - 1); + let parentNode = nodes[parentName]; + let level = name.length - 1; + //let boundingBox = POCLoader.createChildAABB(parentNode.boundingBox, index); + let boundingBox = Utils.createChildAABB(parentNode.boundingBox, index); + + let node = new PointCloudOctreeGeometryNode(name, pco, boundingBox); + node.level = level; + node.numPoints = numPoints; + node.spacing = pco.spacing / Math.pow(2, level); + parentNode.addChild(node); + nodes[name] = node; + } + } + + pco.nodes = nodes; + + callback(pco); + } + }; + + xhr.send(null); + } catch (e) { + console.log("loading failed: '" + url + "'"); + console.log(e); + + callback(); + } + } + + loadPointAttributes(mno){ + let fpa = mno.pointAttributes; + let pa = new PointAttributes(); + + for (let i = 0; i < fpa.length; i++) { + let pointAttribute = PointAttribute[fpa[i]]; + pa.add(pointAttribute); + } + + return pa; + } + + createChildAABB(aabb, index){ + let min = aabb.min.clone(); + let max = aabb.max.clone(); + let size = new Vector3().subVectors(max, min); + + if ((index & 0b0001) > 0) { + min.z += size.z / 2; + } else { + max.z -= size.z / 2; + } + + if ((index & 0b0010) > 0) { + min.y += size.y / 2; + } else { + max.y -= size.y / 2; + } + + if ((index & 0b0100) > 0) { + min.x += size.x / 2; + } else { + max.x -= size.x / 2; + } + + return new Box3(min, max); + } + } + + class OctreeGeometry{ + + constructor(){ + this.url = null; + this.spacing = 0; + this.boundingBox = null; + this.root = null; + this.pointAttributes = null; + this.loader = null; + } + + }; + + class OctreeGeometryNode{ + + constructor(name, octreeGeometry, boundingBox){ + this.id = OctreeGeometryNode.IDCount++; + this.name = name; + this.index = parseInt(name.charAt(name.length - 1)); + this.octreeGeometry = octreeGeometry; + this.boundingBox = boundingBox; + this.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); + this.children = {}; + this.numPoints = 0; + this.level = null; + this.oneTimeDisposeHandlers = []; + } + + isGeometryNode(){ + return true; + } + + getLevel(){ + return this.level; + } + + isTreeNode(){ + return false; + } + + isLoaded(){ + return this.loaded; + } + + getBoundingSphere(){ + return this.boundingSphere; + } + + getBoundingBox(){ + return this.boundingBox; + } + + getChildren(){ + let children = []; + + for (let i = 0; i < 8; i++) { + if (this.children[i]) { + children.push(this.children[i]); + } + } + + return children; + } + + getBoundingBox(){ + return this.boundingBox; + } + + load(){ + + if (Potree.numNodesLoading >= Potree.maxNodesLoading) { + return; + } + + this.octreeGeometry.loader.load(this); + } + + getNumPoints(){ + return this.numPoints; + } + + dispose(){ + if (this.geometry && this.parent != null) { + this.geometry.dispose(); + this.geometry = null; + this.loaded = false; + + // this.dispatchEvent( { type: 'dispose' } ); + for (let i = 0; i < this.oneTimeDisposeHandlers.length; i++) { + let handler = this.oneTimeDisposeHandlers[i]; + handler(); + } + this.oneTimeDisposeHandlers = []; + } + } + + }; + + OctreeGeometryNode.IDCount = 0; + + // let loadedNodes = new Set(); + + class NodeLoader{ + + constructor(url){ + this.url = url; + } + + async load(node){ + + if(node.loaded || node.loading){ + return; + } + + node.loading = true; + Potree.numNodesLoading++; + + // console.log(node.name, node.numPoints); + + // if(loadedNodes.has(node.name)){ + // // debugger; + // } + // loadedNodes.add(node.name); + + try{ + if(node.nodeType === 2){ + await this.loadHierarchy(node); + } + + let {byteOffset, byteSize} = node; + + + let urlOctree = `${this.url}/../octree.bin`; + + let first = byteOffset; + let last = byteOffset + byteSize - 1n; + + let buffer; + + if(byteSize === 0n){ + buffer = new ArrayBuffer(0); + console.warn(`loaded node with 0 bytes: ${node.name}`); + }else { + let response = await fetch(urlOctree, { + headers: { + 'content-type': 'multipart/byteranges', + 'Range': `bytes=${first}-${last}`, + }, + }); + + buffer = await response.arrayBuffer(); + } + + let workerPath; + if(this.metadata.encoding === "BROTLI"){ + workerPath = Potree.scriptPath + '/workers/2.0/DecoderWorker_brotli.js'; + }else { + workerPath = Potree.scriptPath + '/workers/2.0/DecoderWorker.js'; + } + + let worker = Potree.workerPool.getWorker(workerPath); + + worker.onmessage = function (e) { + + let data = e.data; + let buffers = data.attributeBuffers; + + Potree.workerPool.returnWorker(workerPath, worker); + + let geometry = new BufferGeometry(); + + for(let property in buffers){ + + let buffer = buffers[property].buffer; + + if(property === "position"){ + geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3)); + }else if(property === "rgba"){ + geometry.setAttribute('rgba', new BufferAttribute(new Uint8Array(buffer), 4, true)); + }else if(property === "NORMAL"){ + //geometry.setAttribute('rgba', new THREE.BufferAttribute(new Uint8Array(buffer), 4, true)); + geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3)); + }else if (property === "INDICES") { + let bufferAttribute = new BufferAttribute(new Uint8Array(buffer), 4); + bufferAttribute.normalized = true; + geometry.setAttribute('indices', bufferAttribute); + }else { + const bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1); + + let batchAttribute = buffers[property].attribute; + bufferAttribute.potree = { + offset: buffers[property].offset, + scale: buffers[property].scale, + preciseBuffer: buffers[property].preciseBuffer, + range: batchAttribute.range, + }; + + geometry.setAttribute(property, bufferAttribute); + } + + } + // indices ?? + + node.density = data.density; + node.geometry = geometry; + node.loaded = true; + node.loading = false; + Potree.numNodesLoading--; + }; + + let pointAttributes = node.octreeGeometry.pointAttributes; + let scale = node.octreeGeometry.scale; + + let box = node.boundingBox; + let min = node.octreeGeometry.offset.clone().add(box.min); + let size = box.max.clone().sub(box.min); + let max = min.clone().add(size); + let numPoints = node.numPoints; + + let offset = node.octreeGeometry.loader.offset; + + let message = { + name: node.name, + buffer: buffer, + pointAttributes: pointAttributes, + scale: scale, + min: min, + max: max, + size: size, + offset: offset, + numPoints: numPoints + }; + + worker.postMessage(message, [message.buffer]); + }catch(e){ + node.loaded = false; + node.loading = false; + Potree.numNodesLoading--; + + console.log(`failed to load ${node.name}`); + console.log(e); + console.log(`trying again!`); + } + } + + parseHierarchy(node, buffer){ + + let view = new DataView(buffer); + let tStart = performance.now(); + + let bytesPerNode = 22; + let numNodes = buffer.byteLength / bytesPerNode; + + let octree = node.octreeGeometry; + // let nodes = [node]; + let nodes = new Array(numNodes); + nodes[0] = node; + let nodePos = 1; + + for(let i = 0; i < numNodes; i++){ + let current = nodes[i]; + + let type = view.getUint8(i * bytesPerNode + 0); + let childMask = view.getUint8(i * bytesPerNode + 1); + let numPoints = view.getUint32(i * bytesPerNode + 2, true); + let byteOffset = view.getBigInt64(i * bytesPerNode + 6, true); + let byteSize = view.getBigInt64(i * bytesPerNode + 14, true); + + // if(byteSize === 0n){ + // // debugger; + // } + + + if(current.nodeType === 2){ + // replace proxy with real node + current.byteOffset = byteOffset; + current.byteSize = byteSize; + current.numPoints = numPoints; + }else if(type === 2){ + // load proxy + current.hierarchyByteOffset = byteOffset; + current.hierarchyByteSize = byteSize; + current.numPoints = numPoints; + }else { + // load real node + current.byteOffset = byteOffset; + current.byteSize = byteSize; + current.numPoints = numPoints; + } + + current.nodeType = type; + + if(current.nodeType === 2){ + continue; + } + + for(let childIndex = 0; childIndex < 8; childIndex++){ + let childExists = ((1 << childIndex) & childMask) !== 0; + + if(!childExists){ + continue; + } + + let childName = current.name + childIndex; + + let childAABB = createChildAABB(current.boundingBox, childIndex); + let child = new OctreeGeometryNode(childName, octree, childAABB); + child.name = childName; + child.spacing = current.spacing / 2; + child.level = current.level + 1; + + current.children[childIndex] = child; + child.parent = current; + + // nodes.push(child); + nodes[nodePos] = child; + nodePos++; + } + + // if((i % 500) === 0){ + // yield; + // } + } + + let duration = (performance.now() - tStart); + + // if(duration > 20){ + // let msg = `duration: ${duration}ms, numNodes: ${numNodes}`; + // console.log(msg); + // } + } + + async loadHierarchy(node){ + + let {hierarchyByteOffset, hierarchyByteSize} = node; + let hierarchyPath = `${this.url}/../hierarchy.bin`; + + let first = hierarchyByteOffset; + let last = first + hierarchyByteSize - 1n; + + let response = await fetch(hierarchyPath, { + headers: { + 'content-type': 'multipart/byteranges', + 'Range': `bytes=${first}-${last}`, + }, + }); + + + + let buffer = await response.arrayBuffer(); + + this.parseHierarchy(node, buffer); + + // let promise = new Promise((resolve) => { + // let generator = this.parseHierarchy(node, buffer); + + // let repeatUntilDone = () => { + // let result = generator.next(); + + // if(result.done){ + // resolve(); + // }else{ + // requestAnimationFrame(repeatUntilDone); + // } + // }; + + // repeatUntilDone(); + // }); + + // await promise; + + + + + + } + + } + + let tmpVec3 = new Vector3(); + function createChildAABB(aabb, index){ + let min = aabb.min.clone(); + let max = aabb.max.clone(); + let size = tmpVec3.subVectors(max, min); + + if ((index & 0b0001) > 0) { + min.z += size.z / 2; + } else { + max.z -= size.z / 2; + } + + if ((index & 0b0010) > 0) { + min.y += size.y / 2; + } else { + max.y -= size.y / 2; + } + + if ((index & 0b0100) > 0) { + min.x += size.x / 2; + } else { + max.x -= size.x / 2; + } + + return new Box3(min, max); + } + + let typenameTypeattributeMap = { + "double": PointAttributeTypes.DATA_TYPE_DOUBLE, + "float": PointAttributeTypes.DATA_TYPE_FLOAT, + "int8": PointAttributeTypes.DATA_TYPE_INT8, + "uint8": PointAttributeTypes.DATA_TYPE_UINT8, + "int16": PointAttributeTypes.DATA_TYPE_INT16, + "uint16": PointAttributeTypes.DATA_TYPE_UINT16, + "int32": PointAttributeTypes.DATA_TYPE_INT32, + "uint32": PointAttributeTypes.DATA_TYPE_UINT32, + "int64": PointAttributeTypes.DATA_TYPE_INT64, + "uint64": PointAttributeTypes.DATA_TYPE_UINT64, + }; + + class OctreeLoader{ + + static parseAttributes(jsonAttributes){ + + let attributes = new PointAttributes(); + + let replacements = { + "rgb": "rgba", + }; + + for (const jsonAttribute of jsonAttributes) { + let {name, description, size, numElements, elementSize, min, max} = jsonAttribute; + + let type = typenameTypeattributeMap[jsonAttribute.type]; + + let potreeAttributeName = replacements[name] ? replacements[name] : name; + + let attribute = new PointAttribute(potreeAttributeName, type, numElements); + + if(numElements === 1){ + attribute.range = [min[0], max[0]]; + }else { + attribute.range = [min, max]; + } + + if (name === "gps-time") { // HACK: Guard against bad gpsTime range in metadata, see potree/potree#909 + if (attribute.range[0] === attribute.range[1]) { + attribute.range[1] += 1; + } + } + + attribute.initialRange = attribute.range; + + attributes.add(attribute); + } + + { + // check if it has normals + let hasNormals = + attributes.attributes.find(a => a.name === "NormalX") !== undefined && + attributes.attributes.find(a => a.name === "NormalY") !== undefined && + attributes.attributes.find(a => a.name === "NormalZ") !== undefined; + + if(hasNormals){ + let vector = { + name: "NORMAL", + attributes: ["NormalX", "NormalY", "NormalZ"], + }; + attributes.addVector(vector); + } + } + + return attributes; + } + + static async load(url){ + + let response = await fetch(url); + let metadata = await response.json(); + + let attributes = OctreeLoader.parseAttributes(metadata.attributes); + + let loader = new NodeLoader(url); + loader.metadata = metadata; + loader.attributes = attributes; + loader.scale = metadata.scale; + loader.offset = metadata.offset; + + let octree = new OctreeGeometry(); + octree.url = url; + octree.spacing = metadata.spacing; + octree.scale = metadata.scale; + + // let aPosition = metadata.attributes.find(a => a.name === "position"); + // octree + + let min = new Vector3(...metadata.boundingBox.min); + let max = new Vector3(...metadata.boundingBox.max); + let boundingBox = new Box3(min, max); + + let offset = min.clone(); + boundingBox.min.sub(offset); + boundingBox.max.sub(offset); + + octree.projection = metadata.projection; + octree.boundingBox = boundingBox; + octree.tightBoundingBox = boundingBox.clone(); + octree.boundingSphere = boundingBox.getBoundingSphere(new Sphere()); + octree.tightBoundingSphere = boundingBox.getBoundingSphere(new Sphere()); + octree.offset = offset; + octree.pointAttributes = OctreeLoader.parseAttributes(metadata.attributes); + octree.loader = loader; + + let root = new OctreeGeometryNode("r", octree, boundingBox); + root.level = 0; + root.nodeType = 2; + root.hierarchyByteOffset = 0n; + root.hierarchyByteSize = BigInt(metadata.hierarchy.firstChunkSize); + root.hasChildren = false; + root.spacing = octree.spacing; + root.byteOffset = 0; + + octree.root = root; + + loader.load(root); + + let result = { + geometry: octree, + }; + + return result; + + } + + }; + + /** + * @author Connor Manning + */ + + class EptLoader { + static async load(file, callback) { + + let response = await fetch(file); + let json = await response.json(); + + let url = file.substr(0, file.lastIndexOf('ept.json')); + let geometry = new Potree.PointCloudEptGeometry(url, json); + let root = new Potree.PointCloudEptGeometryNode(geometry); + + geometry.root = root; + geometry.root.load(); + + callback(geometry); + } + }; + + class EptBinaryLoader { + extension() { + return '.bin'; + } + + workerPath() { + return Potree.scriptPath + '/workers/EptBinaryDecoderWorker.js'; + } + + load(node) { + if (node.loaded) return; + + let url = node.url() + this.extension(); + + let xhr = XHRFactory.createXMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + let buffer = xhr.response; + this.parse(node, buffer); + } else { + console.log('Failed ' + url + ': ' + xhr.status); + } + } + }; + + try { + xhr.send(null); + } + catch (e) { + console.log('Failed request: ' + e); + } + } + + parse(node, buffer) { + let workerPath = this.workerPath(); + let worker = Potree.workerPool.getWorker(workerPath); + + worker.onmessage = function(e) { + let g = new BufferGeometry(); + let numPoints = e.data.numPoints; + + let position = new Float32Array(e.data.position); + g.setAttribute('position', new BufferAttribute(position, 3)); + + let indices = new Uint8Array(e.data.indices); + g.setAttribute('indices', new BufferAttribute(indices, 4)); + + if (e.data.color) { + let color = new Uint8Array(e.data.color); + g.setAttribute('color', new BufferAttribute(color, 4, true)); + } + if (e.data.intensity) { + let intensity = new Float32Array(e.data.intensity); + g.setAttribute('intensity', + new BufferAttribute(intensity, 1)); + } + if (e.data.classification) { + let classification = new Uint8Array(e.data.classification); + g.setAttribute('classification', + new BufferAttribute(classification, 1)); + } + if (e.data.returnNumber) { + let returnNumber = new Uint8Array(e.data.returnNumber); + g.setAttribute('return number', + new BufferAttribute(returnNumber, 1)); + } + if (e.data.numberOfReturns) { + let numberOfReturns = new Uint8Array(e.data.numberOfReturns); + g.setAttribute('number of returns', + new BufferAttribute(numberOfReturns, 1)); + } + if (e.data.pointSourceId) { + let pointSourceId = new Uint16Array(e.data.pointSourceId); + g.setAttribute('source id', + new BufferAttribute(pointSourceId, 1)); + } + + g.attributes.indices.normalized = true; + + let tightBoundingBox = new Box3( + new Vector3().fromArray(e.data.tightBoundingBox.min), + new Vector3().fromArray(e.data.tightBoundingBox.max) + ); + + node.doneLoading( + g, + tightBoundingBox, + numPoints, + new Vector3(...e.data.mean)); + + Potree.workerPool.returnWorker(workerPath, worker); + }; + + let toArray = (v) => [v.x, v.y, v.z]; + let message = { + buffer: buffer, + schema: node.ept.schema, + scale: node.ept.eptScale, + offset: node.ept.eptOffset, + mins: toArray(node.key.b.min) + }; + + worker.postMessage(message, [message.buffer]); + } + }; + + /** + * laslaz code taken and adapted from plas.io js-laslaz + * http://plas.io/ + * https://github.com/verma/plasio + * + * Thanks to Uday Verma and Howard Butler + * + */ + + class EptLaszipLoader { + load(node) { + if (node.loaded) return; + + let url = node.url() + '.laz'; + + let xhr = XHRFactory.createXMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + let buffer = xhr.response; + this.parse(node, buffer); + } else { + console.log('Failed ' + url + ': ' + xhr.status); + } + } + }; + + xhr.send(null); + } + + async parse(node, buffer){ + let lf = new LASFile(buffer); + let handler = new EptLazBatcher(node); + + try{ + await lf.open(); + + lf.isOpen = true; + + const header = await lf.getHeader(); + + { + let i = 0; + + let toArray = (v) => [v.x, v.y, v.z]; + let mins = toArray(node.key.b.min); + let maxs = toArray(node.key.b.max); + + let hasMoreData = true; + + while(hasMoreData){ + const data = await lf.readData(1000000, 0, 1); + + let d = new LASDecoder( + data.buffer, + header.pointsFormatId, + header.pointsStructSize, + data.count, + header.scale, + header.offset, + mins, + maxs); + + d.extraBytes = header.extraBytes; + d.pointsFormatId = header.pointsFormatId; + handler.push(d); + + i += data.count; + + hasMoreData = data.hasMoreData; + } + + header.totalRead = i; + header.versionAsString = lf.versionAsString; + header.isCompressed = lf.isCompressed; + + await lf.close(); + + lf.isOpen = false; + } + + }catch(err){ + console.error('Error reading LAZ:', err); + + if (lf.isOpen) { + await lf.close(); + + lf.isOpen = false; + } + + throw err; + } + } + }; + + class EptLazBatcher { + constructor(node) { this.node = node; } + + push(las) { + let workerPath = Potree.scriptPath + + '/workers/EptLaszipDecoderWorker.js'; + let worker = Potree.workerPool.getWorker(workerPath); + + worker.onmessage = (e) => { + let g = new BufferGeometry(); + let numPoints = las.pointsCount; + + let positions = new Float32Array(e.data.position); + let colors = new Uint8Array(e.data.color); + + let intensities = new Float32Array(e.data.intensity); + let classifications = new Uint8Array(e.data.classification); + let returnNumbers = new Uint8Array(e.data.returnNumber); + let numberOfReturns = new Uint8Array(e.data.numberOfReturns); + let pointSourceIDs = new Uint16Array(e.data.pointSourceID); + let indices = new Uint8Array(e.data.indices); + let gpsTime = new Float32Array(e.data.gpsTime); + + g.setAttribute('position', + new BufferAttribute(positions, 3)); + g.setAttribute('rgba', + new BufferAttribute(colors, 4, true)); + g.setAttribute('intensity', + new BufferAttribute(intensities, 1)); + g.setAttribute('classification', + new BufferAttribute(classifications, 1)); + g.setAttribute('return number', + new BufferAttribute(returnNumbers, 1)); + g.setAttribute('number of returns', + new BufferAttribute(numberOfReturns, 1)); + g.setAttribute('source id', + new BufferAttribute(pointSourceIDs, 1)); + g.setAttribute('indices', + new BufferAttribute(indices, 4)); + g.setAttribute('gpsTime', + new BufferAttribute(gpsTime, 1)); + this.node.gpsTime = e.data.gpsMeta; + + g.attributes.indices.normalized = true; + + let tightBoundingBox = new Box3( + new Vector3().fromArray(e.data.tightBoundingBox.min), + new Vector3().fromArray(e.data.tightBoundingBox.max) + ); + + this.node.doneLoading( + g, + tightBoundingBox, + numPoints, + new Vector3(...e.data.mean)); + + Potree.workerPool.returnWorker(workerPath, worker); + }; + + let message = { + buffer: las.arrayb, + numPoints: las.pointsCount, + pointSize: las.pointSize, + pointFormatID: las.pointsFormatId, + scale: las.scale, + offset: las.offset, + mins: las.mins, + maxs: las.maxs + }; + + worker.postMessage(message, [message.buffer]); + }; + }; + + class EptZstandardLoader extends EptBinaryLoader { + extension() { + return '.zst'; + } + + workerPath() { + return Potree.scriptPath + '/workers/EptZstandardDecoderWorker.js'; + } + }; + + class ShapefileLoader{ + + constructor(){ + this.transform = null; + } + + async load(path){ + + const matLine = new LineMaterial( { + color: 0xff0000, + linewidth: 3, // in pixels + resolution: new Vector2(1000, 1000), + dashed: false + } ); + + const features = await this.loadShapefileFeatures(path); + const node = new Object3D(); + + for(const feature of features){ + const fnode = this.featureToSceneNode(feature, matLine); + node.add(fnode); + } + + let setResolution = (x, y) => { + matLine.resolution.set(x, y); + }; + + const result = { + features: features, + node: node, + setResolution: setResolution, + }; + + return result; + } + + featureToSceneNode(feature, matLine){ + let geometry = feature.geometry; + + let color = new Color(1, 1, 1); + + let transform = this.transform; + if(transform === null){ + transform = {forward: (v) => v}; + } + + if(feature.geometry.type === "Point"){ + let sg = new SphereGeometry(1, 18, 18); + let sm = new MeshNormalMaterial(); + let s = new Mesh(sg, sm); + + let [long, lat] = geometry.coordinates; + let pos = transform.forward([long, lat]); + + s.position.set(...pos, 20); + + s.scale.set(10, 10, 10); + + return s; + }else if(geometry.type === "LineString"){ + let coordinates = []; + + let min = new Vector3(Infinity, Infinity, Infinity); + for(let i = 0; i < geometry.coordinates.length; i++){ + let [long, lat] = geometry.coordinates[i]; + let pos = transform.forward([long, lat]); + + min.x = Math.min(min.x, pos[0]); + min.y = Math.min(min.y, pos[1]); + min.z = Math.min(min.z, 20); + + coordinates.push(...pos, 20); + if(i > 0 && i < geometry.coordinates.length - 1){ + coordinates.push(...pos, 20); + } + } + + for(let i = 0; i < coordinates.length; i += 3){ + coordinates[i+0] -= min.x; + coordinates[i+1] -= min.y; + coordinates[i+2] -= min.z; + } + + const lineGeometry = new LineGeometry(); + lineGeometry.setPositions( coordinates ); + + const line = new Line2( lineGeometry, matLine ); + line.computeLineDistances(); + line.scale.set( 1, 1, 1 ); + line.position.copy(min); + + return line; + }else if(geometry.type === "Polygon"){ + for(let pc of geometry.coordinates){ + let coordinates = []; + + let min = new Vector3(Infinity, Infinity, Infinity); + for(let i = 0; i < pc.length; i++){ + let [long, lat] = pc[i]; + let pos = transform.forward([long, lat]); + + min.x = Math.min(min.x, pos[0]); + min.y = Math.min(min.y, pos[1]); + min.z = Math.min(min.z, 20); + + coordinates.push(...pos, 20); + if(i > 0 && i < pc.length - 1){ + coordinates.push(...pos, 20); + } + } + + for(let i = 0; i < coordinates.length; i += 3){ + coordinates[i+0] -= min.x; + coordinates[i+1] -= min.y; + coordinates[i+2] -= min.z; + } + + const lineGeometry = new LineGeometry(); + lineGeometry.setPositions( coordinates ); + + const line = new Line2( lineGeometry, matLine ); + line.computeLineDistances(); + line.scale.set( 1, 1, 1 ); + line.position.copy(min); + + return line; + } + }else { + console.log("unhandled feature: ", feature); + } + } + + async loadShapefileFeatures(file){ + let features = []; + + let source = await shapefile.open(file); + + while(true){ + let result = await source.read(); + + if (result.done) { + break; + } + + if (result.value && result.value.type === 'Feature' && result.value.geometry !== undefined) { + features.push(result.value); + } + } + + return features; + } + + }; + + const defaultColors = { + "landuse": [0.5, 0.5, 0.5], + "natural": [0.0, 1.0, 0.0], + "places": [1.0, 0.0, 1.0], + "points": [0.0, 1.0, 1.0], + "roads": [1.0, 1.0, 0.0], + "waterways": [0.0, 0.0, 1.0], + "default": [0.9, 0.6, 0.1], + }; + + function getColor(feature){ + let color = defaultColors[feature]; + + if(!color){ + color = defaultColors["default"]; + } + + return color; + } + + class Geopackage$1{ + constructor(){ + this.path = null; + this.node = null; + } + }; + + class GeoPackageLoader{ + + constructor(){ + + } + + static async loadUrl(url, params){ + + await Promise.all([ + Utils.loadScript(`${Potree.scriptPath}/lazylibs/geopackage/geopackage.js`), + Utils.loadScript(`${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.js`), + ]); + + const result = await fetch(url); + const buffer = await result.arrayBuffer(); + + params = params || {}; + + params.source = url; + + return GeoPackageLoader.loadBuffer(buffer, params); + } + + static async loadBuffer(buffer, params){ + + await Promise.all([ + Utils.loadScript(`${Potree.scriptPath}/lazylibs/geopackage/geopackage.js`), + Utils.loadScript(`${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.js`), + ]); + + params = params || {}; + + const resolver = async (resolve) => { + + let transform = params.transform; + if(!transform){ + transform = {forward: (arg) => arg}; + } + + const wasmPath = `${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.wasm`; + const SQL = await initSqlJs({ locateFile: filename => wasmPath}); + + const u8 = new Uint8Array(buffer); + + const data = await geopackage.open(u8); + window.data = data; + + const geopackageNode = new Object3D(); + geopackageNode.name = params.source; + geopackageNode.potree = { + source: params.source, + }; + + const geo = new Geopackage$1(); + geo.path = params.source; + geo.node = geopackageNode; + + const tables = data.getTables(); + + for(const table of tables.features){ + const dao = data.getFeatureDao(table); + + let boundingBox = dao.getBoundingBox(); + boundingBox = boundingBox.projectBoundingBox(dao.projection, 'EPSG:4326'); + const geoJson = data.queryForGeoJSONFeaturesInTable(table, boundingBox); + + const matLine = new LineMaterial( { + color: new Color().setRGB(...getColor(table)), + linewidth: 2, + resolution: new Vector2(1000, 1000), dashed: false } ); - const node = new Object3D(); - node.name = table; - geo.node.add(node); + const node = new Object3D(); + node.name = table; + geo.node.add(node); + + for(const [index, feature] of Object.entries(geoJson)){ + //const featureNode = GeoPackageLoader.featureToSceneNode(feature, matLine, transform); + const featureNode = GeoPackageLoader.featureToSceneNode(feature, matLine, dao.projection, transform); + node.add(featureNode); + } + } + + resolve(geo); + }; + + return new Promise(resolver); + } + + static featureToSceneNode(feature, matLine, geopackageProjection, transform){ + let geometry = feature.geometry; + + let color = new Color(1, 1, 1); + + if(feature.geometry.type === "Point"){ + let sg = new SphereGeometry(1, 18, 18); + let sm = new MeshNormalMaterial(); + let s = new Mesh(sg, sm); + + let [long, lat] = geometry.coordinates; + let pos = transform.forward(geopackageProjection.forward([long, lat])); + + s.position.set(...pos, 20); + + s.scale.set(10, 10, 10); + + return s; + }else if(geometry.type === "LineString"){ + let coordinates = []; + + let min = new Vector3(Infinity, Infinity, Infinity); + for(let i = 0; i < geometry.coordinates.length; i++){ + let [long, lat] = geometry.coordinates[i]; + let pos = transform.forward(geopackageProjection.forward([long, lat])); + + min.x = Math.min(min.x, pos[0]); + min.y = Math.min(min.y, pos[1]); + min.z = Math.min(min.z, 20); + + coordinates.push(...pos, 20); + if(i > 0 && i < geometry.coordinates.length - 1){ + coordinates.push(...pos, 20); + } + } + + for(let i = 0; i < coordinates.length; i += 3){ + coordinates[i+0] -= min.x; + coordinates[i+1] -= min.y; + coordinates[i+2] -= min.z; + } + + const lineGeometry = new LineGeometry(); + lineGeometry.setPositions( coordinates ); + + const line = new Line2( lineGeometry, matLine ); + line.computeLineDistances(); + line.scale.set( 1, 1, 1 ); + line.position.copy(min); + + return line; + }else if(geometry.type === "Polygon"){ + for(let pc of geometry.coordinates){ + let coordinates = []; + + let min = new Vector3(Infinity, Infinity, Infinity); + for(let i = 0; i < pc.length; i++){ + let [long, lat] = pc[i]; + + let pos = transform.forward(geopackageProjection.forward([long, lat])); + + min.x = Math.min(min.x, pos[0]); + min.y = Math.min(min.y, pos[1]); + min.z = Math.min(min.z, 20); + + coordinates.push(...pos, 20); + if(i > 0 && i < pc.length - 1){ + coordinates.push(...pos, 20); + } + } + + for(let i = 0; i < coordinates.length; i += 3){ + coordinates[i+0] -= min.x; + coordinates[i+1] -= min.y; + coordinates[i+2] -= min.z; + } + + const lineGeometry = new LineGeometry(); + lineGeometry.setPositions( coordinates ); + + const line = new Line2( lineGeometry, matLine ); + line.computeLineDistances(); + line.scale.set( 1, 1, 1 ); + line.position.copy(min); + + return line; + } + }else { + console.log("unhandled feature: ", feature); + } + } + + }; + + class ClipVolume extends Object3D{ + + constructor(args){ + super(); + + this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1; + this.name = "clip_volume_" + this.constructor.counter; + + let alpha = args.alpha || 0; + let beta = args.beta || 0; + let gamma = args.gamma || 0; + + this.rotation.x = alpha; + this.rotation.y = beta; + this.rotation.z = gamma; + + this.clipOffset = 0.001; + this.clipRotOffset = 1; + + let boxGeometry = new BoxGeometry(1, 1, 1); + boxGeometry.computeBoundingBox(); + + let boxFrameGeometry = new Geometry(); + { + // bottom + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); + // top + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); + // sides + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); + + boxFrameGeometry.colors.push(new Vector3(1, 1, 1)); + } + + let planeFrameGeometry = new Geometry(); + { + // middle line + planeFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.0)); + planeFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.0)); + planeFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.0)); + planeFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.0)); + planeFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.0)); + planeFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.0)); + planeFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.0)); + planeFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.0)); + } + + this.dimension = new Vector3(1, 1, 1); + this.material = new MeshBasicMaterial( { + color: 0x00ff00, + transparent: true, + opacity: 0.3, + depthTest: true, + depthWrite: false} ); + this.box = new Mesh(boxGeometry, this.material); + this.box.geometry.computeBoundingBox(); + this.boundingBox = this.box.geometry.boundingBox; + this.add(this.box); + + this.frame = new LineSegments( boxFrameGeometry, new LineBasicMaterial({color: 0x000000})); + this.add(this.frame); + this.planeFrame = new LineSegments( planeFrameGeometry, new LineBasicMaterial({color: 0xff0000})); + this.add(this.planeFrame); + + // set default thickness + this.setScaleZ(0.1); + + // create local coordinate system + let createArrow = (name, direction, color) => { + let material = new MeshBasicMaterial({ + color: color, + depthTest: false, + depthWrite: false}); + + let shaftGeometry = new Geometry(); + shaftGeometry.vertices.push(new Vector3(0, 0, 0)); + shaftGeometry.vertices.push(new Vector3(0, 1, 0)); + + let shaftMaterial = new LineBasicMaterial({ + color: color, + depthTest: false, + depthWrite: false, + transparent: true + }); + let shaft = new Line(shaftGeometry, shaftMaterial); + shaft.name = name + "_shaft"; + + let headGeometry = new CylinderGeometry(0, 0.04, 0.1, 10, 1, false); + let headMaterial = material; + let head = new Mesh(headGeometry, headMaterial); + head.name = name + "_head"; + head.position.y = 1; + + let arrow = new Object3D(); + arrow.name = name; + arrow.add(shaft); + arrow.add(head); + + return arrow; + }; + + this.arrowX = createArrow("arrow_x", new Vector3(1, 0, 0), 0xFF0000); + this.arrowY = createArrow("arrow_y", new Vector3(0, 1, 0), 0x00FF00); + this.arrowZ = createArrow("arrow_z", new Vector3(0, 0, 1), 0x0000FF); + + this.arrowX.rotation.z = -Math.PI / 2; + this.arrowZ.rotation.x = Math.PI / 2; + + this.arrowX.visible = false; + this.arrowY.visible = false; + this.arrowZ.visible = false; + + this.add(this.arrowX); + this.add(this.arrowY); + this.add(this.arrowZ); + + { // event listeners + this.addEventListener("ui_select", e => { + this.arrowX.visible = true; + this.arrowY.visible = true; + this.arrowZ.visible = true; + }); + this.addEventListener("ui_deselect", e => { + this.arrowX.visible = false; + this.arrowY.visible = false; + this.arrowZ.visible = false; + }); + this.addEventListener("select", e => { + let scene_header = $("#" + this.name + " .scene_header"); + if(!scene_header.next().is(":visible")) { + scene_header.click(); + } + }); + this.addEventListener("deselect", e => { + let scene_header = $("#" + this.name + " .scene_header"); + if(scene_header.next().is(":visible")) { + scene_header.click(); + } + }); + } + + this.update(); + }; + + setClipOffset(offset) { + this.clipOffset = offset; + } + + setClipRotOffset(offset) { + this.clipRotOffset = offset; + } + + setScaleX(x) { + this.box.scale.x = x; + this.frame.scale.x = x; + this.planeFrame.scale.x = x; + } + + setScaleY(y) { + this.box.scale.y = y; + this.frame.scale.y = y; + this.planeFrame.scale.y = y; + } + + setScaleZ(z) { + this.box.scale.z = z; + this.frame.scale.z = z; + this.planeFrame.scale.z = z; + } + + offset(args) { + let cs = args.cs || null; + let axis = args.axis || null; + let dir = args.dir || null; + + if(!cs || !axis || !dir) return; + + if(axis === "x") { + if(cs === "local") { + this.position.add(this.localX.clone().multiplyScalar(dir * this.clipOffset)); + } else if(cs === "global") { + this.position.x = this.position.x + dir * this.clipOffset; + } + }else if(axis === "y") { + if(cs === "local") { + this.position.add(this.localY.clone().multiplyScalar(dir * this.clipOffset)); + } else if(cs === "global") { + this.position.y = this.position.y + dir * this.clipOffset; + } + }else if(axis === "z") { + if(cs === "local") { + this.position.add(this.localZ.clone().multiplyScalar(dir * this.clipOffset)); + } else if(cs === "global") { + this.position.z = this.position.z + dir * this.clipOffset; + } + } + + this.dispatchEvent({"type": "clip_volume_changed", "viewer": viewer, "volume": this}); + } + + rotate(args) { + let cs = args.cs || null; + let axis = args.axis || null; + let dir = args.dir || null; + + if(!cs || !axis || !dir) return; + + if(cs === "local") { + if(axis === "x") { + this.rotateOnAxis(new Vector3(1, 0, 0), dir * this.clipRotOffset * Math.PI / 180); + } else if(axis === "y") { + this.rotateOnAxis(new Vector3(0, 1, 0), dir * this.clipRotOffset * Math.PI / 180); + } else if(axis === "z") { + this.rotateOnAxis(new Vector3(0, 0, 1), dir * this.clipRotOffset * Math.PI / 180); + } + } else if(cs === "global") { + let rotaxis = new Vector4(1, 0, 0, 0); + if(axis === "y") { + rotaxis = new Vector4(0, 1, 0, 0); + } else if(axis === "z") { + rotaxis = new Vector4(0, 0, 1, 0); + } + this.updateMatrixWorld(); + let invM = newthis.matrixWorld.clone().invert(); + rotaxis = rotaxis.applyMatrix4(invM).normalize(); + rotaxis = new Vector3(rotaxis.x, rotaxis.y, rotaxis.z); + this.rotateOnAxis(rotaxis, dir * this.clipRotOffset * Math.PI / 180); + } + + this.updateLocalSystem(); + + this.dispatchEvent({"type": "clip_volume_changed", "viewer": viewer, "volume": this}); + } + + update(){ + this.boundingBox = this.box.geometry.boundingBox; + this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere()); + + this.box.visible = false; + + this.updateLocalSystem(); + }; + + updateLocalSystem() { + // extract local coordinate axes + let rotQuat = this.getWorldQuaternion(); + this.localX = new Vector3(1, 0, 0).applyQuaternion(rotQuat).normalize(); + this.localY = new Vector3(0, 1, 0).applyQuaternion(rotQuat).normalize(); + this.localZ = new Vector3(0, 0, 1).applyQuaternion(rotQuat).normalize(); + } + + raycast(raycaster, intersects){ + + let is = []; + this.box.raycast(raycaster, is); + + if(is.length > 0){ + let I = is[0]; + intersects.push({ + distance: I.distance, + object: this, + point: I.point.clone() + }); + } + }; + }; + + class ClippingTool extends EventDispatcher{ + + constructor(viewer){ + super(); + + this.viewer = viewer; + + this.maxPolygonVertices = 8; + + this.addEventListener("start_inserting_clipping_volume", e => { + this.viewer.dispatchEvent({ + type: "cancel_insertions" + }); + }); + + this.sceneMarker = new Scene(); + this.sceneVolume = new Scene(); + this.sceneVolume.name = "scene_clip_volume"; + this.viewer.inputHandler.registerInteractiveScene(this.sceneVolume); + + this.onRemove = e => { + this.sceneVolume.remove(e.volume); + }; + + this.onAdd = e => { + this.sceneVolume.add(e.volume); + }; + + this.viewer.inputHandler.addEventListener("delete", e => { + let volumes = e.selection.filter(e => (e instanceof ClipVolume)); + volumes.forEach(e => this.viewer.scene.removeClipVolume(e)); + let polyVolumes = e.selection.filter(e => (e instanceof PolygonClipVolume)); + polyVolumes.forEach(e => this.viewer.scene.removePolygonClipVolume(e)); + }); + } + + setScene(scene){ + if(this.scene === scene){ + return; + } + + if(this.scene){ + this.scene.removeEventListeners("clip_volume_added", this.onAdd); + this.scene.removeEventListeners("clip_volume_removed", this.onRemove); + this.scene.removeEventListeners("polygon_clip_volume_added", this.onAdd); + this.scene.removeEventListeners("polygon_clip_volume_removed", this.onRemove); + } + + this.scene = scene; + + this.scene.addEventListener("clip_volume_added", this.onAdd); + this.scene.addEventListener("clip_volume_removed", this.onRemove); + this.scene.addEventListener("polygon_clip_volume_added", this.onAdd); + this.scene.addEventListener("polygon_clip_volume_removed", this.onRemove); + } + + startInsertion(args = {}) { + let type = args.type || null; + + if(!type) return null; + + let domElement = this.viewer.renderer.domElement; + let canvasSize = this.viewer.renderer.getSize(new Vector2()); + + let svg = $(` + + + + + + + + + + + + `); + $(domElement.parentElement).append(svg); + + let polyClipVol = new PolygonClipVolume(this.viewer.scene.getActiveCamera().clone()); + + this.dispatchEvent({"type": "start_inserting_clipping_volume"}); + + this.viewer.scene.addPolygonClipVolume(polyClipVol); + this.sceneMarker.add(polyClipVol); + + let cancel = { + callback: null + }; + + let insertionCallback = (e) => { + if(e.button === MOUSE.LEFT){ + + polyClipVol.addMarker(); + + // SVC Screen Line + svg.find("polyline").each((index, target) => { + let newPoint = svg[0].createSVGPoint(); + newPoint.x = e.offsetX; + newPoint.y = e.offsetY; + let polyline = target.points.appendItem(newPoint); + }); + + + if(polyClipVol.markers.length > this.maxPolygonVertices){ + cancel.callback(); + } + + this.viewer.inputHandler.startDragging( + polyClipVol.markers[polyClipVol.markers.length - 1]); + }else if(e.button === MOUSE.RIGHT){ + cancel.callback(e); + } + }; + + cancel.callback = e => { + + //let first = svg.find("polyline")[0].points[0]; + //svg.find("polyline").each((index, target) => { + // let newPoint = svg[0].createSVGPoint(); + // newPoint.x = first.x; + // newPoint.y = first.y; + // let polyline = target.points.appendItem(newPoint); + //}); + svg.remove(); - for(const [index, feature] of Object.entries(geoJson)){ - //const featureNode = GeoPackageLoader.featureToSceneNode(feature, matLine, transform); - const featureNode = GeoPackageLoader.featureToSceneNode(feature, matLine, dao.projection, transform); - node.add(featureNode); - } + if(polyClipVol.markers.length > 3) { + polyClipVol.removeLastMarker(); + polyClipVol.initialized = true; + } else { + this.viewer.scene.removePolygonClipVolume(polyClipVol); } - resolve(geo); + this.viewer.renderer.domElement.removeEventListener("mouseup", insertionCallback, true); + this.viewer.removeEventListener("cancel_insertions", cancel.callback); + this.viewer.inputHandler.enabled = true; }; + + this.viewer.addEventListener("cancel_insertions", cancel.callback); + this.viewer.renderer.domElement.addEventListener("mouseup", insertionCallback , true); + this.viewer.inputHandler.enabled = false; + + polyClipVol.addMarker(); + this.viewer.inputHandler.startDragging( + polyClipVol.markers[polyClipVol.markers.length - 1]); - return new Promise(resolver); + return polyClipVol; } - static featureToSceneNode(feature, matLine, geopackageProjection, transform){ - let geometry = feature.geometry; - - let color = new Color(1, 1, 1); - - if(feature.geometry.type === "Point"){ - let sg = new SphereGeometry(1, 18, 18); - let sm = new MeshNormalMaterial(); - let s = new Mesh(sg, sm); - - let [long, lat] = geometry.coordinates; - let pos = transform.forward(geopackageProjection.forward([long, lat])); - - s.position.set(...pos, 20); - - s.scale.set(10, 10, 10); - - return s; - }else if(geometry.type === "LineString"){ - let coordinates = []; - - let min = new Vector3(Infinity, Infinity, Infinity); - for(let i = 0; i < geometry.coordinates.length; i++){ - let [long, lat] = geometry.coordinates[i]; - let pos = transform.forward(geopackageProjection.forward([long, lat])); - - min.x = Math.min(min.x, pos[0]); - min.y = Math.min(min.y, pos[1]); - min.z = Math.min(min.z, 20); - - coordinates.push(...pos, 20); - if(i > 0 && i < geometry.coordinates.length - 1){ - coordinates.push(...pos, 20); - } - } - - for(let i = 0; i < coordinates.length; i += 3){ - coordinates[i+0] -= min.x; - coordinates[i+1] -= min.y; - coordinates[i+2] -= min.z; - } - - const lineGeometry = new LineGeometry(); - lineGeometry.setPositions( coordinates ); + update() { - const line = new Line2( lineGeometry, matLine ); - line.computeLineDistances(); - line.scale.set( 1, 1, 1 ); - line.position.copy(min); - - return line; - }else if(geometry.type === "Polygon"){ - for(let pc of geometry.coordinates){ - let coordinates = []; - - let min = new Vector3(Infinity, Infinity, Infinity); - for(let i = 0; i < pc.length; i++){ - let [long, lat] = pc[i]; - - let pos = transform.forward(geopackageProjection.forward([long, lat])); - - min.x = Math.min(min.x, pos[0]); - min.y = Math.min(min.y, pos[1]); - min.z = Math.min(min.z, 20); + } + }; + + var GeoTIFF = (function (exports) { + 'use strict'; + + const Endianness = new Enum({ + LITTLE: "II", + BIG: "MM", + }); + + const Type = new Enum({ + BYTE: {value: 1, bytes: 1}, + ASCII: {value: 2, bytes: 1}, + SHORT: {value: 3, bytes: 2}, + LONG: {value: 4, bytes: 4}, + RATIONAL: {value: 5, bytes: 8}, + SBYTE: {value: 6, bytes: 1}, + UNDEFINED: {value: 7, bytes: 1}, + SSHORT: {value: 8, bytes: 2}, + SLONG: {value: 9, bytes: 4}, + SRATIONAL: {value: 10, bytes: 8}, + FLOAT: {value: 11, bytes: 4}, + DOUBLE: {value: 12, bytes: 8}, + }); + + const Tag = new Enum({ + IMAGE_WIDTH: 256, + IMAGE_HEIGHT: 257, + BITS_PER_SAMPLE: 258, + COMPRESSION: 259, + PHOTOMETRIC_INTERPRETATION: 262, + STRIP_OFFSETS: 273, + ORIENTATION: 274, + SAMPLES_PER_PIXEL: 277, + ROWS_PER_STRIP: 278, + STRIP_BYTE_COUNTS: 279, + X_RESOLUTION: 282, + Y_RESOLUTION: 283, + PLANAR_CONFIGURATION: 284, + RESOLUTION_UNIT: 296, + SOFTWARE: 305, + COLOR_MAP: 320, + SAMPLE_FORMAT: 339, + MODEL_PIXEL_SCALE: 33550, // [GeoTIFF] TYPE: double N: 3 + MODEL_TIEPOINT: 33922, // [GeoTIFF] TYPE: double N: 6 * NUM_TIEPOINTS + GEO_KEY_DIRECTORY: 34735, // [GeoTIFF] TYPE: short N: >= 4 + GEO_DOUBLE_PARAMS: 34736, // [GeoTIFF] TYPE: short N: variable + GEO_ASCII_PARAMS: 34737, // [GeoTIFF] TYPE: ascii N: variable + }); + + const typeMapping = new Map([ + [Type.BYTE, Uint8Array], + [Type.ASCII, Uint8Array], + [Type.SHORT, Uint16Array], + [Type.LONG, Uint32Array], + [Type.RATIONAL, Uint32Array], + [Type.SBYTE, Int8Array], + [Type.UNDEFINED, Uint8Array], + [Type.SSHORT, Int16Array], + [Type.SLONG, Int32Array], + [Type.SRATIONAL, Int32Array], + [Type.FLOAT, Float32Array], + [Type.DOUBLE, Float64Array], + ]); + + class IFDEntry{ + + constructor(tag, type, count, offset, value){ + this.tag = tag; + this.type = type; + this.count = count; + this.offset = offset; + this.value = value; + } + + } + + class Image{ + + constructor(){ + this.width = 0; + this.height = 0; + this.buffer = null; + this.metadata = []; + } + + } + + class Reader{ + + constructor(){ + + } + + static read(data){ + + let endiannessTag = String.fromCharCode(...Array.from(data.slice(0, 2))); + let endianness = Endianness.fromValue(endiannessTag); + + let tiffCheckTag = data.readUInt8(2); + + if(tiffCheckTag !== 42){ + throw new Error("not a valid tiff file"); + } + + let offsetToFirstIFD = data.readUInt32LE(4); + + console.log("offsetToFirstIFD", offsetToFirstIFD); + + let ifds = []; + let IFDsRead = false; + let currentIFDOffset = offsetToFirstIFD; + let i = 0; + while(IFDsRead || i < 100){ + + console.log("currentIFDOffset", currentIFDOffset); + let numEntries = data.readUInt16LE(currentIFDOffset); + let nextIFDOffset = data.readUInt32LE(currentIFDOffset + 2 + numEntries * 12); + + console.log("next offset: ", currentIFDOffset + 2 + numEntries * 12); + + let entryBuffer = data.slice(currentIFDOffset + 2, currentIFDOffset + 2 + 12 * numEntries); + + for(let i = 0; i < numEntries; i++){ + let tag = Tag.fromValue(entryBuffer.readUInt16LE(i * 12)); + let type = Type.fromValue(entryBuffer.readUInt16LE(i * 12 + 2)); + let count = entryBuffer.readUInt32LE(i * 12 + 4); + let offsetOrValue = entryBuffer.readUInt32LE(i * 12 + 8); + let valueBytes = type.bytes * count; + + let value; + if(valueBytes <= 4){ + value = offsetOrValue; + }else { + let valueBuffer = new Uint8Array(valueBytes); + valueBuffer.set(data.slice(offsetOrValue, offsetOrValue + valueBytes)); - coordinates.push(...pos, 20); - if(i > 0 && i < pc.length - 1){ - coordinates.push(...pos, 20); - } - } - - for(let i = 0; i < coordinates.length; i += 3){ - coordinates[i+0] -= min.x; - coordinates[i+1] -= min.y; - coordinates[i+2] -= min.z; + let ArrayType = typeMapping.get(type); + + value = new ArrayType(valueBuffer.buffer); + + if(type === Type.ASCII){ + value = String.fromCharCode(...value); + } } - const lineGeometry = new LineGeometry(); - lineGeometry.setPositions( coordinates ); + let ifd = new IFDEntry(tag, type, count, offsetOrValue, value); - const line = new Line2( lineGeometry, matLine ); - line.computeLineDistances(); - line.scale.set( 1, 1, 1 ); - line.position.copy(min); - - return line; + ifds.push(ifd); } - }else { - console.log("unhandled feature: ", feature); + + console.log("nextIFDOffset", nextIFDOffset); + + if(nextIFDOffset === 0){ + break; + } + + currentIFDOffset = nextIFDOffset; + i++; } - } - }; + let ifdForTag = (tag) => { + for(let entry of ifds){ + if(entry.tag === tag){ + return entry; + } + } - class ClipVolume extends Object3D{ - - constructor(args){ - super(); - - this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1; - this.name = "clip_volume_" + this.constructor.counter; + return null; + }; - let alpha = args.alpha || 0; - let beta = args.beta || 0; - let gamma = args.gamma || 0; + let width = ifdForTag(Tag.IMAGE_WIDTH, ifds).value; + let height = ifdForTag(Tag.IMAGE_HEIGHT, ifds).value; + let compression = ifdForTag(Tag.COMPRESSION, ifds).value; + let rowsPerStrip = ifdForTag(Tag.ROWS_PER_STRIP, ifds).value; + let ifdStripOffsets = ifdForTag(Tag.STRIP_OFFSETS, ifds); + let ifdStripByteCounts = ifdForTag(Tag.STRIP_BYTE_COUNTS, ifds); - this.rotation.x = alpha; - this.rotation.y = beta; - this.rotation.z = gamma; + let numStrips = Math.ceil(height / rowsPerStrip); - this.clipOffset = 0.001; - this.clipRotOffset = 1; - - let boxGeometry = new BoxGeometry(1, 1, 1); - boxGeometry.computeBoundingBox(); - - let boxFrameGeometry = new Geometry(); - { - // bottom - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); - // top - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); - // sides - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); + let stripByteCounts = []; + for(let i = 0; i < ifdStripByteCounts.count; i++){ + let type = ifdStripByteCounts.type; + let offset = ifdStripByteCounts.offset + i * type.bytes; - boxFrameGeometry.colors.push(new Vector3(1, 1, 1)); - } + let value; + if(type === Type.SHORT){ + value = data.readUInt16LE(offset); + }else if(type === Type.LONG){ + value = data.readUInt32LE(offset); + } - let planeFrameGeometry = new Geometry(); - { - // middle line - planeFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.0)); - planeFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.0)); - planeFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.0)); - planeFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.0)); - planeFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.0)); - planeFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.0)); - planeFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.0)); - planeFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.0)); + stripByteCounts.push(value); } - this.dimension = new Vector3(1, 1, 1); - this.material = new MeshBasicMaterial( { - color: 0x00ff00, - transparent: true, - opacity: 0.3, - depthTest: true, - depthWrite: false} ); - this.box = new Mesh(boxGeometry, this.material); - this.box.geometry.computeBoundingBox(); - this.boundingBox = this.box.geometry.boundingBox; - this.add(this.box); - - this.frame = new LineSegments( boxFrameGeometry, new LineBasicMaterial({color: 0x000000})); - this.add(this.frame); - this.planeFrame = new LineSegments( planeFrameGeometry, new LineBasicMaterial({color: 0xff0000})); - this.add(this.planeFrame); + let stripOffsets = []; + for(let i = 0; i < ifdStripOffsets.count; i++){ + let type = ifdStripOffsets.type; + let offset = ifdStripOffsets.offset + i * type.bytes; - // set default thickness - this.setScaleZ(0.1); + let value; + if(type === Type.SHORT){ + value = data.readUInt16LE(offset); + }else if(type === Type.LONG){ + value = data.readUInt32LE(offset); + } - // create local coordinate system - let createArrow = (name, direction, color) => { - let material = new MeshBasicMaterial({ - color: color, - depthTest: false, - depthWrite: false}); - - let shaftGeometry = new Geometry(); - shaftGeometry.vertices.push(new Vector3(0, 0, 0)); - shaftGeometry.vertices.push(new Vector3(0, 1, 0)); - - let shaftMaterial = new LineBasicMaterial({ - color: color, - depthTest: false, - depthWrite: false, - transparent: true - }); - let shaft = new Line(shaftGeometry, shaftMaterial); - shaft.name = name + "_shaft"; - - let headGeometry = new CylinderGeometry(0, 0.04, 0.1, 10, 1, false); - let headMaterial = material; - let head = new Mesh(headGeometry, headMaterial); - head.name = name + "_head"; - head.position.y = 1; - - let arrow = new Object3D(); - arrow.name = name; - arrow.add(shaft); - arrow.add(head); + stripOffsets.push(value); + } - return arrow; - }; - - this.arrowX = createArrow("arrow_x", new Vector3(1, 0, 0), 0xFF0000); - this.arrowY = createArrow("arrow_y", new Vector3(0, 1, 0), 0x00FF00); - this.arrowZ = createArrow("arrow_z", new Vector3(0, 0, 1), 0x0000FF); + let imageBuffer = new Uint8Array(width * height * 3); - this.arrowX.rotation.z = -Math.PI / 2; - this.arrowZ.rotation.x = Math.PI / 2; - - this.arrowX.visible = false; - this.arrowY.visible = false; - this.arrowZ.visible = false; - - this.add(this.arrowX); - this.add(this.arrowY); - this.add(this.arrowZ); + let linesProcessed = 0; + for(let i = 0; i < numStrips; i++){ + let stripOffset = stripOffsets[i]; + let stripBytes = stripByteCounts[i]; + let stripData = data.slice(stripOffset, stripOffset + stripBytes); + let lineBytes = width * 3; + for(let y = 0; y < rowsPerStrip; y++){ + let line = stripData.slice(y * lineBytes, y * lineBytes + lineBytes); + imageBuffer.set(line, linesProcessed * lineBytes); - { // event listeners - this.addEventListener("ui_select", e => { - this.arrowX.visible = true; - this.arrowY.visible = true; - this.arrowZ.visible = true; - }); - this.addEventListener("ui_deselect", e => { - this.arrowX.visible = false; - this.arrowY.visible = false; - this.arrowZ.visible = false; - }); - this.addEventListener("select", e => { - let scene_header = $("#" + this.name + " .scene_header"); - if(!scene_header.next().is(":visible")) { - scene_header.click(); - } - }); - this.addEventListener("deselect", e => { - let scene_header = $("#" + this.name + " .scene_header"); - if(scene_header.next().is(":visible")) { - scene_header.click(); + if(line.length === lineBytes){ + linesProcessed++; + }else { + break; } - }); + } } - - this.update(); - }; - setClipOffset(offset) { - this.clipOffset = offset; - } + console.log(`width: ${width}`); + console.log(`height: ${height}`); + console.log(`numStrips: ${numStrips}`); + console.log("stripByteCounts", stripByteCounts.join(", ")); + console.log("stripOffsets", stripOffsets.join(", ")); - setClipRotOffset(offset) { - this.clipRotOffset = offset; - } + let image = new Image(); + image.width = width; + image.height = height; + image.buffer = imageBuffer; + image.metadata = ifds; - setScaleX(x) { - this.box.scale.x = x; - this.frame.scale.x = x; - this.planeFrame.scale.x = x; + return image; } - setScaleY(y) { - this.box.scale.y = y; - this.frame.scale.y = y; - this.planeFrame.scale.y = y; - } + } + + + class Exporter{ + + constructor(){ - setScaleZ(z) { - this.box.scale.z = z; - this.frame.scale.z = z; - this.planeFrame.scale.z = z; } - offset(args) { - let cs = args.cs || null; - let axis = args.axis || null; - let dir = args.dir || null; + static toTiffBuffer(image, params = {}){ - if(!cs || !axis || !dir) return; + let offsetToFirstIFD = 8; + + let headerBuffer = new Uint8Array([0x49, 0x49, 42, 0, offsetToFirstIFD, 0, 0, 0]); - if(axis === "x") { - if(cs === "local") { - this.position.add(this.localX.clone().multiplyScalar(dir * this.clipOffset)); - } else if(cs === "global") { - this.position.x = this.position.x + dir * this.clipOffset; - } - }else if(axis === "y") { - if(cs === "local") { - this.position.add(this.localY.clone().multiplyScalar(dir * this.clipOffset)); - } else if(cs === "global") { - this.position.y = this.position.y + dir * this.clipOffset; - } - }else if(axis === "z") { - if(cs === "local") { - this.position.add(this.localZ.clone().multiplyScalar(dir * this.clipOffset)); - } else if(cs === "global") { - this.position.z = this.position.z + dir * this.clipOffset; - } - } + let [width, height] = [image.width, image.height]; - this.dispatchEvent({"type": "clip_volume_changed", "viewer": viewer, "volume": this}); - } + let ifds = [ + new IFDEntry(Tag.IMAGE_WIDTH, Type.SHORT, 1, null, width), + new IFDEntry(Tag.IMAGE_HEIGHT, Type.SHORT, 1, null, height), + new IFDEntry(Tag.BITS_PER_SAMPLE, Type.SHORT, 4, null, new Uint16Array([8, 8, 8, 8])), + new IFDEntry(Tag.COMPRESSION, Type.SHORT, 1, null, 1), + new IFDEntry(Tag.PHOTOMETRIC_INTERPRETATION, Type.SHORT, 1, null, 2), + new IFDEntry(Tag.ORIENTATION, Type.SHORT, 1, null, 1), + new IFDEntry(Tag.SAMPLES_PER_PIXEL, Type.SHORT, 1, null, 4), + new IFDEntry(Tag.ROWS_PER_STRIP, Type.LONG, 1, null, height), + new IFDEntry(Tag.STRIP_BYTE_COUNTS, Type.LONG, 1, null, width * height * 3), + new IFDEntry(Tag.PLANAR_CONFIGURATION, Type.SHORT, 1, null, 1), + new IFDEntry(Tag.RESOLUTION_UNIT, Type.SHORT, 1, null, 1), + new IFDEntry(Tag.SOFTWARE, Type.ASCII, 6, null, "......"), + new IFDEntry(Tag.STRIP_OFFSETS, Type.LONG, 1, null, null), + new IFDEntry(Tag.X_RESOLUTION, Type.RATIONAL, 1, null, new Uint32Array([1, 1])), + new IFDEntry(Tag.Y_RESOLUTION, Type.RATIONAL, 1, null, new Uint32Array([1, 1])), + ]; - rotate(args) { - let cs = args.cs || null; - let axis = args.axis || null; - let dir = args.dir || null; + if(params.ifdEntries){ + ifds.push(...params.ifdEntries); + } - if(!cs || !axis || !dir) return; + let valueOffset = offsetToFirstIFD + 2 + ifds.length * 12 + 4; - if(cs === "local") { - if(axis === "x") { - this.rotateOnAxis(new Vector3(1, 0, 0), dir * this.clipRotOffset * Math.PI / 180); - } else if(axis === "y") { - this.rotateOnAxis(new Vector3(0, 1, 0), dir * this.clipRotOffset * Math.PI / 180); - } else if(axis === "z") { - this.rotateOnAxis(new Vector3(0, 0, 1), dir * this.clipRotOffset * Math.PI / 180); - } - } else if(cs === "global") { - let rotaxis = new Vector4(1, 0, 0, 0); - if(axis === "y") { - rotaxis = new Vector4(0, 1, 0, 0); - } else if(axis === "z") { - rotaxis = new Vector4(0, 0, 1, 0); - } - this.updateMatrixWorld(); - let invM = newthis.matrixWorld.clone().invert(); - rotaxis = rotaxis.applyMatrix4(invM).normalize(); - rotaxis = new Vector3(rotaxis.x, rotaxis.y, rotaxis.z); - this.rotateOnAxis(rotaxis, dir * this.clipRotOffset * Math.PI / 180); - } + // create 12 byte buffer for each ifd and variable length buffers for ifd values + let ifdEntryBuffers = new Map(); + let ifdValueBuffers = new Map(); + for(let ifd of ifds){ + let entryBuffer = new ArrayBuffer(12); + let entryView = new DataView(entryBuffer); - this.updateLocalSystem(); + let valueBytes = ifd.type.bytes * ifd.count; - this.dispatchEvent({"type": "clip_volume_changed", "viewer": viewer, "volume": this}); - } + entryView.setUint16(0, ifd.tag.value, true); + entryView.setUint16(2, ifd.type.value, true); + entryView.setUint32(4, ifd.count, true); - update(){ - this.boundingBox = this.box.geometry.boundingBox; - this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere()); - - this.box.visible = false; + if(ifd.count === 1 && ifd.type.bytes <= 4){ + entryView.setUint32(8, ifd.value, true); + }else { + entryView.setUint32(8, valueOffset, true); - this.updateLocalSystem(); - }; + let valueBuffer = new Uint8Array(ifd.count * ifd.type.bytes); + if(ifd.type === Type.ASCII){ + valueBuffer.set(new Uint8Array(ifd.value.split("").map(c => c.charCodeAt(0)))); + }else { + valueBuffer.set(new Uint8Array(ifd.value.buffer)); + } + ifdValueBuffers.set(ifd.tag, valueBuffer); - updateLocalSystem() { - // extract local coordinate axes - let rotQuat = this.getWorldQuaternion(); - this.localX = new Vector3(1, 0, 0).applyQuaternion(rotQuat).normalize(); - this.localY = new Vector3(0, 1, 0).applyQuaternion(rotQuat).normalize(); - this.localZ = new Vector3(0, 0, 1).applyQuaternion(rotQuat).normalize(); - } - - raycast(raycaster, intersects){ - - let is = []; - this.box.raycast(raycaster, is); - - if(is.length > 0){ - let I = is[0]; - intersects.push({ - distance: I.distance, - object: this, - point: I.point.clone() - }); + valueOffset = valueOffset + valueBuffer.byteLength; + } + + ifdEntryBuffers.set(ifd.tag, entryBuffer); } - }; - }; - class ClippingTool extends EventDispatcher{ + let imageBufferOffset = valueOffset; - constructor(viewer){ - super(); + new DataView(ifdEntryBuffers.get(Tag.STRIP_OFFSETS)).setUint32(8, imageBufferOffset, true); - this.viewer = viewer; + let concatBuffers = (buffers) => { - this.maxPolygonVertices = 8; - - this.addEventListener("start_inserting_clipping_volume", e => { - this.viewer.dispatchEvent({ - type: "cancel_insertions" - }); - }); + let totalLength = buffers.reduce( (sum, buffer) => (sum + buffer.byteLength), 0); + let merged = new Uint8Array(totalLength); - this.sceneMarker = new Scene(); - this.sceneVolume = new Scene(); - this.sceneVolume.name = "scene_clip_volume"; - this.viewer.inputHandler.registerInteractiveScene(this.sceneVolume); + let offset = 0; + for(let buffer of buffers){ + merged.set(new Uint8Array(buffer), offset); + offset += buffer.byteLength; + } - this.onRemove = e => { - this.sceneVolume.remove(e.volume); - }; - - this.onAdd = e => { - this.sceneVolume.add(e.volume); + return merged; }; - this.viewer.inputHandler.addEventListener("delete", e => { - let volumes = e.selection.filter(e => (e instanceof ClipVolume)); - volumes.forEach(e => this.viewer.scene.removeClipVolume(e)); - let polyVolumes = e.selection.filter(e => (e instanceof PolygonClipVolume)); - polyVolumes.forEach(e => this.viewer.scene.removePolygonClipVolume(e)); - }); - } - - setScene(scene){ - if(this.scene === scene){ - return; - } - - if(this.scene){ - this.scene.removeEventListeners("clip_volume_added", this.onAdd); - this.scene.removeEventListeners("clip_volume_removed", this.onRemove); - this.scene.removeEventListeners("polygon_clip_volume_added", this.onAdd); - this.scene.removeEventListeners("polygon_clip_volume_removed", this.onRemove); - } - - this.scene = scene; - - this.scene.addEventListener("clip_volume_added", this.onAdd); - this.scene.addEventListener("clip_volume_removed", this.onRemove); - this.scene.addEventListener("polygon_clip_volume_added", this.onAdd); - this.scene.addEventListener("polygon_clip_volume_removed", this.onRemove); - } - - startInsertion(args = {}) { - let type = args.type || null; + let ifdBuffer = concatBuffers([ + new Uint16Array([ifds.length]), + ...ifdEntryBuffers.values(), + new Uint32Array([0])]); + let ifdValueBuffer = concatBuffers([...ifdValueBuffers.values()]); - if(!type) return null; + let tiffBuffer = concatBuffers([ + headerBuffer, + ifdBuffer, + ifdValueBuffer, + image.buffer + ]); - let domElement = this.viewer.renderer.domElement; - let canvasSize = this.viewer.renderer.getSize(new Vector2()); + return {width: width, height: height, buffer: tiffBuffer}; + } - let svg = $(` - + } - - - - - + exports.Tag = Tag; + exports.Type = Type; + exports.IFDEntry = IFDEntry; + exports.Image = Image; + exports.Reader = Reader; + exports.Exporter = Exporter; - + return exports; - - `); - $(domElement.parentElement).append(svg); + }({})); - let polyClipVol = new PolygonClipVolume(this.viewer.scene.getActiveCamera().clone()); + function updateAzimuth(viewer, measure){ - this.dispatchEvent({"type": "start_inserting_clipping_volume"}); + const azimuth = measure.azimuth; - this.viewer.scene.addPolygonClipVolume(polyClipVol); - this.sceneMarker.add(polyClipVol); + const isOkay = measure.points.length === 2; - let cancel = { - callback: null - }; + azimuth.node.visible = isOkay && measure.showAzimuth; - let insertionCallback = (e) => { - if(e.button === MOUSE.LEFT){ - - polyClipVol.addMarker(); + if(!azimuth.node.visible){ + return; + } - // SVC Screen Line - svg.find("polyline").each((index, target) => { - let newPoint = svg[0].createSVGPoint(); - newPoint.x = e.offsetX; - newPoint.y = e.offsetY; - let polyline = target.points.appendItem(newPoint); - }); - - - if(polyClipVol.markers.length > this.maxPolygonVertices){ - cancel.callback(); - } - - this.viewer.inputHandler.startDragging( - polyClipVol.markers[polyClipVol.markers.length - 1]); - }else if(e.button === MOUSE.RIGHT){ - cancel.callback(e); - } - }; - - cancel.callback = e => { + const camera = viewer.scene.getActiveCamera(); + const renderAreaSize = viewer.renderer.getSize(new Vector2()); + const width = renderAreaSize.width; + const height = renderAreaSize.height; + + const [p0, p1] = measure.points; + const r = p0.position.distanceTo(p1.position); + const northVec = Utils.getNorthVec(p0.position, r, viewer.getProjection()); + const northPos = p0.position.clone().add(northVec); - //let first = svg.find("polyline")[0].points[0]; - //svg.find("polyline").each((index, target) => { - // let newPoint = svg[0].createSVGPoint(); - // newPoint.x = first.x; - // newPoint.y = first.y; - // let polyline = target.points.appendItem(newPoint); - //}); - svg.remove(); + azimuth.center.position.copy(p0.position); + azimuth.center.scale.set(2, 2, 2); + + azimuth.center.visible = false; + // azimuth.target.visible = false; - if(polyClipVol.markers.length > 3) { - polyClipVol.removeLastMarker(); - polyClipVol.initialized = true; - } else { - this.viewer.scene.removePolygonClipVolume(polyClipVol); - } - this.viewer.renderer.domElement.removeEventListener("mouseup", insertionCallback, true); - this.viewer.removeEventListener("cancel_insertions", cancel.callback); - this.viewer.inputHandler.enabled = true; - }; - - this.viewer.addEventListener("cancel_insertions", cancel.callback); - this.viewer.renderer.domElement.addEventListener("mouseup", insertionCallback , true); - this.viewer.inputHandler.enabled = false; - - polyClipVol.addMarker(); - this.viewer.inputHandler.startDragging( - polyClipVol.markers[polyClipVol.markers.length - 1]); + { // north + azimuth.north.position.copy(northPos); + azimuth.north.scale.set(2, 2, 2); - return polyClipVol; + let distance = azimuth.north.position.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, width, height); + + let scale = (5 / pr); + azimuth.north.scale.set(scale, scale, scale); } - update() { + { // target + azimuth.target.position.copy(p1.position); + azimuth.target.position.z = azimuth.north.position.z; - } - }; + let distance = azimuth.target.position.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, width, height); - var GeoTIFF = (function (exports) { - 'use strict'; + let scale = (5 / pr); + azimuth.target.scale.set(scale, scale, scale); + } - const Endianness = new Enum({ - LITTLE: "II", - BIG: "MM", - }); - const Type = new Enum({ - BYTE: {value: 1, bytes: 1}, - ASCII: {value: 2, bytes: 1}, - SHORT: {value: 3, bytes: 2}, - LONG: {value: 4, bytes: 4}, - RATIONAL: {value: 5, bytes: 8}, - SBYTE: {value: 6, bytes: 1}, - UNDEFINED: {value: 7, bytes: 1}, - SSHORT: {value: 8, bytes: 2}, - SLONG: {value: 9, bytes: 4}, - SRATIONAL: {value: 10, bytes: 8}, - FLOAT: {value: 11, bytes: 4}, - DOUBLE: {value: 12, bytes: 8}, - }); + azimuth.circle.position.copy(p0.position); + azimuth.circle.scale.set(r, r, r); + azimuth.circle.material.resolution.set(width, height); - const Tag = new Enum({ - IMAGE_WIDTH: 256, - IMAGE_HEIGHT: 257, - BITS_PER_SAMPLE: 258, - COMPRESSION: 259, - PHOTOMETRIC_INTERPRETATION: 262, - STRIP_OFFSETS: 273, - ORIENTATION: 274, - SAMPLES_PER_PIXEL: 277, - ROWS_PER_STRIP: 278, - STRIP_BYTE_COUNTS: 279, - X_RESOLUTION: 282, - Y_RESOLUTION: 283, - PLANAR_CONFIGURATION: 284, - RESOLUTION_UNIT: 296, - SOFTWARE: 305, - COLOR_MAP: 320, - SAMPLE_FORMAT: 339, - MODEL_PIXEL_SCALE: 33550, // [GeoTIFF] TYPE: double N: 3 - MODEL_TIEPOINT: 33922, // [GeoTIFF] TYPE: double N: 6 * NUM_TIEPOINTS - GEO_KEY_DIRECTORY: 34735, // [GeoTIFF] TYPE: short N: >= 4 - GEO_DOUBLE_PARAMS: 34736, // [GeoTIFF] TYPE: short N: variable - GEO_ASCII_PARAMS: 34737, // [GeoTIFF] TYPE: ascii N: variable - }); + // to target + azimuth.centerToTarget.geometry.setPositions([ + 0, 0, 0, + ...p1.position.clone().sub(p0.position).toArray(), + ]); + azimuth.centerToTarget.position.copy(p0.position); + azimuth.centerToTarget.geometry.verticesNeedUpdate = true; + azimuth.centerToTarget.geometry.computeBoundingSphere(); + azimuth.centerToTarget.computeLineDistances(); + azimuth.centerToTarget.material.resolution.set(width, height); - const typeMapping = new Map([ - [Type.BYTE, Uint8Array], - [Type.ASCII, Uint8Array], - [Type.SHORT, Uint16Array], - [Type.LONG, Uint32Array], - [Type.RATIONAL, Uint32Array], - [Type.SBYTE, Int8Array], - [Type.UNDEFINED, Uint8Array], - [Type.SSHORT, Int16Array], - [Type.SLONG, Int32Array], - [Type.SRATIONAL, Int32Array], - [Type.FLOAT, Float32Array], - [Type.DOUBLE, Float64Array], - ]); + // to target ground + azimuth.centerToTargetground.geometry.setPositions([ + 0, 0, 0, + p1.position.x - p0.position.x, + p1.position.y - p0.position.y, + 0, + ]); + azimuth.centerToTargetground.position.copy(p0.position); + azimuth.centerToTargetground.geometry.verticesNeedUpdate = true; + azimuth.centerToTargetground.geometry.computeBoundingSphere(); + azimuth.centerToTargetground.computeLineDistances(); + azimuth.centerToTargetground.material.resolution.set(width, height); - class IFDEntry{ + // to north + azimuth.centerToNorth.geometry.setPositions([ + 0, 0, 0, + northPos.x - p0.position.x, + northPos.y - p0.position.y, + 0, + ]); + azimuth.centerToNorth.position.copy(p0.position); + azimuth.centerToNorth.geometry.verticesNeedUpdate = true; + azimuth.centerToNorth.geometry.computeBoundingSphere(); + azimuth.centerToNorth.computeLineDistances(); + azimuth.centerToNorth.material.resolution.set(width, height); - constructor(tag, type, count, offset, value){ - this.tag = tag; - this.type = type; - this.count = count; - this.offset = offset; - this.value = value; + // label + const radians = Utils.computeAzimuth(p0.position, p1.position, viewer.getProjection()); + let degrees = MathUtils.radToDeg(radians); + if(degrees < 0){ + degrees = 360 + degrees; } - + const txtDegrees = `${degrees.toFixed(2)}°`; + const labelDir = northPos.clone().add(p1.position).multiplyScalar(0.5).sub(p0.position); + if(labelDir.length() > 0){ + labelDir.z = 0; + labelDir.normalize(); + const labelVec = labelDir.clone().multiplyScalar(r); + const labelPos = p0.position.clone().add(labelVec); + azimuth.label.position.copy(labelPos); + } + azimuth.label.setText(txtDegrees); + let distance = azimuth.label.position.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, width, height); + let scale = (70 / pr); + azimuth.label.scale.set(scale, scale, scale); } - class Image{ + class MeasuringTool extends EventDispatcher{ + constructor (viewer) { + super(); - constructor(){ - this.width = 0; - this.height = 0; - this.buffer = null; - this.metadata = []; - } + this.viewer = viewer; + this.renderer = viewer.renderer; - } + this.addEventListener('start_inserting_measurement', e => { + this.viewer.dispatchEvent({ + type: 'cancel_insertions' + }); + }); - class Reader{ + this.showLabels = true; + this.scene = new Scene(); + this.scene.name = 'scene_measurement'; + this.light = new PointLight(0xffffff, 1.0); + this.scene.add(this.light); - constructor(){ + this.viewer.inputHandler.registerInteractiveScene(this.scene); + + this.onRemove = (e) => { this.scene.remove(e.measurement);}; + this.onAdd = e => {this.scene.add(e.measurement);}; + + for(let measurement of viewer.scene.measurements){ + this.onAdd({measurement: measurement}); + } + + viewer.addEventListener("update", this.update.bind(this)); + viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this)); + viewer.addEventListener("scene_changed", this.onSceneChange.bind(this)); + viewer.scene.addEventListener('measurement_added', this.onAdd); + viewer.scene.addEventListener('measurement_removed', this.onRemove); } - static read(data){ - - let endiannessTag = String.fromCharCode(...Array.from(data.slice(0, 2))); - let endianness = Endianness.fromValue(endiannessTag); + onSceneChange(e){ + if(e.oldScene){ + e.oldScene.removeEventListener('measurement_added', this.onAdd); + e.oldScene.removeEventListener('measurement_removed', this.onRemove); + } - let tiffCheckTag = data.readUInt8(2); + e.scene.addEventListener('measurement_added', this.onAdd); + e.scene.addEventListener('measurement_removed', this.onRemove); + } - if(tiffCheckTag !== 42){ - throw new Error("not a valid tiff file"); - } + startInsertion (args = {}) { + let domElement = this.viewer.renderer.domElement; - let offsetToFirstIFD = data.readUInt32LE(4); + let measure = new Measure(); - console.log("offsetToFirstIFD", offsetToFirstIFD); + this.dispatchEvent({ + type: 'start_inserting_measurement', + measure: measure + }); - let ifds = []; - let IFDsRead = false; - let currentIFDOffset = offsetToFirstIFD; - let i = 0; - while(IFDsRead || i < 100){ + const pick = (defaul, alternative) => { + if(defaul != null){ + return defaul; + }else { + return alternative; + } + }; - console.log("currentIFDOffset", currentIFDOffset); - let numEntries = data.readUInt16LE(currentIFDOffset); - let nextIFDOffset = data.readUInt32LE(currentIFDOffset + 2 + numEntries * 12); + measure.showDistances = (args.showDistances === null) ? true : args.showDistances; - console.log("next offset: ", currentIFDOffset + 2 + numEntries * 12); + measure.showArea = pick(args.showArea, false); + measure.showAngles = pick(args.showAngles, false); + measure.showCoordinates = pick(args.showCoordinates, false); + measure.showHeight = pick(args.showHeight, false); + measure.showCircle = pick(args.showCircle, false); + measure.showAzimuth = pick(args.showAzimuth, false); + measure.showEdges = pick(args.showEdges, true); + measure.closed = pick(args.closed, false); + measure.maxMarkers = pick(args.maxMarkers, Infinity); - let entryBuffer = data.slice(currentIFDOffset + 2, currentIFDOffset + 2 + 12 * numEntries); + measure.name = args.name || 'Measurement'; - for(let i = 0; i < numEntries; i++){ - let tag = Tag.fromValue(entryBuffer.readUInt16LE(i * 12)); - let type = Type.fromValue(entryBuffer.readUInt16LE(i * 12 + 2)); - let count = entryBuffer.readUInt32LE(i * 12 + 4); - let offsetOrValue = entryBuffer.readUInt32LE(i * 12 + 8); - let valueBytes = type.bytes * count; + this.scene.add(measure); - let value; - if(valueBytes <= 4){ - value = offsetOrValue; - }else { - let valueBuffer = new Uint8Array(valueBytes); - valueBuffer.set(data.slice(offsetOrValue, offsetOrValue + valueBytes)); - - let ArrayType = typeMapping.get(type); + let cancel = { + removeLastMarker: measure.maxMarkers > 3, + callback: null + }; - value = new ArrayType(valueBuffer.buffer); + let insertionCallback = (e) => { + if (e.button === MOUSE.LEFT) { + measure.addMarker(measure.points[measure.points.length - 1].position.clone()); - if(type === Type.ASCII){ - value = String.fromCharCode(...value); - } + if (measure.points.length >= measure.maxMarkers) { + cancel.callback(); } - let ifd = new IFDEntry(tag, type, count, offsetOrValue, value); - - ifds.push(ifd); + this.viewer.inputHandler.startDragging( + measure.spheres[measure.spheres.length - 1]); + } else if (e.button === MOUSE.RIGHT) { + cancel.callback(); } + }; - console.log("nextIFDOffset", nextIFDOffset); - - if(nextIFDOffset === 0){ - break; + cancel.callback = e => { + if (cancel.removeLastMarker) { + measure.removeMarker(measure.points.length - 1); } + domElement.removeEventListener('mouseup', insertionCallback, true); + this.viewer.removeEventListener('cancel_insertions', cancel.callback); + }; - currentIFDOffset = nextIFDOffset; - i++; + if (measure.maxMarkers > 1) { + this.viewer.addEventListener('cancel_insertions', cancel.callback); + domElement.addEventListener('mouseup', insertionCallback, true); } - let ifdForTag = (tag) => { - for(let entry of ifds){ - if(entry.tag === tag){ - return entry; - } - } + measure.addMarker(new Vector3(0, 0, 0)); + this.viewer.inputHandler.startDragging( + measure.spheres[measure.spheres.length - 1]); - return null; - }; + this.viewer.scene.addMeasurement(measure); - let width = ifdForTag(Tag.IMAGE_WIDTH, ifds).value; - let height = ifdForTag(Tag.IMAGE_HEIGHT, ifds).value; - let compression = ifdForTag(Tag.COMPRESSION, ifds).value; - let rowsPerStrip = ifdForTag(Tag.ROWS_PER_STRIP, ifds).value; - let ifdStripOffsets = ifdForTag(Tag.STRIP_OFFSETS, ifds); - let ifdStripByteCounts = ifdForTag(Tag.STRIP_BYTE_COUNTS, ifds); + return measure; + } + + update(){ + let camera = this.viewer.scene.getActiveCamera(); + let domElement = this.renderer.domElement; + let measurements = this.viewer.scene.measurements; - let numStrips = Math.ceil(height / rowsPerStrip); + const renderAreaSize = this.renderer.getSize(new Vector2()); + let clientWidth = renderAreaSize.width; + let clientHeight = renderAreaSize.height; - let stripByteCounts = []; - for(let i = 0; i < ifdStripByteCounts.count; i++){ - let type = ifdStripByteCounts.type; - let offset = ifdStripByteCounts.offset + i * type.bytes; + this.light.position.copy(camera.position); - let value; - if(type === Type.SHORT){ - value = data.readUInt16LE(offset); - }else if(type === Type.LONG){ - value = data.readUInt32LE(offset); + // make size independant of distance + for (let measure of measurements) { + measure.lengthUnit = this.viewer.lengthUnit; + measure.lengthUnitDisplay = this.viewer.lengthUnitDisplay; + measure.update(); + + updateAzimuth(this.viewer, measure); + + // spheres + for(let sphere of measure.spheres){ + let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3())); + let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); + let scale = (15 / pr); + sphere.scale.set(scale, scale, scale); } - stripByteCounts.push(value); - } + // labels + let labels = measure.edgeLabels.concat(measure.angleLabels); + for(let label of labels){ + let distance = camera.position.distanceTo(label.getWorldPosition(new Vector3())); + let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); + let scale = (70 / pr); - let stripOffsets = []; - for(let i = 0; i < ifdStripOffsets.count; i++){ - let type = ifdStripOffsets.type; - let offset = ifdStripOffsets.offset + i * type.bytes; + if(Potree.debug.scale){ + scale = (Potree.debug.scale / pr); + } - let value; - if(type === Type.SHORT){ - value = data.readUInt16LE(offset); - }else if(type === Type.LONG){ - value = data.readUInt32LE(offset); + label.scale.set(scale, scale, scale); } - stripOffsets.push(value); - } + // coordinate labels + for (let j = 0; j < measure.coordinateLabels.length; j++) { + let label = measure.coordinateLabels[j]; + let sphere = measure.spheres[j]; + + let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3())); + + let screenPos = sphere.getWorldPosition(new Vector3()).clone().project(camera); + screenPos.x = Math.round((screenPos.x + 1) * clientWidth / 2); + screenPos.y = Math.round((-screenPos.y + 1) * clientHeight / 2); + screenPos.z = 0; + screenPos.y -= 30; + + let labelPos = new Vector3( + (screenPos.x / clientWidth) * 2 - 1, + -(screenPos.y / clientHeight) * 2 + 1, + 0.5 ); + labelPos.unproject(camera); + if(this.viewer.scene.cameraMode == CameraMode.PERSPECTIVE) { + let direction = labelPos.sub(camera.position).normalize(); + labelPos = new Vector3().addVectors( + camera.position, direction.multiplyScalar(distance)); - let imageBuffer = new Uint8Array(width * height * 3); - - let linesProcessed = 0; - for(let i = 0; i < numStrips; i++){ - let stripOffset = stripOffsets[i]; - let stripBytes = stripByteCounts[i]; - let stripData = data.slice(stripOffset, stripOffset + stripBytes); - let lineBytes = width * 3; - for(let y = 0; y < rowsPerStrip; y++){ - let line = stripData.slice(y * lineBytes, y * lineBytes + lineBytes); - imageBuffer.set(line, linesProcessed * lineBytes); - - if(line.length === lineBytes){ - linesProcessed++; - }else { - break; } + label.position.copy(labelPos); + let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); + let scale = (70 / pr); + label.scale.set(scale, scale, scale); } - } - console.log(`width: ${width}`); - console.log(`height: ${height}`); - console.log(`numStrips: ${numStrips}`); - console.log("stripByteCounts", stripByteCounts.join(", ")); - console.log("stripOffsets", stripOffsets.join(", ")); + // height label + if (measure.showHeight) { + let label = measure.heightLabel; - let image = new Image(); - image.width = width; - image.height = height; - image.buffer = imageBuffer; - image.metadata = ifds; + { + let distance = label.position.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); + let scale = (70 / pr); + label.scale.set(scale, scale, scale); + } - return image; - } + { // height edge + let edge = measure.heightEdge; + + let sorted = measure.points.slice().sort((a, b) => a.position.z - b.position.z); + let lowPoint = sorted[0].position.clone(); + let highPoint = sorted[sorted.length - 1].position.clone(); + let min = lowPoint.z; + let max = highPoint.z; - } + let start = new Vector3(highPoint.x, highPoint.y, min); + let end = new Vector3(highPoint.x, highPoint.y, max); + let lowScreen = lowPoint.clone().project(camera); + let startScreen = start.clone().project(camera); + let endScreen = end.clone().project(camera); - class Exporter{ + let toPixelCoordinates = v => { + let r = v.clone().addScalar(1).divideScalar(2); + r.x = r.x * clientWidth; + r.y = r.y * clientHeight; + r.z = 0; - constructor(){ + return r; + }; - } + let lowEL = toPixelCoordinates(lowScreen); + let startEL = toPixelCoordinates(startScreen); + let endEL = toPixelCoordinates(endScreen); - static toTiffBuffer(image, params = {}){ + let lToS = lowEL.distanceTo(startEL); + let sToE = startEL.distanceTo(endEL); - let offsetToFirstIFD = 8; - - let headerBuffer = new Uint8Array([0x49, 0x49, 42, 0, offsetToFirstIFD, 0, 0, 0]); + edge.geometry.lineDistances = [0, lToS, lToS, lToS + sToE]; + edge.geometry.lineDistancesNeedUpdate = true; - let [width, height] = [image.width, image.height]; + edge.material.dashSize = 10; + edge.material.gapSize = 10; + } + } - let ifds = [ - new IFDEntry(Tag.IMAGE_WIDTH, Type.SHORT, 1, null, width), - new IFDEntry(Tag.IMAGE_HEIGHT, Type.SHORT, 1, null, height), - new IFDEntry(Tag.BITS_PER_SAMPLE, Type.SHORT, 4, null, new Uint16Array([8, 8, 8, 8])), - new IFDEntry(Tag.COMPRESSION, Type.SHORT, 1, null, 1), - new IFDEntry(Tag.PHOTOMETRIC_INTERPRETATION, Type.SHORT, 1, null, 2), - new IFDEntry(Tag.ORIENTATION, Type.SHORT, 1, null, 1), - new IFDEntry(Tag.SAMPLES_PER_PIXEL, Type.SHORT, 1, null, 4), - new IFDEntry(Tag.ROWS_PER_STRIP, Type.LONG, 1, null, height), - new IFDEntry(Tag.STRIP_BYTE_COUNTS, Type.LONG, 1, null, width * height * 3), - new IFDEntry(Tag.PLANAR_CONFIGURATION, Type.SHORT, 1, null, 1), - new IFDEntry(Tag.RESOLUTION_UNIT, Type.SHORT, 1, null, 1), - new IFDEntry(Tag.SOFTWARE, Type.ASCII, 6, null, "......"), - new IFDEntry(Tag.STRIP_OFFSETS, Type.LONG, 1, null, null), - new IFDEntry(Tag.X_RESOLUTION, Type.RATIONAL, 1, null, new Uint32Array([1, 1])), - new IFDEntry(Tag.Y_RESOLUTION, Type.RATIONAL, 1, null, new Uint32Array([1, 1])), - ]; + { // area label + let label = measure.areaLabel; + let distance = label.position.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); - if(params.ifdEntries){ - ifds.push(...params.ifdEntries); - } + let scale = (70 / pr); + label.scale.set(scale, scale, scale); + } - let valueOffset = offsetToFirstIFD + 2 + ifds.length * 12 + 4; + { // radius label + let label = measure.circleRadiusLabel; + let distance = label.position.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); - // create 12 byte buffer for each ifd and variable length buffers for ifd values - let ifdEntryBuffers = new Map(); - let ifdValueBuffers = new Map(); - for(let ifd of ifds){ - let entryBuffer = new ArrayBuffer(12); - let entryView = new DataView(entryBuffer); + let scale = (70 / pr); + label.scale.set(scale, scale, scale); + } - let valueBytes = ifd.type.bytes * ifd.count; + { // edges + const materials = [ + measure.circleRadiusLine.material, + ...measure.edges.map( (e) => e.material), + measure.heightEdge.material, + measure.circleLine.material, + ]; - entryView.setUint16(0, ifd.tag.value, true); - entryView.setUint16(2, ifd.type.value, true); - entryView.setUint32(4, ifd.count, true); + for(const material of materials){ + material.resolution.set(clientWidth, clientHeight); + } + } - if(ifd.count === 1 && ifd.type.bytes <= 4){ - entryView.setUint32(8, ifd.value, true); - }else { - entryView.setUint32(8, valueOffset, true); + if(!this.showLabels){ - let valueBuffer = new Uint8Array(ifd.count * ifd.type.bytes); - if(ifd.type === Type.ASCII){ - valueBuffer.set(new Uint8Array(ifd.value.split("").map(c => c.charCodeAt(0)))); - }else { - valueBuffer.set(new Uint8Array(ifd.value.buffer)); - } - ifdValueBuffers.set(ifd.tag, valueBuffer); + const labels = [ + ...measure.sphereLabels, + ...measure.edgeLabels, + ...measure.angleLabels, + ...measure.coordinateLabels, + measure.heightLabel, + measure.areaLabel, + measure.circleRadiusLabel, + ]; - valueOffset = valueOffset + valueBuffer.byteLength; + for(const label of labels){ + label.visible = false; + } } - - ifdEntryBuffers.set(ifd.tag, entryBuffer); } + } - let imageBufferOffset = valueOffset; + render(){ + this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera()); + } + }; - new DataView(ifdEntryBuffers.get(Tag.STRIP_OFFSETS)).setUint32(8, imageBufferOffset, true); + class Message{ - let concatBuffers = (buffers) => { + constructor(content){ + this.content = content; - let totalLength = buffers.reduce( (sum, buffer) => (sum + buffer.byteLength), 0); - let merged = new Uint8Array(totalLength); + let closeIcon = `${exports.resourcePath}/icons/close.svg`; - let offset = 0; - for(let buffer of buffers){ - merged.set(new Uint8Array(buffer), offset); - offset += buffer.byteLength; - } + this.element = $(` +
+ + +
`); - return merged; - }; - - let ifdBuffer = concatBuffers([ - new Uint16Array([ifds.length]), - ...ifdEntryBuffers.values(), - new Uint32Array([0])]); - let ifdValueBuffer = concatBuffers([...ifdValueBuffers.values()]); + this.elClose = this.element.find("img[name=close]"); - let tiffBuffer = concatBuffers([ - headerBuffer, - ifdBuffer, - ifdValueBuffer, - image.buffer - ]); + this.elContainer = this.element.find("span[name=content_container]"); - return {width: width, height: height, buffer: tiffBuffer}; + if(typeof content === "string"){ + this.elContainer.append($(`${content}`)); + }else { + this.elContainer.append(content); + } + + } + + setMessage(content){ + this.elContainer.empty(); + if(typeof content === "string"){ + this.elContainer.append($(`${content}`)); + }else { + this.elContainer.append(content); + } } } - exports.Tag = Tag; - exports.Type = Type; - exports.IFDEntry = IFDEntry; - exports.Image = Image; - exports.Reader = Reader; - exports.Exporter = Exporter; + class PointCloudSM{ - return exports; + constructor(potreeRenderer){ - }({})); + this.potreeRenderer = potreeRenderer; + this.threeRenderer = this.potreeRenderer.threeRenderer; - function updateAzimuth(viewer, measure){ + this.target = new WebGLRenderTarget(2 * 1024, 2 * 1024, { + minFilter: LinearFilter, + magFilter: LinearFilter, + format: RGBAFormat, + type: FloatType + }); + this.target.depthTexture = new DepthTexture(); + this.target.depthTexture.type = UnsignedIntType; - const azimuth = measure.azimuth; + //this.threeRenderer.setClearColor(0x000000, 1); + this.threeRenderer.setClearColor(0xff0000, 1); - const isOkay = measure.points.length === 2; + //HACK? removed while moving to three.js 109 + //this.threeRenderer.clearTarget(this.target, true, true, true); + { + const oldTarget = this.threeRenderer.getRenderTarget(); - azimuth.node.visible = isOkay && measure.showAzimuth; + this.threeRenderer.setRenderTarget(this.target); + this.threeRenderer.clear(true, true, true); - if(!azimuth.node.visible){ - return; + this.threeRenderer.setRenderTarget(oldTarget); + } } - const camera = viewer.scene.getActiveCamera(); - const renderAreaSize = viewer.renderer.getSize(new Vector2()); - const width = renderAreaSize.width; - const height = renderAreaSize.height; - - const [p0, p1] = measure.points; - const r = p0.position.distanceTo(p1.position); - const northVec = Utils.getNorthVec(p0.position, r, viewer.getProjection()); - const northPos = p0.position.clone().add(northVec); - - azimuth.center.position.copy(p0.position); - azimuth.center.scale.set(2, 2, 2); - - azimuth.center.visible = false; - // azimuth.target.visible = false; - + setLight(light){ + this.light = light; - { // north - azimuth.north.position.copy(northPos); - azimuth.north.scale.set(2, 2, 2); + let fov = (180 * light.angle) / Math.PI; + let aspect = light.shadow.mapSize.width / light.shadow.mapSize.height; + let near = 0.1; + let far = light.distance === 0 ? 10000 : light.distance; + this.camera = new PerspectiveCamera(fov, aspect, near, far); + this.camera.up.set(0, 0, 1); + this.camera.position.copy(light.position); - let distance = azimuth.north.position.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, width, height); + let target = new Vector3().subVectors(light.position, light.getWorldDirection(new Vector3())); + this.camera.lookAt(target); - let scale = (5 / pr); - azimuth.north.scale.set(scale, scale, scale); + this.camera.updateProjectionMatrix(); + this.camera.updateMatrix(); + this.camera.updateMatrixWorld(); + this.camera.matrixWorldInverse.copy(this.camera.matrixWorld).invert(); } - { // target - azimuth.target.position.copy(p1.position); - azimuth.target.position.z = azimuth.north.position.z; - - let distance = azimuth.target.position.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, width, height); - - let scale = (5 / pr); - azimuth.target.scale.set(scale, scale, scale); + setSize(width, height){ + if(this.target.width !== width || this.target.height !== height){ + this.target.dispose(); + } + this.target.setSize(width, height); } + render(scene, camera){ - azimuth.circle.position.copy(p0.position); - azimuth.circle.scale.set(r, r, r); - azimuth.circle.material.resolution.set(width, height); - - // to target - azimuth.centerToTarget.geometry.setPositions([ - 0, 0, 0, - ...p1.position.clone().sub(p0.position).toArray(), - ]); - azimuth.centerToTarget.position.copy(p0.position); - azimuth.centerToTarget.geometry.verticesNeedUpdate = true; - azimuth.centerToTarget.geometry.computeBoundingSphere(); - azimuth.centerToTarget.computeLineDistances(); - azimuth.centerToTarget.material.resolution.set(width, height); + this.threeRenderer.setClearColor(0x000000, 1); + + const oldTarget = this.threeRenderer.getRenderTarget(); - // to target ground - azimuth.centerToTargetground.geometry.setPositions([ - 0, 0, 0, - p1.position.x - p0.position.x, - p1.position.y - p0.position.y, - 0, - ]); - azimuth.centerToTargetground.position.copy(p0.position); - azimuth.centerToTargetground.geometry.verticesNeedUpdate = true; - azimuth.centerToTargetground.geometry.computeBoundingSphere(); - azimuth.centerToTargetground.computeLineDistances(); - azimuth.centerToTargetground.material.resolution.set(width, height); + this.threeRenderer.setRenderTarget(this.target); + this.threeRenderer.clear(true, true, true); - // to north - azimuth.centerToNorth.geometry.setPositions([ - 0, 0, 0, - northPos.x - p0.position.x, - northPos.y - p0.position.y, - 0, - ]); - azimuth.centerToNorth.position.copy(p0.position); - azimuth.centerToNorth.geometry.verticesNeedUpdate = true; - azimuth.centerToNorth.geometry.computeBoundingSphere(); - azimuth.centerToNorth.computeLineDistances(); - azimuth.centerToNorth.material.resolution.set(width, height); + this.potreeRenderer.render(scene, this.camera, this.target, {}); - // label - const radians = Utils.computeAzimuth(p0.position, p1.position, viewer.getProjection()); - let degrees = MathUtils.radToDeg(radians); - if(degrees < 0){ - degrees = 360 + degrees; - } - const txtDegrees = `${degrees.toFixed(2)}°`; - const labelDir = northPos.clone().add(p1.position).multiplyScalar(0.5).sub(p0.position); - if(labelDir.length() > 0){ - labelDir.z = 0; - labelDir.normalize(); - const labelVec = labelDir.clone().multiplyScalar(r); - const labelPos = p0.position.clone().add(labelVec); - azimuth.label.position.copy(labelPos); + this.threeRenderer.setRenderTarget(oldTarget); } - azimuth.label.setText(txtDegrees); - let distance = azimuth.label.position.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, width, height); - let scale = (70 / pr); - azimuth.label.scale.set(scale, scale, scale); + + } - class MeasuringTool extends EventDispatcher{ + class ProfileTool extends EventDispatcher { constructor (viewer) { super(); this.viewer = viewer; this.renderer = viewer.renderer; - this.addEventListener('start_inserting_measurement', e => { + this.addEventListener('start_inserting_profile', e => { this.viewer.dispatchEvent({ type: 'cancel_insertions' }); }); - this.showLabels = true; this.scene = new Scene(); - this.scene.name = 'scene_measurement'; + this.scene.name = 'scene_profile'; this.light = new PointLight(0xffffff, 1.0); this.scene.add(this.light); this.viewer.inputHandler.registerInteractiveScene(this.scene); - this.onRemove = (e) => { this.scene.remove(e.measurement);}; - this.onAdd = e => {this.scene.add(e.measurement);}; + this.onRemove = e => this.scene.remove(e.profile); + this.onAdd = e => this.scene.add(e.profile); - for(let measurement of viewer.scene.measurements){ - this.onAdd({measurement: measurement}); + for(let profile of viewer.scene.profiles){ + this.onAdd({profile: profile}); } viewer.addEventListener("update", this.update.bind(this)); viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this)); viewer.addEventListener("scene_changed", this.onSceneChange.bind(this)); - viewer.scene.addEventListener('measurement_added', this.onAdd); - viewer.scene.addEventListener('measurement_removed', this.onRemove); + viewer.scene.addEventListener('profile_added', this.onAdd); + viewer.scene.addEventListener('profile_removed', this.onRemove); } onSceneChange(e){ if(e.oldScene){ - e.oldScene.removeEventListener('measurement_added', this.onAdd); - e.oldScene.removeEventListener('measurement_removed', this.onRemove); + e.oldScene.removeEventListeners('profile_added', this.onAdd); + e.oldScene.removeEventListeners('profile_removed', this.onRemove); } - e.scene.addEventListener('measurement_added', this.onAdd); - e.scene.addEventListener('measurement_removed', this.onRemove); + e.scene.addEventListener('profile_added', this.onAdd); + e.scene.addEventListener('profile_removed', this.onRemove); } startInsertion (args = {}) { let domElement = this.viewer.renderer.domElement; - let measure = new Measure(); + let profile = new Profile(); + profile.name = args.name || 'Profile'; this.dispatchEvent({ - type: 'start_inserting_measurement', - measure: measure + type: 'start_inserting_profile', + profile: profile }); - const pick = (defaul, alternative) => { - if(defaul != null){ - return defaul; - }else { - return alternative; - } + this.scene.add(profile); + + let cancel = { + callback: null }; - measure.showDistances = (args.showDistances === null) ? true : args.showDistances; + let insertionCallback = (e) => { + if(e.button === MOUSE.LEFT){ + if(profile.points.length <= 1){ + let camera = this.viewer.scene.getActiveCamera(); + let distance = camera.position.distanceTo(profile.points[0]); + let clientSize = this.viewer.renderer.getSize(new Vector2()); + let pr = Utils.projectedRadius(1, camera, distance, clientSize.width, clientSize.height); + let width = (10 / pr); - measure.showArea = pick(args.showArea, false); - measure.showAngles = pick(args.showAngles, false); - measure.showCoordinates = pick(args.showCoordinates, false); - measure.showHeight = pick(args.showHeight, false); - measure.showCircle = pick(args.showCircle, false); - measure.showAzimuth = pick(args.showAzimuth, false); - measure.showEdges = pick(args.showEdges, true); - measure.closed = pick(args.closed, false); - measure.maxMarkers = pick(args.maxMarkers, Infinity); + profile.setWidth(width); + } - measure.name = args.name || 'Measurement'; + profile.addMarker(profile.points[profile.points.length - 1].clone()); - this.scene.add(measure); + this.viewer.inputHandler.startDragging( + profile.spheres[profile.spheres.length - 1]); + } else if (e.button === MOUSE.RIGHT) { + cancel.callback(); + } + }; - let cancel = { - removeLastMarker: measure.maxMarkers > 3, - callback: null + cancel.callback = e => { + profile.removeMarker(profile.points.length - 1); + domElement.removeEventListener('mouseup', insertionCallback, true); + this.viewer.removeEventListener('cancel_insertions', cancel.callback); }; - let insertionCallback = (e) => { - if (e.button === MOUSE.LEFT) { - measure.addMarker(measure.points[measure.points.length - 1].position.clone()); + this.viewer.addEventListener('cancel_insertions', cancel.callback); + domElement.addEventListener('mouseup', insertionCallback, true); - if (measure.points.length >= measure.maxMarkers) { - cancel.callback(); - } + profile.addMarker(new Vector3(0, 0, 0)); + this.viewer.inputHandler.startDragging( + profile.spheres[profile.spheres.length - 1]); + + this.viewer.scene.addProfile(profile); + + return profile; + } + + update(){ + let camera = this.viewer.scene.getActiveCamera(); + let profiles = this.viewer.scene.profiles; + let renderAreaSize = this.viewer.renderer.getSize(new Vector2()); + let clientWidth = renderAreaSize.width; + let clientHeight = renderAreaSize.height; + + this.light.position.copy(camera.position); + + // make size independant of distance + for(let profile of profiles){ + for(let sphere of profile.spheres){ + let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3())); + let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); + let scale = (15 / pr); + sphere.scale.set(scale, scale, scale); + } + } + } + + render(){ + this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera()); + } + + } + + class ScreenBoxSelectTool extends EventDispatcher{ + + constructor(viewer){ + super(); + + this.viewer = viewer; + this.scene = new Scene(); + + viewer.addEventListener("update", this.update.bind(this)); + viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this)); + viewer.addEventListener("scene_changed", this.onSceneChange.bind(this)); + } + + onSceneChange(scene){ + console.log("scene changed"); + } + + startInsertion(){ + let domElement = this.viewer.renderer.domElement; + + let volume = new BoxVolume(); + volume.position.set(12345, 12345, 12345); + volume.showVolumeLabel = false; + volume.visible = false; + volume.update(); + this.viewer.scene.addVolume(volume); + + this.importance = 10; + + let selectionBox = $(`
`); + $(domElement.parentElement).append(selectionBox); + selectionBox.css("right", "10px"); + selectionBox.css("bottom", "10px"); + + let drag = e =>{ + + volume.visible = true; + + let mStart = e.drag.start; + let mEnd = e.drag.end; + + let box2D = new Box2(); + box2D.expandByPoint(mStart); + box2D.expandByPoint(mEnd); + + selectionBox.css("left", `${box2D.min.x}px`); + selectionBox.css("top", `${box2D.min.y}px`); + selectionBox.css("width", `${box2D.max.x - box2D.min.x}px`); + selectionBox.css("height", `${box2D.max.y - box2D.min.y}px`); + + let camera = e.viewer.scene.getActiveCamera(); + let size = e.viewer.renderer.getSize(new Vector2()); + let frustumSize = new Vector2( + camera.right - camera.left, + camera.top - camera.bottom); + + let screenCentroid = new Vector2().addVectors(e.drag.end, e.drag.start).multiplyScalar(0.5); + let ray = Utils.mouseToRay(screenCentroid, camera, size.width, size.height); + + let diff = new Vector2().subVectors(e.drag.end, e.drag.start); + diff.divide(size).multiply(frustumSize); + + volume.position.copy(ray.origin); + volume.up.copy(camera.up); + volume.rotation.copy(camera.rotation); + volume.scale.set(diff.x, diff.y, 1000 * 100); - this.viewer.inputHandler.startDragging( - measure.spheres[measure.spheres.length - 1]); - } else if (e.button === MOUSE.RIGHT) { - cancel.callback(); - } + e.consume(); }; - cancel.callback = e => { - if (cancel.removeLastMarker) { - measure.removeMarker(measure.points.length - 1); - } - domElement.removeEventListener('mouseup', insertionCallback, true); - this.viewer.removeEventListener('cancel_insertions', cancel.callback); - }; + let drop = e => { + this.importance = 0; - if (measure.maxMarkers > 1) { - this.viewer.addEventListener('cancel_insertions', cancel.callback); - domElement.addEventListener('mouseup', insertionCallback, true); - } + $(selectionBox).remove(); - measure.addMarker(new Vector3(0, 0, 0)); - this.viewer.inputHandler.startDragging( - measure.spheres[measure.spheres.length - 1]); + this.viewer.inputHandler.deselectAll(); + this.viewer.inputHandler.toggleSelection(volume); - this.viewer.scene.addMeasurement(measure); + let camera = e.viewer.scene.getActiveCamera(); + let size = e.viewer.renderer.getSize(new Vector2()); + let screenCentroid = new Vector2().addVectors(e.drag.end, e.drag.start).multiplyScalar(0.5); + let ray = Utils.mouseToRay(screenCentroid, camera, size.width, size.height); - return measure; - } - - update(){ - let camera = this.viewer.scene.getActiveCamera(); - let domElement = this.renderer.domElement; - let measurements = this.viewer.scene.measurements; + let line = new Line3(ray.origin, new Vector3().addVectors(ray.origin, ray.direction)); - const renderAreaSize = this.renderer.getSize(new Vector2()); - let clientWidth = renderAreaSize.width; - let clientHeight = renderAreaSize.height; + this.removeEventListener("drag", drag); + this.removeEventListener("drop", drop); - this.light.position.copy(camera.position); + let allPointsNear = []; + let allPointsFar = []; - // make size independant of distance - for (let measure of measurements) { - measure.lengthUnit = this.viewer.lengthUnit; - measure.lengthUnitDisplay = this.viewer.lengthUnitDisplay; - measure.update(); + // TODO support more than one point cloud + for(let pointcloud of this.viewer.scene.pointclouds){ - updateAzimuth(this.viewer, measure); + if(!pointcloud.visible){ + continue; + } - // spheres - for(let sphere of measure.spheres){ - let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3())); - let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); - let scale = (15 / pr); - sphere.scale.set(scale, scale, scale); - } + let volCam = camera.clone(); + volCam.left = -volume.scale.x / 2; + volCam.right = +volume.scale.x / 2; + volCam.top = +volume.scale.y / 2; + volCam.bottom = -volume.scale.y / 2; + volCam.near = -volume.scale.z / 2; + volCam.far = +volume.scale.z / 2; + volCam.rotation.copy(volume.rotation); + volCam.position.copy(volume.position); - // labels - let labels = measure.edgeLabels.concat(measure.angleLabels); - for(let label of labels){ - let distance = camera.position.distanceTo(label.getWorldPosition(new Vector3())); - let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); - let scale = (70 / pr); + volCam.updateMatrix(); + volCam.updateMatrixWorld(); + volCam.updateProjectionMatrix(); + volCam.matrixWorldInverse.copy(volCam.matrixWorld).invert(); - if(Potree.debug.scale){ - scale = (Potree.debug.scale / pr); - } + let ray = new Ray(volCam.getWorldPosition(new Vector3()), volCam.getWorldDirection(new Vector3())); + let rayInverse = new Ray( + ray.origin.clone().add(ray.direction.clone().multiplyScalar(volume.scale.z)), + ray.direction.clone().multiplyScalar(-1)); - label.scale.set(scale, scale, scale); - } + let pickerSettings = { + width: 8, + height: 8, + pickWindowSize: 8, + all: true, + pickClipped: true, + pointSizeType: PointSizeType.FIXED, + pointSize: 1}; + let pointsNear = pointcloud.pick(viewer, volCam, ray, pickerSettings); - // coordinate labels - for (let j = 0; j < measure.coordinateLabels.length; j++) { - let label = measure.coordinateLabels[j]; - let sphere = measure.spheres[j]; + volCam.rotateX(Math.PI); + volCam.updateMatrix(); + volCam.updateMatrixWorld(); + volCam.updateProjectionMatrix(); + volCam.matrixWorldInverse.copy(volCam.matrixWorld).invert(); + let pointsFar = pointcloud.pick(viewer, volCam, rayInverse, pickerSettings); - let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3())); + allPointsNear.push(...pointsNear); + allPointsFar.push(...pointsFar); + } - let screenPos = sphere.getWorldPosition(new Vector3()).clone().project(camera); - screenPos.x = Math.round((screenPos.x + 1) * clientWidth / 2); - screenPos.y = Math.round((-screenPos.y + 1) * clientHeight / 2); - screenPos.z = 0; - screenPos.y -= 30; + if(allPointsNear.length > 0 && allPointsFar.length > 0){ + let viewLine = new Line3(ray.origin, new Vector3().addVectors(ray.origin, ray.direction)); - let labelPos = new Vector3( - (screenPos.x / clientWidth) * 2 - 1, - -(screenPos.y / clientHeight) * 2 + 1, - 0.5 ); - labelPos.unproject(camera); - if(this.viewer.scene.cameraMode == CameraMode.PERSPECTIVE) { - let direction = labelPos.sub(camera.position).normalize(); - labelPos = new Vector3().addVectors( - camera.position, direction.multiplyScalar(distance)); + let closestOnLine = allPointsNear.map(p => viewLine.closestPointToPoint(p.position, false, new Vector3())); + let closest = closestOnLine.sort( (a, b) => ray.origin.distanceTo(a) - ray.origin.distanceTo(b))[0]; - } - label.position.copy(labelPos); - let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); - let scale = (70 / pr); - label.scale.set(scale, scale, scale); - } + let farthestOnLine = allPointsFar.map(p => viewLine.closestPointToPoint(p.position, false, new Vector3())); + let farthest = farthestOnLine.sort( (a, b) => ray.origin.distanceTo(b) - ray.origin.distanceTo(a))[0]; - // height label - if (measure.showHeight) { - let label = measure.heightLabel; + let distance = closest.distanceTo(farthest); + let centroid = new Vector3().addVectors(closest, farthest).multiplyScalar(0.5); + volume.scale.z = distance * 1.1; + volume.position.copy(centroid); + } - { - let distance = label.position.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); - let scale = (70 / pr); - label.scale.set(scale, scale, scale); - } + volume.clip = true; + }; - { // height edge - let edge = measure.heightEdge; + this.addEventListener("drag", drag); + this.addEventListener("drop", drop); - let sorted = measure.points.slice().sort((a, b) => a.position.z - b.position.z); - let lowPoint = sorted[0].position.clone(); - let highPoint = sorted[sorted.length - 1].position.clone(); - let min = lowPoint.z; - let max = highPoint.z; + viewer.inputHandler.addInputListener(this); - let start = new Vector3(highPoint.x, highPoint.y, min); - let end = new Vector3(highPoint.x, highPoint.y, max); + return volume; + } - let lowScreen = lowPoint.clone().project(camera); - let startScreen = start.clone().project(camera); - let endScreen = end.clone().project(camera); + update(e){ + //console.log(e.delta) + } - let toPixelCoordinates = v => { - let r = v.clone().addScalar(1).divideScalar(2); - r.x = r.x * clientWidth; - r.y = r.y * clientHeight; - r.z = 0; + render(){ + this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera()); + } - return r; - }; + } - let lowEL = toPixelCoordinates(lowScreen); - let startEL = toPixelCoordinates(startScreen); - let endEL = toPixelCoordinates(endScreen); + class SpotLightHelper$1 extends Object3D{ - let lToS = lowEL.distanceTo(startEL); - let sToE = startEL.distanceTo(endEL); + constructor(light, color){ + super(); - edge.geometry.lineDistances = [0, lToS, lToS, lToS + sToE]; - edge.geometry.lineDistancesNeedUpdate = true; + this.light = light; + this.color = color; - edge.material.dashSize = 10; - edge.material.gapSize = 10; - } - } + //this.up.set(0, 0, 1); + this.updateMatrix(); + this.updateMatrixWorld(); - { // area label - let label = measure.areaLabel; - let distance = label.position.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); + { // SPHERE + let sg = new SphereGeometry(1, 32, 32); + let sm = new MeshNormalMaterial(); + this.sphere = new Mesh(sg, sm); + this.sphere.scale.set(0.5, 0.5, 0.5); + this.add(this.sphere); + } - let scale = (70 / pr); - label.scale.set(scale, scale, scale); - } + { // LINES + - { // radius label - let label = measure.circleRadiusLabel; - let distance = label.position.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); + let positions = new Float32Array([ + +0, +0, +0, +0, +0, -1, - let scale = (70 / pr); - label.scale.set(scale, scale, scale); - } + +0, +0, +0, -1, -1, -1, + +0, +0, +0, +1, -1, -1, + +0, +0, +0, +1, +1, -1, + +0, +0, +0, -1, +1, -1, - { // edges - const materials = [ - measure.circleRadiusLine.material, - ...measure.edges.map( (e) => e.material), - measure.heightEdge.material, - measure.circleLine.material, - ]; + -1, -1, -1, +1, -1, -1, + +1, -1, -1, +1, +1, -1, + +1, +1, -1, -1, +1, -1, + -1, +1, -1, -1, -1, -1, + ]); - for(const material of materials){ - material.resolution.set(clientWidth, clientHeight); - } - } + let geometry = new BufferGeometry(); + geometry.setAttribute("position", new BufferAttribute(positions, 3)); - if(!this.showLabels){ + let material = new LineBasicMaterial(); - const labels = [ - ...measure.sphereLabels, - ...measure.edgeLabels, - ...measure.angleLabels, - ...measure.coordinateLabels, - measure.heightLabel, - measure.areaLabel, - measure.circleRadiusLabel, - ]; + this.frustum = new LineSegments(geometry, material); + this.add(this.frustum); - for(const label of labels){ - label.visible = false; - } - } } - } - render(){ - this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera()); + this.update(); } - }; - - class Message{ - constructor(content){ - this.content = content; + update(){ - let closeIcon = `${exports.resourcePath}/icons/close.svg`; + this.light.updateMatrix(); + this.light.updateMatrixWorld(); - this.element = $(` -
- - -
`); + let position = this.light.position; + let target = new Vector3().addVectors( + this.light.position, this.light.getWorldDirection(new Vector3()).multiplyScalar(-1)); + + let quat = new Quaternion().setFromRotationMatrix( + new Matrix4().lookAt( position, target, new Vector3( 0, 0, 1 ) ) + ); - this.elClose = this.element.find("img[name=close]"); + this.setRotationFromQuaternion(quat); + this.position.copy(position); - this.elContainer = this.element.find("span[name=content_container]"); - if(typeof content === "string"){ - this.elContainer.append($(`${content}`)); - }else { - this.elContainer.append(content); - } + let coneLength = (this.light.distance > 0) ? this.light.distance : 1000; + let coneWidth = coneLength * Math.tan( this.light.angle * 0.5 ); - } + this.frustum.scale.set(coneWidth, coneWidth, coneLength); - setMessage(content){ - this.elContainer.empty(); - if(typeof content === "string"){ - this.elContainer.append($(`${content}`)); - }else { - this.elContainer.append(content); - } } } - class PointCloudSM{ + class TransformationTool { + constructor(viewer) { + this.viewer = viewer; - constructor(potreeRenderer){ + this.scene = new Scene(); - this.potreeRenderer = potreeRenderer; - this.threeRenderer = this.potreeRenderer.threeRenderer; + this.selection = []; + this.pivot = new Vector3(); + this.dragging = false; + this.showPickVolumes = false; + + this.viewer.inputHandler.registerInteractiveScene(this.scene); + this.viewer.inputHandler.addEventListener('selection_changed', (e) => { + for(let selected of this.selection){ + this.viewer.inputHandler.blacklist.delete(selected); + } + + this.selection = e.selection; + + for(let selected of this.selection){ + this.viewer.inputHandler.blacklist.add(selected); + } - this.target = new WebGLRenderTarget(2 * 1024, 2 * 1024, { - minFilter: LinearFilter, - magFilter: LinearFilter, - format: RGBAFormat, - type: FloatType }); - this.target.depthTexture = new DepthTexture(); - this.target.depthTexture.type = UnsignedIntType; - //this.threeRenderer.setClearColor(0x000000, 1); - this.threeRenderer.setClearColor(0xff0000, 1); + let red = 0xE73100; + let green = 0x44A24A; + let blue = 0x2669E7; + + this.activeHandle = null; + this.scaleHandles = { + "scale.x+": {name: "scale.x+", node: new Object3D(), color: red, alignment: [+1, +0, +0]}, + "scale.x-": {name: "scale.x-", node: new Object3D(), color: red, alignment: [-1, +0, +0]}, + "scale.y+": {name: "scale.y+", node: new Object3D(), color: green, alignment: [+0, +1, +0]}, + "scale.y-": {name: "scale.y-", node: new Object3D(), color: green, alignment: [+0, -1, +0]}, + "scale.z+": {name: "scale.z+", node: new Object3D(), color: blue, alignment: [+0, +0, +1]}, + "scale.z-": {name: "scale.z-", node: new Object3D(), color: blue, alignment: [+0, +0, -1]}, + }; + this.focusHandles = { + "focus.x+": {name: "focus.x+", node: new Object3D(), color: red, alignment: [+1, +0, +0]}, + "focus.x-": {name: "focus.x-", node: new Object3D(), color: red, alignment: [-1, +0, +0]}, + "focus.y+": {name: "focus.y+", node: new Object3D(), color: green, alignment: [+0, +1, +0]}, + "focus.y-": {name: "focus.y-", node: new Object3D(), color: green, alignment: [+0, -1, +0]}, + "focus.z+": {name: "focus.z+", node: new Object3D(), color: blue, alignment: [+0, +0, +1]}, + "focus.z-": {name: "focus.z-", node: new Object3D(), color: blue, alignment: [+0, +0, -1]}, + }; + this.translationHandles = { + "translation.x": {name: "translation.x", node: new Object3D(), color: red, alignment: [1, 0, 0]}, + "translation.y": {name: "translation.y", node: new Object3D(), color: green, alignment: [0, 1, 0]}, + "translation.z": {name: "translation.z", node: new Object3D(), color: blue, alignment: [0, 0, 1]}, + }; + this.rotationHandles = { + "rotation.x": {name: "rotation.x", node: new Object3D(), color: red, alignment: [1, 0, 0]}, + "rotation.y": {name: "rotation.y", node: new Object3D(), color: green, alignment: [0, 1, 0]}, + "rotation.z": {name: "rotation.z", node: new Object3D(), color: blue, alignment: [0, 0, 1]}, + }; + this.handles = Object.assign({}, this.scaleHandles, this.focusHandles, this.translationHandles, this.rotationHandles); + this.pickVolumes = []; - //HACK? removed while moving to three.js 109 - //this.threeRenderer.clearTarget(this.target, true, true, true); - { - const oldTarget = this.threeRenderer.getRenderTarget(); + this.initializeScaleHandles(); + this.initializeFocusHandles(); + this.initializeTranslationHandles(); + this.initializeRotationHandles(); - this.threeRenderer.setRenderTarget(this.target); - this.threeRenderer.clear(true, true, true); - this.threeRenderer.setRenderTarget(oldTarget); + let boxFrameGeometry = new Geometry(); + { + // bottom + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); + // top + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); + // sides + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); + boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); } + this.frame = new LineSegments(boxFrameGeometry, new LineBasicMaterial({color: 0xffff00})); + this.scene.add(this.frame); + + } - setLight(light){ - this.light = light; + initializeScaleHandles(){ + let sgSphere = new SphereGeometry(1, 32, 32); + let sgLowPolySphere = new SphereGeometry(1, 16, 16); - let fov = (180 * light.angle) / Math.PI; - let aspect = light.shadow.mapSize.width / light.shadow.mapSize.height; - let near = 0.1; - let far = light.distance === 0 ? 10000 : light.distance; - this.camera = new PerspectiveCamera(fov, aspect, near, far); - this.camera.up.set(0, 0, 1); - this.camera.position.copy(light.position); + for(let handleName of Object.keys(this.scaleHandles)){ + let handle = this.scaleHandles[handleName]; + let node = handle.node; + this.scene.add(node); + node.position.set(...handle.alignment).multiplyScalar(0.5); - let target = new Vector3().subVectors(light.position, light.getWorldDirection(new Vector3())); - this.camera.lookAt(target); + let material = new MeshBasicMaterial({ + color: handle.color, + opacity: 0.4, + transparent: true + }); - this.camera.updateProjectionMatrix(); - this.camera.updateMatrix(); - this.camera.updateMatrixWorld(); - this.camera.matrixWorldInverse.copy(this.camera.matrixWorld).invert(); - } + let outlineMaterial = new MeshBasicMaterial({ + color: 0x000000, + side: BackSide, + opacity: 0.4, + transparent: true}); - setSize(width, height){ - if(this.target.width !== width || this.target.height !== height){ - this.target.dispose(); + let pickMaterial = new MeshNormalMaterial({ + opacity: 0.2, + transparent: true, + visible: this.showPickVolumes}); + + let sphere = new Mesh(sgSphere, material); + sphere.scale.set(1.3, 1.3, 1.3); + sphere.name = `${handleName}.handle`; + node.add(sphere); + + let outline = new Mesh(sgSphere, outlineMaterial); + outline.scale.set(1.4, 1.4, 1.4); + outline.name = `${handleName}.outline`; + sphere.add(outline); + + let pickSphere = new Mesh(sgLowPolySphere, pickMaterial); + pickSphere.name = `${handleName}.pick_volume`; + pickSphere.scale.set(3, 3, 3); + sphere.add(pickSphere); + pickSphere.handle = handleName; + this.pickVolumes.push(pickSphere); + + node.setOpacity = (target) => { + let opacity = {x: material.opacity}; + let t = new TWEEN.Tween(opacity).to({x: target}, 100); + t.onUpdate(() => { + sphere.visible = opacity.x > 0; + pickSphere.visible = opacity.x > 0; + material.opacity = opacity.x; + outlineMaterial.opacity = opacity.x; + pickSphere.material.opacity = opacity.x * 0.5; + }); + t.start(); + }; + + pickSphere.addEventListener("drag", (e) => this.dragScaleHandle(e)); + pickSphere.addEventListener("drop", (e) => this.dropScaleHandle(e)); + + pickSphere.addEventListener("mouseover", e => { + //node.setOpacity(1); + }); + + pickSphere.addEventListener("click", e => { + e.consume(); + }); + + pickSphere.addEventListener("mouseleave", e => { + //node.setOpacity(0.4); + }); } - this.target.setSize(width, height); } - render(scene, camera){ + initializeFocusHandles(){ + //let sgBox = new THREE.BoxGeometry(1, 1, 1); + let sgPlane = new PlaneGeometry(4, 4, 1, 1); + let sgLowPolySphere = new SphereGeometry(1, 16, 16); - this.threeRenderer.setClearColor(0x000000, 1); - - const oldTarget = this.threeRenderer.getRenderTarget(); + let texture = new TextureLoader().load(`${exports.resourcePath}/icons/eye_2.png`); - this.threeRenderer.setRenderTarget(this.target); - this.threeRenderer.clear(true, true, true); + for(let handleName of Object.keys(this.focusHandles)){ + let handle = this.focusHandles[handleName]; + let node = handle.node; + this.scene.add(node); + let align = handle.alignment; - this.potreeRenderer.render(scene, this.camera, this.target, {}); + //node.lookAt(new THREE.Vector3().addVectors(node.position, new THREE.Vector3(...align))); + node.lookAt(new Vector3(...align)); - this.threeRenderer.setRenderTarget(oldTarget); - } + let off = 0.8; + if(align[0] === 1){ + node.position.set(1, off, -off).multiplyScalar(0.5); + node.rotation.z = Math.PI / 2; + }else if(align[0] === -1){ + node.position.set(-1, -off, -off).multiplyScalar(0.5); + node.rotation.z = Math.PI / 2; + }else if(align[1] === 1){ + node.position.set(-off, 1, -off).multiplyScalar(0.5); + node.rotation.set(Math.PI / 2, Math.PI, 0.0); + }else if(align[1] === -1){ + node.position.set(off, -1, -off).multiplyScalar(0.5); + node.rotation.set(Math.PI / 2, 0.0, 0.0); + }else if(align[2] === 1){ + node.position.set(off, off, 1).multiplyScalar(0.5); + }else if(align[2] === -1){ + node.position.set(-off, off, -1).multiplyScalar(0.5); + } + let material = new MeshBasicMaterial({ + color: handle.color, + opacity: 0, + transparent: true, + map: texture + }); - } + //let outlineMaterial = new THREE.MeshBasicMaterial({ + // color: 0x000000, + // side: THREE.BackSide, + // opacity: 0, + // transparent: true}); - class ProfileTool extends EventDispatcher { - constructor (viewer) { - super(); + let pickMaterial = new MeshNormalMaterial({ + //opacity: 0, + transparent: true, + visible: this.showPickVolumes}); - this.viewer = viewer; - this.renderer = viewer.renderer; + let box = new Mesh(sgPlane, material); + box.name = `${handleName}.handle`; + box.scale.set(1.5, 1.5, 1.5); + box.position.set(0, 0, 0); + box.visible = false; + node.add(box); + //handle.focusNode = box; + + //let outline = new THREE.Mesh(sgPlane, outlineMaterial); + //outline.scale.set(1.4, 1.4, 1.4); + //outline.name = `${handleName}.outline`; + //box.add(outline); - this.addEventListener('start_inserting_profile', e => { - this.viewer.dispatchEvent({ - type: 'cancel_insertions' + let pickSphere = new Mesh(sgLowPolySphere, pickMaterial); + pickSphere.name = `${handleName}.pick_volume`; + pickSphere.scale.set(3, 3, 3); + box.add(pickSphere); + pickSphere.handle = handleName; + this.pickVolumes.push(pickSphere); + + node.setOpacity = (target) => { + let opacity = {x: material.opacity}; + let t = new TWEEN.Tween(opacity).to({x: target}, 100); + t.onUpdate(() => { + pickSphere.visible = opacity.x > 0; + box.visible = opacity.x > 0; + material.opacity = opacity.x; + //outlineMaterial.opacity = opacity.x; + pickSphere.material.opacity = opacity.x * 0.5; + }); + t.start(); + }; + + pickSphere.addEventListener("drag", e => {}); + + pickSphere.addEventListener("mouseup", e => { + e.consume(); }); - }); - this.scene = new Scene(); - this.scene.name = 'scene_profile'; - this.light = new PointLight(0xffffff, 1.0); - this.scene.add(this.light); + pickSphere.addEventListener("mousedown", e => { + e.consume(); + }); - this.viewer.inputHandler.registerInteractiveScene(this.scene); + pickSphere.addEventListener("click", e => { + e.consume(); - this.onRemove = e => this.scene.remove(e.profile); - this.onAdd = e => this.scene.add(e.profile); + let selected = this.selection[0]; + let maxScale = Math.max(...selected.scale.toArray()); + let minScale = Math.min(...selected.scale.toArray()); + let handleLength = Math.abs(selected.scale.dot(new Vector3(...handle.alignment))); + let alignment = new Vector3(...handle.alignment).multiplyScalar(2 * maxScale / handleLength); + alignment.applyMatrix4(selected.matrixWorld); + let newCamPos = alignment; + let newCamTarget = selected.getWorldPosition(new Vector3()); - for(let profile of viewer.scene.profiles){ - this.onAdd({profile: profile}); - } + Utils.moveTo(this.viewer.scene, newCamPos, newCamTarget); + }); - viewer.addEventListener("update", this.update.bind(this)); - viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this)); - viewer.addEventListener("scene_changed", this.onSceneChange.bind(this)); + pickSphere.addEventListener("mouseover", e => { + //box.setOpacity(1); + }); - viewer.scene.addEventListener('profile_added', this.onAdd); - viewer.scene.addEventListener('profile_removed', this.onRemove); + pickSphere.addEventListener("mouseleave", e => { + //box.setOpacity(0.4); + }); + } } - onSceneChange(e){ - if(e.oldScene){ - e.oldScene.removeEventListeners('profile_added', this.onAdd); - e.oldScene.removeEventListeners('profile_removed', this.onRemove); - } + initializeTranslationHandles(){ + let boxGeometry = new BoxGeometry(1, 1, 1); - e.scene.addEventListener('profile_added', this.onAdd); - e.scene.addEventListener('profile_removed', this.onRemove); - } + for(let handleName of Object.keys(this.translationHandles)){ + let handle = this.handles[handleName]; + let node = handle.node; + this.scene.add(node); - startInsertion (args = {}) { - let domElement = this.viewer.renderer.domElement; + let material = new MeshBasicMaterial({ + color: handle.color, + opacity: 0.4, + transparent: true}); - let profile = new Profile(); - profile.name = args.name || 'Profile'; + let outlineMaterial = new MeshBasicMaterial({ + color: 0x000000, + side: BackSide, + opacity: 0.4, + transparent: true}); - this.dispatchEvent({ - type: 'start_inserting_profile', - profile: profile - }); + let pickMaterial = new MeshNormalMaterial({ + opacity: 0.2, + transparent: true, + visible: this.showPickVolumes + }); - this.scene.add(profile); + let box = new Mesh(boxGeometry, material); + box.name = `${handleName}.handle`; + box.scale.set(0.2, 0.2, 40); + box.lookAt(new Vector3(...handle.alignment)); + box.renderOrder = 10; + node.add(box); + handle.translateNode = box; - let cancel = { - callback: null - }; + let outline = new Mesh(boxGeometry, outlineMaterial); + outline.name = `${handleName}.outline`; + outline.scale.set(3, 3, 1.03); + outline.renderOrder = 0; + box.add(outline); - let insertionCallback = (e) => { - if(e.button === MOUSE.LEFT){ - if(profile.points.length <= 1){ - let camera = this.viewer.scene.getActiveCamera(); - let distance = camera.position.distanceTo(profile.points[0]); - let clientSize = this.viewer.renderer.getSize(new Vector2()); - let pr = Utils.projectedRadius(1, camera, distance, clientSize.width, clientSize.height); - let width = (10 / pr); + let pickVolume = new Mesh(boxGeometry, pickMaterial); + pickVolume.name = `${handleName}.pick_volume`; + pickVolume.scale.set(12, 12, 1.1); + pickVolume.handle = handleName; + box.add(pickVolume); + this.pickVolumes.push(pickVolume); - profile.setWidth(width); - } + node.setOpacity = (target) => { + let opacity = {x: material.opacity}; + let t = new TWEEN.Tween(opacity).to({x: target}, 100); + t.onUpdate(() => { + box.visible = opacity.x > 0; + pickVolume.visible = opacity.x > 0; + material.opacity = opacity.x; + outlineMaterial.opacity = opacity.x; + pickMaterial.opacity = opacity.x * 0.5; + }); + t.start(); + }; - profile.addMarker(profile.points[profile.points.length - 1].clone()); + pickVolume.addEventListener("drag", (e) => {this.dragTranslationHandle(e);}); + pickVolume.addEventListener("drop", (e) => {this.dropTranslationHandle(e);}); + } + } - this.viewer.inputHandler.startDragging( - profile.spheres[profile.spheres.length - 1]); - } else if (e.button === MOUSE.RIGHT) { - cancel.callback(); - } - }; + initializeRotationHandles(){ + let adjust = 0.5; + let torusGeometry = new TorusGeometry(1, adjust * 0.015, 8, 64, Math.PI / 2); + let outlineGeometry = new TorusGeometry(1, adjust * 0.04, 8, 64, Math.PI / 2); + let pickGeometry = new TorusGeometry(1, adjust * 0.1, 6, 4, Math.PI / 2); - cancel.callback = e => { - profile.removeMarker(profile.points.length - 1); - domElement.removeEventListener('mouseup', insertionCallback, true); - this.viewer.removeEventListener('cancel_insertions', cancel.callback); - }; + for(let handleName of Object.keys(this.rotationHandles)){ + let handle = this.handles[handleName]; + let node = handle.node; + this.scene.add(node); + + let material = new MeshBasicMaterial({ + color: handle.color, + opacity: 0.4, + transparent: true}); + + let outlineMaterial = new MeshBasicMaterial({ + color: 0x000000, + side: BackSide, + opacity: 0.4, + transparent: true}); - this.viewer.addEventListener('cancel_insertions', cancel.callback); - domElement.addEventListener('mouseup', insertionCallback, true); + let pickMaterial = new MeshNormalMaterial({ + opacity: 0.2, + transparent: true, + visible: this.showPickVolumes + }); - profile.addMarker(new Vector3(0, 0, 0)); - this.viewer.inputHandler.startDragging( - profile.spheres[profile.spheres.length - 1]); + let box = new Mesh(torusGeometry, material); + box.name = `${handleName}.handle`; + box.scale.set(20, 20, 20); + box.lookAt(new Vector3(...handle.alignment)); + node.add(box); + handle.translateNode = box; - this.viewer.scene.addProfile(profile); + let outline = new Mesh(outlineGeometry, outlineMaterial); + outline.name = `${handleName}.outline`; + outline.scale.set(1, 1, 1); + outline.renderOrder = 0; + box.add(outline); - return profile; - } - - update(){ - let camera = this.viewer.scene.getActiveCamera(); - let profiles = this.viewer.scene.profiles; - let renderAreaSize = this.viewer.renderer.getSize(new Vector2()); - let clientWidth = renderAreaSize.width; - let clientHeight = renderAreaSize.height; + let pickVolume = new Mesh(pickGeometry, pickMaterial); + pickVolume.name = `${handleName}.pick_volume`; + pickVolume.scale.set(1, 1, 1); + pickVolume.handle = handleName; + box.add(pickVolume); + this.pickVolumes.push(pickVolume); - this.light.position.copy(camera.position); + node.setOpacity = (target) => { + let opacity = {x: material.opacity}; + let t = new TWEEN.Tween(opacity).to({x: target}, 100); + t.onUpdate(() => { + box.visible = opacity.x > 0; + pickVolume.visible = opacity.x > 0; + material.opacity = opacity.x; + outlineMaterial.opacity = opacity.x; + pickMaterial.opacity = opacity.x * 0.5; + }); + t.start(); + }; - // make size independant of distance - for(let profile of profiles){ - for(let sphere of profile.spheres){ - let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3())); - let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); - let scale = (15 / pr); - sphere.scale.set(scale, scale, scale); - } - } - } - render(){ - this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera()); + //pickVolume.addEventListener("mouseover", (e) => { + // //let a = this.viewer.scene.getActiveCamera().getWorldDirection(new THREE.Vector3()).dot(pickVolume.getWorldDirection(new THREE.Vector3())); + // console.log(pickVolume.getWorldDirection(new THREE.Vector3())); + //}); + + pickVolume.addEventListener("drag", (e) => {this.dragRotationHandle(e);}); + pickVolume.addEventListener("drop", (e) => {this.dropRotationHandle(e);}); + } } - } - - class ScreenBoxSelectTool extends EventDispatcher{ + dragRotationHandle(e){ + let drag = e.drag; + let handle = this.activeHandle; + let camera = this.viewer.scene.getActiveCamera(); - constructor(viewer){ - super(); + if(!handle){ + return + }; - this.viewer = viewer; - this.scene = new Scene(); + let localNormal = new Vector3(...handle.alignment); + let n = new Vector3(); + n.copy(new Vector4(...localNormal.toArray(), 0).applyMatrix4(handle.node.matrixWorld)); + n.normalize(); - viewer.addEventListener("update", this.update.bind(this)); - viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this)); - viewer.addEventListener("scene_changed", this.onSceneChange.bind(this)); - } + if (!drag.intersectionStart){ - onSceneChange(scene){ - console.log("scene changed"); - } + //this.viewer.scene.scene.remove(this.debug); + //this.debug = new THREE.Object3D(); + //this.viewer.scene.scene.add(this.debug); + //Utils.debugSphere(this.debug, drag.location, 3, 0xaaaaaa); + //let debugEnd = drag.location.clone().add(n.clone().multiplyScalar(20)); + //Utils.debugLine(this.debug, drag.location, debugEnd, 0xff0000); - startInsertion(){ - let domElement = this.viewer.renderer.domElement; + drag.intersectionStart = drag.location; + drag.objectStart = drag.object.getWorldPosition(new Vector3()); + drag.handle = handle; - let volume = new BoxVolume(); - volume.position.set(12345, 12345, 12345); - volume.showVolumeLabel = false; - volume.visible = false; - volume.update(); - this.viewer.scene.addVolume(volume); + let plane = new Plane().setFromNormalAndCoplanarPoint(n, drag.intersectionStart); - this.importance = 10; + drag.dragPlane = plane; + drag.pivot = drag.intersectionStart; + }else { + handle = drag.handle; + } - let selectionBox = $(`
`); - $(domElement.parentElement).append(selectionBox); - selectionBox.css("right", "10px"); - selectionBox.css("bottom", "10px"); + this.dragging = true; - let drag = e =>{ + let mouse = drag.end; + let domElement = this.viewer.renderer.domElement; + let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); + + let I = ray.intersectPlane(drag.dragPlane, new Vector3()); - volume.visible = true; + if (I) { + let center = this.scene.getWorldPosition(new Vector3()); + let from = drag.pivot; + let to = I; - let mStart = e.drag.start; - let mEnd = e.drag.end; + let v1 = from.clone().sub(center).normalize(); + let v2 = to.clone().sub(center).normalize(); - let box2D = new Box2(); - box2D.expandByPoint(mStart); - box2D.expandByPoint(mEnd); + let angle = Math.acos(v1.dot(v2)); + let sign = Math.sign(v1.cross(v2).dot(n)); + angle = angle * sign; + if (Number.isNaN(angle)) { + return; + } - selectionBox.css("left", `${box2D.min.x}px`); - selectionBox.css("top", `${box2D.min.y}px`); - selectionBox.css("width", `${box2D.max.x - box2D.min.x}px`); - selectionBox.css("height", `${box2D.max.y - box2D.min.y}px`); + let normal = new Vector3(...handle.alignment); + for (let selection of this.selection) { + selection.rotateOnAxis(normal, angle); + selection.dispatchEvent({ + type: "orientation_changed", + object: selection + }); + } - let camera = e.viewer.scene.getActiveCamera(); - let size = e.viewer.renderer.getSize(new Vector2()); - let frustumSize = new Vector2( - camera.right - camera.left, - camera.top - camera.bottom); + drag.pivot = I; + } + } - let screenCentroid = new Vector2().addVectors(e.drag.end, e.drag.start).multiplyScalar(0.5); - let ray = Utils.mouseToRay(screenCentroid, camera, size.width, size.height); + dropRotationHandle(e){ + this.dragging = false; + this.setActiveHandle(null); + } - let diff = new Vector2().subVectors(e.drag.end, e.drag.start); - diff.divide(size).multiply(frustumSize); + dragTranslationHandle(e){ + let drag = e.drag; + let handle = this.activeHandle; + let camera = this.viewer.scene.getActiveCamera(); - volume.position.copy(ray.origin); - volume.up.copy(camera.up); - volume.rotation.copy(camera.rotation); - volume.scale.set(diff.x, diff.y, 1000 * 100); - - e.consume(); - }; - - let drop = e => { - this.importance = 0; - - $(selectionBox).remove(); + if(!drag.intersectionStart && handle){ + drag.intersectionStart = drag.location; + drag.objectStart = drag.object.getWorldPosition(new Vector3()); - this.viewer.inputHandler.deselectAll(); - this.viewer.inputHandler.toggleSelection(volume); + let start = drag.intersectionStart; + let dir = new Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld); + let end = new Vector3().addVectors(start, dir); + let line = new Line3(start.clone(), end.clone()); + drag.line = line; - let camera = e.viewer.scene.getActiveCamera(); - let size = e.viewer.renderer.getSize(new Vector2()); - let screenCentroid = new Vector2().addVectors(e.drag.end, e.drag.start).multiplyScalar(0.5); - let ray = Utils.mouseToRay(screenCentroid, camera, size.width, size.height); + let camOnLine = line.closestPointToPoint(camera.position, false, new Vector3()); + let normal = new Vector3().subVectors(camera.position, camOnLine); + let plane = new Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart); + drag.dragPlane = plane; + drag.pivot = drag.intersectionStart; + }else { + handle = drag.handle; + } - let line = new Line3(ray.origin, new Vector3().addVectors(ray.origin, ray.direction)); + this.dragging = true; - this.removeEventListener("drag", drag); - this.removeEventListener("drop", drop); + { + let mouse = drag.end; + let domElement = this.viewer.renderer.domElement; + let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); + let I = ray.intersectPlane(drag.dragPlane, new Vector3()); - let allPointsNear = []; - let allPointsFar = []; + if (I) { + let iOnLine = drag.line.closestPointToPoint(I, false, new Vector3()); - // TODO support more than one point cloud - for(let pointcloud of this.viewer.scene.pointclouds){ + let diff = new Vector3().subVectors(iOnLine, drag.pivot); - if(!pointcloud.visible){ - continue; + for (let selection of this.selection) { + selection.position.add(diff); + selection.dispatchEvent({ + type: "position_changed", + object: selection + }); } - let volCam = camera.clone(); - volCam.left = -volume.scale.x / 2; - volCam.right = +volume.scale.x / 2; - volCam.top = +volume.scale.y / 2; - volCam.bottom = -volume.scale.y / 2; - volCam.near = -volume.scale.z / 2; - volCam.far = +volume.scale.z / 2; - volCam.rotation.copy(volume.rotation); - volCam.position.copy(volume.position); + drag.pivot = drag.pivot.add(diff); + } + } + } + + dropTranslationHandle(e){ + this.dragging = false; + this.setActiveHandle(null); + } - volCam.updateMatrix(); - volCam.updateMatrixWorld(); - volCam.updateProjectionMatrix(); - volCam.matrixWorldInverse.copy(volCam.matrixWorld).invert(); + dropScaleHandle(e){ + this.dragging = false; + this.setActiveHandle(null); + } - let ray = new Ray(volCam.getWorldPosition(new Vector3()), volCam.getWorldDirection(new Vector3())); - let rayInverse = new Ray( - ray.origin.clone().add(ray.direction.clone().multiplyScalar(volume.scale.z)), - ray.direction.clone().multiplyScalar(-1)); + dragScaleHandle(e){ + let drag = e.drag; + let handle = this.activeHandle; + let camera = this.viewer.scene.getActiveCamera(); - let pickerSettings = { - width: 8, - height: 8, - pickWindowSize: 8, - all: true, - pickClipped: true, - pointSizeType: PointSizeType.FIXED, - pointSize: 1}; - let pointsNear = pointcloud.pick(viewer, volCam, ray, pickerSettings); + if(!drag.intersectionStart){ + drag.intersectionStart = drag.location; + drag.objectStart = drag.object.getWorldPosition(new Vector3()); + drag.handle = handle; - volCam.rotateX(Math.PI); - volCam.updateMatrix(); - volCam.updateMatrixWorld(); - volCam.updateProjectionMatrix(); - volCam.matrixWorldInverse.copy(volCam.matrixWorld).invert(); - let pointsFar = pointcloud.pick(viewer, volCam, rayInverse, pickerSettings); + let start = drag.intersectionStart; + let dir = new Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld); + let end = new Vector3().addVectors(start, dir); + let line = new Line3(start.clone(), end.clone()); + drag.line = line; - allPointsNear.push(...pointsNear); - allPointsFar.push(...pointsFar); - } + let camOnLine = line.closestPointToPoint(camera.position, false, new Vector3()); + let normal = new Vector3().subVectors(camera.position, camOnLine); + let plane = new Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart); + drag.dragPlane = plane; + drag.pivot = drag.intersectionStart; - if(allPointsNear.length > 0 && allPointsFar.length > 0){ - let viewLine = new Line3(ray.origin, new Vector3().addVectors(ray.origin, ray.direction)); + //Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05); + }else { + handle = drag.handle; + } - let closestOnLine = allPointsNear.map(p => viewLine.closestPointToPoint(p.position, false, new Vector3())); - let closest = closestOnLine.sort( (a, b) => ray.origin.distanceTo(a) - ray.origin.distanceTo(b))[0]; + this.dragging = true; - let farthestOnLine = allPointsFar.map(p => viewLine.closestPointToPoint(p.position, false, new Vector3())); - let farthest = farthestOnLine.sort( (a, b) => ray.origin.distanceTo(b) - ray.origin.distanceTo(a))[0]; + { + let mouse = drag.end; + let domElement = this.viewer.renderer.domElement; + let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); + let I = ray.intersectPlane(drag.dragPlane, new Vector3()); - let distance = closest.distanceTo(farthest); - let centroid = new Vector3().addVectors(closest, farthest).multiplyScalar(0.5); - volume.scale.z = distance * 1.1; - volume.position.copy(centroid); - } + if (I) { + let iOnLine = drag.line.closestPointToPoint(I, false, new Vector3()); + let direction = handle.alignment.reduce( (a, v) => a + v, 0); - volume.clip = true; - }; + let toObjectSpace = this.selection[0].matrixWorld.clone().invert(); + let iOnLineOS = iOnLine.clone().applyMatrix4(toObjectSpace); + let pivotOS = drag.pivot.clone().applyMatrix4(toObjectSpace); + let diffOS = new Vector3().subVectors(iOnLineOS, pivotOS); + let dragDirectionOS = diffOS.clone().normalize(); + if(iOnLine.distanceTo(drag.pivot) === 0){ + dragDirectionOS.set(0, 0, 0); + } + let dragDirection = dragDirectionOS.dot(new Vector3(...handle.alignment)); - this.addEventListener("drag", drag); - this.addEventListener("drop", drop); + let diff = new Vector3().subVectors(iOnLine, drag.pivot); + let diffScale = new Vector3(...handle.alignment).multiplyScalar(diff.length() * direction * dragDirection); + let diffPosition = diff.clone().multiplyScalar(0.5); - viewer.inputHandler.addInputListener(this); + for (let selection of this.selection) { + selection.scale.add(diffScale); + selection.scale.x = Math.max(0.1, selection.scale.x); + selection.scale.y = Math.max(0.1, selection.scale.y); + selection.scale.z = Math.max(0.1, selection.scale.z); + selection.position.add(diffPosition); + selection.dispatchEvent({ + type: "position_changed", + object: selection + }); + selection.dispatchEvent({ + type: "scale_changed", + object: selection + }); + } - return volume; + drag.pivot.copy(iOnLine); + //Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05); + } + } } - update(e){ - //console.log(e.delta) - } + setActiveHandle(handle){ + if(this.dragging){ + return; + } - render(){ - this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera()); - } + if(this.activeHandle === handle){ + return; + } - } + this.activeHandle = handle; - class SpotLightHelper$1 extends Object3D{ + if(handle === null){ + for(let handleName of Object.keys(this.handles)){ + let handle = this.handles[handleName]; + handle.node.setOpacity(0); + } + } - constructor(light, color){ - super(); + for(let handleName of Object.keys(this.focusHandles)){ + let handle = this.focusHandles[handleName]; - this.light = light; - this.color = color; + if(this.activeHandle === handle){ + handle.node.setOpacity(1.0); + }else { + handle.node.setOpacity(0.4); + } + } - //this.up.set(0, 0, 1); - this.updateMatrix(); - this.updateMatrixWorld(); + for(let handleName of Object.keys(this.translationHandles)){ + let handle = this.translationHandles[handleName]; - { // SPHERE - let sg = new SphereGeometry(1, 32, 32); - let sm = new MeshNormalMaterial(); - this.sphere = new Mesh(sg, sm); - this.sphere.scale.set(0.5, 0.5, 0.5); - this.add(this.sphere); + if(this.activeHandle === handle){ + handle.node.setOpacity(1.0); + }else { + handle.node.setOpacity(0.4); + } } - { // LINES - + for(let handleName of Object.keys(this.rotationHandles)){ + let handle = this.rotationHandles[handleName]; - let positions = new Float32Array([ - +0, +0, +0, +0, +0, -1, + //if(this.activeHandle === handle){ + // handle.node.setOpacity(1.0); + //}else{ + // handle.node.setOpacity(0.4) + //} - +0, +0, +0, -1, -1, -1, - +0, +0, +0, +1, -1, -1, - +0, +0, +0, +1, +1, -1, - +0, +0, +0, -1, +1, -1, + handle.node.setOpacity(0.4); + } - -1, -1, -1, +1, -1, -1, - +1, -1, -1, +1, +1, -1, - +1, +1, -1, -1, +1, -1, - -1, +1, -1, -1, -1, -1, - ]); + for(let handleName of Object.keys(this.scaleHandles)){ + let handle = this.scaleHandles[handleName]; - let geometry = new BufferGeometry(); - geometry.setAttribute("position", new BufferAttribute(positions, 3)); + if(this.activeHandle === handle){ + handle.node.setOpacity(1.0); - let material = new LineBasicMaterial(); + let relatedFocusHandle = this.focusHandles[handle.name.replace("scale", "focus")]; + let relatedFocusNode = relatedFocusHandle.node; + relatedFocusNode.setOpacity(0.4); - this.frustum = new LineSegments(geometry, material); - this.add(this.frustum); + for(let translationHandleName of Object.keys(this.translationHandles)){ + let translationHandle = this.translationHandles[translationHandleName]; + translationHandle.node.setOpacity(0.4); + } + + //let relatedTranslationHandle = this.translationHandles[ + // handle.name.replace("scale", "translation").replace(/[+-]/g, "")]; + //let relatedTranslationNode = relatedTranslationHandle.node; + //relatedTranslationNode.setOpacity(0.4); + + }else { + handle.node.setOpacity(0.4); + } } - this.update(); - } + - update(){ - this.light.updateMatrix(); - this.light.updateMatrixWorld(); - let position = this.light.position; - let target = new Vector3().addVectors( - this.light.position, this.light.getWorldDirection(new Vector3()).multiplyScalar(-1)); + if(handle){ + handle.node.setOpacity(1.0); + } + - let quat = new Quaternion().setFromRotationMatrix( - new Matrix4().lookAt( position, target, new Vector3( 0, 0, 1 ) ) - ); + } - this.setRotationFromQuaternion(quat); - this.position.copy(position); + update () { + if(this.selection.length === 1){ - let coneLength = (this.light.distance > 0) ? this.light.distance : 1000; - let coneWidth = coneLength * Math.tan( this.light.angle * 0.5 ); + this.scene.visible = true; - this.frustum.scale.set(coneWidth, coneWidth, coneLength); + this.scene.updateMatrix(); + this.scene.updateMatrixWorld(); - } + let selected = this.selection[0]; + let world = selected.matrixWorld; + let camera = this.viewer.scene.getActiveCamera(); + let domElement = this.viewer.renderer.domElement; + let mouse = this.viewer.inputHandler.mouse; - } + let center = selected.boundingBox.getCenter(new Vector3()).clone().applyMatrix4(selected.matrixWorld); - class TransformationTool { - constructor(viewer) { - this.viewer = viewer; + this.scene.scale.copy(selected.boundingBox.getSize(new Vector3()).multiply(selected.scale)); + this.scene.position.copy(center); + this.scene.rotation.copy(selected.rotation); - this.scene = new Scene(); + this.scene.updateMatrixWorld(); + + { + // adjust scale of components + for(let handleName of Object.keys(this.handles)){ + let handle = this.handles[handleName]; + let node = handle.node; - this.selection = []; - this.pivot = new Vector3(); - this.dragging = false; - this.showPickVolumes = false; + let handlePos = node.getWorldPosition(new Vector3()); + let distance = handlePos.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight); - this.viewer.inputHandler.registerInteractiveScene(this.scene); - this.viewer.inputHandler.addEventListener('selection_changed', (e) => { - for(let selected of this.selection){ - this.viewer.inputHandler.blacklist.delete(selected); - } + let ws = node.parent.getWorldScale(new Vector3()); - this.selection = e.selection; + let s = (7 / pr); + let scale = new Vector3(s, s, s).divide(ws); - for(let selected of this.selection){ - this.viewer.inputHandler.blacklist.add(selected); - } + let rot = new Matrix4().makeRotationFromEuler(node.rotation); + let rotInv = rot.clone().invert(); - }); + scale.applyMatrix4(rotInv); + scale.x = Math.abs(scale.x); + scale.y = Math.abs(scale.y); + scale.z = Math.abs(scale.z); - let red = 0xE73100; - let green = 0x44A24A; - let blue = 0x2669E7; - - this.activeHandle = null; - this.scaleHandles = { - "scale.x+": {name: "scale.x+", node: new Object3D(), color: red, alignment: [+1, +0, +0]}, - "scale.x-": {name: "scale.x-", node: new Object3D(), color: red, alignment: [-1, +0, +0]}, - "scale.y+": {name: "scale.y+", node: new Object3D(), color: green, alignment: [+0, +1, +0]}, - "scale.y-": {name: "scale.y-", node: new Object3D(), color: green, alignment: [+0, -1, +0]}, - "scale.z+": {name: "scale.z+", node: new Object3D(), color: blue, alignment: [+0, +0, +1]}, - "scale.z-": {name: "scale.z-", node: new Object3D(), color: blue, alignment: [+0, +0, -1]}, - }; - this.focusHandles = { - "focus.x+": {name: "focus.x+", node: new Object3D(), color: red, alignment: [+1, +0, +0]}, - "focus.x-": {name: "focus.x-", node: new Object3D(), color: red, alignment: [-1, +0, +0]}, - "focus.y+": {name: "focus.y+", node: new Object3D(), color: green, alignment: [+0, +1, +0]}, - "focus.y-": {name: "focus.y-", node: new Object3D(), color: green, alignment: [+0, -1, +0]}, - "focus.z+": {name: "focus.z+", node: new Object3D(), color: blue, alignment: [+0, +0, +1]}, - "focus.z-": {name: "focus.z-", node: new Object3D(), color: blue, alignment: [+0, +0, -1]}, - }; - this.translationHandles = { - "translation.x": {name: "translation.x", node: new Object3D(), color: red, alignment: [1, 0, 0]}, - "translation.y": {name: "translation.y", node: new Object3D(), color: green, alignment: [0, 1, 0]}, - "translation.z": {name: "translation.z", node: new Object3D(), color: blue, alignment: [0, 0, 1]}, - }; - this.rotationHandles = { - "rotation.x": {name: "rotation.x", node: new Object3D(), color: red, alignment: [1, 0, 0]}, - "rotation.y": {name: "rotation.y", node: new Object3D(), color: green, alignment: [0, 1, 0]}, - "rotation.z": {name: "rotation.z", node: new Object3D(), color: blue, alignment: [0, 0, 1]}, - }; - this.handles = Object.assign({}, this.scaleHandles, this.focusHandles, this.translationHandles, this.rotationHandles); - this.pickVolumes = []; + node.scale.copy(scale); + } - this.initializeScaleHandles(); - this.initializeFocusHandles(); - this.initializeTranslationHandles(); - this.initializeRotationHandles(); + // adjust rotation handles + if(!this.dragging){ + let tWorld = this.scene.matrixWorld; + let tObject = tWorld.clone().invert(); + let camObjectPos = camera.getWorldPosition(new Vector3()).applyMatrix4(tObject); + let x = this.rotationHandles["rotation.x"].node.rotation; + let y = this.rotationHandles["rotation.y"].node.rotation; + let z = this.rotationHandles["rotation.z"].node.rotation; - let boxFrameGeometry = new Geometry(); - { - // bottom - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); - // top - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); - // sides - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5)); - boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5)); - } - this.frame = new LineSegments(boxFrameGeometry, new LineBasicMaterial({color: 0xffff00})); - this.scene.add(this.frame); + x.order = "ZYX"; + y.order = "ZYX"; - - } + let above = camObjectPos.z > 0; + let below = !above; + let PI_HALF = Math.PI / 2; - initializeScaleHandles(){ - let sgSphere = new SphereGeometry(1, 32, 32); - let sgLowPolySphere = new SphereGeometry(1, 16, 16); + if(above){ + if(camObjectPos.x > 0 && camObjectPos.y > 0){ + x.x = 1 * PI_HALF; + y.y = 3 * PI_HALF; + z.z = 0 * PI_HALF; + }else if(camObjectPos.x < 0 && camObjectPos.y > 0){ + x.x = 1 * PI_HALF; + y.y = 2 * PI_HALF; + z.z = 1 * PI_HALF; + }else if(camObjectPos.x < 0 && camObjectPos.y < 0){ + x.x = 2 * PI_HALF; + y.y = 2 * PI_HALF; + z.z = 2 * PI_HALF; + }else if(camObjectPos.x > 0 && camObjectPos.y < 0){ + x.x = 2 * PI_HALF; + y.y = 3 * PI_HALF; + z.z = 3 * PI_HALF; + } + }else if(below){ + if(camObjectPos.x > 0 && camObjectPos.y > 0){ + x.x = 0 * PI_HALF; + y.y = 0 * PI_HALF; + z.z = 0 * PI_HALF; + }else if(camObjectPos.x < 0 && camObjectPos.y > 0){ + x.x = 0 * PI_HALF; + y.y = 1 * PI_HALF; + z.z = 1 * PI_HALF; + }else if(camObjectPos.x < 0 && camObjectPos.y < 0){ + x.x = 3 * PI_HALF; + y.y = 1 * PI_HALF; + z.z = 2 * PI_HALF; + }else if(camObjectPos.x > 0 && camObjectPos.y < 0){ + x.x = 3 * PI_HALF; + y.y = 0 * PI_HALF; + z.z = 3 * PI_HALF; + } + } + } - for(let handleName of Object.keys(this.scaleHandles)){ - let handle = this.scaleHandles[handleName]; - let node = handle.node; - this.scene.add(node); - node.position.set(...handle.alignment).multiplyScalar(0.5); + { + let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); + let raycaster = new Raycaster(ray.origin, ray.direction); + let intersects = raycaster.intersectObjects(this.pickVolumes.filter(v => v.visible), true); - let material = new MeshBasicMaterial({ - color: handle.color, - opacity: 0.4, - transparent: true - }); + if(intersects.length > 0){ + let I = intersects[0]; + let handleName = I.object.handle; + this.setActiveHandle(this.handles[handleName]); + }else { + this.setActiveHandle(null); + } + } - let outlineMaterial = new MeshBasicMaterial({ - color: 0x000000, - side: BackSide, - opacity: 0.4, - transparent: true}); + // + for(let handleName of Object.keys(this.scaleHandles)){ + let handle = this.handles[handleName]; + let node = handle.node; + let alignment = handle.alignment; - let pickMaterial = new MeshNormalMaterial({ - opacity: 0.2, - transparent: true, - visible: this.showPickVolumes}); + - let sphere = new Mesh(sgSphere, material); - sphere.scale.set(1.3, 1.3, 1.3); - sphere.name = `${handleName}.handle`; - node.add(sphere); - - let outline = new Mesh(sgSphere, outlineMaterial); - outline.scale.set(1.4, 1.4, 1.4); - outline.name = `${handleName}.outline`; - sphere.add(outline); + } + } - let pickSphere = new Mesh(sgLowPolySphere, pickMaterial); - pickSphere.name = `${handleName}.pick_volume`; - pickSphere.scale.set(3, 3, 3); - sphere.add(pickSphere); - pickSphere.handle = handleName; - this.pickVolumes.push(pickSphere); + }else { + this.scene.visible = false; + } + + } - node.setOpacity = (target) => { - let opacity = {x: material.opacity}; - let t = new TWEEN.Tween(opacity).to({x: target}, 100); - t.onUpdate(() => { - sphere.visible = opacity.x > 0; - pickSphere.visible = opacity.x > 0; - material.opacity = opacity.x; - outlineMaterial.opacity = opacity.x; - pickSphere.material.opacity = opacity.x * 0.5; - }); - t.start(); - }; + }; - pickSphere.addEventListener("drag", (e) => this.dragScaleHandle(e)); - pickSphere.addEventListener("drop", (e) => this.dropScaleHandle(e)); + class VolumeTool extends EventDispatcher{ + constructor (viewer) { + super(); - pickSphere.addEventListener("mouseover", e => { - //node.setOpacity(1); - }); + this.viewer = viewer; + this.renderer = viewer.renderer; - pickSphere.addEventListener("click", e => { - e.consume(); + this.addEventListener('start_inserting_volume', e => { + this.viewer.dispatchEvent({ + type: 'cancel_insertions' }); + }); - pickSphere.addEventListener("mouseleave", e => { - //node.setOpacity(0.4); - }); - } - } + this.scene = new Scene(); + this.scene.name = 'scene_volume'; - initializeFocusHandles(){ - //let sgBox = new THREE.BoxGeometry(1, 1, 1); - let sgPlane = new PlaneGeometry(4, 4, 1, 1); - let sgLowPolySphere = new SphereGeometry(1, 16, 16); + this.viewer.inputHandler.registerInteractiveScene(this.scene); - let texture = new TextureLoader().load(`${exports.resourcePath}/icons/eye_2.png`); + this.onRemove = e => { + this.scene.remove(e.volume); + }; - for(let handleName of Object.keys(this.focusHandles)){ - let handle = this.focusHandles[handleName]; - let node = handle.node; - this.scene.add(node); - let align = handle.alignment; + this.onAdd = e => { + this.scene.add(e.volume); + }; - //node.lookAt(new THREE.Vector3().addVectors(node.position, new THREE.Vector3(...align))); - node.lookAt(new Vector3(...align)); + for(let volume of viewer.scene.volumes){ + this.onAdd({volume: volume}); + } - let off = 0.8; - if(align[0] === 1){ - node.position.set(1, off, -off).multiplyScalar(0.5); - node.rotation.z = Math.PI / 2; - }else if(align[0] === -1){ - node.position.set(-1, -off, -off).multiplyScalar(0.5); - node.rotation.z = Math.PI / 2; - }else if(align[1] === 1){ - node.position.set(-off, 1, -off).multiplyScalar(0.5); - node.rotation.set(Math.PI / 2, Math.PI, 0.0); - }else if(align[1] === -1){ - node.position.set(off, -1, -off).multiplyScalar(0.5); - node.rotation.set(Math.PI / 2, 0.0, 0.0); - }else if(align[2] === 1){ - node.position.set(off, off, 1).multiplyScalar(0.5); - }else if(align[2] === -1){ - node.position.set(-off, off, -1).multiplyScalar(0.5); - } + this.viewer.inputHandler.addEventListener('delete', e => { + let volumes = e.selection.filter(e => (e instanceof Volume)); + volumes.forEach(e => this.viewer.scene.removeVolume(e)); + }); - let material = new MeshBasicMaterial({ - color: handle.color, - opacity: 0, - transparent: true, - map: texture - }); + viewer.addEventListener("update", this.update.bind(this)); + viewer.addEventListener("render.pass.scene", e => this.render(e)); + viewer.addEventListener("scene_changed", this.onSceneChange.bind(this)); - //let outlineMaterial = new THREE.MeshBasicMaterial({ - // color: 0x000000, - // side: THREE.BackSide, - // opacity: 0, - // transparent: true}); + viewer.scene.addEventListener('volume_added', this.onAdd); + viewer.scene.addEventListener('volume_removed', this.onRemove); + } - let pickMaterial = new MeshNormalMaterial({ - //opacity: 0, - transparent: true, - visible: this.showPickVolumes}); + onSceneChange(e){ + if(e.oldScene){ + e.oldScene.removeEventListeners('volume_added', this.onAdd); + e.oldScene.removeEventListeners('volume_removed', this.onRemove); + } - let box = new Mesh(sgPlane, material); - box.name = `${handleName}.handle`; - box.scale.set(1.5, 1.5, 1.5); - box.position.set(0, 0, 0); - box.visible = false; - node.add(box); - //handle.focusNode = box; - - //let outline = new THREE.Mesh(sgPlane, outlineMaterial); - //outline.scale.set(1.4, 1.4, 1.4); - //outline.name = `${handleName}.outline`; - //box.add(outline); + e.scene.addEventListener('volume_added', this.onAdd); + e.scene.addEventListener('volume_removed', this.onRemove); + } - let pickSphere = new Mesh(sgLowPolySphere, pickMaterial); - pickSphere.name = `${handleName}.pick_volume`; - pickSphere.scale.set(3, 3, 3); - box.add(pickSphere); - pickSphere.handle = handleName; - this.pickVolumes.push(pickSphere); + startInsertion (args = {}) { + let volume; + if(args.type){ + volume = new args.type(); + }else { + volume = new BoxVolume(); + } + + volume.clip = args.clip || false; + volume.name = args.name || 'Volume'; - node.setOpacity = (target) => { - let opacity = {x: material.opacity}; - let t = new TWEEN.Tween(opacity).to({x: target}, 100); - t.onUpdate(() => { - pickSphere.visible = opacity.x > 0; - box.visible = opacity.x > 0; - material.opacity = opacity.x; - //outlineMaterial.opacity = opacity.x; - pickSphere.material.opacity = opacity.x * 0.5; - }); - t.start(); - }; + this.dispatchEvent({ + type: 'start_inserting_volume', + volume: volume + }); - pickSphere.addEventListener("drag", e => {}); + this.viewer.scene.addVolume(volume); + this.scene.add(volume); - pickSphere.addEventListener("mouseup", e => { - e.consume(); - }); + let cancel = { + callback: null + }; - pickSphere.addEventListener("mousedown", e => { - e.consume(); - }); + let drag = e => { + let camera = this.viewer.scene.getActiveCamera(); + + let I = Utils.getMousePointCloudIntersection( + e.drag.end, + this.viewer.scene.getActiveCamera(), + this.viewer, + this.viewer.scene.pointclouds, + {pickClipped: false}); - pickSphere.addEventListener("click", e => { - e.consume(); + if (I) { + volume.position.copy(I.location); - let selected = this.selection[0]; - let maxScale = Math.max(...selected.scale.toArray()); - let minScale = Math.min(...selected.scale.toArray()); - let handleLength = Math.abs(selected.scale.dot(new Vector3(...handle.alignment))); - let alignment = new Vector3(...handle.alignment).multiplyScalar(2 * maxScale / handleLength); - alignment.applyMatrix4(selected.matrixWorld); - let newCamPos = alignment; - let newCamTarget = selected.getWorldPosition(new Vector3()); + let wp = volume.getWorldPosition(new Vector3()).applyMatrix4(camera.matrixWorldInverse); + // let pp = new THREE.Vector4(wp.x, wp.y, wp.z).applyMatrix4(camera.projectionMatrix); + let w = Math.abs((wp.z / 5)); + volume.scale.set(w, w, w); + } + }; - Utils.moveTo(this.viewer.scene, newCamPos, newCamTarget); - }); + let drop = e => { + volume.removeEventListener('drag', drag); + volume.removeEventListener('drop', drop); - pickSphere.addEventListener("mouseover", e => { - //box.setOpacity(1); - }); + cancel.callback(); + }; - pickSphere.addEventListener("mouseleave", e => { - //box.setOpacity(0.4); - }); - } - } + cancel.callback = e => { + volume.removeEventListener('drag', drag); + volume.removeEventListener('drop', drop); + this.viewer.removeEventListener('cancel_insertions', cancel.callback); + }; - initializeTranslationHandles(){ - let boxGeometry = new BoxGeometry(1, 1, 1); + volume.addEventListener('drag', drag); + volume.addEventListener('drop', drop); + this.viewer.addEventListener('cancel_insertions', cancel.callback); - for(let handleName of Object.keys(this.translationHandles)){ - let handle = this.handles[handleName]; - let node = handle.node; - this.scene.add(node); + this.viewer.inputHandler.startDragging(volume); - let material = new MeshBasicMaterial({ - color: handle.color, - opacity: 0.4, - transparent: true}); + return volume; + } - let outlineMaterial = new MeshBasicMaterial({ - color: 0x000000, - side: BackSide, - opacity: 0.4, - transparent: true}); + update(){ + if (!this.viewer.scene) { + return; + } + + let camera = this.viewer.scene.getActiveCamera(); + let renderAreaSize = this.viewer.renderer.getSize(new Vector2()); + let clientWidth = renderAreaSize.width; + let clientHeight = renderAreaSize.height; - let pickMaterial = new MeshNormalMaterial({ - opacity: 0.2, - transparent: true, - visible: this.showPickVolumes - }); + let volumes = this.viewer.scene.volumes; + for (let volume of volumes) { + let label = volume.label; + + { - let box = new Mesh(boxGeometry, material); - box.name = `${handleName}.handle`; - box.scale.set(0.2, 0.2, 40); - box.lookAt(new Vector3(...handle.alignment)); - box.renderOrder = 10; - node.add(box); - handle.translateNode = box; + let distance = label.position.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); - let outline = new Mesh(boxGeometry, outlineMaterial); - outline.name = `${handleName}.outline`; - outline.scale.set(3, 3, 1.03); - outline.renderOrder = 0; - box.add(outline); + let scale = (70 / pr); + label.scale.set(scale, scale, scale); + } - let pickVolume = new Mesh(boxGeometry, pickMaterial); - pickVolume.name = `${handleName}.pick_volume`; - pickVolume.scale.set(12, 12, 1.1); - pickVolume.handle = handleName; - box.add(pickVolume); - this.pickVolumes.push(pickVolume); + let calculatedVolume = volume.getVolume(); + calculatedVolume = calculatedVolume / Math.pow(this.viewer.lengthUnit.unitspermeter, 3) * Math.pow(this.viewer.lengthUnitDisplay.unitspermeter, 3); //convert to cubic meters then to the cubic display unit + let text = Utils.addCommas(calculatedVolume.toFixed(3)) + ' ' + this.viewer.lengthUnitDisplay.code + '\u00B3'; + label.setText(text); + } + } - node.setOpacity = (target) => { - let opacity = {x: material.opacity}; - let t = new TWEEN.Tween(opacity).to({x: target}, 100); - t.onUpdate(() => { - box.visible = opacity.x > 0; - pickVolume.visible = opacity.x > 0; - material.opacity = opacity.x; - outlineMaterial.opacity = opacity.x; - pickMaterial.opacity = opacity.x * 0.5; - }); - t.start(); - }; + render(params){ + const renderer = this.viewer.renderer; - pickVolume.addEventListener("drag", (e) => {this.dragTranslationHandle(e);}); - pickVolume.addEventListener("drop", (e) => {this.dropTranslationHandle(e);}); + const oldTarget = renderer.getRenderTarget(); + + if(params.renderTarget){ + renderer.setRenderTarget(params.renderTarget); } + renderer.render(this.scene, this.viewer.scene.getActiveCamera()); + renderer.setRenderTarget(oldTarget); } - initializeRotationHandles(){ - let adjust = 0.5; - let torusGeometry = new TorusGeometry(1, adjust * 0.015, 8, 64, Math.PI / 2); - let outlineGeometry = new TorusGeometry(1, adjust * 0.04, 8, 64, Math.PI / 2); - let pickGeometry = new TorusGeometry(1, adjust * 0.1, 6, 4, Math.PI / 2); + } - for(let handleName of Object.keys(this.rotationHandles)){ - let handle = this.handles[handleName]; - let node = handle.node; - this.scene.add(node); + class Compass{ - let material = new MeshBasicMaterial({ - color: handle.color, - opacity: 0.4, - transparent: true}); + constructor(viewer){ + this.viewer = viewer; - let outlineMaterial = new MeshBasicMaterial({ - color: 0x000000, - side: BackSide, - opacity: 0.4, - transparent: true}); + this.visible = false; + this.dom = this.createElement(); - let pickMaterial = new MeshNormalMaterial({ - opacity: 0.2, - transparent: true, - visible: this.showPickVolumes - }); + viewer.addEventListener("update", () => { + const direction = viewer.scene.view.direction.clone(); + direction.z = 0; + direction.normalize(); - let box = new Mesh(torusGeometry, material); - box.name = `${handleName}.handle`; - box.scale.set(20, 20, 20); - box.lookAt(new Vector3(...handle.alignment)); - node.add(box); - handle.translateNode = box; + const camera = viewer.scene.getActiveCamera(); - let outline = new Mesh(outlineGeometry, outlineMaterial); - outline.name = `${handleName}.outline`; - outline.scale.set(1, 1, 1); - outline.renderOrder = 0; - box.add(outline); + const p1 = camera.getWorldPosition(new Vector3()); + const p2 = p1.clone().add(direction); - let pickVolume = new Mesh(pickGeometry, pickMaterial); - pickVolume.name = `${handleName}.pick_volume`; - pickVolume.scale.set(1, 1, 1); - pickVolume.handle = handleName; - box.add(pickVolume); - this.pickVolumes.push(pickVolume); + const projection = viewer.getProjection(); + const azimuth = Utils.computeAzimuth(p1, p2, projection); + + this.dom.css("transform", `rotateZ(${-azimuth}rad)`); + }); - node.setOpacity = (target) => { - let opacity = {x: material.opacity}; - let t = new TWEEN.Tween(opacity).to({x: target}, 100); - t.onUpdate(() => { - box.visible = opacity.x > 0; - pickVolume.visible = opacity.x > 0; - material.opacity = opacity.x; - outlineMaterial.opacity = opacity.x; - pickMaterial.opacity = opacity.x * 0.5; - }); - t.start(); - }; + this.dom.click( () => { + viewer.setTopView(); + }); + const renderArea = $(viewer.renderArea); + renderArea.append(this.dom); - //pickVolume.addEventListener("mouseover", (e) => { - // //let a = this.viewer.scene.getActiveCamera().getWorldDirection(new THREE.Vector3()).dot(pickVolume.getWorldDirection(new THREE.Vector3())); - // console.log(pickVolume.getWorldDirection(new THREE.Vector3())); - //}); - - pickVolume.addEventListener("drag", (e) => {this.dragRotationHandle(e);}); - pickVolume.addEventListener("drop", (e) => {this.dropRotationHandle(e);}); - } + this.setVisible(this.visible); } - dragRotationHandle(e){ - let drag = e.drag; - let handle = this.activeHandle; - let camera = this.viewer.scene.getActiveCamera(); + setVisible(visible){ + this.visible = visible; - if(!handle){ - return - }; + const value = visible ? "" : "none"; + this.dom.css("display", value); + } - let localNormal = new Vector3(...handle.alignment); - let n = new Vector3(); - n.copy(new Vector4(...localNormal.toArray(), 0).applyMatrix4(handle.node.matrixWorld)); - n.normalize(); + isVisible(){ + return this.visible; + } - if (!drag.intersectionStart){ + createElement(){ + const style = `style="position: absolute; top: 10px; right: 10px; z-index: 10000; width: 64px;"`; + const img = $(``); - //this.viewer.scene.scene.remove(this.debug); - //this.debug = new THREE.Object3D(); - //this.viewer.scene.scene.add(this.debug); - //Utils.debugSphere(this.debug, drag.location, 3, 0xaaaaaa); - //let debugEnd = drag.location.clone().add(n.clone().multiplyScalar(20)); - //Utils.debugLine(this.debug, drag.location, debugEnd, 0xff0000); + return img; + } - drag.intersectionStart = drag.location; - drag.objectStart = drag.object.getWorldPosition(new Vector3()); - drag.handle = handle; + }; - let plane = new Plane().setFromNormalAndCoplanarPoint(n, drag.intersectionStart); + class PotreeRenderer { - drag.dragPlane = plane; - drag.pivot = drag.intersectionStart; + constructor (viewer) { + this.viewer = viewer; + this.renderer = viewer.renderer; + + { + let dummyScene = new Scene(); + let geometry = new SphereGeometry(0.001, 2, 2); + let mesh = new Mesh(geometry, new MeshBasicMaterial()); + mesh.position.set(36453, 35163, 764712); + dummyScene.add(mesh); + + this.dummyMesh = mesh; + this.dummyScene = dummyScene; + } + } + + clearTargets(){ + + } + + clear(){ + let {viewer, renderer} = this; + + + // render skybox + if(viewer.background === "skybox"){ + renderer.setClearColor(0xff0000, 1); + }else if(viewer.background === "gradient"){ + renderer.setClearColor(0x00ff00, 1); + }else if(viewer.background === "black"){ + renderer.setClearColor(0x000000, 1); + }else if(viewer.background === "white"){ + renderer.setClearColor(0xFFFFFF, 1); }else { - handle = drag.handle; + renderer.setClearColor(0x000000, 0); } - this.dragging = true; + renderer.clear(); + } + + render(params){ + let {viewer, renderer} = this; - let mouse = drag.end; - let domElement = this.viewer.renderer.domElement; - let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); + const camera = params.camera ? params.camera : viewer.scene.getActiveCamera(); + + viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer}); + + const renderAreaSize = renderer.getSize(new Vector2()); + const width = params.viewport ? params.viewport[2] : renderAreaSize.x; + const height = params.viewport ? params.viewport[3] : renderAreaSize.y; + + // render skybox + if(viewer.background === "skybox"){ + viewer.skybox.camera.rotation.copy(viewer.scene.cameraP.rotation); + viewer.skybox.camera.fov = viewer.scene.cameraP.fov; + viewer.skybox.camera.aspect = viewer.scene.cameraP.aspect; + + viewer.skybox.parent.rotation.x = 0; + viewer.skybox.parent.updateMatrixWorld(); + + viewer.skybox.camera.updateProjectionMatrix(); + renderer.render(viewer.skybox.scene, viewer.skybox.camera); + }else if(viewer.background === "gradient"){ + renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG); + } - let I = ray.intersectPlane(drag.dragPlane, new Vector3()); + for(let pointcloud of this.viewer.scene.pointclouds){ + const {material} = pointcloud; + material.useEDL = false; + } + + viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, null, { + clipSpheres: viewer.scene.volumes.filter(v => (v instanceof Potree.SphereVolume)), + }); + + // render scene + renderer.render(viewer.scene.scene, camera); - if (I) { - let center = this.scene.getWorldPosition(new Vector3()); - let from = drag.pivot; - let to = I; + viewer.dispatchEvent({type: "render.pass.scene",viewer: viewer}); + + viewer.clippingTool.update(); + renderer.render(viewer.clippingTool.sceneMarker, viewer.scene.cameraScreenSpace); //viewer.scene.cameraScreenSpace); + renderer.render(viewer.clippingTool.sceneVolume, camera); - let v1 = from.clone().sub(center).normalize(); - let v2 = to.clone().sub(center).normalize(); + renderer.render(viewer.controls.sceneControls, camera); + + renderer.clearDepth(); + + viewer.transformationTool.update(); + + viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer}); - let angle = Math.acos(v1.dot(v2)); - let sign = Math.sign(v1.cross(v2).dot(n)); - angle = angle * sign; - if (Number.isNaN(angle)) { - return; - } + // renderer.render(viewer.controls.sceneControls, camera); + // renderer.render(viewer.clippingTool.sceneVolume, camera); + // renderer.render(viewer.transformationTool.scene, camera); + + // renderer.setViewport(width - viewer.navigationCube.width, + // height - viewer.navigationCube.width, + // viewer.navigationCube.width, viewer.navigationCube.width); + // renderer.render(viewer.navigationCube, viewer.navigationCube.camera); + // renderer.setViewport(0, 0, width, height); + + viewer.dispatchEvent({type: "render.pass.end",viewer: viewer}); + } - let normal = new Vector3(...handle.alignment); - for (let selection of this.selection) { - selection.rotateOnAxis(normal, angle); - selection.dispatchEvent({ - type: "orientation_changed", - object: selection - }); - } + } + + class EDLRenderer{ + constructor(viewer){ + this.viewer = viewer; + + this.edlMaterial = null; + + this.rtRegular; + this.rtEDL; + + this.gl = viewer.renderer.getContext(); + + this.shadowMap = new PointCloudSM(this.viewer.pRenderer); + } + + initEDL(){ + if (this.edlMaterial != null) { + return; + } + + this.edlMaterial = new EyeDomeLightingMaterial(); + this.edlMaterial.depthTest = true; + this.edlMaterial.depthWrite = true; + this.edlMaterial.transparent = true; + + this.rtEDL = new WebGLRenderTarget(1024, 1024, { + minFilter: NearestFilter, + magFilter: NearestFilter, + format: RGBAFormat, + type: FloatType, + depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType) + }); + + this.rtRegular = new WebGLRenderTarget(1024, 1024, { + minFilter: NearestFilter, + magFilter: NearestFilter, + format: RGBAFormat, + depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType) + }); + }; - drag.pivot = I; + resize(width, height){ + if(this.screenshot){ + width = this.screenshot.target.width; + height = this.screenshot.target.height; } - } - dropRotationHandle(e){ - this.dragging = false; - this.setActiveHandle(null); + this.rtEDL.setSize(width , height); + this.rtRegular.setSize(width , height); } - dragTranslationHandle(e){ - let drag = e.drag; - let handle = this.activeHandle; - let camera = this.viewer.scene.getActiveCamera(); - - if(!drag.intersectionStart && handle){ - drag.intersectionStart = drag.location; - drag.objectStart = drag.object.getWorldPosition(new Vector3()); - - let start = drag.intersectionStart; - let dir = new Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld); - let end = new Vector3().addVectors(start, dir); - let line = new Line3(start.clone(), end.clone()); - drag.line = line; + makeScreenshot(camera, size, callback){ - let camOnLine = line.closestPointToPoint(camera.position, false, new Vector3()); - let normal = new Vector3().subVectors(camera.position, camOnLine); - let plane = new Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart); - drag.dragPlane = plane; - drag.pivot = drag.intersectionStart; - }else { - handle = drag.handle; + if(camera === undefined || camera === null){ + camera = this.viewer.scene.getActiveCamera(); } - this.dragging = true; - - { - let mouse = drag.end; - let domElement = this.viewer.renderer.domElement; - let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); - let I = ray.intersectPlane(drag.dragPlane, new Vector3()); - - if (I) { - let iOnLine = drag.line.closestPointToPoint(I, false, new Vector3()); + if(size === undefined || size === null){ + size = this.viewer.renderer.getSize(new Vector2()); + } - let diff = new Vector3().subVectors(iOnLine, drag.pivot); + let {width, height} = size; - for (let selection of this.selection) { - selection.position.add(diff); - selection.dispatchEvent({ - type: "position_changed", - object: selection - }); - } + //let maxTextureSize = viewer.renderer.capabilities.maxTextureSize; + //if(width * 4 < + width = 2 * width; + height = 2 * height; - drag.pivot = drag.pivot.add(diff); - } - } - } + let target = new WebGLRenderTarget(width, height, { + format: RGBAFormat, + }); - dropTranslationHandle(e){ - this.dragging = false; - this.setActiveHandle(null); - } + this.screenshot = { + target: target + }; - dropScaleHandle(e){ - this.dragging = false; - this.setActiveHandle(null); - } + // HACK? removed because of error, was this important? + //this.viewer.renderer.clearTarget(target, true, true, true); - dragScaleHandle(e){ - let drag = e.drag; - let handle = this.activeHandle; - let camera = this.viewer.scene.getActiveCamera(); + this.render(); - if(!drag.intersectionStart){ - drag.intersectionStart = drag.location; - drag.objectStart = drag.object.getWorldPosition(new Vector3()); - drag.handle = handle; + let pixelCount = width * height; + let buffer = new Uint8Array(4 * pixelCount); - let start = drag.intersectionStart; - let dir = new Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld); - let end = new Vector3().addVectors(start, dir); - let line = new Line3(start.clone(), end.clone()); - drag.line = line; + this.viewer.renderer.readRenderTargetPixels(target, 0, 0, width, height, buffer); - let camOnLine = line.closestPointToPoint(camera.position, false, new Vector3()); - let normal = new Vector3().subVectors(camera.position, camOnLine); - let plane = new Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart); - drag.dragPlane = plane; - drag.pivot = drag.intersectionStart; + // flip vertically + let bytesPerLine = width * 4; + for(let i = 0; i < parseInt(height / 2); i++){ + let j = height - i - 1; - //Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05); - }else { - handle = drag.handle; + let lineI = buffer.slice(i * bytesPerLine, i * bytesPerLine + bytesPerLine); + let lineJ = buffer.slice(j * bytesPerLine, j * bytesPerLine + bytesPerLine); + buffer.set(lineJ, i * bytesPerLine); + buffer.set(lineI, j * bytesPerLine); } - this.dragging = true; + this.screenshot.target.dispose(); + delete this.screenshot; - { - let mouse = drag.end; - let domElement = this.viewer.renderer.domElement; - let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); - let I = ray.intersectPlane(drag.dragPlane, new Vector3()); + return { + width: width, + height: height, + buffer: buffer + }; + } - if (I) { - let iOnLine = drag.line.closestPointToPoint(I, false, new Vector3()); - let direction = handle.alignment.reduce( (a, v) => a + v, 0); + clearTargets(){ + const viewer = this.viewer; + const {renderer} = viewer; - let toObjectSpace = this.selection[0].matrixWorld.clone().invert(); - let iOnLineOS = iOnLine.clone().applyMatrix4(toObjectSpace); - let pivotOS = drag.pivot.clone().applyMatrix4(toObjectSpace); - let diffOS = new Vector3().subVectors(iOnLineOS, pivotOS); - let dragDirectionOS = diffOS.clone().normalize(); - if(iOnLine.distanceTo(drag.pivot) === 0){ - dragDirectionOS.set(0, 0, 0); - } - let dragDirection = dragDirectionOS.dot(new Vector3(...handle.alignment)); + const oldTarget = renderer.getRenderTarget(); - let diff = new Vector3().subVectors(iOnLine, drag.pivot); - let diffScale = new Vector3(...handle.alignment).multiplyScalar(diff.length() * direction * dragDirection); - let diffPosition = diff.clone().multiplyScalar(0.5); + renderer.setRenderTarget( this.rtEDL ); + renderer.clear( true, true, true ); - for (let selection of this.selection) { - selection.scale.add(diffScale); - selection.scale.x = Math.max(0.1, selection.scale.x); - selection.scale.y = Math.max(0.1, selection.scale.y); - selection.scale.z = Math.max(0.1, selection.scale.z); - selection.position.add(diffPosition); - selection.dispatchEvent({ - type: "position_changed", - object: selection - }); - selection.dispatchEvent({ - type: "scale_changed", - object: selection - }); - } + renderer.setRenderTarget( this.rtRegular ); + renderer.clear( true, true, false ); - drag.pivot.copy(iOnLine); - //Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05); - } - } + renderer.setRenderTarget(oldTarget); } - setActiveHandle(handle){ - if(this.dragging){ - return; - } + clear(){ + this.initEDL(); + const viewer = this.viewer; - if(this.activeHandle === handle){ - return; + const {renderer, background} = viewer; + + if(background === "skybox"){ + renderer.setClearColor(0x000000, 0); + } else if (background === 'gradient') { + renderer.setClearColor(0x000000, 0); + } else if (background === 'black') { + renderer.setClearColor(0x000000, 1); + } else if (background === 'white') { + renderer.setClearColor(0xFFFFFF, 1); + } else { + renderer.setClearColor(0x000000, 0); } + + renderer.clear(); - this.activeHandle = handle; + this.clearTargets(); + } - if(handle === null){ - for(let handleName of Object.keys(this.handles)){ - let handle = this.handles[handleName]; - handle.node.setOpacity(0); - } - } + renderShadowMap(visiblePointClouds, camera, lights){ - for(let handleName of Object.keys(this.focusHandles)){ - let handle = this.focusHandles[handleName]; + const {viewer} = this; - if(this.activeHandle === handle){ - handle.node.setOpacity(1.0); - }else { - handle.node.setOpacity(0.4); - } - } + const doShadows = lights.length > 0 && !(lights[0].disableShadowUpdates); + if(doShadows){ + let light = lights[0]; - for(let handleName of Object.keys(this.translationHandles)){ - let handle = this.translationHandles[handleName]; + this.shadowMap.setLight(light); - if(this.activeHandle === handle){ - handle.node.setOpacity(1.0); - }else { - handle.node.setOpacity(0.4); + let originalAttributes = new Map(); + for(let pointcloud of viewer.scene.pointclouds){ + // TODO IMPORTANT !!! check + originalAttributes.set(pointcloud, pointcloud.material.activeAttributeName); + pointcloud.material.disableEvents(); + pointcloud.material.activeAttributeName = "depth"; + //pointcloud.material.pointColorType = PointColorType.DEPTH; } - } - for(let handleName of Object.keys(this.rotationHandles)){ - let handle = this.rotationHandles[handleName]; + this.shadowMap.render(viewer.scene.scenePointCloud, camera); - //if(this.activeHandle === handle){ - // handle.node.setOpacity(1.0); - //}else{ - // handle.node.setOpacity(0.4) - //} + for(let pointcloud of visiblePointClouds){ + let originalAttribute = originalAttributes.get(pointcloud); + // TODO IMPORTANT !!! check + pointcloud.material.activeAttributeName = originalAttribute; + pointcloud.material.enableEvents(); + } - handle.node.setOpacity(0.4); + viewer.shadowTestCam.updateMatrixWorld(); + viewer.shadowTestCam.matrixWorldInverse.copy(viewer.shadowTestCam.matrixWorld).invert(); + viewer.shadowTestCam.updateProjectionMatrix(); } - for(let handleName of Object.keys(this.scaleHandles)){ - let handle = this.scaleHandles[handleName]; + } - if(this.activeHandle === handle){ - handle.node.setOpacity(1.0); + render(params){ + this.initEDL(); - let relatedFocusHandle = this.focusHandles[handle.name.replace("scale", "focus")]; - let relatedFocusNode = relatedFocusHandle.node; - relatedFocusNode.setOpacity(0.4); + const viewer = this.viewer; + let camera = params.camera ? params.camera : viewer.scene.getActiveCamera(); + const {width, height} = this.viewer.renderer.getSize(new Vector2()); - for(let translationHandleName of Object.keys(this.translationHandles)){ - let translationHandle = this.translationHandles[translationHandleName]; - translationHandle.node.setOpacity(0.4); - } - //let relatedTranslationHandle = this.translationHandles[ - // handle.name.replace("scale", "translation").replace(/[+-]/g, "")]; - //let relatedTranslationNode = relatedTranslationHandle.node; - //relatedTranslationNode.setOpacity(0.4); + viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer}); + + this.resize(width, height); + const visiblePointClouds = viewer.scene.pointclouds.filter(pc => pc.visible); - }else { - handle.node.setOpacity(0.4); - } + if(this.screenshot){ + let oldBudget = Potree.pointBudget; + Potree.pointBudget = Math.max(10 * 1000 * 1000, 2 * oldBudget); + let result = Potree.updatePointClouds( + viewer.scene.pointclouds, + camera, + viewer.renderer); + Potree.pointBudget = oldBudget; } - + let lights = []; + viewer.scene.scene.traverse(node => { + if(node.type === "SpotLight"){ + lights.push(node); + } + }); + if(viewer.background === "skybox"){ + viewer.skybox.camera.rotation.copy(viewer.scene.cameraP.rotation); + viewer.skybox.camera.fov = viewer.scene.cameraP.fov; + viewer.skybox.camera.aspect = viewer.scene.cameraP.aspect; + viewer.skybox.parent.rotation.x = 0; + viewer.skybox.parent.updateMatrixWorld(); - if(handle){ - handle.node.setOpacity(1.0); - } + viewer.skybox.camera.updateProjectionMatrix(); + viewer.renderer.render(viewer.skybox.scene, viewer.skybox.camera); + } else if (viewer.background === 'gradient') { + viewer.renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG); + } - - } + //TODO adapt to multiple lights + this.renderShadowMap(visiblePointClouds, camera, lights); - update () { + { // COLOR & DEPTH PASS + for (let pointcloud of visiblePointClouds) { + let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x; - if(this.selection.length === 1){ + let material = pointcloud.material; + material.weighted = false; + material.useLogarithmicDepthBuffer = false; + material.useEDL = true; - this.scene.visible = true; + material.screenWidth = width; + material.screenHeight = height; + material.uniforms.visibleNodes.value = pointcloud.material.visibleNodesTexture; + material.uniforms.octreeSize.value = octreeSize; + material.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(pointcloud.scale.x, pointcloud.scale.y, pointcloud.scale.z); + } + + // TODO adapt to multiple lights + viewer.renderer.setRenderTarget(this.rtEDL); + + if(lights.length > 0){ + viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtEDL, { + clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)), + shadowMaps: [this.shadowMap], + transparent: false, + }); + }else { - this.scene.updateMatrix(); - this.scene.updateMatrixWorld(); + + // let test = camera.clone(); + // test.matrixAutoUpdate = false; - let selected = this.selection[0]; - let world = selected.matrixWorld; - let camera = this.viewer.scene.getActiveCamera(); - let domElement = this.viewer.renderer.domElement; - let mouse = this.viewer.inputHandler.mouse; + // //test.updateMatrixWorld = () => {}; - let center = selected.boundingBox.getCenter(new Vector3()).clone().applyMatrix4(selected.matrixWorld); + // let mat = new THREE.Matrix4().set( + // 1, 0, 0, 0, + // 0, 0, 1, 0, + // 0, -1, 0, 0, + // 0, 0, 0, 1, + // ); + // mat.invert() - this.scene.scale.copy(selected.boundingBox.getSize(new Vector3()).multiply(selected.scale)); - this.scene.position.copy(center); - this.scene.rotation.copy(selected.rotation); + // test.matrix.multiplyMatrices(mat, test.matrix); + // test.updateMatrixWorld(); - this.scene.updateMatrixWorld(); + //test.matrixWorld.multiplyMatrices(mat, test.matrixWorld); + //test.matrixWorld.multiply(mat); + //test.matrixWorldInverse.invert(test.matrixWorld); + //test.matrixWorldInverse.multiplyMatrices(test.matrixWorldInverse, mat); + - { - // adjust scale of components - for(let handleName of Object.keys(this.handles)){ - let handle = this.handles[handleName]; - let node = handle.node; + viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtEDL, { + clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)), + transparent: false, + }); + } - let handlePos = node.getWorldPosition(new Vector3()); - let distance = handlePos.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight); + + } - let ws = node.parent.getWorldScale(new Vector3()); + viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer, renderTarget: this.rtRegular}); + viewer.renderer.setRenderTarget(null); + viewer.renderer.render(viewer.scene.scene, camera); - let s = (7 / pr); - let scale = new Vector3(s, s, s).divide(ws); + { // EDL PASS - let rot = new Matrix4().makeRotationFromEuler(node.rotation); - let rotInv = rot.clone().invert(); + const uniforms = this.edlMaterial.uniforms; - scale.applyMatrix4(rotInv); - scale.x = Math.abs(scale.x); - scale.y = Math.abs(scale.y); - scale.z = Math.abs(scale.z); + uniforms.screenWidth.value = width; + uniforms.screenHeight.value = height; - node.scale.copy(scale); - } + let proj = camera.projectionMatrix; + let projArray = new Float32Array(16); + projArray.set(proj.elements); - // adjust rotation handles - if(!this.dragging){ - let tWorld = this.scene.matrixWorld; - let tObject = tWorld.clone().invert(); - let camObjectPos = camera.getWorldPosition(new Vector3()).applyMatrix4(tObject); + uniforms.uNear.value = camera.near; + uniforms.uFar.value = camera.far; + uniforms.uEDLColor.value = this.rtEDL.texture; + uniforms.uEDLDepth.value = this.rtEDL.depthTexture; + uniforms.uProj.value = projArray; - let x = this.rotationHandles["rotation.x"].node.rotation; - let y = this.rotationHandles["rotation.y"].node.rotation; - let z = this.rotationHandles["rotation.z"].node.rotation; + uniforms.edlStrength.value = viewer.edlStrength; + uniforms.radius.value = viewer.edlRadius; + uniforms.opacity.value = viewer.edlOpacity; // HACK + + Utils.screenPass.render(viewer.renderer, this.edlMaterial); - x.order = "ZYX"; - y.order = "ZYX"; + if(this.screenshot){ + Utils.screenPass.render(viewer.renderer, this.edlMaterial, this.screenshot.target); + } - let above = camObjectPos.z > 0; - let below = !above; - let PI_HALF = Math.PI / 2; + } - if(above){ - if(camObjectPos.x > 0 && camObjectPos.y > 0){ - x.x = 1 * PI_HALF; - y.y = 3 * PI_HALF; - z.z = 0 * PI_HALF; - }else if(camObjectPos.x < 0 && camObjectPos.y > 0){ - x.x = 1 * PI_HALF; - y.y = 2 * PI_HALF; - z.z = 1 * PI_HALF; - }else if(camObjectPos.x < 0 && camObjectPos.y < 0){ - x.x = 2 * PI_HALF; - y.y = 2 * PI_HALF; - z.z = 2 * PI_HALF; - }else if(camObjectPos.x > 0 && camObjectPos.y < 0){ - x.x = 2 * PI_HALF; - y.y = 3 * PI_HALF; - z.z = 3 * PI_HALF; - } - }else if(below){ - if(camObjectPos.x > 0 && camObjectPos.y > 0){ - x.x = 0 * PI_HALF; - y.y = 0 * PI_HALF; - z.z = 0 * PI_HALF; - }else if(camObjectPos.x < 0 && camObjectPos.y > 0){ - x.x = 0 * PI_HALF; - y.y = 1 * PI_HALF; - z.z = 1 * PI_HALF; - }else if(camObjectPos.x < 0 && camObjectPos.y < 0){ - x.x = 3 * PI_HALF; - y.y = 1 * PI_HALF; - z.z = 2 * PI_HALF; - }else if(camObjectPos.x > 0 && camObjectPos.y < 0){ - x.x = 3 * PI_HALF; - y.y = 0 * PI_HALF; - z.z = 3 * PI_HALF; - } - } - } + viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer}); - { - let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); - let raycaster = new Raycaster(ray.origin, ray.direction); - let intersects = raycaster.intersectObjects(this.pickVolumes.filter(v => v.visible), true); + viewer.renderer.clearDepth(); - if(intersects.length > 0){ - let I = intersects[0]; - let handleName = I.object.handle; - this.setActiveHandle(this.handles[handleName]); - }else { - this.setActiveHandle(null); - } - } + viewer.transformationTool.update(); - // - for(let handleName of Object.keys(this.scaleHandles)){ - let handle = this.handles[handleName]; - let node = handle.node; - let alignment = handle.alignment; + viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer}); + + viewer.renderer.render(viewer.controls.sceneControls, camera); + viewer.renderer.render(viewer.clippingTool.sceneVolume, camera); + viewer.renderer.render(viewer.transformationTool.scene, camera); + + viewer.dispatchEvent({type: "render.pass.end",viewer: viewer}); + + } + } + + class HQSplatRenderer{ + + constructor(viewer){ + this.viewer = viewer; + + this.depthMaterials = new Map(); + this.attributeMaterials = new Map(); + this.normalizationMaterial = null; - + this.rtDepth = null; + this.rtAttribute = null; + this.gl = viewer.renderer.getContext(); - } - } + this.initialized = false; + } - }else { - this.scene.visible = false; + init(){ + if (this.initialized) { + return; } - - } - }; + this.normalizationMaterial = new NormalizationMaterial(); + this.normalizationMaterial.depthTest = true; + this.normalizationMaterial.depthWrite = true; + this.normalizationMaterial.transparent = true; - class VolumeTool extends EventDispatcher{ - constructor (viewer) { - super(); + this.normalizationEDLMaterial = new NormalizationEDLMaterial(); + this.normalizationEDLMaterial.depthTest = true; + this.normalizationEDLMaterial.depthWrite = true; + this.normalizationEDLMaterial.transparent = true; - this.viewer = viewer; - this.renderer = viewer.renderer; + this.rtDepth = new WebGLRenderTarget(1024, 1024, { + minFilter: NearestFilter, + magFilter: NearestFilter, + format: RGBAFormat, + type: FloatType, + depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType) + }); - this.addEventListener('start_inserting_volume', e => { - this.viewer.dispatchEvent({ - type: 'cancel_insertions' - }); + this.rtAttribute = new WebGLRenderTarget(1024, 1024, { + minFilter: NearestFilter, + magFilter: NearestFilter, + format: RGBAFormat, + type: FloatType, + depthTexture: this.rtDepth.depthTexture, }); - this.scene = new Scene(); - this.scene.name = 'scene_volume'; + this.initialized = true; + }; - this.viewer.inputHandler.registerInteractiveScene(this.scene); + resize(width, height){ + this.rtDepth.setSize(width, height); + this.rtAttribute.setSize(width, height); + } - this.onRemove = e => { - this.scene.remove(e.volume); - }; + clearTargets(){ + const viewer = this.viewer; + const {renderer} = viewer; - this.onAdd = e => { - this.scene.add(e.volume); - }; + const oldTarget = renderer.getRenderTarget(); - for(let volume of viewer.scene.volumes){ - this.onAdd({volume: volume}); - } + renderer.setClearColor(0x000000, 0); - this.viewer.inputHandler.addEventListener('delete', e => { - let volumes = e.selection.filter(e => (e instanceof Volume)); - volumes.forEach(e => this.viewer.scene.removeVolume(e)); - }); + renderer.setRenderTarget( this.rtDepth ); + renderer.clear( true, true, true ); - viewer.addEventListener("update", this.update.bind(this)); - viewer.addEventListener("render.pass.scene", e => this.render(e)); - viewer.addEventListener("scene_changed", this.onSceneChange.bind(this)); + renderer.setRenderTarget( this.rtAttribute ); + renderer.clear( true, true, true ); - viewer.scene.addEventListener('volume_added', this.onAdd); - viewer.scene.addEventListener('volume_removed', this.onRemove); + renderer.setRenderTarget(oldTarget); } - onSceneChange(e){ - if(e.oldScene){ - e.oldScene.removeEventListeners('volume_added', this.onAdd); - e.oldScene.removeEventListeners('volume_removed', this.onRemove); - } - e.scene.addEventListener('volume_added', this.onAdd); - e.scene.addEventListener('volume_removed', this.onRemove); - } + clear(){ + this.init(); - startInsertion (args = {}) { - let volume; - if(args.type){ - volume = new args.type(); - }else { - volume = new BoxVolume(); - } - - volume.clip = args.clip || false; - volume.name = args.name || 'Volume'; + const {renderer, background} = this.viewer; - this.dispatchEvent({ - type: 'start_inserting_volume', - volume: volume - }); + if(background === "skybox"){ + renderer.setClearColor(0x000000, 0); + } else if (background === 'gradient') { + renderer.setClearColor(0x000000, 0); + } else if (background === 'black') { + renderer.setClearColor(0x000000, 1); + } else if (background === 'white') { + renderer.setClearColor(0xFFFFFF, 1); + } else { + renderer.setClearColor(0x000000, 0); + } - this.viewer.scene.addVolume(volume); - this.scene.add(volume); + renderer.clear(); - let cancel = { - callback: null - }; + this.clearTargets(); + } - let drag = e => { - let camera = this.viewer.scene.getActiveCamera(); - - let I = Utils.getMousePointCloudIntersection( - e.drag.end, - this.viewer.scene.getActiveCamera(), - this.viewer, - this.viewer.scene.pointclouds, - {pickClipped: false}); + render (params) { + this.init(); - if (I) { - volume.position.copy(I.location); + const viewer = this.viewer; + const camera = params.camera ? params.camera : viewer.scene.getActiveCamera(); + const {width, height} = this.viewer.renderer.getSize(new Vector2()); - let wp = volume.getWorldPosition(new Vector3()).applyMatrix4(camera.matrixWorldInverse); - // let pp = new THREE.Vector4(wp.x, wp.y, wp.z).applyMatrix4(camera.projectionMatrix); - let w = Math.abs((wp.z / 5)); - volume.scale.set(w, w, w); - } - }; + viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer}); - let drop = e => { - volume.removeEventListener('drag', drag); - volume.removeEventListener('drop', drop); + this.resize(width, height); - cancel.callback(); - }; + const visiblePointClouds = viewer.scene.pointclouds.filter(pc => pc.visible); + const originalMaterials = new Map(); - cancel.callback = e => { - volume.removeEventListener('drag', drag); - volume.removeEventListener('drop', drop); - this.viewer.removeEventListener('cancel_insertions', cancel.callback); - }; + for(let pointcloud of visiblePointClouds){ + originalMaterials.set(pointcloud, pointcloud.material); - volume.addEventListener('drag', drag); - volume.addEventListener('drop', drop); - this.viewer.addEventListener('cancel_insertions', cancel.callback); + if(!this.attributeMaterials.has(pointcloud)){ + let attributeMaterial = new PointCloudMaterial$1(); + this.attributeMaterials.set(pointcloud, attributeMaterial); + } - this.viewer.inputHandler.startDragging(volume); + if(!this.depthMaterials.has(pointcloud)){ + let depthMaterial = new PointCloudMaterial$1(); - return volume; - } + depthMaterial.setDefine("depth_pass", "#define hq_depth_pass"); + depthMaterial.setDefine("use_edl", "#define use_edl"); - update(){ - if (!this.viewer.scene) { - return; + this.depthMaterials.set(pointcloud, depthMaterial); + } } - - let camera = this.viewer.scene.getActiveCamera(); - let renderAreaSize = this.viewer.renderer.getSize(new Vector2()); - let clientWidth = renderAreaSize.width; - let clientHeight = renderAreaSize.height; - let volumes = this.viewer.scene.volumes; - for (let volume of volumes) { - let label = volume.label; - - { + { // DEPTH PASS + for (let pointcloud of visiblePointClouds) { + let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x; - let distance = label.position.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight); + let material = originalMaterials.get(pointcloud); + let depthMaterial = this.depthMaterials.get(pointcloud); - let scale = (70 / pr); - label.scale.set(scale, scale, scale); - } + depthMaterial.size = material.size; + depthMaterial.minSize = material.minSize; + depthMaterial.maxSize = material.maxSize; - let calculatedVolume = volume.getVolume(); - calculatedVolume = calculatedVolume / Math.pow(this.viewer.lengthUnit.unitspermeter, 3) * Math.pow(this.viewer.lengthUnitDisplay.unitspermeter, 3); //convert to cubic meters then to the cubic display unit - let text = Utils.addCommas(calculatedVolume.toFixed(3)) + ' ' + this.viewer.lengthUnitDisplay.code + '\u00B3'; - label.setText(text); - } - } + depthMaterial.pointSizeType = material.pointSizeType; + depthMaterial.visibleNodesTexture = material.visibleNodesTexture; + depthMaterial.weighted = false; + depthMaterial.screenWidth = width; + depthMaterial.shape = PointShape.CIRCLE; + depthMaterial.screenHeight = height; + depthMaterial.uniforms.visibleNodes.value = material.visibleNodesTexture; + depthMaterial.uniforms.octreeSize.value = octreeSize; + depthMaterial.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(...pointcloud.scale.toArray()); + depthMaterial.classification = material.classification; + depthMaterial.uniforms.classificationLUT.value.image.data = material.uniforms.classificationLUT.value.image.data; + depthMaterial.classificationTexture.needsUpdate = true; - render(params){ - const renderer = this.viewer.renderer; + depthMaterial.uniforms.uFilterReturnNumberRange.value = material.uniforms.uFilterReturnNumberRange.value; + depthMaterial.uniforms.uFilterNumberOfReturnsRange.value = material.uniforms.uFilterNumberOfReturnsRange.value; + depthMaterial.uniforms.uFilterGPSTimeClipRange.value = material.uniforms.uFilterGPSTimeClipRange.value; + depthMaterial.uniforms.uFilterPointSourceIDClipRange.value = material.uniforms.uFilterPointSourceIDClipRange.value; - const oldTarget = renderer.getRenderTarget(); - - if(params.renderTarget){ - renderer.setRenderTarget(params.renderTarget); - } - renderer.render(this.scene, this.viewer.scene.getActiveCamera()); - renderer.setRenderTarget(oldTarget); - } + depthMaterial.clipTask = material.clipTask; + depthMaterial.clipMethod = material.clipMethod; + depthMaterial.setClipBoxes(material.clipBoxes); + depthMaterial.setClipPolygons(material.clipPolygons); - } + pointcloud.material = depthMaterial; + } + + viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtDepth, { + clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)), + }); + } - class Compass{ - - constructor(viewer){ - this.viewer = viewer; - - this.visible = false; - this.dom = this.createElement(); - - viewer.addEventListener("update", () => { - const direction = viewer.scene.view.direction.clone(); - direction.z = 0; - direction.normalize(); - - const camera = viewer.scene.getActiveCamera(); - - const p1 = camera.getWorldPosition(new Vector3()); - const p2 = p1.clone().add(direction); - - const projection = viewer.getProjection(); - const azimuth = Utils.computeAzimuth(p1, p2, projection); - - this.dom.css("transform", `rotateZ(${-azimuth}rad)`); - }); - - this.dom.click( () => { - viewer.setTopView(); - }); - - const renderArea = $(viewer.renderArea); - renderArea.append(this.dom); - - this.setVisible(this.visible); - } - - setVisible(visible){ - this.visible = visible; - - const value = visible ? "" : "none"; - this.dom.css("display", value); - } - - isVisible(){ - return this.visible; - } - - createElement(){ - const style = `style="position: absolute; top: 10px; right: 10px; z-index: 10000; width: 64px;"`; - const img = $(``); - - return img; - } - - }; + { // ATTRIBUTE PASS + for (let pointcloud of visiblePointClouds) { + let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x; - class PotreeRenderer { + let material = originalMaterials.get(pointcloud); + let attributeMaterial = this.attributeMaterials.get(pointcloud); - constructor (viewer) { - this.viewer = viewer; - this.renderer = viewer.renderer; + attributeMaterial.size = material.size; + attributeMaterial.minSize = material.minSize; + attributeMaterial.maxSize = material.maxSize; - { - let dummyScene = new Scene(); - let geometry = new SphereGeometry(0.001, 2, 2); - let mesh = new Mesh(geometry, new MeshBasicMaterial()); - mesh.position.set(36453, 35163, 764712); - dummyScene.add(mesh); + attributeMaterial.pointSizeType = material.pointSizeType; + attributeMaterial.activeAttributeName = material.activeAttributeName; + attributeMaterial.visibleNodesTexture = material.visibleNodesTexture; + attributeMaterial.weighted = true; + attributeMaterial.screenWidth = width; + attributeMaterial.screenHeight = height; + attributeMaterial.shape = PointShape.CIRCLE; + attributeMaterial.uniforms.visibleNodes.value = material.visibleNodesTexture; + attributeMaterial.uniforms.octreeSize.value = octreeSize; + attributeMaterial.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(...pointcloud.scale.toArray()); + attributeMaterial.classification = material.classification; + attributeMaterial.uniforms.classificationLUT.value.image.data = material.uniforms.classificationLUT.value.image.data; + attributeMaterial.classificationTexture.needsUpdate = true; - this.dummyMesh = mesh; - this.dummyScene = dummyScene; - } - } + attributeMaterial.uniforms.uFilterReturnNumberRange.value = material.uniforms.uFilterReturnNumberRange.value; + attributeMaterial.uniforms.uFilterNumberOfReturnsRange.value = material.uniforms.uFilterNumberOfReturnsRange.value; + attributeMaterial.uniforms.uFilterGPSTimeClipRange.value = material.uniforms.uFilterGPSTimeClipRange.value; + attributeMaterial.uniforms.uFilterPointSourceIDClipRange.value = material.uniforms.uFilterPointSourceIDClipRange.value; - clearTargets(){ + attributeMaterial.elevationGradientRepeat = material.elevationGradientRepeat; + attributeMaterial.elevationRange = material.elevationRange; + attributeMaterial.gradient = material.gradient; + attributeMaterial.matcap = material.matcap; - } + attributeMaterial.intensityRange = material.intensityRange; + attributeMaterial.intensityGamma = material.intensityGamma; + attributeMaterial.intensityContrast = material.intensityContrast; + attributeMaterial.intensityBrightness = material.intensityBrightness; - clear(){ - let {viewer, renderer} = this; + attributeMaterial.rgbGamma = material.rgbGamma; + attributeMaterial.rgbContrast = material.rgbContrast; + attributeMaterial.rgbBrightness = material.rgbBrightness; + attributeMaterial.weightRGB = material.weightRGB; + attributeMaterial.weightIntensity = material.weightIntensity; + attributeMaterial.weightElevation = material.weightElevation; + attributeMaterial.weightRGB = material.weightRGB; + attributeMaterial.weightClassification = material.weightClassification; + attributeMaterial.weightReturnNumber = material.weightReturnNumber; + attributeMaterial.weightSourceID = material.weightSourceID; - // render skybox - if(viewer.background === "skybox"){ - renderer.setClearColor(0xff0000, 1); - }else if(viewer.background === "gradient"){ - renderer.setClearColor(0x00ff00, 1); - }else if(viewer.background === "black"){ - renderer.setClearColor(0x000000, 1); - }else if(viewer.background === "white"){ - renderer.setClearColor(0xFFFFFF, 1); - }else { - renderer.setClearColor(0x000000, 0); - } + attributeMaterial.color = material.color; - renderer.clear(); - } - - render(params){ - let {viewer, renderer} = this; + attributeMaterial.clipTask = material.clipTask; + attributeMaterial.clipMethod = material.clipMethod; + attributeMaterial.setClipBoxes(material.clipBoxes); + attributeMaterial.setClipPolygons(material.clipPolygons); - const camera = params.camera ? params.camera : viewer.scene.getActiveCamera(); + pointcloud.material = attributeMaterial; + } + + let gl = this.gl; - viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer}); + viewer.renderer.setRenderTarget(null); + viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtAttribute, { + clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)), + //material: this.attributeMaterial, + blendFunc: [gl.SRC_ALPHA, gl.ONE], + //depthTest: false, + depthWrite: false + }); + } - const renderAreaSize = renderer.getSize(new Vector2()); - const width = params.viewport ? params.viewport[2] : renderAreaSize.x; - const height = params.viewport ? params.viewport[3] : renderAreaSize.y; + for(let [pointcloud, material] of originalMaterials){ + pointcloud.material = material; + } - // render skybox + viewer.renderer.setRenderTarget(null); if(viewer.background === "skybox"){ + viewer.renderer.setClearColor(0x000000, 0); + viewer.renderer.clear(); viewer.skybox.camera.rotation.copy(viewer.scene.cameraP.rotation); viewer.skybox.camera.fov = viewer.scene.cameraP.fov; viewer.skybox.camera.aspect = viewer.scene.cameraP.aspect; @@ -70335,3821 +70976,3938 @@ void main() { viewer.skybox.parent.updateMatrixWorld(); viewer.skybox.camera.updateProjectionMatrix(); - renderer.render(viewer.skybox.scene, viewer.skybox.camera); - }else if(viewer.background === "gradient"){ - renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG); - } - - for(let pointcloud of this.viewer.scene.pointclouds){ - const {material} = pointcloud; - material.useEDL = false; - } - - viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, null, { - clipSpheres: viewer.scene.volumes.filter(v => (v instanceof Potree.SphereVolume)), - }); - - // render scene - renderer.render(viewer.scene.scene, camera); - - viewer.dispatchEvent({type: "render.pass.scene",viewer: viewer}); - - viewer.clippingTool.update(); - renderer.render(viewer.clippingTool.sceneMarker, viewer.scene.cameraScreenSpace); //viewer.scene.cameraScreenSpace); - renderer.render(viewer.clippingTool.sceneVolume, camera); - - renderer.render(viewer.controls.sceneControls, camera); - - renderer.clearDepth(); - - viewer.transformationTool.update(); - - viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer}); - - // renderer.render(viewer.controls.sceneControls, camera); - // renderer.render(viewer.clippingTool.sceneVolume, camera); - // renderer.render(viewer.transformationTool.scene, camera); - - // renderer.setViewport(width - viewer.navigationCube.width, - // height - viewer.navigationCube.width, - // viewer.navigationCube.width, viewer.navigationCube.width); - // renderer.render(viewer.navigationCube, viewer.navigationCube.camera); - // renderer.setViewport(0, 0, width, height); - - viewer.dispatchEvent({type: "render.pass.end",viewer: viewer}); - } - - } - - class EDLRenderer{ - constructor(viewer){ - this.viewer = viewer; - - this.edlMaterial = null; - - this.rtRegular; - this.rtEDL; - - this.gl = viewer.renderer.getContext(); - - this.shadowMap = new PointCloudSM(this.viewer.pRenderer); - } - - initEDL(){ - if (this.edlMaterial != null) { - return; + viewer.renderer.render(viewer.skybox.scene, viewer.skybox.camera); + } else if (viewer.background === 'gradient') { + viewer.renderer.setClearColor(0x000000, 0); + viewer.renderer.clear(); + viewer.renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG); + } else if (viewer.background === 'black') { + viewer.renderer.setClearColor(0x000000, 1); + viewer.renderer.clear(); + } else if (viewer.background === 'white') { + viewer.renderer.setClearColor(0xFFFFFF, 1); + viewer.renderer.clear(); + } else { + viewer.renderer.setClearColor(0x000000, 0); + viewer.renderer.clear(); } - this.edlMaterial = new EyeDomeLightingMaterial(); - this.edlMaterial.depthTest = true; - this.edlMaterial.depthWrite = true; - this.edlMaterial.transparent = true; - - this.rtEDL = new WebGLRenderTarget(1024, 1024, { - minFilter: NearestFilter, - magFilter: NearestFilter, - format: RGBAFormat, - type: FloatType, - depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType) - }); + { // NORMALIZATION PASS + let normalizationMaterial = this.useEDL ? this.normalizationEDLMaterial : this.normalizationMaterial; - this.rtRegular = new WebGLRenderTarget(1024, 1024, { - minFilter: NearestFilter, - magFilter: NearestFilter, - format: RGBAFormat, - depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType) - }); - }; + if(this.useEDL){ + normalizationMaterial.uniforms.edlStrength.value = viewer.edlStrength; + normalizationMaterial.uniforms.radius.value = viewer.edlRadius; + normalizationMaterial.uniforms.screenWidth.value = width; + normalizationMaterial.uniforms.screenHeight.value = height; + normalizationMaterial.uniforms.uEDLMap.value = this.rtDepth.texture; + } - resize(width, height){ - if(this.screenshot){ - width = this.screenshot.target.width; - height = this.screenshot.target.height; + normalizationMaterial.uniforms.uWeightMap.value = this.rtAttribute.texture; + normalizationMaterial.uniforms.uDepthMap.value = this.rtAttribute.depthTexture; + + Utils.screenPass.render(viewer.renderer, normalizationMaterial); } - this.rtEDL.setSize(width , height); - this.rtRegular.setSize(width , height); - } - - makeScreenshot(camera, size, callback){ - - if(camera === undefined || camera === null){ - camera = this.viewer.scene.getActiveCamera(); - } + viewer.renderer.render(viewer.scene.scene, camera); - if(size === undefined || size === null){ - size = this.viewer.renderer.getSize(new Vector2()); - } + viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer}); - let {width, height} = size; + viewer.renderer.clearDepth(); - //let maxTextureSize = viewer.renderer.capabilities.maxTextureSize; - //if(width * 4 < - width = 2 * width; - height = 2 * height; + viewer.transformationTool.update(); - let target = new WebGLRenderTarget(width, height, { - format: RGBAFormat, - }); + viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer}); - this.screenshot = { - target: target - }; + viewer.renderer.render(viewer.controls.sceneControls, camera); + viewer.renderer.render(viewer.clippingTool.sceneVolume, camera); + viewer.renderer.render(viewer.transformationTool.scene, camera); - // HACK? removed because of error, was this important? - //this.viewer.renderer.clearTarget(target, true, true, true); + viewer.renderer.setViewport(width - viewer.navigationCube.width, + height - viewer.navigationCube.width, + viewer.navigationCube.width, viewer.navigationCube.width); + viewer.renderer.render(viewer.navigationCube, viewer.navigationCube.camera); + viewer.renderer.setViewport(0, 0, width, height); + + viewer.dispatchEvent({type: "render.pass.end",viewer: viewer}); - this.render(); + } - let pixelCount = width * height; - let buffer = new Uint8Array(4 * pixelCount); + } - this.viewer.renderer.readRenderTargetPixels(target, 0, 0, width, height, buffer); + class View{ + constructor () { + this.position = new Vector3(0, 0, 0); - // flip vertically - let bytesPerLine = width * 4; - for(let i = 0; i < parseInt(height / 2); i++){ - let j = height - i - 1; + this.yaw = Math.PI / 4; + this._pitch = -Math.PI / 4; + this.radius = 1; - let lineI = buffer.slice(i * bytesPerLine, i * bytesPerLine + bytesPerLine); - let lineJ = buffer.slice(j * bytesPerLine, j * bytesPerLine + bytesPerLine); - buffer.set(lineJ, i * bytesPerLine); - buffer.set(lineI, j * bytesPerLine); - } + this.maxPitch = Math.PI / 2; + this.minPitch = -Math.PI / 2; + } - this.screenshot.target.dispose(); - delete this.screenshot; + clone () { + let c = new View(); + c.yaw = this.yaw; + c._pitch = this.pitch; + c.radius = this.radius; + c.maxPitch = this.maxPitch; + c.minPitch = this.minPitch; - return { - width: width, - height: height, - buffer: buffer - }; + return c; } - clearTargets(){ - const viewer = this.viewer; - const {renderer} = viewer; + get pitch () { + return this._pitch; + } - const oldTarget = renderer.getRenderTarget(); + set pitch (angle) { + this._pitch = Math.max(Math.min(angle, this.maxPitch), this.minPitch); + } - renderer.setRenderTarget( this.rtEDL ); - renderer.clear( true, true, true ); + get direction () { + let dir = new Vector3(0, 1, 0); - renderer.setRenderTarget( this.rtRegular ); - renderer.clear( true, true, false ); + dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch); + dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); - renderer.setRenderTarget(oldTarget); + return dir; } - clear(){ - this.initEDL(); - const viewer = this.viewer; + set direction (dir) { - const {renderer, background} = viewer; + //if(dir.x === dir.y){ + if(dir.x === 0 && dir.y === 0){ + this.pitch = Math.PI / 2 * Math.sign(dir.z); + }else { + let yaw = Math.atan2(dir.y, dir.x) - Math.PI / 2; + let pitch = Math.atan2(dir.z, Math.sqrt(dir.x * dir.x + dir.y * dir.y)); - if(background === "skybox"){ - renderer.setClearColor(0x000000, 0); - } else if (background === 'gradient') { - renderer.setClearColor(0x000000, 0); - } else if (background === 'black') { - renderer.setClearColor(0x000000, 1); - } else if (background === 'white') { - renderer.setClearColor(0xFFFFFF, 1); - } else { - renderer.setClearColor(0x000000, 0); + this.yaw = yaw; + this.pitch = pitch; } - renderer.clear(); + } - this.clearTargets(); + lookAt(t){ + let V; + if(arguments.length === 1){ + V = new Vector3().subVectors(t, this.position); + }else if(arguments.length === 3){ + V = new Vector3().subVectors(new Vector3(...arguments), this.position); + } + + let radius = V.length(); + let dir = V.normalize(); + + this.radius = radius; + this.direction = dir; } - renderShadowMap(visiblePointClouds, camera, lights){ + getPivot () { + return new Vector3().addVectors(this.position, this.direction.multiplyScalar(this.radius)); + } - const {viewer} = this; + getSide () { + let side = new Vector3(1, 0, 0); + side.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); - const doShadows = lights.length > 0 && !(lights[0].disableShadowUpdates); - if(doShadows){ - let light = lights[0]; + return side; + } - this.shadowMap.setLight(light); + pan (x, y) { + let dir = new Vector3(0, 1, 0); + dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch); + dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); - let originalAttributes = new Map(); - for(let pointcloud of viewer.scene.pointclouds){ - // TODO IMPORTANT !!! check - originalAttributes.set(pointcloud, pointcloud.material.activeAttributeName); - pointcloud.material.disableEvents(); - pointcloud.material.activeAttributeName = "depth"; - //pointcloud.material.pointColorType = PointColorType.DEPTH; - } + // let side = new THREE.Vector3(1, 0, 0); + // side.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.yaw); - this.shadowMap.render(viewer.scene.scenePointCloud, camera); + let side = this.getSide(); - for(let pointcloud of visiblePointClouds){ - let originalAttribute = originalAttributes.get(pointcloud); - // TODO IMPORTANT !!! check - pointcloud.material.activeAttributeName = originalAttribute; - pointcloud.material.enableEvents(); - } + let up = side.clone().cross(dir); - viewer.shadowTestCam.updateMatrixWorld(); - viewer.shadowTestCam.matrixWorldInverse.copy(viewer.shadowTestCam.matrixWorld).invert(); - viewer.shadowTestCam.updateProjectionMatrix(); - } + let pan = side.multiplyScalar(x).add(up.multiplyScalar(y)); + this.position = this.position.add(pan); + // this.target = this.target.add(pan); } - render(params){ - this.initEDL(); - - const viewer = this.viewer; - let camera = params.camera ? params.camera : viewer.scene.getActiveCamera(); - const {width, height} = this.viewer.renderer.getSize(new Vector2()); + translate (x, y, z) { + let dir = new Vector3(0, 1, 0); + dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch); + dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); + let side = new Vector3(1, 0, 0); + side.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); - viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer}); - - this.resize(width, height); + let up = side.clone().cross(dir); - const visiblePointClouds = viewer.scene.pointclouds.filter(pc => pc.visible); + let t = side.multiplyScalar(x) + .add(dir.multiplyScalar(y)) + .add(up.multiplyScalar(z)); - if(this.screenshot){ - let oldBudget = Potree.pointBudget; - Potree.pointBudget = Math.max(10 * 1000 * 1000, 2 * oldBudget); - let result = Potree.updatePointClouds( - viewer.scene.pointclouds, - camera, - viewer.renderer); - Potree.pointBudget = oldBudget; - } + this.position = this.position.add(t); + } - let lights = []; - viewer.scene.scene.traverse(node => { - if(node.type === "SpotLight"){ - lights.push(node); - } - }); + translateWorld (x, y, z) { + this.position.x += x; + this.position.y += y; + this.position.z += z; + } - if(viewer.background === "skybox"){ - viewer.skybox.camera.rotation.copy(viewer.scene.cameraP.rotation); - viewer.skybox.camera.fov = viewer.scene.cameraP.fov; - viewer.skybox.camera.aspect = viewer.scene.cameraP.aspect; + setView(position, target, duration = 0, callback = null){ - viewer.skybox.parent.rotation.x = 0; - viewer.skybox.parent.updateMatrixWorld(); + let endPosition = null; + if(position instanceof Array){ + endPosition = new Vector3(...position); + }else if(position.x != null){ + endPosition = position.clone(); + } - viewer.skybox.camera.updateProjectionMatrix(); - viewer.renderer.render(viewer.skybox.scene, viewer.skybox.camera); - } else if (viewer.background === 'gradient') { - viewer.renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG); - } + let endTarget = null; + if(target instanceof Array){ + endTarget = new Vector3(...target); + }else if(target.x != null){ + endTarget = target.clone(); + } + + const startPosition = this.position.clone(); + const startTarget = this.getPivot(); - //TODO adapt to multiple lights - this.renderShadowMap(visiblePointClouds, camera, lights); + //const endPosition = position.clone(); + //const endTarget = target.clone(); - { // COLOR & DEPTH PASS - for (let pointcloud of visiblePointClouds) { - let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x; + let easing = TWEEN.Easing.Quartic.Out; - let material = pointcloud.material; - material.weighted = false; - material.useLogarithmicDepthBuffer = false; - material.useEDL = true; + if(duration === 0){ + this.position.copy(endPosition); + this.lookAt(endTarget); + }else { + let value = {x: 0}; + let tween = new TWEEN.Tween(value).to({x: 1}, duration); + tween.easing(easing); + //this.tweens.push(tween); - material.screenWidth = width; - material.screenHeight = height; - material.uniforms.visibleNodes.value = pointcloud.material.visibleNodesTexture; - material.uniforms.octreeSize.value = octreeSize; - material.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(pointcloud.scale.x, pointcloud.scale.y, pointcloud.scale.z); - } - - // TODO adapt to multiple lights - viewer.renderer.setRenderTarget(this.rtEDL); - - if(lights.length > 0){ - viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtEDL, { - clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)), - shadowMaps: [this.shadowMap], - transparent: false, - }); - }else { + tween.onUpdate(() => { + let t = value.x; - - // let test = camera.clone(); - // test.matrixAutoUpdate = false; + //console.log(t); - // //test.updateMatrixWorld = () => {}; + const pos = new Vector3( + (1 - t) * startPosition.x + t * endPosition.x, + (1 - t) * startPosition.y + t * endPosition.y, + (1 - t) * startPosition.z + t * endPosition.z, + ); - // let mat = new THREE.Matrix4().set( - // 1, 0, 0, 0, - // 0, 0, 1, 0, - // 0, -1, 0, 0, - // 0, 0, 0, 1, - // ); - // mat.invert() + const target = new Vector3( + (1 - t) * startTarget.x + t * endTarget.x, + (1 - t) * startTarget.y + t * endTarget.y, + (1 - t) * startTarget.z + t * endTarget.z, + ); - // test.matrix.multiplyMatrices(mat, test.matrix); - // test.updateMatrixWorld(); + this.position.copy(pos); + this.lookAt(target); - //test.matrixWorld.multiplyMatrices(mat, test.matrixWorld); - //test.matrixWorld.multiply(mat); - //test.matrixWorldInverse.invert(test.matrixWorld); - //test.matrixWorldInverse.multiplyMatrices(test.matrixWorldInverse, mat); - + }); - viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtEDL, { - clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)), - transparent: false, - }); - } + tween.start(); - + tween.onComplete(() => { + if(callback){ + callback(); + } + }); } - viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer, renderTarget: this.rtRegular}); - viewer.renderer.setRenderTarget(null); - viewer.renderer.render(viewer.scene.scene, camera); + } - { // EDL PASS + }; - const uniforms = this.edlMaterial.uniforms; + class Scene$1 extends EventDispatcher{ - uniforms.screenWidth.value = width; - uniforms.screenHeight.value = height; + constructor(){ + super(); - let proj = camera.projectionMatrix; - let projArray = new Float32Array(16); - projArray.set(proj.elements); + this.annotations = new Annotation(); + + this.scene = new Scene(); + this.sceneBG = new Scene(); + this.scenePointCloud = new Scene(); - uniforms.uNear.value = camera.near; - uniforms.uFar.value = camera.far; - uniforms.uEDLColor.value = this.rtEDL.texture; - uniforms.uEDLDepth.value = this.rtEDL.depthTexture; - uniforms.uProj.value = projArray; + this.cameraP = new PerspectiveCamera(this.fov, 1, 0.1, 1000*1000); + this.cameraO = new OrthographicCamera(-1, 1, 1, -1, 0.1, 1000*1000); + this.cameraVR = new PerspectiveCamera(); + this.cameraBG = new Camera(); + this.cameraScreenSpace = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10); + this.cameraMode = CameraMode.PERSPECTIVE; + this.overrideCamera = null; + this.pointclouds = []; - uniforms.edlStrength.value = viewer.edlStrength; - uniforms.radius.value = viewer.edlRadius; - uniforms.opacity.value = viewer.edlOpacity; // HACK - - Utils.screenPass.render(viewer.renderer, this.edlMaterial); + this.measurements = []; + this.profiles = []; + this.volumes = []; + this.polygonClipVolumes = []; + this.cameraAnimations = []; + this.orientedImages = []; + this.images360 = []; + this.geopackages = []; + + this.fpControls = null; + this.orbitControls = null; + this.earthControls = null; + this.geoControls = null; + this.deviceControls = null; + this.inputHandler = null; - if(this.screenshot){ - Utils.screenPass.render(viewer.renderer, this.edlMaterial, this.screenshot.target); - } + this.view = new View(); - } + this.directionalLight = null; - viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer}); + this.initialize(); + } - viewer.renderer.clearDepth(); + estimateHeightAt (position) { + let height = null; + let fromSpacing = Infinity; - viewer.transformationTool.update(); + for (let pointcloud of this.pointclouds) { + if (pointcloud.root.geometryNode === undefined) { + continue; + } - viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer}); + let pHeight = null; + let pFromSpacing = Infinity; - viewer.renderer.render(viewer.controls.sceneControls, camera); - viewer.renderer.render(viewer.clippingTool.sceneVolume, camera); - viewer.renderer.render(viewer.transformationTool.scene, camera); - - viewer.dispatchEvent({type: "render.pass.end",viewer: viewer}); + let lpos = position.clone().sub(pointcloud.position); + lpos.z = 0; + let ray = new Ray(lpos, new Vector3(0, 0, 1)); - } - } + let stack = [pointcloud.root]; + while (stack.length > 0) { + let node = stack.pop(); + let box = node.getBoundingBox(); - class HQSplatRenderer{ - - constructor(viewer){ - this.viewer = viewer; + let inside = ray.intersectBox(box); - this.depthMaterials = new Map(); - this.attributeMaterials = new Map(); - this.normalizationMaterial = null; + if (!inside) { + continue; + } - this.rtDepth = null; - this.rtAttribute = null; - this.gl = viewer.renderer.getContext(); + let h = node.geometryNode.mean.z + + pointcloud.position.z + + node.geometryNode.boundingBox.min.z; - this.initialized = false; + if (node.geometryNode.spacing <= pFromSpacing) { + pHeight = h; + pFromSpacing = node.geometryNode.spacing; + } + + for (let index of Object.keys(node.children)) { + let child = node.children[index]; + if (child.geometryNode) { + stack.push(node.children[index]); + } + } + } + + if (height === null || pFromSpacing < fromSpacing) { + height = pHeight; + fromSpacing = pFromSpacing; + } + } + + return height; } + + getBoundingBox(pointclouds = this.pointclouds){ + let box = new Box3(); - init(){ - if (this.initialized) { - return; + this.scenePointCloud.updateMatrixWorld(true); + this.referenceFrame.updateMatrixWorld(true); + + for (let pointcloud of pointclouds) { + pointcloud.updateMatrixWorld(true); + + let pointcloudBox = pointcloud.pcoGeometry.tightBoundingBox ? pointcloud.pcoGeometry.tightBoundingBox : pointcloud.boundingBox; + let boxWorld = Utils.computeTransformedBoundingBox(pointcloudBox, pointcloud.matrixWorld); + box.union(boxWorld); } - this.normalizationMaterial = new NormalizationMaterial(); - this.normalizationMaterial.depthTest = true; - this.normalizationMaterial.depthWrite = true; - this.normalizationMaterial.transparent = true; + return box; + } - this.normalizationEDLMaterial = new NormalizationEDLMaterial(); - this.normalizationEDLMaterial.depthTest = true; - this.normalizationEDLMaterial.depthWrite = true; - this.normalizationEDLMaterial.transparent = true; + addPointCloud (pointcloud) { + this.pointclouds.push(pointcloud); + this.scenePointCloud.add(pointcloud); - this.rtDepth = new WebGLRenderTarget(1024, 1024, { - minFilter: NearestFilter, - magFilter: NearestFilter, - format: RGBAFormat, - type: FloatType, - depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType) + this.dispatchEvent({ + type: 'pointcloud_added', + pointcloud: pointcloud }); + } - this.rtAttribute = new WebGLRenderTarget(1024, 1024, { - minFilter: NearestFilter, - magFilter: NearestFilter, - format: RGBAFormat, - type: FloatType, - depthTexture: this.rtDepth.depthTexture, + addVolume (volume) { + this.volumes.push(volume); + this.dispatchEvent({ + 'type': 'volume_added', + 'scene': this, + 'volume': volume }); + } - this.initialized = true; + addOrientedImages(images){ + this.orientedImages.push(images); + this.scene.add(images.node); + + this.dispatchEvent({ + 'type': 'oriented_images_added', + 'scene': this, + 'images': images + }); }; - resize(width, height){ - this.rtDepth.setSize(width, height); - this.rtAttribute.setSize(width, height); - } + removeOrientedImages(images){ + let index = this.orientedImages.indexOf(images); + if (index > -1) { + this.orientedImages.splice(index, 1); - clearTargets(){ - const viewer = this.viewer; - const {renderer} = viewer; + this.dispatchEvent({ + 'type': 'oriented_images_removed', + 'scene': this, + 'images': images + }); + } + }; - const oldTarget = renderer.getRenderTarget(); + add360Images(images){ + this.images360.push(images); + this.scene.add(images.node); + + this.dispatchEvent({ + 'type': '360_images_added', + 'scene': this, + 'images': images + }); + } - renderer.setClearColor(0x000000, 0); + remove360Images(images){ + let index = this.images360.indexOf(images); + if (index > -1) { + this.images360.splice(index, 1); - renderer.setRenderTarget( this.rtDepth ); - renderer.clear( true, true, true ); + this.dispatchEvent({ + 'type': '360_images_removed', + 'scene': this, + 'images': images + }); + } + } - renderer.setRenderTarget( this.rtAttribute ); - renderer.clear( true, true, true ); + addGeopackage(geopackage){ + this.geopackages.push(geopackage); + this.scene.add(geopackage.node); - renderer.setRenderTarget(oldTarget); - } + this.dispatchEvent({ + 'type': 'geopackage_added', + 'scene': this, + 'geopackage': geopackage + }); + }; + removeGeopackage(geopackage){ + let index = this.geopackages.indexOf(geopackage); + if (index > -1) { + this.geopackages.splice(index, 1); - clear(){ - this.init(); + this.dispatchEvent({ + 'type': 'geopackage_removed', + 'scene': this, + 'geopackage': geopackage + }); + } + }; - const {renderer, background} = this.viewer; + removeVolume (volume) { + let index = this.volumes.indexOf(volume); + if (index > -1) { + this.volumes.splice(index, 1); - if(background === "skybox"){ - renderer.setClearColor(0x000000, 0); - } else if (background === 'gradient') { - renderer.setClearColor(0x000000, 0); - } else if (background === 'black') { - renderer.setClearColor(0x000000, 1); - } else if (background === 'white') { - renderer.setClearColor(0xFFFFFF, 1); - } else { - renderer.setClearColor(0x000000, 0); + this.dispatchEvent({ + 'type': 'volume_removed', + 'scene': this, + 'volume': volume + }); } + }; - renderer.clear(); + addCameraAnimation(animation) { + this.cameraAnimations.push(animation); + this.dispatchEvent({ + 'type': 'camera_animation_added', + 'scene': this, + 'animation': animation + }); + }; - this.clearTargets(); - } + removeCameraAnimation(animation){ + let index = this.cameraAnimations.indexOf(volume); + if (index > -1) { + this.cameraAnimations.splice(index, 1); - render (params) { - this.init(); + this.dispatchEvent({ + 'type': 'camera_animation_removed', + 'scene': this, + 'animation': animation + }); + } + }; - const viewer = this.viewer; - const camera = params.camera ? params.camera : viewer.scene.getActiveCamera(); - const {width, height} = this.viewer.renderer.getSize(new Vector2()); + addPolygonClipVolume(volume){ + this.polygonClipVolumes.push(volume); + this.dispatchEvent({ + "type": "polygon_clip_volume_added", + "scene": this, + "volume": volume + }); + }; + + removePolygonClipVolume(volume){ + let index = this.polygonClipVolumes.indexOf(volume); + if (index > -1) { + this.polygonClipVolumes.splice(index, 1); + this.dispatchEvent({ + "type": "polygon_clip_volume_removed", + "scene": this, + "volume": volume + }); + } + }; + + addMeasurement(measurement){ + measurement.lengthUnit = this.lengthUnit; + measurement.lengthUnitDisplay = this.lengthUnitDisplay; + this.measurements.push(measurement); + this.dispatchEvent({ + 'type': 'measurement_added', + 'scene': this, + 'measurement': measurement + }); + }; - viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer}); + removeMeasurement (measurement) { + let index = this.measurements.indexOf(measurement); + if (index > -1) { + this.measurements.splice(index, 1); + this.dispatchEvent({ + 'type': 'measurement_removed', + 'scene': this, + 'measurement': measurement + }); + } + } - this.resize(width, height); + addProfile (profile) { + this.profiles.push(profile); + this.dispatchEvent({ + 'type': 'profile_added', + 'scene': this, + 'profile': profile + }); + } - const visiblePointClouds = viewer.scene.pointclouds.filter(pc => pc.visible); - const originalMaterials = new Map(); + removeProfile (profile) { + let index = this.profiles.indexOf(profile); + if (index > -1) { + this.profiles.splice(index, 1); + this.dispatchEvent({ + 'type': 'profile_removed', + 'scene': this, + 'profile': profile + }); + } + } - for(let pointcloud of visiblePointClouds){ - originalMaterials.set(pointcloud, pointcloud.material); + removeAllMeasurements () { + while (this.measurements.length > 0) { + this.removeMeasurement(this.measurements[0]); + } - if(!this.attributeMaterials.has(pointcloud)){ - let attributeMaterial = new PointCloudMaterial$1(); - this.attributeMaterials.set(pointcloud, attributeMaterial); - } + while (this.profiles.length > 0) { + this.removeProfile(this.profiles[0]); + } - if(!this.depthMaterials.has(pointcloud)){ - let depthMaterial = new PointCloudMaterial$1(); + while (this.volumes.length > 0) { + this.removeVolume(this.volumes[0]); + } + } - depthMaterial.setDefine("depth_pass", "#define hq_depth_pass"); - depthMaterial.setDefine("use_edl", "#define use_edl"); + removeAllClipVolumes(){ + let clipVolumes = this.volumes.filter(volume => volume.clip === true); + for(let clipVolume of clipVolumes){ + this.removeVolume(clipVolume); + } - this.depthMaterials.set(pointcloud, depthMaterial); - } + while(this.polygonClipVolumes.length > 0){ + this.removePolygonClipVolume(this.polygonClipVolumes[0]); } + } - { // DEPTH PASS - for (let pointcloud of visiblePointClouds) { - let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x; + getActiveCamera() { - let material = originalMaterials.get(pointcloud); - let depthMaterial = this.depthMaterials.get(pointcloud); + if(this.overrideCamera){ + return this.overrideCamera; + } - depthMaterial.size = material.size; - depthMaterial.minSize = material.minSize; - depthMaterial.maxSize = material.maxSize; + if(this.cameraMode === CameraMode.PERSPECTIVE){ + return this.cameraP; + }else if(this.cameraMode === CameraMode.ORTHOGRAPHIC){ + return this.cameraO; + }else if(this.cameraMode === CameraMode.VR){ + return this.cameraVR; + } - depthMaterial.pointSizeType = material.pointSizeType; - depthMaterial.visibleNodesTexture = material.visibleNodesTexture; - depthMaterial.weighted = false; - depthMaterial.screenWidth = width; - depthMaterial.shape = PointShape.CIRCLE; - depthMaterial.screenHeight = height; - depthMaterial.uniforms.visibleNodes.value = material.visibleNodesTexture; - depthMaterial.uniforms.octreeSize.value = octreeSize; - depthMaterial.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(...pointcloud.scale.toArray()); - depthMaterial.classification = material.classification; - depthMaterial.uniforms.classificationLUT.value.image.data = material.uniforms.classificationLUT.value.image.data; - depthMaterial.classificationTexture.needsUpdate = true; + return null; + } + + initialize(){ + + this.referenceFrame = new Object3D(); + this.referenceFrame.matrixAutoUpdate = false; + this.scenePointCloud.add(this.referenceFrame); - depthMaterial.uniforms.uFilterReturnNumberRange.value = material.uniforms.uFilterReturnNumberRange.value; - depthMaterial.uniforms.uFilterNumberOfReturnsRange.value = material.uniforms.uFilterNumberOfReturnsRange.value; - depthMaterial.uniforms.uFilterGPSTimeClipRange.value = material.uniforms.uFilterGPSTimeClipRange.value; - depthMaterial.uniforms.uFilterPointSourceIDClipRange.value = material.uniforms.uFilterPointSourceIDClipRange.value; + this.cameraP.up.set(0, 0, 1); + this.cameraP.position.set(1000, 1000, 1000); + this.cameraO.up.set(0, 0, 1); + this.cameraO.position.set(1000, 1000, 1000); + //this.camera.rotation.y = -Math.PI / 4; + //this.camera.rotation.x = -Math.PI / 6; + this.cameraScreenSpace.lookAt(new Vector3(0, 0, 0), new Vector3(0, 0, -1), new Vector3(0, 1, 0)); + + this.directionalLight = new DirectionalLight( 0xffffff, 0.5 ); + this.directionalLight.position.set( 10, 10, 10 ); + this.directionalLight.lookAt( new Vector3(0, 0, 0)); + this.scenePointCloud.add( this.directionalLight ); + + let light = new AmbientLight( 0x555555 ); // soft white light + this.scenePointCloud.add( light ); - depthMaterial.clipTask = material.clipTask; - depthMaterial.clipMethod = material.clipMethod; - depthMaterial.setClipBoxes(material.clipBoxes); - depthMaterial.setClipPolygons(material.clipPolygons); + { // background + let texture = Utils.createBackgroundTexture(512, 512); - pointcloud.material = depthMaterial; - } - - viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtDepth, { - clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)), - }); + texture.minFilter = texture.magFilter = NearestFilter; + texture.minFilter = texture.magFilter = LinearFilter; + let bg = new Mesh( + new PlaneBufferGeometry(2, 2, 1), + new MeshBasicMaterial({ + map: texture + }) + ); + bg.material.depthTest = false; + bg.material.depthWrite = false; + this.sceneBG.add(bg); } - { // ATTRIBUTE PASS - for (let pointcloud of visiblePointClouds) { - let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x; + // { // lights + // { + // let light = new THREE.DirectionalLight(0xffffff); + // light.position.set(10, 10, 1); + // light.target.position.set(0, 0, 0); + // this.scene.add(light); + // } - let material = originalMaterials.get(pointcloud); - let attributeMaterial = this.attributeMaterials.get(pointcloud); + // { + // let light = new THREE.DirectionalLight(0xffffff); + // light.position.set(-10, 10, 1); + // light.target.position.set(0, 0, 0); + // this.scene.add(light); + // } - attributeMaterial.size = material.size; - attributeMaterial.minSize = material.minSize; - attributeMaterial.maxSize = material.maxSize; + // { + // let light = new THREE.DirectionalLight(0xffffff); + // light.position.set(0, -10, 20); + // light.target.position.set(0, 0, 0); + // this.scene.add(light); + // } + // } + } + + addAnnotation(position, args = {}){ + if(position instanceof Array){ + args.position = new Vector3().fromArray(position); + } else if (position.x != null) { + args.position = position; + } + let annotation = new Annotation(args); + this.annotations.add(annotation); - attributeMaterial.pointSizeType = material.pointSizeType; - attributeMaterial.activeAttributeName = material.activeAttributeName; - attributeMaterial.visibleNodesTexture = material.visibleNodesTexture; - attributeMaterial.weighted = true; - attributeMaterial.screenWidth = width; - attributeMaterial.screenHeight = height; - attributeMaterial.shape = PointShape.CIRCLE; - attributeMaterial.uniforms.visibleNodes.value = material.visibleNodesTexture; - attributeMaterial.uniforms.octreeSize.value = octreeSize; - attributeMaterial.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(...pointcloud.scale.toArray()); - attributeMaterial.classification = material.classification; - attributeMaterial.uniforms.classificationLUT.value.image.data = material.uniforms.classificationLUT.value.image.data; - attributeMaterial.classificationTexture.needsUpdate = true; + return annotation; + } - attributeMaterial.uniforms.uFilterReturnNumberRange.value = material.uniforms.uFilterReturnNumberRange.value; - attributeMaterial.uniforms.uFilterNumberOfReturnsRange.value = material.uniforms.uFilterNumberOfReturnsRange.value; - attributeMaterial.uniforms.uFilterGPSTimeClipRange.value = material.uniforms.uFilterGPSTimeClipRange.value; - attributeMaterial.uniforms.uFilterPointSourceIDClipRange.value = material.uniforms.uFilterPointSourceIDClipRange.value; + getAnnotations () { + return this.annotations; + }; - attributeMaterial.elevationGradientRepeat = material.elevationGradientRepeat; - attributeMaterial.elevationRange = material.elevationRange; - attributeMaterial.gradient = material.gradient; - attributeMaterial.matcap = material.matcap; + removeAnnotation(annotationToRemove) { + this.annotations.remove(annotationToRemove); + } + }; - attributeMaterial.intensityRange = material.intensityRange; - attributeMaterial.intensityGamma = material.intensityGamma; - attributeMaterial.intensityContrast = material.intensityContrast; - attributeMaterial.intensityBrightness = material.intensityBrightness; + // http://epsg.io/ + proj4.defs([ + ['UTM10N', '+proj=utm +zone=10 +ellps=GRS80 +datum=NAD83 +units=m +no_defs'], + ['EPSG:6339', '+proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6340', '+proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6341', '+proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6342', '+proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6343', '+proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6344', '+proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6345', '+proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6346', '+proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6347', '+proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:6348', '+proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs'], + ['EPSG:26910', '+proj=utm +zone=10 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26911', '+proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26912', '+proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26913', '+proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26914', '+proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26915', '+proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26916', '+proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26917', '+proj=utm +zone=17 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26918', '+proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ['EPSG:26919', '+proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], + ]); - attributeMaterial.rgbGamma = material.rgbGamma; - attributeMaterial.rgbContrast = material.rgbContrast; - attributeMaterial.rgbBrightness = material.rgbBrightness; + class MapView{ - attributeMaterial.weightRGB = material.weightRGB; - attributeMaterial.weightIntensity = material.weightIntensity; - attributeMaterial.weightElevation = material.weightElevation; - attributeMaterial.weightRGB = material.weightRGB; - attributeMaterial.weightClassification = material.weightClassification; - attributeMaterial.weightReturnNumber = material.weightReturnNumber; - attributeMaterial.weightSourceID = material.weightSourceID; + constructor (viewer) { + this.viewer = viewer; - attributeMaterial.color = material.color; + this.webMapService = 'WMTS'; + this.mapProjectionName = 'EPSG:3857'; + this.mapProjection = proj4.defs(this.mapProjectionName); + this.sceneProjection = null; - attributeMaterial.clipTask = material.clipTask; - attributeMaterial.clipMethod = material.clipMethod; - attributeMaterial.setClipBoxes(material.clipBoxes); - attributeMaterial.setClipPolygons(material.clipPolygons); + this.extentsLayer = null; + this.cameraLayer = null; + this.toolLayer = null; + this.sourcesLayer = null; + this.sourcesLabelLayer = null; + this.images360Layer = null; + this.enabled = false; - pointcloud.material = attributeMaterial; - } - - let gl = this.gl; + this.createAnnotationStyle = (text) => { + return [ + new ol.style.Style({ + image: new ol.style.Circle({ + radius: 10, + stroke: new ol.style.Stroke({ + color: [255, 255, 255, 0.5], + width: 2 + }), + fill: new ol.style.Fill({ + color: [0, 0, 0, 0.5] + }) + }) + }) + ]; + }; - viewer.renderer.setRenderTarget(null); - viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtAttribute, { - clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)), - //material: this.attributeMaterial, - blendFunc: [gl.SRC_ALPHA, gl.ONE], - //depthTest: false, - depthWrite: false + this.createLabelStyle = (text) => { + let style = new ol.style.Style({ + image: new ol.style.Circle({ + radius: 6, + stroke: new ol.style.Stroke({ + color: 'white', + width: 2 + }), + fill: new ol.style.Fill({ + color: 'green' + }) + }), + text: new ol.style.Text({ + font: '12px helvetica,sans-serif', + text: text, + fill: new ol.style.Fill({ + color: '#000' + }), + stroke: new ol.style.Stroke({ + color: '#fff', + width: 2 + }) + }) }); - } - for(let [pointcloud, material] of originalMaterials){ - pointcloud.material = material; - } + return style; + }; + } - viewer.renderer.setRenderTarget(null); - if(viewer.background === "skybox"){ - viewer.renderer.setClearColor(0x000000, 0); - viewer.renderer.clear(); - viewer.skybox.camera.rotation.copy(viewer.scene.cameraP.rotation); - viewer.skybox.camera.fov = viewer.scene.cameraP.fov; - viewer.skybox.camera.aspect = viewer.scene.cameraP.aspect; - - viewer.skybox.parent.rotation.x = 0; - viewer.skybox.parent.updateMatrixWorld(); + showSources (show) { + this.sourcesLayer.setVisible(show); + this.sourcesLabelLayer.setVisible(show); + } - viewer.skybox.camera.updateProjectionMatrix(); - viewer.renderer.render(viewer.skybox.scene, viewer.skybox.camera); - } else if (viewer.background === 'gradient') { - viewer.renderer.setClearColor(0x000000, 0); - viewer.renderer.clear(); - viewer.renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG); - } else if (viewer.background === 'black') { - viewer.renderer.setClearColor(0x000000, 1); - viewer.renderer.clear(); - } else if (viewer.background === 'white') { - viewer.renderer.setClearColor(0xFFFFFF, 1); - viewer.renderer.clear(); - } else { - viewer.renderer.setClearColor(0x000000, 0); - viewer.renderer.clear(); + init () { + + if(typeof ol === "undefined"){ + return; } - { // NORMALIZATION PASS - let normalizationMaterial = this.useEDL ? this.normalizationEDLMaterial : this.normalizationMaterial; + this.elMap = $('#potree_map'); + this.elMap.draggable({ handle: $('#potree_map_header') }); + this.elMap.resizable(); - if(this.useEDL){ - normalizationMaterial.uniforms.edlStrength.value = viewer.edlStrength; - normalizationMaterial.uniforms.radius.value = viewer.edlRadius; - normalizationMaterial.uniforms.screenWidth.value = width; - normalizationMaterial.uniforms.screenHeight.value = height; - normalizationMaterial.uniforms.uEDLMap.value = this.rtDepth.texture; - } + this.elTooltip = $(`
`); + this.elMap.append(this.elTooltip); - normalizationMaterial.uniforms.uWeightMap.value = this.rtAttribute.texture; - normalizationMaterial.uniforms.uDepthMap.value = this.rtAttribute.depthTexture; - - Utils.screenPass.render(viewer.renderer, normalizationMaterial); - } + let extentsLayer = this.getExtentsLayer(); + let cameraLayer = this.getCameraLayer(); + this.getToolLayer(); + let sourcesLayer = this.getSourcesLayer(); + this.images360Layer = this.getImages360Layer(); + this.getSourcesLabelLayer(); + this.getAnnotationsLayer(); + + let mousePositionControl = new ol.control.MousePosition({ + coordinateFormat: ol.coordinate.createStringXY(5), + projection: 'EPSG:4326', + undefinedHTML: ' ' + }); - viewer.renderer.render(viewer.scene.scene, camera); + let _this = this; + let DownloadSelectionControl = function (optOptions) { + let options = optOptions || {}; - viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer}); + // TOGGLE TILES + let btToggleTiles = document.createElement('button'); + btToggleTiles.innerHTML = 'T'; + btToggleTiles.addEventListener('click', () => { + let visible = sourcesLayer.getVisible(); + _this.showSources(!visible); + }, false); + btToggleTiles.style.float = 'left'; + btToggleTiles.title = 'show / hide tiles'; - viewer.renderer.clearDepth(); + // DOWNLOAD SELECTED TILES + let link = document.createElement('a'); + link.href = '#'; + link.download = 'list.txt'; + link.style.float = 'left'; - viewer.transformationTool.update(); + let button = document.createElement('button'); + button.innerHTML = 'D'; + link.appendChild(button); - viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer}); + let handleDownload = (e) => { + let features = selectedFeatures.getArray(); - viewer.renderer.render(viewer.controls.sceneControls, camera); - viewer.renderer.render(viewer.clippingTool.sceneVolume, camera); - viewer.renderer.render(viewer.transformationTool.scene, camera); + let url = [document.location.protocol, '//', document.location.host, document.location.pathname].join(''); - viewer.renderer.setViewport(width - viewer.navigationCube.width, - height - viewer.navigationCube.width, - viewer.navigationCube.width, viewer.navigationCube.width); - viewer.renderer.render(viewer.navigationCube, viewer.navigationCube.camera); - viewer.renderer.setViewport(0, 0, width, height); - - viewer.dispatchEvent({type: "render.pass.end",viewer: viewer}); + if (features.length === 0) { + alert('No tiles were selected. Select area with ctrl + left mouse button!'); + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } else if (features.length === 1) { + let feature = features[0]; - } + if (feature.source) { + let cloudjsurl = feature.pointcloud.pcoGeometry.url; + let sourceurl = new URL(url + '/../' + cloudjsurl + '/../source/' + feature.source.name); + link.href = sourceurl.href; + link.download = feature.source.name; + } + } else { + let content = ''; + for (let i = 0; i < features.length; i++) { + let feature = features[i]; - } + if (feature.source) { + let cloudjsurl = feature.pointcloud.pcoGeometry.url; + let sourceurl = new URL(url + '/../' + cloudjsurl + '/../source/' + feature.source.name); + content += sourceurl.href + '\n'; + } + } - class View{ - constructor () { - this.position = new Vector3(0, 0, 0); + let uri = 'data:application/octet-stream;base64,' + btoa(content); + link.href = uri; + link.download = 'list_of_files.txt'; + } + }; - this.yaw = Math.PI / 4; - this._pitch = -Math.PI / 4; - this.radius = 1; + button.addEventListener('click', handleDownload, false); - this.maxPitch = Math.PI / 2; - this.minPitch = -Math.PI / 2; - } + // assemble container + let element = document.createElement('div'); + element.className = 'ol-unselectable ol-control'; + element.appendChild(link); + element.appendChild(btToggleTiles); + element.style.bottom = '0.5em'; + element.style.left = '0.5em'; + element.title = 'Download file or list of selected tiles. Select tile with left mouse button or area using ctrl + left mouse.'; - clone () { - let c = new View(); - c.yaw = this.yaw; - c._pitch = this.pitch; - c.radius = this.radius; - c.maxPitch = this.maxPitch; - c.minPitch = this.minPitch; + ol.control.Control.call(this, { + element: element, + target: options.target + }); + }; + ol.inherits(DownloadSelectionControl, ol.control.Control); - return c; - } + this.map = new ol.Map({ + controls: ol.control.defaults({ + attributionOptions: ({ + collapsible: false + }) + }).extend([ + // this.controls.zoomToExtent, + new DownloadSelectionControl(), + mousePositionControl + ]), + layers: [ + new ol.layer.Tile({source: new ol.source.OSM()}), + this.toolLayer, + this.annotationsLayer, + this.sourcesLayer, + this.sourcesLabelLayer, + this.images360Layer, + extentsLayer, + cameraLayer + ], + target: 'potree_map_content', + view: new ol.View({ + center: this.olCenter, + zoom: 9 + }) + }); - get pitch () { - return this._pitch; - } + // DRAGBOX / SELECTION + this.dragBoxLayer = new ol.layer.Vector({ + source: new ol.source.Vector({}), + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: 'rgba(0, 0, 255, 1)', + width: 2 + }) + }) + }); + this.map.addLayer(this.dragBoxLayer); - set pitch (angle) { - this._pitch = Math.max(Math.min(angle, this.maxPitch), this.minPitch); - } + let select = new ol.interaction.Select(); + this.map.addInteraction(select); - get direction () { - let dir = new Vector3(0, 1, 0); + let selectedFeatures = select.getFeatures(); - dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch); - dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); + let dragBox = new ol.interaction.DragBox({ + condition: ol.events.condition.platformModifierKeyOnly + }); - return dir; - } + this.map.addInteraction(dragBox); - set direction (dir) { + // this.map.on('pointermove', evt => { + // let pixel = evt.pixel; + // let feature = this.map.forEachFeatureAtPixel(pixel, function (feature) { + // return feature; + // }); - //if(dir.x === dir.y){ - if(dir.x === 0 && dir.y === 0){ - this.pitch = Math.PI / 2 * Math.sign(dir.z); - }else { - let yaw = Math.atan2(dir.y, dir.x) - Math.PI / 2; - let pitch = Math.atan2(dir.z, Math.sqrt(dir.x * dir.x + dir.y * dir.y)); + // // console.log(feature); + // // this.elTooltip.css("display", feature ? '' : 'none'); + // this.elTooltip.css('display', 'none'); + // if (feature && feature.onHover) { + // feature.onHover(evt); + // // overlay.setPosition(evt.coordinate); + // // tooltip.innerHTML = feature.get('name'); + // } + // }); - this.yaw = yaw; - this.pitch = pitch; - } - - } + this.map.on('click', evt => { + let pixel = evt.pixel; + let feature = this.map.forEachFeatureAtPixel(pixel, function (feature) { + return feature; + }); - lookAt(t){ - let V; - if(arguments.length === 1){ - V = new Vector3().subVectors(t, this.position); - }else if(arguments.length === 3){ - V = new Vector3().subVectors(new Vector3(...arguments), this.position); - } + if (feature && feature.onClick) { + feature.onClick(evt); + } + }); - let radius = V.length(); - let dir = V.normalize(); + dragBox.on('boxend', (e) => { + // features that intersect the box are added to the collection of + // selected features, and their names are displayed in the "info" + // div + let extent = dragBox.getGeometry().getExtent(); + this.getSourcesLayer().getSource().forEachFeatureIntersectingExtent(extent, (feature) => { + selectedFeatures.push(feature); + }); + }); - this.radius = radius; - this.direction = dir; - } + // clear selection when drawing a new box and when clicking on the map + dragBox.on('boxstart', (e) => { + selectedFeatures.clear(); + }); + this.map.on('click', () => { + selectedFeatures.clear(); + }); - getPivot () { - return new Vector3().addVectors(this.position, this.direction.multiplyScalar(this.radius)); - } + this.viewer.addEventListener('scene_changed', e => { + this.setScene(e.scene); + }); - getSide () { - let side = new Vector3(1, 0, 0); - side.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); + this.onPointcloudAdded = e => { + this.load(e.pointcloud); + }; - return side; - } + this.on360ImagesAdded = e => { + this.addImages360(e.images); + }; - pan (x, y) { - let dir = new Vector3(0, 1, 0); - dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch); - dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); + this.onAnnotationAdded = e => { + if (!this.sceneProjection) { + return; + } - // let side = new THREE.Vector3(1, 0, 0); - // side.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.yaw); + let annotation = e.annotation; + let position = annotation.position; + let mapPos = this.toMap.forward([position.x, position.y]); + let feature = new ol.Feature({ + geometry: new ol.geom.Point(mapPos), + name: annotation.title + }); + feature.setStyle(this.createAnnotationStyle(annotation.title)); - let side = this.getSide(); + feature.onHover = evt => { + let coordinates = feature.getGeometry().getCoordinates(); + let p = this.map.getPixelFromCoordinate(coordinates); - let up = side.clone().cross(dir); + this.elTooltip.html(annotation.title); + this.elTooltip.css('display', ''); + this.elTooltip.css('left', `${p[0]}px`); + this.elTooltip.css('top', `${p[1]}px`); + }; - let pan = side.multiplyScalar(x).add(up.multiplyScalar(y)); + feature.onClick = evt => { + annotation.clickTitle(); + }; - this.position = this.position.add(pan); - // this.target = this.target.add(pan); - } + this.getAnnotationsLayer().getSource().addFeature(feature); + }; - translate (x, y, z) { - let dir = new Vector3(0, 1, 0); - dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch); - dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); + this.setScene(this.viewer.scene); + } - let side = new Vector3(1, 0, 0); - side.applyAxisAngle(new Vector3(0, 0, 1), this.yaw); + setScene (scene) { + if (this.scene === scene) { + return; + }; - let up = side.clone().cross(dir); + if (this.scene) { + this.scene.removeEventListener('pointcloud_added', this.onPointcloudAdded); + this.scene.removeEventListener('360_images_added', this.on360ImagesAdded); + this.scene.annotations.removeEventListener('annotation_added', this.onAnnotationAdded); + } - let t = side.multiplyScalar(x) - .add(dir.multiplyScalar(y)) - .add(up.multiplyScalar(z)); + this.scene = scene; - this.position = this.position.add(t); - } + this.scene.addEventListener('pointcloud_added', this.onPointcloudAdded); + this.scene.addEventListener('360_images_added', this.on360ImagesAdded); + this.scene.annotations.addEventListener('annotation_added', this.onAnnotationAdded); - translateWorld (x, y, z) { - this.position.x += x; - this.position.y += y; - this.position.z += z; - } + for (let pointcloud of this.viewer.scene.pointclouds) { + this.load(pointcloud); + } - setView(position, target, duration = 0, callback = null){ + this.viewer.scene.annotations.traverseDescendants(annotation => { + this.onAnnotationAdded({annotation: annotation}); + }); - let endPosition = null; - if(position instanceof Array){ - endPosition = new Vector3(...position); - }else if(position.x != null){ - endPosition = position.clone(); + for(let images of this.viewer.scene.images360){ + this.on360ImagesAdded({images: images}); } + } - let endTarget = null; - if(target instanceof Array){ - endTarget = new Vector3(...target); - }else if(target.x != null){ - endTarget = target.clone(); + getExtentsLayer () { + if (this.extentsLayer) { + return this.extentsLayer; } - - const startPosition = this.position.clone(); - const startTarget = this.getPivot(); - //const endPosition = position.clone(); - //const endTarget = target.clone(); + this.gExtent = new ol.geom.LineString([[0, 0], [0, 0]]); - let easing = TWEEN.Easing.Quartic.Out; + let feature = new ol.Feature(this.gExtent); + let featureVector = new ol.source.Vector({ + features: [feature] + }); - if(duration === 0){ - this.position.copy(endPosition); - this.lookAt(endTarget); - }else { - let value = {x: 0}; - let tween = new TWEEN.Tween(value).to({x: 1}, duration); - tween.easing(easing); - //this.tweens.push(tween); + this.extentsLayer = new ol.layer.Vector({ + source: featureVector, + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new ol.style.Stroke({ + color: '#0000ff', + width: 2 + }), + image: new ol.style.Circle({ + radius: 3, + fill: new ol.style.Fill({ + color: '#0000ff' + }) + }) + }) + }); - tween.onUpdate(() => { - let t = value.x; + return this.extentsLayer; + } - //console.log(t); + getAnnotationsLayer () { + if (this.annotationsLayer) { + return this.annotationsLayer; + } - const pos = new Vector3( - (1 - t) * startPosition.x + t * endPosition.x, - (1 - t) * startPosition.y + t * endPosition.y, - (1 - t) * startPosition.z + t * endPosition.z, - ); + this.annotationsLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + }), + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 0, 0, 1)' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(255, 0, 0, 1)', + width: 2 + }) + }) + }); - const target = new Vector3( - (1 - t) * startTarget.x + t * endTarget.x, - (1 - t) * startTarget.y + t * endTarget.y, - (1 - t) * startTarget.z + t * endTarget.z, - ); + return this.annotationsLayer; + } - this.position.copy(pos); - this.lookAt(target); + getCameraLayer () { + if (this.cameraLayer) { + return this.cameraLayer; + } - }); + // CAMERA LAYER + this.gCamera = new ol.geom.LineString([[0, 0], [0, 0], [0, 0], [0, 0]]); + let feature = new ol.Feature(this.gCamera); + let featureVector = new ol.source.Vector({ + features: [feature] + }); - tween.start(); + this.cameraLayer = new ol.layer.Vector({ + source: featureVector, + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: '#0000ff', + width: 2 + }) + }) + }); - tween.onComplete(() => { - if(callback){ - callback(); - } - }); + return this.cameraLayer; + } + + getToolLayer () { + if (this.toolLayer) { + return this.toolLayer; } + this.toolLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + }), + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 0, 0, 1)' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(255, 0, 0, 1)', + width: 2 + }) + }) + }); + + return this.toolLayer; } - }; + getImages360Layer(){ + if(this.images360Layer){ + return this.images360Layer; + } - class Scene$1 extends EventDispatcher{ + let style = new ol.style.Style({ + image: new ol.style.Circle({ + radius: 4, + stroke: new ol.style.Stroke({ + color: [255, 0, 0, 1], + width: 2 + }), + fill: new ol.style.Fill({ + color: [255, 100, 100, 1] + }) + }) + }); + + let layer = new ol.layer.Vector({ + source: new ol.source.Vector({}), + style: style, + }); - constructor(){ - super(); + this.images360Layer = layer; - this.annotations = new Annotation(); - - this.scene = new Scene(); - this.sceneBG = new Scene(); - this.scenePointCloud = new Scene(); + return this.images360Layer; + } - this.cameraP = new PerspectiveCamera(this.fov, 1, 0.1, 1000*1000); - this.cameraO = new OrthographicCamera(-1, 1, 1, -1, 0.1, 1000*1000); - this.cameraVR = new PerspectiveCamera(); - this.cameraBG = new Camera(); - this.cameraScreenSpace = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10); - this.cameraMode = CameraMode.PERSPECTIVE; - this.overrideCamera = null; - this.pointclouds = []; + getSourcesLayer () { + if (this.sourcesLayer) { + return this.sourcesLayer; + } - this.measurements = []; - this.profiles = []; - this.volumes = []; - this.polygonClipVolumes = []; - this.cameraAnimations = []; - this.orientedImages = []; - this.images360 = []; - this.geopackages = []; - - this.fpControls = null; - this.orbitControls = null; - this.earthControls = null; - this.geoControls = null; - this.deviceControls = null; - this.inputHandler = null; + this.sourcesLayer = new ol.layer.Vector({ + source: new ol.source.Vector({}), + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(0, 0, 150, 0.1)' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(0, 0, 150, 1)', + width: 1 + }) + }) + }); - this.view = new View(); + return this.sourcesLayer; + } - this.directionalLight = null; + getSourcesLabelLayer () { + if (this.sourcesLabelLayer) { + return this.sourcesLabelLayer; + } - this.initialize(); + this.sourcesLabelLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + }), + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 0, 0, 0.1)' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(255, 0, 0, 1)', + width: 2 + }) + }), + minResolution: 0.01, + maxResolution: 20 + }); + + return this.sourcesLabelLayer; } - estimateHeightAt (position) { - let height = null; - let fromSpacing = Infinity; + setSceneProjection (sceneProjection) { + this.sceneProjection = sceneProjection; + this.toMap = proj4(this.sceneProjection, this.mapProjection); + this.toScene = proj4(this.mapProjection, this.sceneProjection); + }; - for (let pointcloud of this.pointclouds) { - if (pointcloud.root.geometryNode === undefined) { - continue; - } + getMapExtent () { + let bb = this.viewer.getBoundingBox(); - let pHeight = null; - let pFromSpacing = Infinity; + let bottomLeft = this.toMap.forward([bb.min.x, bb.min.y]); + let bottomRight = this.toMap.forward([bb.max.x, bb.min.y]); + let topRight = this.toMap.forward([bb.max.x, bb.max.y]); + let topLeft = this.toMap.forward([bb.min.x, bb.max.y]); - let lpos = position.clone().sub(pointcloud.position); - lpos.z = 0; - let ray = new Ray(lpos, new Vector3(0, 0, 1)); + let extent = { + bottomLeft: bottomLeft, + bottomRight: bottomRight, + topRight: topRight, + topLeft: topLeft + }; - let stack = [pointcloud.root]; - while (stack.length > 0) { - let node = stack.pop(); - let box = node.getBoundingBox(); + return extent; + }; - let inside = ray.intersectBox(box); + getMapCenter () { + let mapExtent = this.getMapExtent(); - if (!inside) { - continue; - } + let mapCenter = [ + (mapExtent.bottomLeft[0] + mapExtent.topRight[0]) / 2, + (mapExtent.bottomLeft[1] + mapExtent.topRight[1]) / 2 + ]; - let h = node.geometryNode.mean.z + - pointcloud.position.z + - node.geometryNode.boundingBox.min.z; + return mapCenter; + }; - if (node.geometryNode.spacing <= pFromSpacing) { - pHeight = h; - pFromSpacing = node.geometryNode.spacing; - } + updateToolDrawings () { + this.toolLayer.getSource().clear(); - for (let index of Object.keys(node.children)) { - let child = node.children[index]; - if (child.geometryNode) { - stack.push(node.children[index]); - } - } - } + let profiles = this.viewer.profileTool.profiles; + for (let i = 0; i < profiles.length; i++) { + let profile = profiles[i]; + let coordinates = []; - if (height === null || pFromSpacing < fromSpacing) { - height = pHeight; - fromSpacing = pFromSpacing; + for (let j = 0; j < profile.points.length; j++) { + let point = profile.points[j]; + let pointMap = this.toMap.forward([point.x, point.y]); + coordinates.push(pointMap); } + + let line = new ol.geom.LineString(coordinates); + let feature = new ol.Feature(line); + this.toolLayer.getSource().addFeature(feature); } - return height; - } - - getBoundingBox(pointclouds = this.pointclouds){ - let box = new Box3(); + let measurements = this.viewer.measuringTool.measurements; + for (let i = 0; i < measurements.length; i++) { + let measurement = measurements[i]; + let coordinates = []; - this.scenePointCloud.updateMatrixWorld(true); - this.referenceFrame.updateMatrixWorld(true); + for (let j = 0; j < measurement.points.length; j++) { + let point = measurement.points[j].position; + let pointMap = this.toMap.forward([point.x, point.y]); + coordinates.push(pointMap); + } - for (let pointcloud of pointclouds) { - pointcloud.updateMatrixWorld(true); + if (measurement.closed && measurement.points.length > 0) { + coordinates.push(coordinates[0]); + } - let pointcloudBox = pointcloud.pcoGeometry.tightBoundingBox ? pointcloud.pcoGeometry.tightBoundingBox : pointcloud.boundingBox; - let boxWorld = Utils.computeTransformedBoundingBox(pointcloudBox, pointcloud.matrixWorld); - box.union(boxWorld); + let line = new ol.geom.LineString(coordinates); + let feature = new ol.Feature(line); + this.toolLayer.getSource().addFeature(feature); } - - return box; } - addPointCloud (pointcloud) { - this.pointclouds.push(pointcloud); - this.scenePointCloud.add(pointcloud); - - this.dispatchEvent({ - type: 'pointcloud_added', - pointcloud: pointcloud - }); - } + addImages360(images){ + let transform = this.toMap.forward; + let layer = this.getImages360Layer(); - addVolume (volume) { - this.volumes.push(volume); - this.dispatchEvent({ - 'type': 'volume_added', - 'scene': this, - 'volume': volume - }); - } + for(let image of images.images){ - addOrientedImages(images){ - this.orientedImages.push(images); - this.scene.add(images.node); + let p = transform([image.position[0], image.position[1]]); - this.dispatchEvent({ - 'type': 'oriented_images_added', - 'scene': this, - 'images': images - }); - }; + let feature = new ol.Feature({ + 'geometry': new ol.geom.Point(p), + }); - removeOrientedImages(images){ - let index = this.orientedImages.indexOf(images); - if (index > -1) { - this.orientedImages.splice(index, 1); + feature.onClick = () => { + images.focus(image); + }; - this.dispatchEvent({ - 'type': 'oriented_images_removed', - 'scene': this, - 'images': images - }); + layer.getSource().addFeature(feature); } - }; + } - add360Images(images){ - this.images360.push(images); - this.scene.add(images.node); + async load (pointcloud) { + if (!pointcloud) { + return; + } - this.dispatchEvent({ - 'type': '360_images_added', - 'scene': this, - 'images': images - }); - } + if (!pointcloud.projection) { + return; + } - remove360Images(images){ - let index = this.images360.indexOf(images); - if (index > -1) { - this.images360.splice(index, 1); + if (!this.sceneProjection) { + try { + this.setSceneProjection(pointcloud.projection); + }catch (e) { + console.log('Failed projection:', e); - this.dispatchEvent({ - 'type': '360_images_removed', - 'scene': this, - 'images': images - }); + if (pointcloud.fallbackProjection) { + try { + console.log('Trying fallback projection...'); + this.setSceneProjection(pointcloud.fallbackProjection); + console.log('Set projection from fallback'); + }catch (e) { + console.log('Failed fallback projection:', e); + return; + } + }else { + return; + }; + } } - } - addGeopackage(geopackage){ - this.geopackages.push(geopackage); - this.scene.add(geopackage.node); + let mapExtent = this.getMapExtent(); + let mapCenter = this.getMapCenter(); - this.dispatchEvent({ - 'type': 'geopackage_added', - 'scene': this, - 'geopackage': geopackage - }); - }; + let view = this.map.getView(); + view.setCenter(mapCenter); + + this.gExtent.setCoordinates([ + mapExtent.bottomLeft, + mapExtent.bottomRight, + mapExtent.topRight, + mapExtent.topLeft, + mapExtent.bottomLeft + ]); - removeGeopackage(geopackage){ - let index = this.geopackages.indexOf(geopackage); - if (index > -1) { - this.geopackages.splice(index, 1); + view.fit(this.gExtent, [300, 300], { + constrainResolution: false + }); - this.dispatchEvent({ - 'type': 'geopackage_removed', - 'scene': this, - 'geopackage': geopackage - }); + if (pointcloud.pcoGeometry.type == 'ept'){ + return; } - }; - removeVolume (volume) { - let index = this.volumes.indexOf(volume); - if (index > -1) { - this.volumes.splice(index, 1); - - this.dispatchEvent({ - 'type': 'volume_removed', - 'scene': this, - 'volume': volume - }); - } - }; + let url = `${pointcloud.pcoGeometry.url}/../sources.json`; + //let response = await fetch(url); - addCameraAnimation(animation) { - this.cameraAnimations.push(animation); - this.dispatchEvent({ - 'type': 'camera_animation_added', - 'scene': this, - 'animation': animation - }); - }; + fetch(url).then(async (response) => { + let data = await response.json(); + + let sources = data.sources; - removeCameraAnimation(animation){ - let index = this.cameraAnimations.indexOf(volume); - if (index > -1) { - this.cameraAnimations.splice(index, 1); + for (let i = 0; i < sources.length; i++) { + let source = sources[i]; + let name = source.name; + let bounds = source.bounds; - this.dispatchEvent({ - 'type': 'camera_animation_removed', - 'scene': this, - 'animation': animation - }); - } - }; + let mapBounds = { + min: this.toMap.forward([bounds.min[0], bounds.min[1]]), + max: this.toMap.forward([bounds.max[0], bounds.max[1]]) + }; + let mapCenter = [ + (mapBounds.min[0] + mapBounds.max[0]) / 2, + (mapBounds.min[1] + mapBounds.max[1]) / 2 + ]; - addPolygonClipVolume(volume){ - this.polygonClipVolumes.push(volume); - this.dispatchEvent({ - "type": "polygon_clip_volume_added", - "scene": this, - "volume": volume - }); - }; - - removePolygonClipVolume(volume){ - let index = this.polygonClipVolumes.indexOf(volume); - if (index > -1) { - this.polygonClipVolumes.splice(index, 1); - this.dispatchEvent({ - "type": "polygon_clip_volume_removed", - "scene": this, - "volume": volume - }); - } - }; - - addMeasurement(measurement){ - measurement.lengthUnit = this.lengthUnit; - measurement.lengthUnitDisplay = this.lengthUnitDisplay; - this.measurements.push(measurement); - this.dispatchEvent({ - 'type': 'measurement_added', - 'scene': this, - 'measurement': measurement - }); - }; + let p1 = this.toMap.forward([bounds.min[0], bounds.min[1]]); + let p2 = this.toMap.forward([bounds.max[0], bounds.min[1]]); + let p3 = this.toMap.forward([bounds.max[0], bounds.max[1]]); + let p4 = this.toMap.forward([bounds.min[0], bounds.max[1]]); - removeMeasurement (measurement) { - let index = this.measurements.indexOf(measurement); - if (index > -1) { - this.measurements.splice(index, 1); - this.dispatchEvent({ - 'type': 'measurement_removed', - 'scene': this, - 'measurement': measurement - }); - } - } + // let feature = new ol.Feature({ + // 'geometry': new ol.geom.LineString([p1, p2, p3, p4, p1]) + // }); + let feature = new ol.Feature({ + 'geometry': new ol.geom.Polygon([[p1, p2, p3, p4, p1]]) + }); + feature.source = source; + feature.pointcloud = pointcloud; + this.getSourcesLayer().getSource().addFeature(feature); - addProfile (profile) { - this.profiles.push(profile); - this.dispatchEvent({ - 'type': 'profile_added', - 'scene': this, - 'profile': profile + feature = new ol.Feature({ + geometry: new ol.geom.Point(mapCenter), + name: name + }); + feature.setStyle(this.createLabelStyle(name)); + this.sourcesLabelLayer.getSource().addFeature(feature); + } + }).catch(() => { + }); + } - removeProfile (profile) { - let index = this.profiles.indexOf(profile); - if (index > -1) { - this.profiles.splice(index, 1); - this.dispatchEvent({ - 'type': 'profile_removed', - 'scene': this, - 'profile': profile - }); + toggle () { + if (this.elMap.is(':visible')) { + this.elMap.css('display', 'none'); + this.enabled = false; + } else { + this.elMap.css('display', 'block'); + this.enabled = true; } } - removeAllMeasurements () { - while (this.measurements.length > 0) { - this.removeMeasurement(this.measurements[0]); + update (delta) { + if (!this.sceneProjection) { + return; } - while (this.profiles.length > 0) { - this.removeProfile(this.profiles[0]); - } + let pm = $('#potree_map'); - while (this.volumes.length > 0) { - this.removeVolume(this.volumes[0]); + if (!this.enabled) { + return; } - } - removeAllClipVolumes(){ - let clipVolumes = this.volumes.filter(volume => volume.clip === true); - for(let clipVolume of clipVolumes){ - this.removeVolume(clipVolume); + // resize + let mapSize = this.map.getSize(); + let resized = (pm.width() !== mapSize[0] || pm.height() !== mapSize[1]); + if (resized) { + this.map.updateSize(); } - while(this.polygonClipVolumes.length > 0){ - this.removePolygonClipVolume(this.polygonClipVolumes[0]); - } - } + // + let camera = this.viewer.scene.getActiveCamera(); - getActiveCamera() { + let scale = this.map.getView().getResolution(); + let campos = camera.position; + let camdir = camera.getWorldDirection(new Vector3()); + let sceneLookAt = camdir.clone().multiplyScalar(30 * scale).add(campos); + let geoPos = camera.position; + let geoLookAt = sceneLookAt; + let mapPos = new Vector2().fromArray(this.toMap.forward([geoPos.x, geoPos.y])); + let mapLookAt = new Vector2().fromArray(this.toMap.forward([geoLookAt.x, geoLookAt.y])); + let mapDir = new Vector2().subVectors(mapLookAt, mapPos).normalize(); - if(this.overrideCamera){ - return this.overrideCamera; - } + mapLookAt = mapPos.clone().add(mapDir.clone().multiplyScalar(30 * scale)); + let mapLength = mapPos.distanceTo(mapLookAt); + let mapSide = new Vector2(-mapDir.y, mapDir.x); - if(this.cameraMode === CameraMode.PERSPECTIVE){ - return this.cameraP; - }else if(this.cameraMode === CameraMode.ORTHOGRAPHIC){ - return this.cameraO; - }else if(this.cameraMode === CameraMode.VR){ - return this.cameraVR; - } + let p1 = mapPos.toArray(); + let p2 = mapLookAt.clone().sub(mapSide.clone().multiplyScalar(0.3 * mapLength)).toArray(); + let p3 = mapLookAt.clone().add(mapSide.clone().multiplyScalar(0.3 * mapLength)).toArray(); - return null; + this.gCamera.setCoordinates([p1, p2, p3, p1]); } - - initialize(){ - - this.referenceFrame = new Object3D(); - this.referenceFrame.matrixAutoUpdate = false; - this.scenePointCloud.add(this.referenceFrame); - this.cameraP.up.set(0, 0, 1); - this.cameraP.position.set(1000, 1000, 1000); - this.cameraO.up.set(0, 0, 1); - this.cameraO.position.set(1000, 1000, 1000); - //this.camera.rotation.y = -Math.PI / 4; - //this.camera.rotation.x = -Math.PI / 6; - this.cameraScreenSpace.lookAt(new Vector3(0, 0, 0), new Vector3(0, 0, -1), new Vector3(0, 1, 0)); - - this.directionalLight = new DirectionalLight( 0xffffff, 0.5 ); - this.directionalLight.position.set( 10, 10, 10 ); - this.directionalLight.lookAt( new Vector3(0, 0, 0)); - this.scenePointCloud.add( this.directionalLight ); - - let light = new AmbientLight( 0x555555 ); // soft white light - this.scenePointCloud.add( light ); + get sourcesVisible () { + return this.getSourcesLayer().getVisible(); + } - { // background - let texture = Utils.createBackgroundTexture(512, 512); + set sourcesVisible (value) { + this.getSourcesLayer().setVisible(value); + } + + } + + class CSVExporter { + static toString (points) { + let string = ''; + + let attributes = Object.keys(points.data) + .filter(a => a !== 'normal') + .sort((a, b) => { + if (a === 'position') return -1; + if (b === 'position') return 1; + if (a === 'rgba') return -1; + if (b === 'rgba') return 1; + }); + + let headerValues = []; + for (let attribute of attributes) { + let itemSize = points.data[attribute].length / points.numPoints; - texture.minFilter = texture.magFilter = NearestFilter; - texture.minFilter = texture.magFilter = LinearFilter; - let bg = new Mesh( - new PlaneBufferGeometry(2, 2, 1), - new MeshBasicMaterial({ - map: texture - }) - ); - bg.material.depthTest = false; - bg.material.depthWrite = false; - this.sceneBG.add(bg); + if (attribute === 'position') { + headerValues = headerValues.concat(['x', 'y', 'z']); + } else if (attribute === 'rgba') { + headerValues = headerValues.concat(['r', 'g', 'b', 'a']); + } else if (itemSize > 1) { + for (let i = 0; i < itemSize; i++) { + headerValues.push(`${attribute}_${i}`); + } + } else { + headerValues.push(attribute); + } } + string = headerValues.join(', ') + '\n'; - // { // lights - // { - // let light = new THREE.DirectionalLight(0xffffff); - // light.position.set(10, 10, 1); - // light.target.position.set(0, 0, 0); - // this.scene.add(light); - // } + for (let i = 0; i < points.numPoints; i++) { + let values = []; - // { - // let light = new THREE.DirectionalLight(0xffffff); - // light.position.set(-10, 10, 1); - // light.target.position.set(0, 0, 0); - // this.scene.add(light); - // } + for (let attribute of attributes) { + let itemSize = points.data[attribute].length / points.numPoints; + let value = points.data[attribute] + .subarray(itemSize * i, itemSize * i + itemSize) + .join(', '); + values.push(value); + } - // { - // let light = new THREE.DirectionalLight(0xffffff); - // light.position.set(0, -10, 20); - // light.target.position.set(0, 0, 0); - // this.scene.add(light); - // } - // } - } - - addAnnotation(position, args = {}){ - if(position instanceof Array){ - args.position = new Vector3().fromArray(position); - } else if (position.x != null) { - args.position = position; + string += values.join(', ') + '\n'; } - let annotation = new Annotation(args); - this.annotations.add(annotation); - return annotation; + return string; } + }; - getAnnotations () { - return this.annotations; - }; + class LASExporter { + static toLAS (points) { + // TODO Unused: let string = ''; - removeAnnotation(annotationToRemove) { - this.annotations.remove(annotationToRemove); - } - }; + let boundingBox = points.boundingBox; + let offset = boundingBox.min.clone(); + let diagonal = boundingBox.min.distanceTo(boundingBox.max); + let scale = new Vector3(0.001, 0.001, 0.001); + if (diagonal > 1000 * 1000) { + scale = new Vector3(0.01, 0.01, 0.01); + } else { + scale = new Vector3(0.001, 0.001, 0.001); + } - // http://epsg.io/ - proj4.defs([ - ['UTM10N', '+proj=utm +zone=10 +ellps=GRS80 +datum=NAD83 +units=m +no_defs'], - ['EPSG:6339', '+proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6340', '+proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6341', '+proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6342', '+proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6343', '+proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6344', '+proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6345', '+proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6346', '+proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6347', '+proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:6348', '+proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs'], - ['EPSG:26910', '+proj=utm +zone=10 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26911', '+proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26912', '+proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26913', '+proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26914', '+proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26915', '+proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26916', '+proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26917', '+proj=utm +zone=17 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26918', '+proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ['EPSG:26919', '+proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], - ]); + let setString = function (string, offset, buffer) { + let view = new Uint8Array(buffer); - class MapView{ + for (let i = 0; i < string.length; i++) { + let charCode = string.charCodeAt(i); + view[offset + i] = charCode; + } + }; - constructor (viewer) { - this.viewer = viewer; + let buffer = new ArrayBuffer(227 + 28 * points.numPoints); + let view = new DataView(buffer); + let u8View = new Uint8Array(buffer); + // let u16View = new Uint16Array(buffer); - this.webMapService = 'WMTS'; - this.mapProjectionName = 'EPSG:3857'; - this.mapProjection = proj4.defs(this.mapProjectionName); - this.sceneProjection = null; + setString('LASF', 0, buffer); + u8View[24] = 1; + u8View[25] = 2; - this.extentsLayer = null; - this.cameraLayer = null; - this.toolLayer = null; - this.sourcesLayer = null; - this.sourcesLabelLayer = null; - this.images360Layer = null; - this.enabled = false; + // system identifier o:26 l:32 - this.createAnnotationStyle = (text) => { - return [ - new ol.style.Style({ - image: new ol.style.Circle({ - radius: 10, - stroke: new ol.style.Stroke({ - color: [255, 255, 255, 0.5], - width: 2 - }), - fill: new ol.style.Fill({ - color: [0, 0, 0, 0.5] - }) - }) - }) - ]; - }; + // generating software o:58 l:32 + setString('Potree 1.7', 58, buffer); - this.createLabelStyle = (text) => { - let style = new ol.style.Style({ - image: new ol.style.Circle({ - radius: 6, - stroke: new ol.style.Stroke({ - color: 'white', - width: 2 - }), - fill: new ol.style.Fill({ - color: 'green' - }) - }), - text: new ol.style.Text({ - font: '12px helvetica,sans-serif', - text: text, - fill: new ol.style.Fill({ - color: '#000' - }), - stroke: new ol.style.Stroke({ - color: '#fff', - width: 2 - }) - }) - }); + // file creation day of year o:90 l:2 + // file creation year o:92 l:2 - return style; - }; - } + // header size o:94 l:2 + view.setUint16(94, 227, true); - showSources (show) { - this.sourcesLayer.setVisible(show); - this.sourcesLabelLayer.setVisible(show); - } + // offset to point data o:96 l:4 + view.setUint32(96, 227, true); - init () { + // number of letiable length records o:100 l:4 - if(typeof ol === "undefined"){ - return; - } + // point data record format 104 1 + u8View[104] = 2; - this.elMap = $('#potree_map'); - this.elMap.draggable({ handle: $('#potree_map_header') }); - this.elMap.resizable(); + // point data record length 105 2 + view.setUint16(105, 28, true); - this.elTooltip = $(`
`); - this.elMap.append(this.elTooltip); + // number of point records 107 4 + view.setUint32(107, points.numPoints, true); - let extentsLayer = this.getExtentsLayer(); - let cameraLayer = this.getCameraLayer(); - this.getToolLayer(); - let sourcesLayer = this.getSourcesLayer(); - this.images360Layer = this.getImages360Layer(); - this.getSourcesLabelLayer(); - this.getAnnotationsLayer(); + // number of points by return 111 20 - let mousePositionControl = new ol.control.MousePosition({ - coordinateFormat: ol.coordinate.createStringXY(5), - projection: 'EPSG:4326', - undefinedHTML: ' ' - }); + // x scale factor 131 8 + view.setFloat64(131, scale.x, true); - let _this = this; - let DownloadSelectionControl = function (optOptions) { - let options = optOptions || {}; + // y scale factor 139 8 + view.setFloat64(139, scale.y, true); - // TOGGLE TILES - let btToggleTiles = document.createElement('button'); - btToggleTiles.innerHTML = 'T'; - btToggleTiles.addEventListener('click', () => { - let visible = sourcesLayer.getVisible(); - _this.showSources(!visible); - }, false); - btToggleTiles.style.float = 'left'; - btToggleTiles.title = 'show / hide tiles'; + // z scale factor 147 8 + view.setFloat64(147, scale.z, true); - // DOWNLOAD SELECTED TILES - let link = document.createElement('a'); - link.href = '#'; - link.download = 'list.txt'; - link.style.float = 'left'; + // x offset 155 8 + view.setFloat64(155, offset.x, true); + + // y offset 163 8 + view.setFloat64(163, offset.y, true); + + // z offset 171 8 + view.setFloat64(171, offset.z, true); + + // max x 179 8 + view.setFloat64(179, boundingBox.max.x, true); - let button = document.createElement('button'); - button.innerHTML = 'D'; - link.appendChild(button); + // min x 187 8 + view.setFloat64(187, boundingBox.min.x, true); - let handleDownload = (e) => { - let features = selectedFeatures.getArray(); + // max y 195 8 + view.setFloat64(195, boundingBox.max.y, true); - let url = [document.location.protocol, '//', document.location.host, document.location.pathname].join(''); + // min y 203 8 + view.setFloat64(203, boundingBox.min.y, true); - if (features.length === 0) { - alert('No tiles were selected. Select area with ctrl + left mouse button!'); - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } else if (features.length === 1) { - let feature = features[0]; + // max z 211 8 + view.setFloat64(211, boundingBox.max.z, true); - if (feature.source) { - let cloudjsurl = feature.pointcloud.pcoGeometry.url; - let sourceurl = new URL(url + '/../' + cloudjsurl + '/../source/' + feature.source.name); - link.href = sourceurl.href; - link.download = feature.source.name; - } - } else { - let content = ''; - for (let i = 0; i < features.length; i++) { - let feature = features[i]; + // min z 219 8 + view.setFloat64(219, boundingBox.min.z, true); - if (feature.source) { - let cloudjsurl = feature.pointcloud.pcoGeometry.url; - let sourceurl = new URL(url + '/../' + cloudjsurl + '/../source/' + feature.source.name); - content += sourceurl.href + '\n'; - } - } + let boffset = 227; + for (let i = 0; i < points.numPoints; i++) { - let uri = 'data:application/octet-stream;base64,' + btoa(content); - link.href = uri; - link.download = 'list_of_files.txt'; - } - }; + let px = points.data.position[3 * i + 0]; + let py = points.data.position[3 * i + 1]; + let pz = points.data.position[3 * i + 2]; - button.addEventListener('click', handleDownload, false); + let ux = parseInt((px - offset.x) / scale.x); + let uy = parseInt((py - offset.y) / scale.y); + let uz = parseInt((pz - offset.z) / scale.z); - // assemble container - let element = document.createElement('div'); - element.className = 'ol-unselectable ol-control'; - element.appendChild(link); - element.appendChild(btToggleTiles); - element.style.bottom = '0.5em'; - element.style.left = '0.5em'; - element.title = 'Download file or list of selected tiles. Select tile with left mouse button or area using ctrl + left mouse.'; + view.setUint32(boffset + 0, ux, true); + view.setUint32(boffset + 4, uy, true); + view.setUint32(boffset + 8, uz, true); - ol.control.Control.call(this, { - element: element, - target: options.target - }); - }; - ol.inherits(DownloadSelectionControl, ol.control.Control); + if (points.data.intensity) { + view.setUint16(boffset + 12, (points.data.intensity[i]), true); + } - this.map = new ol.Map({ - controls: ol.control.defaults({ - attributionOptions: ({ - collapsible: false - }) - }).extend([ - // this.controls.zoomToExtent, - new DownloadSelectionControl(), - mousePositionControl - ]), - layers: [ - new ol.layer.Tile({source: new ol.source.OSM()}), - this.toolLayer, - this.annotationsLayer, - this.sourcesLayer, - this.sourcesLabelLayer, - this.images360Layer, - extentsLayer, - cameraLayer - ], - target: 'potree_map_content', - view: new ol.View({ - center: this.olCenter, - zoom: 9 - }) - }); + let rt = 0; + if (points.data.returnNumber) { + rt += points.data.returnNumber[i]; + } + if (points.data.numberOfReturns) { + rt += (points.data.numberOfReturns[i] << 3); + } + view.setUint8(boffset + 14, rt); - // DRAGBOX / SELECTION - this.dragBoxLayer = new ol.layer.Vector({ - source: new ol.source.Vector({}), - style: new ol.style.Style({ - stroke: new ol.style.Stroke({ - color: 'rgba(0, 0, 255, 1)', - width: 2 - }) - }) - }); - this.map.addLayer(this.dragBoxLayer); + if (points.data.classification) { + view.setUint8(boffset + 15, points.data.classification[i]); + } + // scan angle rank + // user data + // point source id + if (points.data.pointSourceID) { + view.setUint16(boffset + 18, points.data.pointSourceID[i]); + } - let select = new ol.interaction.Select(); - this.map.addInteraction(select); + if (points.data.rgba) { + let rgba = points.data.rgba; + view.setUint16(boffset + 20, (rgba[4 * i + 0] * 255), true); + view.setUint16(boffset + 22, (rgba[4 * i + 1] * 255), true); + view.setUint16(boffset + 24, (rgba[4 * i + 2] * 255), true); + } - let selectedFeatures = select.getFeatures(); + boffset += 28; + } - let dragBox = new ol.interaction.DragBox({ - condition: ol.events.condition.platformModifierKeyOnly - }); + return buffer; + } + + } - this.map.addInteraction(dragBox); + function copyMaterial(source, target){ - // this.map.on('pointermove', evt => { - // let pixel = evt.pixel; - // let feature = this.map.forEachFeatureAtPixel(pixel, function (feature) { - // return feature; - // }); + for(let name of Object.keys(target.uniforms)){ + target.uniforms[name].value = source.uniforms[name].value; + } - // // console.log(feature); - // // this.elTooltip.css("display", feature ? '' : 'none'); - // this.elTooltip.css('display', 'none'); - // if (feature && feature.onHover) { - // feature.onHover(evt); - // // overlay.setPosition(evt.coordinate); - // // tooltip.innerHTML = feature.get('name'); - // } - // }); + target.gradientTexture = source.gradientTexture; + target.visibleNodesTexture = source.visibleNodesTexture; + target.classificationTexture = source.classificationTexture; + target.matcapTexture = source.matcapTexture; - this.map.on('click', evt => { - let pixel = evt.pixel; - let feature = this.map.forEachFeatureAtPixel(pixel, function (feature) { - return feature; - }); + target.activeAttributeName = source.activeAttributeName; + target.ranges = source.ranges; - if (feature && feature.onClick) { - feature.onClick(evt); - } - }); + //target.updateShaderSource(); + } - dragBox.on('boxend', (e) => { - // features that intersect the box are added to the collection of - // selected features, and their names are displayed in the "info" - // div - let extent = dragBox.getGeometry().getExtent(); - this.getSourcesLayer().getSource().forEachFeatureIntersectingExtent(extent, (feature) => { - selectedFeatures.push(feature); - }); - }); - // clear selection when drawing a new box and when clicking on the map - dragBox.on('boxstart', (e) => { - selectedFeatures.clear(); - }); - this.map.on('click', () => { - selectedFeatures.clear(); - }); + class Batch{ - this.viewer.addEventListener('scene_changed', e => { - this.setScene(e.scene); - }); + constructor(geometry, material){ + this.geometry = geometry; + this.material = material; - this.onPointcloudAdded = e => { - this.load(e.pointcloud); - }; + this.sceneNode = new Points(geometry, material); - this.on360ImagesAdded = e => { - this.addImages360(e.images); + this.geometryNode = { + estimatedSpacing: 1.0, + geometry: geometry, }; + } - this.onAnnotationAdded = e => { - if (!this.sceneProjection) { - return; - } + getLevel(){ + return 0; + } - let annotation = e.annotation; - let position = annotation.position; - let mapPos = this.toMap.forward([position.x, position.y]); - let feature = new ol.Feature({ - geometry: new ol.geom.Point(mapPos), - name: annotation.title - }); - feature.setStyle(this.createAnnotationStyle(annotation.title)); + } - feature.onHover = evt => { - let coordinates = feature.getGeometry().getCoordinates(); - let p = this.map.getPixelFromCoordinate(coordinates); + class ProfileFakeOctree extends PointCloudTree{ - this.elTooltip.html(annotation.title); - this.elTooltip.css('display', ''); - this.elTooltip.css('left', `${p[0]}px`); - this.elTooltip.css('top', `${p[1]}px`); - }; + constructor(octree){ + super(); + + this.trueOctree = octree; + this.pcoGeometry = octree.pcoGeometry; + this.points = []; + this.visibleNodes = []; + + //this.material = this.trueOctree.material; + this.material = new PointCloudMaterial$1(); + //this.material.copy(this.trueOctree.material); + copyMaterial(this.trueOctree.material, this.material); + this.material.pointSizeType = PointSizeType.FIXED; + + this.batchSize = 100 * 1000; + this.currentBatch = null; + } - feature.onClick = evt => { - annotation.clickTitle(); - }; + getAttribute(name){ + return this.trueOctree.getAttribute(name); + } - this.getAnnotationsLayer().getSource().addFeature(feature); - }; + dispose(){ + for(let node of this.visibleNodes){ + node.geometry.dispose(); + } - this.setScene(this.viewer.scene); + this.visibleNodes = []; + this.currentBatch = null; + this.points = []; } - setScene (scene) { - if (this.scene === scene) { - return; - }; + addPoints(data){ + // since each call to addPoints can deliver very very few points, + // we're going to batch them into larger buffers for efficiency. - if (this.scene) { - this.scene.removeEventListener('pointcloud_added', this.onPointcloudAdded); - this.scene.removeEventListener('360_images_added', this.on360ImagesAdded); - this.scene.annotations.removeEventListener('annotation_added', this.onAnnotationAdded); + if(this.currentBatch === null){ + this.currentBatch = this.createNewBatch(data); } - this.scene = scene; + this.points.push(data); - this.scene.addEventListener('pointcloud_added', this.onPointcloudAdded); - this.scene.addEventListener('360_images_added', this.on360ImagesAdded); - this.scene.annotations.addEventListener('annotation_added', this.onAnnotationAdded); - for (let pointcloud of this.viewer.scene.pointclouds) { - this.load(pointcloud); - } + let updateRange = { + start: this.currentBatch.geometry.drawRange.count, + count: 0 + }; + let projectedBox = new Box3(); - this.viewer.scene.annotations.traverseDescendants(annotation => { - this.onAnnotationAdded({annotation: annotation}); - }); + let truePos = new Vector3(); - for(let images of this.viewer.scene.images360){ - this.on360ImagesAdded({images: images}); - } - } + for(let i = 0; i < data.numPoints; i++){ - getExtentsLayer () { - if (this.extentsLayer) { - return this.extentsLayer; - } + if(updateRange.start + updateRange.count >= this.batchSize){ + // current batch full, start new batch - this.gExtent = new ol.geom.LineString([[0, 0], [0, 0]]); + for(let key of Object.keys(this.currentBatch.geometry.attributes)){ + let attribute = this.currentBatch.geometry.attributes[key]; + attribute.updateRange.offset = updateRange.start; + attribute.updateRange.count = updateRange.count; + attribute.needsUpdate = true; + } - let feature = new ol.Feature(this.gExtent); - let featureVector = new ol.source.Vector({ - features: [feature] - }); + this.currentBatch.geometry.computeBoundingBox(); + this.currentBatch.geometry.computeBoundingSphere(); - this.extentsLayer = new ol.layer.Vector({ - source: featureVector, - style: new ol.style.Style({ - fill: new ol.style.Fill({ - color: 'rgba(255, 255, 255, 0.2)' - }), - stroke: new ol.style.Stroke({ - color: '#0000ff', - width: 2 - }), - image: new ol.style.Circle({ - radius: 3, - fill: new ol.style.Fill({ - color: '#0000ff' - }) - }) - }) - }); + this.currentBatch = this.createNewBatch(data); + updateRange = { + start: 0, + count: 0 + }; + } - return this.extentsLayer; - } + truePos.set( + data.data.position[3 * i + 0] + this.trueOctree.position.x, + data.data.position[3 * i + 1] + this.trueOctree.position.y, + data.data.position[3 * i + 2] + this.trueOctree.position.z, + ); - getAnnotationsLayer () { - if (this.annotationsLayer) { - return this.annotationsLayer; - } + let x = data.data.mileage[i]; + let y = 0; + let z = truePos.z; - this.annotationsLayer = new ol.layer.Vector({ - source: new ol.source.Vector({ - }), - style: new ol.style.Style({ - fill: new ol.style.Fill({ - color: 'rgba(255, 0, 0, 1)' - }), - stroke: new ol.style.Stroke({ - color: 'rgba(255, 0, 0, 1)', - width: 2 - }) - }) - }); + projectedBox.expandByPoint(new Vector3(x, y, z)); - return this.annotationsLayer; - } + let index = updateRange.start + updateRange.count; + let geometry = this.currentBatch.geometry; - getCameraLayer () { - if (this.cameraLayer) { - return this.cameraLayer; - } + for(let attributeName of Object.keys(data.data)){ + let source = data.data[attributeName]; + let target = geometry.attributes[attributeName]; + let numElements = target.itemSize; + + for(let item = 0; item < numElements; item++){ + target.array[numElements * index + item] = source[numElements * i + item]; + } + } - // CAMERA LAYER - this.gCamera = new ol.geom.LineString([[0, 0], [0, 0], [0, 0], [0, 0]]); - let feature = new ol.Feature(this.gCamera); - let featureVector = new ol.source.Vector({ - features: [feature] - }); + { + let position = geometry.attributes.position; - this.cameraLayer = new ol.layer.Vector({ - source: featureVector, - style: new ol.style.Style({ - stroke: new ol.style.Stroke({ - color: '#0000ff', - width: 2 - }) - }) - }); + position.array[3 * index + 0] = x; + position.array[3 * index + 1] = y; + position.array[3 * index + 2] = z; + } - return this.cameraLayer; - } + updateRange.count++; + this.currentBatch.geometry.drawRange.count++; + } - getToolLayer () { - if (this.toolLayer) { - return this.toolLayer; + for(let key of Object.keys(this.currentBatch.geometry.attributes)){ + let attribute = this.currentBatch.geometry.attributes[key]; + attribute.updateRange.offset = updateRange.start; + attribute.updateRange.count = updateRange.count; + attribute.needsUpdate = true; } - this.toolLayer = new ol.layer.Vector({ - source: new ol.source.Vector({ - }), - style: new ol.style.Style({ - fill: new ol.style.Fill({ - color: 'rgba(255, 0, 0, 1)' - }), - stroke: new ol.style.Stroke({ - color: 'rgba(255, 0, 0, 1)', - width: 2 - }) - }) - }); + data.projectedBox = projectedBox; - return this.toolLayer; + this.projectedBox = this.points.reduce( (a, i) => a.union(i.projectedBox), new Box3()); } - getImages360Layer(){ - if(this.images360Layer){ - return this.images360Layer; - } + createNewBatch(data){ + let geometry = new BufferGeometry(); - let style = new ol.style.Style({ - image: new ol.style.Circle({ - radius: 4, - stroke: new ol.style.Stroke({ - color: [255, 0, 0, 1], - width: 2 - }), - fill: new ol.style.Fill({ - color: [255, 100, 100, 1] - }) - }) - }); - - let layer = new ol.layer.Vector({ - source: new ol.source.Vector({}), - style: style, - }); + // create new batches with batch_size elements of the same type as the attribute + for(let attributeName of Object.keys(data.data)){ + let buffer = data.data[attributeName]; + let numElements = buffer.length / data.numPoints; // 3 for pos, 4 for col, 1 for scalars + let constructor = buffer.constructor; + let normalized = false; + + if(this.trueOctree.root.sceneNode){ + if(this.trueOctree.root.sceneNode.geometry.attributes[attributeName]){ + normalized = this.trueOctree.root.sceneNode.geometry.attributes[attributeName].normalized; + } + } + - this.images360Layer = layer; + let batchBuffer = new constructor(numElements * this.batchSize); - return this.images360Layer; - } + let bufferAttribute = new BufferAttribute(batchBuffer, numElements, normalized); + bufferAttribute.potree = { + range: [0, 1], + }; - getSourcesLayer () { - if (this.sourcesLayer) { - return this.sourcesLayer; + geometry.setAttribute(attributeName, bufferAttribute); } - this.sourcesLayer = new ol.layer.Vector({ - source: new ol.source.Vector({}), - style: new ol.style.Style({ - fill: new ol.style.Fill({ - color: 'rgba(0, 0, 150, 0.1)' - }), - stroke: new ol.style.Stroke({ - color: 'rgba(0, 0, 150, 1)', - width: 1 - }) - }) - }); + geometry.drawRange.start = 0; + geometry.drawRange.count = 0; + + let batch = new Batch(geometry, this.material); + + this.visibleNodes.push(batch); - return this.sourcesLayer; + return batch; } + + computeVisibilityTextureData(){ + let data = new Uint8Array(this.visibleNodes.length * 4); + let offsets = new Map(); - getSourcesLabelLayer () { - if (this.sourcesLabelLayer) { - return this.sourcesLabelLayer; + for(let i = 0; i < this.visibleNodes.length; i++){ + let node = this.visibleNodes[i]; + + offsets[node] = i; } - this.sourcesLabelLayer = new ol.layer.Vector({ - source: new ol.source.Vector({ - }), - style: new ol.style.Style({ - fill: new ol.style.Fill({ - color: 'rgba(255, 0, 0, 0.1)' - }), - stroke: new ol.style.Stroke({ - color: 'rgba(255, 0, 0, 1)', - width: 2 - }) - }), - minResolution: 0.01, - maxResolution: 20 - }); - return this.sourcesLabelLayer; + return { + data: data, + offsets: offsets, + }; } - setSceneProjection (sceneProjection) { - this.sceneProjection = sceneProjection; - this.toMap = proj4(this.sceneProjection, this.mapProjection); - this.toScene = proj4(this.mapProjection, this.sceneProjection); - }; - - getMapExtent () { - let bb = this.viewer.getBoundingBox(); + } - let bottomLeft = this.toMap.forward([bb.min.x, bb.min.y]); - let bottomRight = this.toMap.forward([bb.max.x, bb.min.y]); - let topRight = this.toMap.forward([bb.max.x, bb.max.y]); - let topLeft = this.toMap.forward([bb.min.x, bb.max.y]); + class ProfileWindow extends EventDispatcher { + constructor (viewer) { + super(); - let extent = { - bottomLeft: bottomLeft, - bottomRight: bottomRight, - topRight: topRight, - topLeft: topLeft - }; + this.viewer = viewer; + this.elRoot = $('#profile_window'); + this.renderArea = this.elRoot.find('#profileCanvasContainer'); + this.svg = d3.select('svg#profileSVG'); + this.mouseIsDown = false; - return extent; - }; + this.projectedBox = new Box3(); + this.pointclouds = new Map(); + this.numPoints = 0; + this.lastAddPointsTimestamp = undefined; - getMapCenter () { - let mapExtent = this.getMapExtent(); + this.mouse = new Vector2(0, 0); + this.scale = new Vector3(1, 1, 1); - let mapCenter = [ - (mapExtent.bottomLeft[0] + mapExtent.topRight[0]) / 2, - (mapExtent.bottomLeft[1] + mapExtent.topRight[1]) / 2 - ]; + this.autoFitEnabled = true; // completely disable/enable + this.autoFit = false; // internal - return mapCenter; - }; + let cwIcon = `${exports.resourcePath}/icons/arrow_cw.svg`; + $('#potree_profile_rotate_cw').attr('src', cwIcon); - updateToolDrawings () { - this.toolLayer.getSource().clear(); + let ccwIcon = `${exports.resourcePath}/icons/arrow_ccw.svg`; + $('#potree_profile_rotate_ccw').attr('src', ccwIcon); + + let forwardIcon = `${exports.resourcePath}/icons/arrow_up.svg`; + $('#potree_profile_move_forward').attr('src', forwardIcon); - let profiles = this.viewer.profileTool.profiles; - for (let i = 0; i < profiles.length; i++) { - let profile = profiles[i]; - let coordinates = []; + let backwardIcon = `${exports.resourcePath}/icons/arrow_down.svg`; + $('#potree_profile_move_backward').attr('src', backwardIcon); - for (let j = 0; j < profile.points.length; j++) { - let point = profile.points[j]; - let pointMap = this.toMap.forward([point.x, point.y]); - coordinates.push(pointMap); - } + let csvIcon = `${exports.resourcePath}/icons/file_csv_2d.svg`; + $('#potree_download_csv_icon').attr('src', csvIcon); - let line = new ol.geom.LineString(coordinates); - let feature = new ol.Feature(line); - this.toolLayer.getSource().addFeature(feature); - } + let lasIcon = `${exports.resourcePath}/icons/file_las_3d.svg`; + $('#potree_download_las_icon').attr('src', lasIcon); - let measurements = this.viewer.measuringTool.measurements; - for (let i = 0; i < measurements.length; i++) { - let measurement = measurements[i]; - let coordinates = []; + let closeIcon = `${exports.resourcePath}/icons/close.svg`; + $('#closeProfileContainer').attr("src", closeIcon); - for (let j = 0; j < measurement.points.length; j++) { - let point = measurement.points[j].position; - let pointMap = this.toMap.forward([point.x, point.y]); - coordinates.push(pointMap); - } + this.initTHREE(); + this.initSVG(); + this.initListeners(); - if (measurement.closed && measurement.points.length > 0) { - coordinates.push(coordinates[0]); - } + this.pRenderer = new Renderer(this.renderer); - let line = new ol.geom.LineString(coordinates); - let feature = new ol.Feature(line); - this.toolLayer.getSource().addFeature(feature); - } + this.elRoot.i18n(); } - addImages360(images){ - let transform = this.toMap.forward; - let layer = this.getImages360Layer(); + initListeners () { + $(window).resize(() => { + if (this.enabled) { + this.render(); + } + }); - for(let image of images.images){ + this.renderArea.mousedown(e => { + this.mouseIsDown = true; + }); - let p = transform([image.position[0], image.position[1]]); + this.renderArea.mouseup(e => { + this.mouseIsDown = false; + }); - let feature = new ol.Feature({ - 'geometry': new ol.geom.Point(p), - }); + let viewerPickSphereSizeHandler = () => { + let camera = this.viewer.scene.getActiveCamera(); + let domElement = this.viewer.renderer.domElement; + let distance = this.viewerPickSphere.position.distanceTo(camera.position); + let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight); + let scale = (10 / pr); + this.viewerPickSphere.scale.set(scale, scale, scale); + }; - feature.onClick = () => { - images.focus(image); - }; + this.renderArea.mousemove(e => { + if (this.pointclouds.size === 0) { + return; + } - layer.getSource().addFeature(feature); - } - } + let rect = this.renderArea[0].getBoundingClientRect(); + let x = e.clientX - rect.left; + let y = e.clientY - rect.top; - async load (pointcloud) { - if (!pointcloud) { - return; - } + let newMouse = new Vector2(x, y); - if (!pointcloud.projection) { - return; - } + if (this.mouseIsDown) { + // DRAG + this.autoFit = false; + this.lastDrag = new Date().getTime(); - if (!this.sceneProjection) { - try { - this.setSceneProjection(pointcloud.projection); - }catch (e) { - console.log('Failed projection:', e); + let cPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)]; + let ncPos = [this.scaleX.invert(newMouse.x), this.scaleY.invert(newMouse.y)]; - if (pointcloud.fallbackProjection) { - try { - console.log('Trying fallback projection...'); - this.setSceneProjection(pointcloud.fallbackProjection); - console.log('Set projection from fallback'); - }catch (e) { - console.log('Failed fallback projection:', e); - return; - } - }else { - return; - }; - } - } + this.camera.position.x -= ncPos[0] - cPos[0]; + this.camera.position.z -= ncPos[1] - cPos[1]; - let mapExtent = this.getMapExtent(); - let mapCenter = this.getMapCenter(); + this.render(); + } else if (this.pointclouds.size > 0) { + // FIND HOVERED POINT + let radius = Math.abs(this.scaleX.invert(0) - this.scaleX.invert(40)); + let mileage = this.scaleX.invert(newMouse.x); + let elevation = this.scaleY.invert(newMouse.y); - let view = this.map.getView(); - view.setCenter(mapCenter); + let closest = this.selectPoint(mileage, elevation, radius); - this.gExtent.setCoordinates([ - mapExtent.bottomLeft, - mapExtent.bottomRight, - mapExtent.topRight, - mapExtent.topLeft, - mapExtent.bottomLeft - ]); + if (closest) { + let point = closest.point; - view.fit(this.gExtent, [300, 300], { - constrainResolution: false - }); + let position = new Float64Array([ + point.position[0] + closest.pointcloud.position.x, + point.position[1] + closest.pointcloud.position.y, + point.position[2] + closest.pointcloud.position.z + ]); - if (pointcloud.pcoGeometry.type == 'ept'){ - return; - } + this.elRoot.find('#profileSelectionProperties').fadeIn(200); + this.pickSphere.visible = true; + this.pickSphere.scale.set(0.5 * radius, 0.5 * radius, 0.5 * radius); + this.pickSphere.position.set(point.mileage, 0, position[2]); - let url = `${pointcloud.pcoGeometry.url}/../sources.json`; - //let response = await fetch(url); + this.viewerPickSphere.position.set(...position); + + if(!this.viewer.scene.scene.children.includes(this.viewerPickSphere)){ + this.viewer.scene.scene.add(this.viewerPickSphere); + if(!this.viewer.hasEventListener("update", viewerPickSphereSizeHandler)){ + this.viewer.addEventListener("update", viewerPickSphereSizeHandler); + } + } + - fetch(url).then(async (response) => { - let data = await response.json(); - - let sources = data.sources; + let info = this.elRoot.find('#profileSelectionProperties'); + let html = ''; - for (let i = 0; i < sources.length; i++) { - let source = sources[i]; - let name = source.name; - let bounds = source.bounds; + for (let attributeName of Object.keys(point)) { - let mapBounds = { - min: this.toMap.forward([bounds.min[0], bounds.min[1]]), - max: this.toMap.forward([bounds.max[0], bounds.max[1]]) - }; - let mapCenter = [ - (mapBounds.min[0] + mapBounds.max[0]) / 2, - (mapBounds.min[1] + mapBounds.max[1]) / 2 - ]; + let value = point[attributeName]; + let attribute = closest.pointcloud.getAttribute(attributeName); - let p1 = this.toMap.forward([bounds.min[0], bounds.min[1]]); - let p2 = this.toMap.forward([bounds.max[0], bounds.min[1]]); - let p3 = this.toMap.forward([bounds.max[0], bounds.max[1]]); - let p4 = this.toMap.forward([bounds.min[0], bounds.max[1]]); + let transform = value => value; + if(attribute && attribute.type.size > 4){ + let range = attribute.initialRange; + let scale = 1 / (range[1] - range[0]); + let offset = range[0]; + transform = value => value / scale + offset; + } - // let feature = new ol.Feature({ - // 'geometry': new ol.geom.LineString([p1, p2, p3, p4, p1]) - // }); - let feature = new ol.Feature({ - 'geometry': new ol.geom.Polygon([[p1, p2, p3, p4, p1]]) - }); - feature.source = source; - feature.pointcloud = pointcloud; - this.getSourcesLayer().getSource().addFeature(feature); + - feature = new ol.Feature({ - geometry: new ol.geom.Point(mapCenter), - name: name - }); - feature.setStyle(this.createLabelStyle(name)); - this.sourcesLabelLayer.getSource().addFeature(feature); - } - }).catch(() => { - - }); + - } + if (attributeName === 'position') { + let values = [...position].map(v => Utils.addCommas(v.toFixed(3))); + html += ` + + + + + + + + + + + + `; + } else if (attributeName === 'rgba') { + html += ` + + + + `; + } else if (attributeName === 'normal') { + continue; + } else if (attributeName === 'mileage') { + html += ` + + + + `; + } else { + html += ` + + + + `; + } + } + html += '
x${values[0]}
y${values[1]}
z${values[2]}
${attributeName}${value.join(', ')}
${attributeName}${value.toFixed(3)}
${attributeName}${transform(value)}
'; + info.html(html); - toggle () { - if (this.elMap.is(':visible')) { - this.elMap.css('display', 'none'); - this.enabled = false; - } else { - this.elMap.css('display', 'block'); - this.enabled = true; - } - } + this.selectedPoint = point; + } else { + // this.pickSphere.visible = false; + // this.selectedPoint = null; - update (delta) { - if (!this.sceneProjection) { - return; - } + this.viewer.scene.scene.add(this.viewerPickSphere); - let pm = $('#potree_map'); + let index = this.viewer.scene.scene.children.indexOf(this.viewerPickSphere); + if(index >= 0){ + this.viewer.scene.scene.children.splice(index, 1); + } + this.viewer.removeEventListener("update", viewerPickSphereSizeHandler); + - if (!this.enabled) { - return; - } + } + this.render(); + } - // resize - let mapSize = this.map.getSize(); - let resized = (pm.width() !== mapSize[0] || pm.height() !== mapSize[1]); - if (resized) { - this.map.updateSize(); - } + this.mouse.copy(newMouse); + }); - // - let camera = this.viewer.scene.getActiveCamera(); + let onWheel = e => { + this.autoFit = false; - let scale = this.map.getView().getResolution(); - let campos = camera.position; - let camdir = camera.getWorldDirection(new Vector3()); - let sceneLookAt = camdir.clone().multiplyScalar(30 * scale).add(campos); - let geoPos = camera.position; - let geoLookAt = sceneLookAt; - let mapPos = new Vector2().fromArray(this.toMap.forward([geoPos.x, geoPos.y])); - let mapLookAt = new Vector2().fromArray(this.toMap.forward([geoLookAt.x, geoLookAt.y])); - let mapDir = new Vector2().subVectors(mapLookAt, mapPos).normalize(); + let delta = 0; + if (e.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9 + delta = e.wheelDelta; + } else if (e.detail !== undefined) { // Firefox + delta = -e.detail; + } - mapLookAt = mapPos.clone().add(mapDir.clone().multiplyScalar(30 * scale)); - let mapLength = mapPos.distanceTo(mapLookAt); - let mapSide = new Vector2(-mapDir.y, mapDir.x); + let ndelta = Math.sign(delta); - let p1 = mapPos.toArray(); - let p2 = mapLookAt.clone().sub(mapSide.clone().multiplyScalar(0.3 * mapLength)).toArray(); - let p3 = mapLookAt.clone().add(mapSide.clone().multiplyScalar(0.3 * mapLength)).toArray(); + let cPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)]; - this.gCamera.setCoordinates([p1, p2, p3, p1]); - } + if (ndelta > 0) { + // + 10% + this.scale.multiplyScalar(1.1); + } else { + // - 10% + this.scale.multiplyScalar(100 / 110); + } - get sourcesVisible () { - return this.getSourcesLayer().getVisible(); - } + this.updateScales(); + let ncPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)]; - set sourcesVisible (value) { - this.getSourcesLayer().setVisible(value); - } + this.camera.position.x -= ncPos[0] - cPos[0]; + this.camera.position.z -= ncPos[1] - cPos[1]; - } + this.render(); + this.updateScales(); + }; + $(this.renderArea)[0].addEventListener('mousewheel', onWheel, false); + $(this.renderArea)[0].addEventListener('DOMMouseScroll', onWheel, false); // Firefox - class CSVExporter { - static toString (points) { - let string = ''; + $('#closeProfileContainer').click(() => { + this.hide(); + }); - let attributes = Object.keys(points.data) - .filter(a => a !== 'normal') - .sort((a, b) => { - if (a === 'position') return -1; - if (b === 'position') return 1; - if (a === 'rgba') return -1; - if (b === 'rgba') return 1; - }); + let getProfilePoints = () => { + let points = new Points$1(); + + for(let [pointcloud, entry] of this.pointclouds){ + for(let pointSet of entry.points){ - let headerValues = []; - for (let attribute of attributes) { - let itemSize = points.data[attribute].length / points.numPoints; + let originPos = pointSet.data.position; + let trueElevationPosition = new Float32Array(originPos); + for(let i = 0; i < pointSet.numPoints; i++){ + trueElevationPosition[3 * i + 2] += pointcloud.position.z; + } - if (attribute === 'position') { - headerValues = headerValues.concat(['x', 'y', 'z']); - } else if (attribute === 'rgba') { - headerValues = headerValues.concat(['r', 'g', 'b', 'a']); - } else if (itemSize > 1) { - for (let i = 0; i < itemSize; i++) { - headerValues.push(`${attribute}_${i}`); + pointSet.data.position = trueElevationPosition; + points.add(pointSet); + pointSet.data.position = originPos; } - } else { - headerValues.push(attribute); } - } - string = headerValues.join(', ') + '\n'; - for (let i = 0; i < points.numPoints; i++) { - let values = []; + return points; + }; - for (let attribute of attributes) { - let itemSize = points.data[attribute].length / points.numPoints; - let value = points.data[attribute] - .subarray(itemSize * i, itemSize * i + itemSize) - .join(', '); - values.push(value); - } + $('#potree_download_csv_icon').click(() => { + + let points = getProfilePoints(); - string += values.join(', ') + '\n'; - } + let string = CSVExporter.toString(points); - return string; - } - }; + let blob = new Blob([string], {type: "text/string"}); + $('#potree_download_profile_ortho_link').attr('href', URL.createObjectURL(blob)); + }); - class LASExporter { - static toLAS (points) { - // TODO Unused: let string = ''; + $('#potree_download_las_icon').click(() => { - let boundingBox = points.boundingBox; - let offset = boundingBox.min.clone(); - let diagonal = boundingBox.min.distanceTo(boundingBox.max); - let scale = new Vector3(0.001, 0.001, 0.001); - if (diagonal > 1000 * 1000) { - scale = new Vector3(0.01, 0.01, 0.01); - } else { - scale = new Vector3(0.001, 0.001, 0.001); - } + let points = getProfilePoints(); - let setString = function (string, offset, buffer) { - let view = new Uint8Array(buffer); + let buffer = LASExporter.toLAS(points); - for (let i = 0; i < string.length; i++) { - let charCode = string.charCodeAt(i); - view[offset + i] = charCode; - } - }; + let blob = new Blob([buffer], {type: "application/octet-binary"}); + $('#potree_download_profile_link').attr('href', URL.createObjectURL(blob)); + }); + } - let buffer = new ArrayBuffer(227 + 28 * points.numPoints); - let view = new DataView(buffer); - let u8View = new Uint8Array(buffer); - // let u16View = new Uint16Array(buffer); + selectPoint (mileage, elevation, radius) { + let closest = { + distance: Infinity, + pointcloud: null, + points: null, + index: null + }; - setString('LASF', 0, buffer); - u8View[24] = 1; - u8View[25] = 2; + let pointBox = new Box2( + new Vector2(mileage - radius, elevation - radius), + new Vector2(mileage + radius, elevation + radius)); - // system identifier o:26 l:32 + let numTested = 0; + let numSkipped = 0; + let numTestedPoints = 0; + let numSkippedPoints = 0; - // generating software o:58 l:32 - setString('Potree 1.7', 58, buffer); + for (let [pointcloud, entry] of this.pointclouds) { + for(let points of entry.points){ - // file creation day of year o:90 l:2 - // file creation year o:92 l:2 + let collisionBox = new Box2( + new Vector2(points.projectedBox.min.x, points.projectedBox.min.z), + new Vector2(points.projectedBox.max.x, points.projectedBox.max.z) + ); - // header size o:94 l:2 - view.setUint16(94, 227, true); + let intersects = collisionBox.intersectsBox(pointBox); - // offset to point data o:96 l:4 - view.setUint32(96, 227, true); + if(!intersects){ + numSkipped++; + numSkippedPoints += points.numPoints; + continue; + } - // number of letiable length records o:100 l:4 + numTested++; + numTestedPoints += points.numPoints; - // point data record format 104 1 - u8View[104] = 2; + for (let i = 0; i < points.numPoints; i++) { - // point data record length 105 2 - view.setUint16(105, 28, true); + let m = points.data.mileage[i] - mileage; + let e = points.data.position[3 * i + 2] - elevation + pointcloud.position.z; + let r = Math.sqrt(m * m + e * e); - // number of point records 107 4 - view.setUint32(107, points.numPoints, true); + const withinDistance = r < radius && r < closest.distance; + let unfilteredClass = true; - // number of points by return 111 20 + if(points.data.classification){ + const classification = pointcloud.material.classification; - // x scale factor 131 8 - view.setFloat64(131, scale.x, true); + const pointClassID = points.data.classification[i]; + const pointClassValue = classification[pointClassID]; - // y scale factor 139 8 - view.setFloat64(139, scale.y, true); + if(pointClassValue && (!pointClassValue.visible || pointClassValue.color.w === 0)){ + unfilteredClass = false; + } + } - // z scale factor 147 8 - view.setFloat64(147, scale.z, true); + if (withinDistance && unfilteredClass) { + closest = { + distance: r, + pointcloud: pointcloud, + points: points, + index: i + }; + } + } + } + } - // x offset 155 8 - view.setFloat64(155, offset.x, true); - // y offset 163 8 - view.setFloat64(163, offset.y, true); + //console.log(`nodes: ${numTested}, ${numSkipped} || points: ${numTestedPoints}, ${numSkippedPoints}`); - // z offset 171 8 - view.setFloat64(171, offset.z, true); + if (closest.distance < Infinity) { + let points = closest.points; - // max x 179 8 - view.setFloat64(179, boundingBox.max.x, true); + let point = {}; - // min x 187 8 - view.setFloat64(187, boundingBox.min.x, true); + let attributes = Object.keys(points.data); + for (let attribute of attributes) { + let attributeData = points.data[attribute]; + let itemSize = attributeData.length / points.numPoints; + let value = attributeData.subarray(itemSize * closest.index, itemSize * closest.index + itemSize); - // max y 195 8 - view.setFloat64(195, boundingBox.max.y, true); + if (value.length === 1) { + point[attribute] = value[0]; + } else { + point[attribute] = value; + } + } - // min y 203 8 - view.setFloat64(203, boundingBox.min.y, true); + closest.point = point; - // max z 211 8 - view.setFloat64(211, boundingBox.max.z, true); + return closest; + } else { + return null; + } + } - // min z 219 8 - view.setFloat64(219, boundingBox.min.z, true); + initTHREE () { + this.renderer = new WebGLRenderer({alpha: true, premultipliedAlpha: false}); + this.renderer.setClearColor(0x000000, 0); + this.renderer.setSize(10, 10); + this.renderer.autoClear = false; + this.renderArea.append($(this.renderer.domElement)); + this.renderer.domElement.tabIndex = '2222'; + $(this.renderer.domElement).css('width', '100%'); + $(this.renderer.domElement).css('height', '100%'); - let boffset = 227; - for (let i = 0; i < points.numPoints; i++) { - let px = points.data.position[3 * i + 0]; - let py = points.data.position[3 * i + 1]; - let pz = points.data.position[3 * i + 2]; + { + let gl = this.renderer.getContext(); - let ux = parseInt((px - offset.x) / scale.x); - let uy = parseInt((py - offset.y) / scale.y); - let uz = parseInt((pz - offset.z) / scale.z); + if(gl.createVertexArray == null){ + let extVAO = gl.getExtension('OES_vertex_array_object'); - view.setUint32(boffset + 0, ux, true); - view.setUint32(boffset + 4, uy, true); - view.setUint32(boffset + 8, uz, true); + if(!extVAO){ + throw new Error("OES_vertex_array_object extension not supported"); + } - if (points.data.intensity) { - view.setUint16(boffset + 12, (points.data.intensity[i]), true); + gl.createVertexArray = extVAO.createVertexArrayOES.bind(extVAO); + gl.bindVertexArray = extVAO.bindVertexArrayOES.bind(extVAO); } + + } - let rt = 0; - if (points.data.returnNumber) { - rt += points.data.returnNumber[i]; - } - if (points.data.numberOfReturns) { - rt += (points.data.numberOfReturns[i] << 3); - } - view.setUint8(boffset + 14, rt); + this.camera = new OrthographicCamera(-1000, 1000, 1000, -1000, -1000, 1000); + this.camera.up.set(0, 0, 1); + this.camera.rotation.order = "ZXY"; + this.camera.rotation.x = Math.PI / 2.0; + - if (points.data.classification) { - view.setUint8(boffset + 15, points.data.classification[i]); - } - // scan angle rank - // user data - // point source id - if (points.data.pointSourceID) { - view.setUint16(boffset + 18, points.data.pointSourceID[i]); - } + this.scene = new Scene(); + this.profileScene = new Scene(); - if (points.data.rgba) { - let rgba = points.data.rgba; - view.setUint16(boffset + 20, (rgba[4 * i + 0] * 255), true); - view.setUint16(boffset + 22, (rgba[4 * i + 1] * 255), true); - view.setUint16(boffset + 24, (rgba[4 * i + 2] * 255), true); - } + let sg = new SphereGeometry(1, 16, 16); + let sm = new MeshNormalMaterial(); + this.pickSphere = new Mesh(sg, sm); + this.scene.add(this.pickSphere); - boffset += 28; + { + const sg = new SphereGeometry(2); + const sm = new MeshNormalMaterial(); + const s = new Mesh(sg, sm); + + s.position.set(589530.450, 231398.860, 769.735); + + this.scene.add(s); } - return buffer; + this.viewerPickSphere = new Mesh(sg, sm); } - - } - function copyMaterial(source, target){ + initSVG () { + let width = this.renderArea[0].clientWidth; + let height = this.renderArea[0].clientHeight; + let marginLeft = this.renderArea[0].offsetLeft; - for(let name of Object.keys(target.uniforms)){ - target.uniforms[name].value = source.uniforms[name].value; + this.svg.selectAll('*').remove(); + + this.scaleX = d3.scale.linear() + .domain([this.camera.left + this.camera.position.x, this.camera.right + this.camera.position.x]) + .range([0, width]); + this.scaleY = d3.scale.linear() + .domain([this.camera.bottom + this.camera.position.z, this.camera.top + this.camera.position.z]) + .range([height, 0]); + + this.xAxis = d3.svg.axis() + .scale(this.scaleX) + .orient('bottom') + .innerTickSize(-height) + .outerTickSize(1) + .tickPadding(10) + .ticks(width / 50); + + this.yAxis = d3.svg.axis() + .scale(this.scaleY) + .orient('left') + .innerTickSize(-width) + .outerTickSize(1) + .tickPadding(10) + .ticks(height / 20); + + this.elXAxis = this.svg.append('g') + .attr('class', 'x axis') + .attr('transform', `translate(${marginLeft}, ${height})`) + .call(this.xAxis); + + this.elYAxis = this.svg.append('g') + .attr('class', 'y axis') + .attr('transform', `translate(${marginLeft}, 0)`) + .call(this.yAxis); } - target.gradientTexture = source.gradientTexture; - target.visibleNodesTexture = source.visibleNodesTexture; - target.classificationTexture = source.classificationTexture; - target.matcapTexture = source.matcapTexture; - - target.activeAttributeName = source.activeAttributeName; - target.ranges = source.ranges; + addPoints (pointcloud, points) { - //target.updateShaderSource(); - } + if(points.numPoints === 0){ + return; + } + let entry = this.pointclouds.get(pointcloud); + if(!entry){ + entry = new ProfileFakeOctree(pointcloud); + this.pointclouds.set(pointcloud, entry); + this.profileScene.add(entry); - class Batch{ + let materialChanged = () => { + this.render(); + }; - constructor(geometry, material){ - this.geometry = geometry; - this.material = material; + materialChanged(); - this.sceneNode = new Points(geometry, material); + pointcloud.material.addEventListener('material_property_changed', materialChanged); + this.addEventListener("on_reset_once", () => { + pointcloud.material.removeEventListener('material_property_changed', materialChanged); + }); + } - this.geometryNode = { - estimatedSpacing: 1.0, - geometry: geometry, - }; - } + entry.addPoints(points); + this.projectedBox.union(entry.projectedBox); - getLevel(){ - return 0; - } + if (this.autoFit && this.autoFitEnabled) { + let width = this.renderArea[0].clientWidth; + let height = this.renderArea[0].clientHeight; - } + let size = this.projectedBox.getSize(new Vector3()); - class ProfileFakeOctree extends PointCloudTree{ + let sx = width / size.x; + let sy = height / size.z; + let scale = Math.min(sx, sy); - constructor(octree){ - super(); + let center = this.projectedBox.getCenter(new Vector3()); + this.scale.set(scale, scale, 1); + this.camera.position.copy(center); - this.trueOctree = octree; - this.pcoGeometry = octree.pcoGeometry; - this.points = []; - this.visibleNodes = []; - - //this.material = this.trueOctree.material; - this.material = new PointCloudMaterial$1(); - //this.material.copy(this.trueOctree.material); - copyMaterial(this.trueOctree.material, this.material); - this.material.pointSizeType = PointSizeType.FIXED; + //console.log("camera: ", this.camera.position.toArray().join(", ")); + } - this.batchSize = 100 * 1000; - this.currentBatch = null; - } + //console.log(entry); - getAttribute(name){ - return this.trueOctree.getAttribute(name); - } + this.render(); - dispose(){ - for(let node of this.visibleNodes){ - node.geometry.dispose(); + let numPoints = 0; + for (let [key, value] of this.pointclouds.entries()) { + numPoints += value.points.reduce( (a, i) => a + i.numPoints, 0); } + $(`#profile_num_points`).html(Utils.addCommas(numPoints)); - this.visibleNodes = []; - this.currentBatch = null; - this.points = []; } - addPoints(data){ - // since each call to addPoints can deliver very very few points, - // we're going to batch them into larger buffers for efficiency. - - if(this.currentBatch === null){ - this.currentBatch = this.createNewBatch(data); - } + reset () { + this.lastReset = new Date().getTime(); - this.points.push(data); + this.dispatchEvent({type: "on_reset_once"}); + this.removeEventListeners("on_reset_once"); + this.autoFit = true; + this.projectedBox = new Box3(); - let updateRange = { - start: this.currentBatch.geometry.drawRange.count, - count: 0 - }; - let projectedBox = new Box3(); + for(let [key, entry] of this.pointclouds){ + entry.dispose(); + } - let truePos = new Vector3(); + this.pointclouds.clear(); + this.mouseIsDown = false; + this.mouse.set(0, 0); - for(let i = 0; i < data.numPoints; i++){ + if(this.autoFitEnabled){ + this.scale.set(1, 1, 1); + } + this.pickSphere.visible = false; - if(updateRange.start + updateRange.count >= this.batchSize){ - // current batch full, start new batch + this.elRoot.find('#profileSelectionProperties').hide(); - for(let key of Object.keys(this.currentBatch.geometry.attributes)){ - let attribute = this.currentBatch.geometry.attributes[key]; - attribute.updateRange.offset = updateRange.start; - attribute.updateRange.count = updateRange.count; - attribute.needsUpdate = true; - } + this.render(); + } - this.currentBatch.geometry.computeBoundingBox(); - this.currentBatch.geometry.computeBoundingSphere(); + show () { + this.elRoot.fadeIn(); + this.enabled = true; + } - this.currentBatch = this.createNewBatch(data); - updateRange = { - start: 0, - count: 0 - }; - } + hide () { + this.elRoot.fadeOut(); + this.enabled = false; + } - truePos.set( - data.data.position[3 * i + 0] + this.trueOctree.position.x, - data.data.position[3 * i + 1] + this.trueOctree.position.y, - data.data.position[3 * i + 2] + this.trueOctree.position.z, - ); + updateScales () { - let x = data.data.mileage[i]; - let y = 0; - let z = truePos.z; + let width = this.renderArea[0].clientWidth; + let height = this.renderArea[0].clientHeight; - projectedBox.expandByPoint(new Vector3(x, y, z)); + let left = (-width / 2) / this.scale.x; + let right = (+width / 2) / this.scale.x; + let top = (+height / 2) / this.scale.y; + let bottom = (-height / 2) / this.scale.y; - let index = updateRange.start + updateRange.count; - let geometry = this.currentBatch.geometry; + this.camera.left = left; + this.camera.right = right; + this.camera.top = top; + this.camera.bottom = bottom; + this.camera.updateProjectionMatrix(); - for(let attributeName of Object.keys(data.data)){ - let source = data.data[attributeName]; - let target = geometry.attributes[attributeName]; - let numElements = target.itemSize; - - for(let item = 0; item < numElements; item++){ - target.array[numElements * index + item] = source[numElements * i + item]; - } - } + this.scaleX.domain([this.camera.left + this.camera.position.x, this.camera.right + this.camera.position.x]) + .range([0, width]); + this.scaleY.domain([this.camera.bottom + this.camera.position.z, this.camera.top + this.camera.position.z]) + .range([height, 0]); - { - let position = geometry.attributes.position; + let marginLeft = this.renderArea[0].offsetLeft; - position.array[3 * index + 0] = x; - position.array[3 * index + 1] = y; - position.array[3 * index + 2] = z; - } + this.xAxis.scale(this.scaleX) + .orient('bottom') + .innerTickSize(-height) + .outerTickSize(1) + .tickPadding(10) + .ticks(width / 50); + this.yAxis.scale(this.scaleY) + .orient('left') + .innerTickSize(-width) + .outerTickSize(1) + .tickPadding(10) + .ticks(height / 20); - updateRange.count++; - this.currentBatch.geometry.drawRange.count++; - } - for(let key of Object.keys(this.currentBatch.geometry.attributes)){ - let attribute = this.currentBatch.geometry.attributes[key]; - attribute.updateRange.offset = updateRange.start; - attribute.updateRange.count = updateRange.count; - attribute.needsUpdate = true; - } + this.elXAxis + .attr('transform', `translate(${marginLeft}, ${height})`) + .call(this.xAxis); + this.elYAxis + .attr('transform', `translate(${marginLeft}, 0)`) + .call(this.yAxis); + } - data.projectedBox = projectedBox; + requestScaleUpdate(){ - this.projectedBox = this.points.reduce( (a, i) => a.union(i.projectedBox), new Box3()); - } + let threshold = 100; + let allowUpdate = ((this.lastReset === undefined) || (this.lastScaleUpdate === undefined)) + || ((new Date().getTime() - this.lastReset) > threshold && (new Date().getTime() - this.lastScaleUpdate) > threshold); - createNewBatch(data){ - let geometry = new BufferGeometry(); + if(allowUpdate){ - // create new batches with batch_size elements of the same type as the attribute - for(let attributeName of Object.keys(data.data)){ - let buffer = data.data[attributeName]; - let numElements = buffer.length / data.numPoints; // 3 for pos, 4 for col, 1 for scalars - let constructor = buffer.constructor; - let normalized = false; - - if(this.trueOctree.root.sceneNode){ - if(this.trueOctree.root.sceneNode.geometry.attributes[attributeName]){ - normalized = this.trueOctree.root.sceneNode.geometry.attributes[attributeName].normalized; - } - } - + this.updateScales(); - let batchBuffer = new constructor(numElements * this.batchSize); + this.lastScaleUpdate = new Date().getTime(); - let bufferAttribute = new BufferAttribute(batchBuffer, numElements, normalized); - bufferAttribute.potree = { - range: [0, 1], - }; + - geometry.setAttribute(attributeName, bufferAttribute); + this.scaleUpdatePending = false; + }else if(!this.scaleUpdatePending) { + setTimeout(this.requestScaleUpdate.bind(this), 100); + this.scaleUpdatePending = true; } + + } - geometry.drawRange.start = 0; - geometry.drawRange.count = 0; - - let batch = new Batch(geometry, this.material); + render () { + let width = this.renderArea[0].clientWidth; + let height = this.renderArea[0].clientHeight; - this.visibleNodes.push(batch); + let {renderer, pRenderer, camera, profileScene, scene} = this; + let {scaleX, pickSphere} = this; - return batch; - } - - computeVisibilityTextureData(){ - let data = new Uint8Array(this.visibleNodes.length * 4); - let offsets = new Map(); + renderer.setSize(width, height); - for(let i = 0; i < this.visibleNodes.length; i++){ - let node = this.visibleNodes[i]; + renderer.setClearColor(0x000000, 0); + renderer.clear(true, true, false); - offsets[node] = i; + for(let pointcloud of this.pointclouds.keys()){ + let source = pointcloud.material; + let target = this.pointclouds.get(pointcloud).material; + + copyMaterial(source, target); + target.size = 2; } + + pRenderer.render(profileScene, camera, null); + let radius = Math.abs(scaleX.invert(0) - scaleX.invert(5)); - return { - data: data, - offsets: offsets, - }; - } + if (radius === 0) { + pickSphere.visible = false; + } else { + pickSphere.scale.set(radius, radius, radius); + pickSphere.visible = true; + } + + renderer.render(scene, camera); - } + this.requestScaleUpdate(); + } + }; - class ProfileWindow extends EventDispatcher { + class ProfileWindowController { constructor (viewer) { - super(); - this.viewer = viewer; - this.elRoot = $('#profile_window'); - this.renderArea = this.elRoot.find('#profileCanvasContainer'); - this.svg = d3.select('svg#profileSVG'); - this.mouseIsDown = false; - - this.projectedBox = new Box3(); - this.pointclouds = new Map(); + this.profileWindow = viewer.profileWindow; + this.profile = null; this.numPoints = 0; - this.lastAddPointsTimestamp = undefined; + this.threshold = 60 * 1000; + this.rotateAmount = 10; - this.mouse = new Vector2(0, 0); - this.scale = new Vector3(1, 1, 1); + this.scheduledRecomputeTime = null; - this.autoFitEnabled = true; // completely disable/enable - this.autoFit = false; // internal + this.enabled = true; - let cwIcon = `${exports.resourcePath}/icons/arrow_cw.svg`; - $('#potree_profile_rotate_cw').attr('src', cwIcon); + this.requests = []; - let ccwIcon = `${exports.resourcePath}/icons/arrow_ccw.svg`; - $('#potree_profile_rotate_ccw').attr('src', ccwIcon); - - let forwardIcon = `${exports.resourcePath}/icons/arrow_up.svg`; - $('#potree_profile_move_forward').attr('src', forwardIcon); + this._recompute = () => { this.recompute(); }; - let backwardIcon = `${exports.resourcePath}/icons/arrow_down.svg`; - $('#potree_profile_move_backward').attr('src', backwardIcon); + this.viewer.addEventListener("scene_changed", e => { + e.oldScene.removeEventListener("pointcloud_added", this._recompute); + e.scene.addEventListener("pointcloud_added", this._recompute); + }); + this.viewer.scene.addEventListener("pointcloud_added", this._recompute); - let csvIcon = `${exports.resourcePath}/icons/file_csv_2d.svg`; - $('#potree_download_csv_icon').attr('src', csvIcon); + $("#potree_profile_rotate_amount").val(parseInt(this.rotateAmount)); + $("#potree_profile_rotate_amount").on("input", (e) => { + const str = $("#potree_profile_rotate_amount").val(); - let lasIcon = `${exports.resourcePath}/icons/file_las_3d.svg`; - $('#potree_download_las_icon').attr('src', lasIcon); + if(!isNaN(str)){ + const value = parseFloat(str); + this.rotateAmount = value; + $("#potree_profile_rotate_amount").css("background-color", ""); + }else { + $("#potree_profile_rotate_amount").css("background-color", "#ff9999"); + } - let closeIcon = `${exports.resourcePath}/icons/close.svg`; - $('#closeProfileContainer').attr("src", closeIcon); + }); - this.initTHREE(); - this.initSVG(); - this.initListeners(); + const rotate = (radians) => { + const profile = this.profile; + const points = profile.points; + const start = points[0]; + const end = points[points.length - 1]; + const center = start.clone().add(end).multiplyScalar(0.5); - this.pRenderer = new Renderer(this.renderer); + const mMoveOrigin = new Matrix4().makeTranslation(-center.x, -center.y, -center.z); + const mRotate = new Matrix4().makeRotationZ(radians); + const mMoveBack = new Matrix4().makeTranslation(center.x, center.y, center.z); + //const transform = mMoveOrigin.multiply(mRotate).multiply(mMoveBack); + const transform = mMoveBack.multiply(mRotate).multiply(mMoveOrigin); - this.elRoot.i18n(); - } + const rotatedPoints = points.map( point => point.clone().applyMatrix4(transform) ); - initListeners () { - $(window).resize(() => { - if (this.enabled) { - this.render(); + this.profileWindow.autoFitEnabled = false; + + for(let i = 0; i < points.length; i++){ + profile.setPosition(i, rotatedPoints[i]); } - }); + }; - this.renderArea.mousedown(e => { - this.mouseIsDown = true; + $("#potree_profile_rotate_cw").click( () => { + const radians = MathUtils.degToRad(this.rotateAmount); + rotate(-radians); }); - this.renderArea.mouseup(e => { - this.mouseIsDown = false; + $("#potree_profile_rotate_ccw").click( () => { + const radians = MathUtils.degToRad(this.rotateAmount); + rotate(radians); }); - let viewerPickSphereSizeHandler = () => { - let camera = this.viewer.scene.getActiveCamera(); - let domElement = this.viewer.renderer.domElement; - let distance = this.viewerPickSphere.position.distanceTo(camera.position); - let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight); - let scale = (10 / pr); - this.viewerPickSphere.scale.set(scale, scale, scale); - }; + $("#potree_profile_move_forward").click( () => { + const profile = this.profile; + const points = profile.points; + const start = points[0]; + const end = points[points.length - 1]; - this.renderArea.mousemove(e => { - if (this.pointclouds.size === 0) { - return; + const dir = end.clone().sub(start).normalize(); + const up = new Vector3(0, 0, 1); + const forward = up.cross(dir); + const move = forward.clone().multiplyScalar(profile.width / 2); + + this.profileWindow.autoFitEnabled = false; + + for(let i = 0; i < points.length; i++){ + profile.setPosition(i, points[i].clone().add(move)); } + }); - let rect = this.renderArea[0].getBoundingClientRect(); - let x = e.clientX - rect.left; - let y = e.clientY - rect.top; + $("#potree_profile_move_backward").click( () => { + const profile = this.profile; + const points = profile.points; + const start = points[0]; + const end = points[points.length - 1]; - let newMouse = new Vector2(x, y); + const dir = end.clone().sub(start).normalize(); + const up = new Vector3(0, 0, 1); + const forward = up.cross(dir); + const move = forward.clone().multiplyScalar(-profile.width / 2); - if (this.mouseIsDown) { - // DRAG - this.autoFit = false; - this.lastDrag = new Date().getTime(); + this.profileWindow.autoFitEnabled = false; - let cPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)]; - let ncPos = [this.scaleX.invert(newMouse.x), this.scaleY.invert(newMouse.y)]; + for(let i = 0; i < points.length; i++){ + profile.setPosition(i, points[i].clone().add(move)); + } + }); + } - this.camera.position.x -= ncPos[0] - cPos[0]; - this.camera.position.z -= ncPos[1] - cPos[1]; + setProfile (profile) { + if (this.profile !== null && this.profile !== profile) { + this.profile.removeEventListener('marker_moved', this._recompute); + this.profile.removeEventListener('marker_added', this._recompute); + this.profile.removeEventListener('marker_removed', this._recompute); + this.profile.removeEventListener('width_changed', this._recompute); + } - this.render(); - } else if (this.pointclouds.size > 0) { - // FIND HOVERED POINT - let radius = Math.abs(this.scaleX.invert(0) - this.scaleX.invert(40)); - let mileage = this.scaleX.invert(newMouse.x); - let elevation = this.scaleY.invert(newMouse.y); + this.profile = profile; - let closest = this.selectPoint(mileage, elevation, radius); + { + this.profile.addEventListener('marker_moved', this._recompute); + this.profile.addEventListener('marker_added', this._recompute); + this.profile.addEventListener('marker_removed', this._recompute); + this.profile.addEventListener('width_changed', this._recompute); + } - if (closest) { - let point = closest.point; + this.recompute(); + } - let position = new Float64Array([ - point.position[0] + closest.pointcloud.position.x, - point.position[1] + closest.pointcloud.position.y, - point.position[2] + closest.pointcloud.position.z - ]); + reset () { + this.profileWindow.reset(); - this.elRoot.find('#profileSelectionProperties').fadeIn(200); - this.pickSphere.visible = true; - this.pickSphere.scale.set(0.5 * radius, 0.5 * radius, 0.5 * radius); - this.pickSphere.position.set(point.mileage, 0, position[2]); + this.numPoints = 0; - this.viewerPickSphere.position.set(...position); - - if(!this.viewer.scene.scene.children.includes(this.viewerPickSphere)){ - this.viewer.scene.scene.add(this.viewerPickSphere); - if(!this.viewer.hasEventListener("update", viewerPickSphereSizeHandler)){ - this.viewer.addEventListener("update", viewerPickSphereSizeHandler); - } - } - + if (this.profile) { + for (let request of this.requests) { + request.cancel(); + } + } + } + + progressHandler (pointcloud, progress) { + for (let segment of progress.segments) { + this.profileWindow.addPoints(pointcloud, segment.points); + this.numPoints += segment.points.numPoints; + } + } + + cancel () { + for (let request of this.requests) { + request.cancel(); + // request.finishLevelThenCancel(); + } - let info = this.elRoot.find('#profileSelectionProperties'); - let html = ''; + this.requests = []; + }; - for (let attributeName of Object.keys(point)) { + finishLevelThenCancel(){ + for (let request of this.requests) { + request.finishLevelThenCancel(); + } - let value = point[attributeName]; - let attribute = closest.pointcloud.getAttribute(attributeName); + this.requests = []; + } - let transform = value => value; - if(attribute && attribute.type.size > 4){ - let range = attribute.initialRange; - let scale = 1 / (range[1] - range[0]); - let offset = range[0]; - transform = value => value / scale + offset; - } + recompute () { + if (!this.profile) { + return; + } - + if (this.scheduledRecomputeTime !== null && this.scheduledRecomputeTime > new Date().getTime()) { + return; + } else { + this.scheduledRecomputeTime = new Date().getTime() + 100; + } + this.scheduledRecomputeTime = null; - + this.reset(); - if (attributeName === 'position') { - let values = [...position].map(v => Utils.addCommas(v.toFixed(3))); - html += ` - - - - - - - - - - - - `; - } else if (attributeName === 'rgba') { - html += ` - - - - `; - } else if (attributeName === 'normal') { - continue; - } else if (attributeName === 'mileage') { - html += ` - - - - `; - } else { - html += ` - - - - `; - } + for (let pointcloud of this.viewer.scene.pointclouds.filter(p => p.visible)) { + let request = pointcloud.getPointsInProfile(this.profile, null, { + 'onProgress': (event) => { + if (!this.enabled) { + return; } - html += '
x${values[0]}
y${values[1]}
z${values[2]}
${attributeName}${value.join(', ')}
${attributeName}${value.toFixed(3)}
${attributeName}${transform(value)}
'; - info.html(html); - this.selectedPoint = point; - } else { - // this.pickSphere.visible = false; - // this.selectedPoint = null; + this.progressHandler(pointcloud, event.points); - this.viewer.scene.scene.add(this.viewerPickSphere); + if (this.numPoints > this.threshold) { + this.finishLevelThenCancel(); + } + }, + 'onFinish': (event) => { + if (!this.enabled) { - let index = this.viewer.scene.scene.children.indexOf(this.viewerPickSphere); - if(index >= 0){ - this.viewer.scene.scene.children.splice(index, 1); } - this.viewer.removeEventListener("update", viewerPickSphereSizeHandler); - + }, + 'onCancel': () => { + if (!this.enabled) { + } } - this.render(); - } - - this.mouse.copy(newMouse); - }); - - let onWheel = e => { - this.autoFit = false; - - let delta = 0; - if (e.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9 - delta = e.wheelDelta; - } else if (e.detail !== undefined) { // Firefox - delta = -e.detail; - } - - let ndelta = Math.sign(delta); + }); - let cPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)]; + this.requests.push(request); + } + } + }; - if (ndelta > 0) { - // + 10% - this.scale.multiplyScalar(1.1); - } else { - // - 10% - this.scale.multiplyScalar(100 / 110); - } + /** + * + * @author sigeom sa / http://sigeom.ch + * @author Ioda-Net Sàrl / https://www.ioda-net.ch/ + * @author Markus Schütz / http://potree.org + * + */ - this.updateScales(); - let ncPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)]; + class GeoJSONExporter{ - this.camera.position.x -= ncPos[0] - cPos[0]; - this.camera.position.z -= ncPos[1] - cPos[1]; + static measurementToFeatures (measurement) { + let coords = measurement.points.map(e => e.position.toArray()); - this.render(); - this.updateScales(); - }; - $(this.renderArea)[0].addEventListener('mousewheel', onWheel, false); - $(this.renderArea)[0].addEventListener('DOMMouseScroll', onWheel, false); // Firefox + let features = []; - $('#closeProfileContainer').click(() => { - this.hide(); - }); + if (coords.length === 1) { + let feature = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: coords[0] + }, + properties: { + name: measurement.name + } + }; + features.push(feature); + } else if (coords.length > 1 && !measurement.closed) { + let object = { + 'type': 'Feature', + 'geometry': { + 'type': 'LineString', + 'coordinates': coords + }, + 'properties': { + name: measurement.name + } + }; - let getProfilePoints = () => { - let points = new Points$1(); - - for(let [pointcloud, entry] of this.pointclouds){ - for(let pointSet of entry.points){ + features.push(object); + } else if (coords.length > 1 && measurement.closed) { + let object = { + 'type': 'Feature', + 'geometry': { + 'type': 'Polygon', + 'coordinates': [[...coords, coords[0]]] + }, + 'properties': { + name: measurement.name + } + }; + features.push(object); + } - let originPos = pointSet.data.position; - let trueElevationPosition = new Float32Array(originPos); - for(let i = 0; i < pointSet.numPoints; i++){ - trueElevationPosition[3 * i + 2] += pointcloud.position.z; + if (measurement.showDistances) { + measurement.edgeLabels.forEach((label) => { + let labelPoint = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: label.position.toArray() + }, + properties: { + distance: label.text } + }; + features.push(labelPoint); + }); + } - pointSet.data.position = trueElevationPosition; - points.add(pointSet); - pointSet.data.position = originPos; + if (measurement.showArea) { + let point = measurement.areaLabel.position; + let labelArea = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: point.toArray() + }, + properties: { + area: measurement.areaLabel.text } - } - - return points; - }; + }; + features.push(labelArea); + } - $('#potree_download_csv_icon').click(() => { - - let points = getProfilePoints(); + return features; + } - let string = CSVExporter.toString(points); + static toString (measurements) { + if (!(measurements instanceof Array)) { + measurements = [measurements]; + } - let blob = new Blob([string], {type: "text/string"}); - $('#potree_download_profile_ortho_link').attr('href', URL.createObjectURL(blob)); - }); + measurements = measurements.filter(m => m instanceof Measure); - $('#potree_download_las_icon').click(() => { + let features = []; + for (let measure of measurements) { + let f = GeoJSONExporter.measurementToFeatures(measure); - let points = getProfilePoints(); + features = features.concat(f); + } - let buffer = LASExporter.toLAS(points); + let geojson = { + 'type': 'FeatureCollection', + 'features': features + }; - let blob = new Blob([buffer], {type: "application/octet-binary"}); - $('#potree_download_profile_link').attr('href', URL.createObjectURL(blob)); - }); + return JSON.stringify(geojson, null, '\t'); } - selectPoint (mileage, elevation, radius) { - let closest = { - distance: Infinity, - pointcloud: null, - points: null, - index: null - }; + } - let pointBox = new Box2( - new Vector2(mileage - radius, elevation - radius), - new Vector2(mileage + radius, elevation + radius)); + /** + * + * @author sigeom sa / http://sigeom.ch + * @author Ioda-Net Sàrl / https://www.ioda-net.ch/ + * @author Markus Schuetz / http://potree.org + * + */ - let numTested = 0; - let numSkipped = 0; - let numTestedPoints = 0; - let numSkippedPoints = 0; + class DXFExporter { - for (let [pointcloud, entry] of this.pointclouds) { - for(let points of entry.points){ + static measurementPointSection (measurement) { + let position = measurement.points[0].position; - let collisionBox = new Box2( - new Vector2(points.projectedBox.min.x, points.projectedBox.min.z), - new Vector2(points.projectedBox.max.x, points.projectedBox.max.z) - ); + if (!position) { + return ''; + } - let intersects = collisionBox.intersectsBox(pointBox); + let dxfSection = `0 +CIRCLE +8 +layer_point +10 +${position.x} +20 +${position.y} +30 +${position.z} +40 +1.0 +`; - if(!intersects){ - numSkipped++; - numSkippedPoints += points.numPoints; - continue; - } + return dxfSection; + } - numTested++; - numTestedPoints += points.numPoints; + static measurementPolylineSection (measurement) { + // bit code for polygons/polylines: + // https://www.autodesk.com/techpubs/autocad/acad2000/dxf/polyline_dxf_06.htm + let geomCode = 8; + if (measurement.closed) { + geomCode += 1; + } - for (let i = 0; i < points.numPoints; i++) { + let dxfSection = `0 +POLYLINE +8 +layer_polyline +62 +1 +66 +1 +10 +0.0 +20 +0.0 +30 +0.0 +70 +${geomCode} +`; - let m = points.data.mileage[i] - mileage; - let e = points.data.position[3 * i + 2] - elevation + pointcloud.position.z; - let r = Math.sqrt(m * m + e * e); + let xMax = 0.0; + let yMax = 0.0; + let zMax = 0.0; + for (let point of measurement.points) { + point = point.position; + xMax = Math.max(xMax, point.x); + yMax = Math.max(yMax, point.y); + zMax = Math.max(zMax, point.z); - const withinDistance = r < radius && r < closest.distance; - let unfilteredClass = true; + dxfSection += `0 +VERTEX +8 +0 +10 +${point.x} +20 +${point.y} +30 +${point.z} +70 +32 +`; + } + dxfSection += `0 +SEQEND +`; - if(points.data.classification){ - const classification = pointcloud.material.classification; + return dxfSection; + } - const pointClassID = points.data.classification[i]; - const pointClassValue = classification[pointClassID]; + static measurementSection (measurement) { + // if(measurement.points.length <= 1){ + // return ""; + // } - if(pointClassValue && (!pointClassValue.visible || pointClassValue.color.w === 0)){ - unfilteredClass = false; - } - } + if (measurement.points.length === 0) { + return ''; + } else if (measurement.points.length === 1) { + return DXFExporter.measurementPointSection(measurement); + } else if (measurement.points.length >= 2) { + return DXFExporter.measurementPolylineSection(measurement); + } + } - if (withinDistance && unfilteredClass) { - closest = { - distance: r, - pointcloud: pointcloud, - points: points, - index: i - }; - } - } - } + static toString(measurements){ + if (!(measurements instanceof Array)) { + measurements = [measurements]; } + measurements = measurements.filter(m => m instanceof Measure); + let points = measurements.filter(m => (m instanceof Measure)) + .map(m => m.points) + .reduce((a, v) => a.concat(v)) + .map(p => p.position); - //console.log(`nodes: ${numTested}, ${numSkipped} || points: ${numTestedPoints}, ${numSkippedPoints}`); + let min = new Vector3(Infinity, Infinity, Infinity); + let max = new Vector3(-Infinity, -Infinity, -Infinity); + for (let point of points) { + min.min(point); + max.max(point); + } - if (closest.distance < Infinity) { - let points = closest.points; + let dxfHeader = `999 +DXF created from potree +0 +SECTION +2 +HEADER +9 +$ACADVER +1 +AC1006 +9 +$INSBASE +10 +0.0 +20 +0.0 +30 +0.0 +9 +$EXTMIN +10 +${min.x} +20 +${min.y} +30 +${min.z} +9 +$EXTMAX +10 +${max.x} +20 +${max.y} +30 +${max.z} +0 +ENDSEC +`; - let point = {}; + let dxfBody = `0 +SECTION +2 +ENTITIES +`; - let attributes = Object.keys(points.data); - for (let attribute of attributes) { - let attributeData = points.data[attribute]; - let itemSize = attributeData.length / points.numPoints; - let value = attributeData.subarray(itemSize * closest.index, itemSize * closest.index + itemSize); + for (let measurement of measurements) { + dxfBody += DXFExporter.measurementSection(measurement); + } - if (value.length === 1) { - point[attribute] = value[0]; - } else { - point[attribute] = value; - } - } + dxfBody += `0 +ENDSEC +`; - closest.point = point; + let dxf = dxfHeader + dxfBody + '0\nEOF'; - return closest; - } else { - return null; - } + return dxf; } - initTHREE () { - this.renderer = new WebGLRenderer({alpha: true, premultipliedAlpha: false}); - this.renderer.setClearColor(0x000000, 0); - this.renderer.setSize(10, 10); - this.renderer.autoClear = false; - this.renderArea.append($(this.renderer.domElement)); - this.renderer.domElement.tabIndex = '2222'; - $(this.renderer.domElement).css('width', '100%'); - $(this.renderer.domElement).css('height', '100%'); - - - { - let gl = this.renderer.getContext(); + } - if(gl.createVertexArray == null){ - let extVAO = gl.getExtension('OES_vertex_array_object'); + class MeasurePanel{ - if(!extVAO){ - throw new Error("OES_vertex_array_object extension not supported"); - } + constructor(viewer, measurement, propertiesPanel){ + this.viewer = viewer; + this.measurement = measurement; + this.propertiesPanel = propertiesPanel; - gl.createVertexArray = extVAO.createVertexArrayOES.bind(extVAO); - gl.bindVertexArray = extVAO.bindVertexArrayOES.bind(extVAO); - } - - } + this._update = () => { this.update(); }; + } - this.camera = new OrthographicCamera(-1000, 1000, 1000, -1000, -1000, 1000); - this.camera.up.set(0, 0, 1); - this.camera.rotation.order = "ZXY"; - this.camera.rotation.x = Math.PI / 2.0; - + createCoordinatesTable(points){ + let table = $(` + + + + + + + +
xyz
+ `); - this.scene = new Scene(); - this.profileScene = new Scene(); + let copyIconPath = Potree.resourcePath + '/icons/copy.svg'; - let sg = new SphereGeometry(1, 16, 16); - let sm = new MeshNormalMaterial(); - this.pickSphere = new Mesh(sg, sm); - this.scene.add(this.pickSphere); + for (let point of points) { + let x = Utils.addCommas(point.x.toFixed(3)); + let y = Utils.addCommas(point.y.toFixed(3)); + let z = Utils.addCommas(point.z.toFixed(3)); - { - const sg = new SphereGeometry(2); - const sm = new MeshNormalMaterial(); - const s = new Mesh(sg, sm); + let row = $(` + + ${x} + ${y} + ${z} + + + + + `); - s.position.set(589530.450, 231398.860, 769.735); + this.elCopy = row.find("img[name=copy]"); + this.elCopy.click( () => { + let msg = point.toArray().map(c => c.toFixed(3)).join(", "); + Utils.clipboardCopy(msg); - this.scene.add(s); + this.viewer.postMessage( + `Copied value to clipboard:
'${msg}'`, + {duration: 3000}); + }); + + table.append(row); } - this.viewerPickSphere = new Mesh(sg, sm); - } + return table; + }; - initSVG () { - let width = this.renderArea[0].clientWidth; - let height = this.renderArea[0].clientHeight; - let marginLeft = this.renderArea[0].offsetLeft; + createAttributesTable(){ + let elTable = $('
'); - this.svg.selectAll('*').remove(); + let point = this.measurement.points[0]; + + for(let attributeName of Object.keys(point)){ + if(attributeName === "position"){ + + }else if(attributeName === "rgba"){ + let color = point.rgba; + let text = color.join(', '); - this.scaleX = d3.scale.linear() - .domain([this.camera.left + this.camera.position.x, this.camera.right + this.camera.position.x]) - .range([0, width]); - this.scaleY = d3.scale.linear() - .domain([this.camera.bottom + this.camera.position.z, this.camera.top + this.camera.position.z]) - .range([height, 0]); + elTable.append($(` + + rgb + ${text} + + `)); + }else { + let value = point[attributeName]; + let text = value.join(', '); - this.xAxis = d3.svg.axis() - .scale(this.scaleX) - .orient('bottom') - .innerTickSize(-height) - .outerTickSize(1) - .tickPadding(10) - .ticks(width / 50); + elTable.append($(` + + ${attributeName} + ${text} + + `)); + } + } - this.yAxis = d3.svg.axis() - .scale(this.scaleY) - .orient('left') - .innerTickSize(-width) - .outerTickSize(1) - .tickPadding(10) - .ticks(height / 20); + return elTable; + } - this.elXAxis = this.svg.append('g') - .attr('class', 'x axis') - .attr('transform', `translate(${marginLeft}, ${height})`) - .call(this.xAxis); + update(){ - this.elYAxis = this.svg.append('g') - .attr('class', 'y axis') - .attr('transform', `translate(${marginLeft}, 0)`) - .call(this.yAxis); } + }; - addPoints (pointcloud, points) { + class DistancePanel extends MeasurePanel{ + constructor(viewer, measurement, propertiesPanel){ + super(viewer, measurement, propertiesPanel); - if(points.numPoints === 0){ - return; - } + let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; + this.elContent = $(` +
+ +
+
- let entry = this.pointclouds.get(pointcloud); - if(!entry){ - entry = new ProfileFakeOctree(pointcloud); - this.pointclouds.set(pointcloud, entry); - this.profileScene.add(entry); + +
+ + + + + +
+
+ `); - let materialChanged = () => { - this.render(); - }; + this.elRemove = this.elContent.find("img[name=remove]"); + this.elRemove.click( () => { + this.viewer.scene.removeMeasurement(measurement); + }); + + this.elMakeProfile = this.elContent.find("input[name=make_profile]"); + this.elMakeProfile.click( () => { + //measurement.points; + const profile = new Profile(); - materialChanged(); + profile.name = measurement.name; + profile.width = measurement.getTotalDistance() / 50; - pointcloud.material.addEventListener('material_property_changed', materialChanged); - this.addEventListener("on_reset_once", () => { - pointcloud.material.removeEventListener('material_property_changed', materialChanged); - }); - } + for(const point of measurement.points){ + profile.addMarker(point.position.clone()); + } - entry.addPoints(points); - this.projectedBox.union(entry.projectedBox); + this.viewer.scene.addProfile(profile); - if (this.autoFit && this.autoFitEnabled) { - let width = this.renderArea[0].clientWidth; - let height = this.renderArea[0].clientHeight; + }); - let size = this.projectedBox.getSize(new Vector3()); + this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); - let sx = width / size.x; - let sy = height / size.z; - let scale = Math.min(sx, sy); + this.update(); + } - let center = this.projectedBox.getCenter(new Vector3()); - this.scale.set(scale, scale, 1); - this.camera.position.copy(center); + update(){ + let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); + elCoordiantesContainer.empty(); + elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); - //console.log("camera: ", this.camera.position.toArray().join(", ")); + let positions = this.measurement.points.map(p => p.position); + let distances = []; + for (let i = 0; i < positions.length - 1; i++) { + let d = positions[i].distanceTo(positions[i + 1]); + distances.push(d.toFixed(3)); } - //console.log(entry); - - this.render(); + let totalDistance = this.measurement.getTotalDistance().toFixed(3); + let elDistanceTable = this.elContent.find(`#distances_table`); + elDistanceTable.empty(); - let numPoints = 0; - for (let [key, value] of this.pointclouds.entries()) { - numPoints += value.points.reduce( (a, i) => a + i.numPoints, 0); + for (let i = 0; i < distances.length; i++) { + let label = (i === 0) ? 'Distances: ' : ''; + let distance = distances[i]; + let elDistance = $(` + + ${label} + ${distance} + `); + elDistanceTable.append(elDistance); } - $(`#profile_num_points`).html(Utils.addCommas(numPoints)); + let elTotal = $(` + + Total: ${totalDistance} + `); + elDistanceTable.append(elTotal); } + }; - reset () { - this.lastReset = new Date().getTime(); - - this.dispatchEvent({type: "on_reset_once"}); - this.removeEventListeners("on_reset_once"); - - this.autoFit = true; - this.projectedBox = new Box3(); + class PointPanel extends MeasurePanel{ + constructor(viewer, measurement, propertiesPanel){ + super(viewer, measurement, propertiesPanel); - for(let [key, entry] of this.pointclouds){ - entry.dispose(); - } + let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; + this.elContent = $(` +
+ +
+ - this.pointclouds.clear(); - this.mouseIsDown = false; - this.mouse.set(0, 0); + +
+ + + +
+
+ `); - if(this.autoFitEnabled){ - this.scale.set(1, 1, 1); - } - this.pickSphere.visible = false; + this.elRemove = this.elContent.find("img[name=remove]"); + this.elRemove.click( () => { + this.viewer.scene.removeMeasurement(measurement); + }); - this.elRoot.find('#profileSelectionProperties').hide(); + this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); - this.render(); + this.update(); } - show () { - this.elRoot.fadeIn(); - this.enabled = true; - } + update(){ + let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); + elCoordiantesContainer.empty(); + elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); - hide () { - this.elRoot.fadeOut(); - this.enabled = false; + let elAttributesContainer = this.elContent.find('.attributes_table_container'); + elAttributesContainer.empty(); + elAttributesContainer.append(this.createAttributesTable()); } + }; - updateScales () { - - let width = this.renderArea[0].clientWidth; - let height = this.renderArea[0].clientHeight; + class AreaPanel extends MeasurePanel{ + constructor(viewer, measurement, propertiesPanel){ + super(viewer, measurement, propertiesPanel); - let left = (-width / 2) / this.scale.x; - let right = (+width / 2) / this.scale.x; - let top = (+height / 2) / this.scale.y; - let bottom = (-height / 2) / this.scale.y; + let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; + this.elContent = $(` +
+ +
+ Area: + - this.camera.left = left; - this.camera.right = right; - this.camera.top = top; - this.camera.bottom = bottom; - this.camera.updateProjectionMatrix(); + +
+ + + +
+
+ `); - this.scaleX.domain([this.camera.left + this.camera.position.x, this.camera.right + this.camera.position.x]) - .range([0, width]); - this.scaleY.domain([this.camera.bottom + this.camera.position.z, this.camera.top + this.camera.position.z]) - .range([height, 0]); + this.elRemove = this.elContent.find("img[name=remove]"); + this.elRemove.click( () => { + this.viewer.scene.removeMeasurement(measurement); + }); - let marginLeft = this.renderArea[0].offsetLeft; + this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); - this.xAxis.scale(this.scaleX) - .orient('bottom') - .innerTickSize(-height) - .outerTickSize(1) - .tickPadding(10) - .ticks(width / 50); - this.yAxis.scale(this.scaleY) - .orient('left') - .innerTickSize(-width) - .outerTickSize(1) - .tickPadding(10) - .ticks(height / 20); + this.update(); + } + update(){ + let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); + elCoordiantesContainer.empty(); + elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); - this.elXAxis - .attr('transform', `translate(${marginLeft}, ${height})`) - .call(this.xAxis); - this.elYAxis - .attr('transform', `translate(${marginLeft}, 0)`) - .call(this.yAxis); + let elArea = this.elContent.find(`#measurement_area`); + elArea.html(this.measurement.getArea().toFixed(3)); } + }; - requestScaleUpdate(){ - - let threshold = 100; - let allowUpdate = ((this.lastReset === undefined) || (this.lastScaleUpdate === undefined)) - || ((new Date().getTime() - this.lastReset) > threshold && (new Date().getTime() - this.lastScaleUpdate) > threshold); + class AnglePanel extends MeasurePanel{ + constructor(viewer, measurement, propertiesPanel){ + super(viewer, measurement, propertiesPanel); - if(allowUpdate){ + let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; + this.elContent = $(` +
+ +
+ + + + + + + + + + + +
\u03b1\u03b2\u03b3
- this.updateScales(); + +
+ + + +
+
+ `); - this.lastScaleUpdate = new Date().getTime(); + this.elRemove = this.elContent.find("img[name=remove]"); + this.elRemove.click( () => { + this.viewer.scene.removeMeasurement(measurement); + }); - + this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); - this.scaleUpdatePending = false; - }else if(!this.scaleUpdatePending) { - setTimeout(this.requestScaleUpdate.bind(this), 100); - this.scaleUpdatePending = true; - } - + this.update(); } - render () { - let width = this.renderArea[0].clientWidth; - let height = this.renderArea[0].clientHeight; - - let {renderer, pRenderer, camera, profileScene, scene} = this; - let {scaleX, pickSphere} = this; - - renderer.setSize(width, height); - - renderer.setClearColor(0x000000, 0); - renderer.clear(true, true, false); + update(){ + let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); + elCoordiantesContainer.empty(); + elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); - for(let pointcloud of this.pointclouds.keys()){ - let source = pointcloud.material; - let target = this.pointclouds.get(pointcloud).material; - - copyMaterial(source, target); - target.size = 2; + let angles = []; + for(let i = 0; i < this.measurement.points.length; i++){ + angles.push(this.measurement.getAngle(i) * (180.0 / Math.PI)); } - - pRenderer.render(profileScene, camera, null); - - let radius = Math.abs(scaleX.invert(0) - scaleX.invert(5)); + angles = angles.map(a => a.toFixed(1) + '\u00B0'); - if (radius === 0) { - pickSphere.visible = false; - } else { - pickSphere.scale.set(radius, radius, radius); - pickSphere.visible = true; - } - - renderer.render(scene, camera); + let elAlpha = this.elContent.find(`#angle_cell_alpha`); + let elBetta = this.elContent.find(`#angle_cell_betta`); + let elGamma = this.elContent.find(`#angle_cell_gamma`); - this.requestScaleUpdate(); + elAlpha.html(angles[0]); + elBetta.html(angles[1]); + elGamma.html(angles[2]); } }; - class ProfileWindowController { - constructor (viewer) { - this.viewer = viewer; - this.profileWindow = viewer.profileWindow; - this.profile = null; - this.numPoints = 0; - this.threshold = 60 * 1000; - this.rotateAmount = 10; - - this.scheduledRecomputeTime = null; - - this.enabled = true; + class CirclePanel extends MeasurePanel{ + constructor(viewer, measurement, propertiesPanel){ + super(viewer, measurement, propertiesPanel); - this.requests = []; + let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; + this.elContent = $(` +
+ +
+
- this._recompute = () => { this.recompute(); }; + +
+ + + +
+
+ `); - this.viewer.addEventListener("scene_changed", e => { - e.oldScene.removeEventListener("pointcloud_added", this._recompute); - e.scene.addEventListener("pointcloud_added", this._recompute); + this.elRemove = this.elContent.find("img[name=remove]"); + this.elRemove.click( () => { + this.viewer.scene.removeMeasurement(measurement); }); - this.viewer.scene.addEventListener("pointcloud_added", this._recompute); - $("#potree_profile_rotate_amount").val(parseInt(this.rotateAmount)); - $("#potree_profile_rotate_amount").on("input", (e) => { - const str = $("#potree_profile_rotate_amount").val(); - - if(!isNaN(str)){ - const value = parseFloat(str); - this.rotateAmount = value; - $("#potree_profile_rotate_amount").css("background-color", ""); - }else { - $("#potree_profile_rotate_amount").css("background-color", "#ff9999"); - } + this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); - }); + this.update(); + } - const rotate = (radians) => { - const profile = this.profile; - const points = profile.points; - const start = points[0]; - const end = points[points.length - 1]; - const center = start.clone().add(end).multiplyScalar(0.5); + update(){ + let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); + elCoordiantesContainer.empty(); + elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); - const mMoveOrigin = new Matrix4().makeTranslation(-center.x, -center.y, -center.z); - const mRotate = new Matrix4().makeRotationZ(radians); - const mMoveBack = new Matrix4().makeTranslation(center.x, center.y, center.z); - //const transform = mMoveOrigin.multiply(mRotate).multiply(mMoveBack); - const transform = mMoveBack.multiply(mRotate).multiply(mMoveOrigin); + const elInfos = this.elContent.find(`#infos_table`); - const rotatedPoints = points.map( point => point.clone().applyMatrix4(transform) ); + if(this.measurement.points.length !== 3){ + elInfos.empty(); + + return; + } - this.profileWindow.autoFitEnabled = false; + const A = this.measurement.points[0].position; + const B = this.measurement.points[1].position; + const C = this.measurement.points[2].position; - for(let i = 0; i < points.length; i++){ - profile.setPosition(i, rotatedPoints[i]); - } + const center = Potree.Utils.computeCircleCenter(A, B, C); + const radius = center.distanceTo(A); + const circumference = 2 * Math.PI * radius; + + const format = (number) => { + return Potree.Utils.addCommas(number.toFixed(3)); }; - $("#potree_profile_rotate_cw").click( () => { - const radians = MathUtils.degToRad(this.rotateAmount); - rotate(-radians); - }); + + const txtCenter = `${format(center.x)} ${format(center.y)} ${format(center.z)}`; + const txtRadius = format(radius); + const txtCircumference = format(circumference); - $("#potree_profile_rotate_ccw").click( () => { - const radians = MathUtils.degToRad(this.rotateAmount); - rotate(radians); - }); + const thStyle = `style="text-align: left"`; + const tdStyle = `style="width: 100%; padding: 5px;"`; + + elInfos.html(` + + Center: + + + + + ${txtCenter} + + + + Radius: + ${txtRadius} + + + Circumference: + ${txtCircumference} + + `); + } + }; - $("#potree_profile_move_forward").click( () => { - const profile = this.profile; - const points = profile.points; - const start = points[0]; - const end = points[points.length - 1]; + class HeightPanel extends MeasurePanel{ + constructor(viewer, measurement, propertiesPanel){ + super(viewer, measurement, propertiesPanel); - const dir = end.clone().sub(start).normalize(); - const up = new Vector3(0, 0, 1); - const forward = up.cross(dir); - const move = forward.clone().multiplyScalar(profile.width / 2); + let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; + this.elContent = $(` +
+ +
+ Height:
- this.profileWindow.autoFitEnabled = false; + +
+ + + +
+
+ `); - for(let i = 0; i < points.length; i++){ - profile.setPosition(i, points[i].clone().add(move)); - } + this.elRemove = this.elContent.find("img[name=remove]"); + this.elRemove.click( () => { + this.viewer.scene.removeMeasurement(measurement); }); - $("#potree_profile_move_backward").click( () => { - const profile = this.profile; - const points = profile.points; - const start = points[0]; - const end = points[points.length - 1]; - - const dir = end.clone().sub(start).normalize(); - const up = new Vector3(0, 0, 1); - const forward = up.cross(dir); - const move = forward.clone().multiplyScalar(-profile.width / 2); - - this.profileWindow.autoFitEnabled = false; + this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); - for(let i = 0; i < points.length; i++){ - profile.setPosition(i, points[i].clone().add(move)); - } - }); + this.update(); } - setProfile (profile) { - if (this.profile !== null && this.profile !== profile) { - this.profile.removeEventListener('marker_moved', this._recompute); - this.profile.removeEventListener('marker_added', this._recompute); - this.profile.removeEventListener('marker_removed', this._recompute); - this.profile.removeEventListener('width_changed', this._recompute); - } - - this.profile = profile; + update(){ + let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); + elCoordiantesContainer.empty(); + elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); { - this.profile.addEventListener('marker_moved', this._recompute); - this.profile.addEventListener('marker_added', this._recompute); - this.profile.addEventListener('marker_removed', this._recompute); - this.profile.addEventListener('width_changed', this._recompute); - } - - this.recompute(); - } - - reset () { - this.profileWindow.reset(); + let points = this.measurement.points; - this.numPoints = 0; + let sorted = points.slice().sort((a, b) => a.position.z - b.position.z); + let lowPoint = sorted[0].position.clone(); + let highPoint = sorted[sorted.length - 1].position.clone(); + let min = lowPoint.z; + let max = highPoint.z; + let height = max - min; + height = height.toFixed(3); - if (this.profile) { - for (let request of this.requests) { - request.cancel(); - } + this.elHeightLabel = this.elContent.find(`#height_label`); + this.elHeightLabel.html(`Height: ${height}`); } } + }; - progressHandler (pointcloud, progress) { - for (let segment of progress.segments) { - this.profileWindow.addPoints(pointcloud, segment.points); - this.numPoints += segment.points.numPoints; - } - } + class VolumePanel extends MeasurePanel{ + constructor(viewer, measurement, propertiesPanel){ + super(viewer, measurement, propertiesPanel); - cancel () { - for (let request of this.requests) { - request.cancel(); - // request.finishLevelThenCancel(); - } + let copyIconPath = Potree.resourcePath + '/icons/copy.svg'; + let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; - this.requests = []; - }; + let lblLengthText = new Map([ + [BoxVolume, "length"], + [SphereVolume, "rx"], + ]).get(measurement.constructor); - finishLevelThenCancel(){ - for (let request of this.requests) { - request.finishLevelThenCancel(); - } + let lblWidthText = new Map([ + [BoxVolume, "width"], + [SphereVolume, "ry"], + ]).get(measurement.constructor); - this.requests = []; - } + let lblHeightText = new Map([ + [BoxVolume, "height"], + [SphereVolume, "rz"], + ]).get(measurement.constructor); - recompute () { - if (!this.profile) { - return; - } + this.elContent = $(` +
+ - if (this.scheduledRecomputeTime !== null && this.scheduledRecomputeTime > new Date().getTime()) { - return; - } else { - this.scheduledRecomputeTime = new Date().getTime() + 100; - } - this.scheduledRecomputeTime = null; + + + + + + + + + + + + + +
\u03b1\u03b2\u03b3
+ +
- this.reset(); + + + + + + + + + + + + + +
${lblLengthText}${lblWidthText}${lblHeightText}
+ +
- for (let pointcloud of this.viewer.scene.pointclouds.filter(p => p.visible)) { - let request = pointcloud.getPointsInProfile(this.profile, null, { - 'onProgress': (event) => { - if (!this.enabled) { - return; - } +
+ Volume: + - this.progressHandler(pointcloud, event.points); + - if (this.numPoints > this.threshold) { - this.finishLevelThenCancel(); - } - }, - 'onFinish': (event) => { - if (!this.enabled) { +
  • + +
  • - } - }, - 'onCancel': () => { - if (!this.enabled) { +
  • + +
    +
  • - } - } - }); - this.requests.push(request); - } - } - }; + +
  • + + +
  • +
    + + + +
    +
    + `); - /** - * - * @author sigeom sa / http://sigeom.ch - * @author Ioda-Net Sàrl / https://www.ioda-net.ch/ - * @author Markus Schütz / http://potree.org - * - */ + { // download + this.elDownloadButton = this.elContent.find("input[name=download_volume]"); - class GeoJSONExporter{ + if(this.propertiesPanel.viewer.server){ + this.elDownloadButton.click(() => this.download()); + } else { + this.elDownloadButton.hide(); + } + } - static measurementToFeatures (measurement) { - let coords = measurement.points.map(e => e.position.toArray()); + this.elCopyRotation = this.elContent.find("img[name=copyRotation]"); + this.elCopyRotation.click( () => { + let rotation = this.measurement.rotation.toArray().slice(0, 3); + let msg = rotation.map(c => c.toFixed(3)).join(", "); + Utils.clipboardCopy(msg); - let features = []; + this.viewer.postMessage( + `Copied value to clipboard:
    '${msg}'`, + {duration: 3000}); + }); - if (coords.length === 1) { - let feature = { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: coords[0] - }, - properties: { - name: measurement.name - } - }; - features.push(feature); - } else if (coords.length > 1 && !measurement.closed) { - let object = { - 'type': 'Feature', - 'geometry': { - 'type': 'LineString', - 'coordinates': coords - }, - 'properties': { - name: measurement.name - } - }; + this.elCopyScale = this.elContent.find("img[name=copyScale]"); + this.elCopyScale.click( () => { + let scale = this.measurement.scale.toArray(); + let msg = scale.map(c => c.toFixed(3)).join(", "); + Utils.clipboardCopy(msg); - features.push(object); - } else if (coords.length > 1 && measurement.closed) { - let object = { - 'type': 'Feature', - 'geometry': { - 'type': 'Polygon', - 'coordinates': [[...coords, coords[0]]] - }, - 'properties': { - name: measurement.name - } - }; - features.push(object); - } + this.viewer.postMessage( + `Copied value to clipboard:
    '${msg}'`, + {duration: 3000}); + }); - if (measurement.showDistances) { - measurement.edgeLabels.forEach((label) => { - let labelPoint = { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: label.position.toArray() - }, - properties: { - distance: label.text - } - }; - features.push(labelPoint); - }); - } + this.elRemove = this.elContent.find("img[name=remove]"); + this.elRemove.click( () => { + this.viewer.scene.removeVolume(measurement); + }); - if (measurement.showArea) { - let point = measurement.areaLabel.position; - let labelArea = { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: point.toArray() - }, - properties: { - area: measurement.areaLabel.text - } - }; - features.push(labelArea); - } + this.elContent.find("#volume_reset_orientation").click(() => { + measurement.rotation.set(0, 0, 0); + }); - return features; - } + this.elContent.find("#volume_make_uniform").click(() => { + let mean = (measurement.scale.x + measurement.scale.y + measurement.scale.z) / 3; + measurement.scale.set(mean, mean, mean); + }); - static toString (measurements) { - if (!(measurements instanceof Array)) { - measurements = [measurements]; - } + this.elCheckClip = this.elContent.find('#volume_clip'); + this.elCheckClip.click(event => { + this.measurement.clip = event.target.checked; + }); - measurements = measurements.filter(m => m instanceof Measure); + this.elCheckShow = this.elContent.find('#volume_show'); + this.elCheckShow.click(event => { + this.measurement.visible = event.target.checked; + }); - let features = []; - for (let measure of measurements) { - let f = GeoJSONExporter.measurementToFeatures(measure); + this.propertiesPanel.addVolatileListener(measurement, "position_changed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "orientation_changed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "scale_changed", this._update); + this.propertiesPanel.addVolatileListener(measurement, "clip_changed", this._update); - features = features.concat(f); - } + this.update(); + } - let geojson = { - 'type': 'FeatureCollection', - 'features': features - }; + async download(){ - return JSON.stringify(geojson, null, '\t'); - } + let clipBox = this.measurement; - } + let regions = []; + //for(let clipBox of boxes){ + { + let toClip = clipBox.matrixWorld; - /** - * - * @author sigeom sa / http://sigeom.ch - * @author Ioda-Net Sàrl / https://www.ioda-net.ch/ - * @author Markus Schuetz / http://potree.org - * - */ + let px = new Vector3(+0.5, 0, 0).applyMatrix4(toClip); + let nx = new Vector3(-0.5, 0, 0).applyMatrix4(toClip); + let py = new Vector3(0, +0.5, 0).applyMatrix4(toClip); + let ny = new Vector3(0, -0.5, 0).applyMatrix4(toClip); + let pz = new Vector3(0, 0, +0.5).applyMatrix4(toClip); + let nz = new Vector3(0, 0, -0.5).applyMatrix4(toClip); - class DXFExporter { + let pxN = new Vector3().subVectors(nx, px).normalize(); + let nxN = pxN.clone().multiplyScalar(-1); + let pyN = new Vector3().subVectors(ny, py).normalize(); + let nyN = pyN.clone().multiplyScalar(-1); + let pzN = new Vector3().subVectors(nz, pz).normalize(); + let nzN = pzN.clone().multiplyScalar(-1); - static measurementPointSection (measurement) { - let position = measurement.points[0].position; + let planes = [ + new Plane().setFromNormalAndCoplanarPoint(pxN, px), + new Plane().setFromNormalAndCoplanarPoint(nxN, nx), + new Plane().setFromNormalAndCoplanarPoint(pyN, py), + new Plane().setFromNormalAndCoplanarPoint(nyN, ny), + new Plane().setFromNormalAndCoplanarPoint(pzN, pz), + new Plane().setFromNormalAndCoplanarPoint(nzN, nz), + ]; - if (!position) { - return ''; + let planeQueryParts = []; + for(let plane of planes){ + let part = [plane.normal.toArray(), plane.constant].join(","); + part = `[${part}]`; + planeQueryParts.push(part); + } + let region = "[" + planeQueryParts.join(",") + "]"; + regions.push(region); } - let dxfSection = `0 -CIRCLE -8 -layer_point -10 -${position.x} -20 -${position.y} -30 -${position.z} -40 -1.0 -`; + let regionsArg = regions.join(","); + + let pointcloudArgs = []; + for(let pointcloud of this.viewer.scene.pointclouds){ + if(!pointcloud.visible){ + continue; + } - return dxfSection; - } + let offset = pointcloud.pcoGeometry.offset.clone(); + let negateOffset = new Matrix4().makeTranslation(...offset.multiplyScalar(-1).toArray()); + let matrixWorld = pointcloud.matrixWorld; - static measurementPolylineSection (measurement) { - // bit code for polygons/polylines: - // https://www.autodesk.com/techpubs/autocad/acad2000/dxf/polyline_dxf_06.htm - let geomCode = 8; - if (measurement.closed) { - geomCode += 1; - } + let transform = new Matrix4().multiplyMatrices(matrixWorld, negateOffset); - let dxfSection = `0 -POLYLINE -8 -layer_polyline -62 -1 -66 -1 -10 -0.0 -20 -0.0 -30 -0.0 -70 -${geomCode} -`; + let path = `${window.location.pathname}/../${pointcloud.pcoGeometry.url}`; - let xMax = 0.0; - let yMax = 0.0; - let zMax = 0.0; - for (let point of measurement.points) { - point = point.position; - xMax = Math.max(xMax, point.x); - yMax = Math.max(yMax, point.y); - zMax = Math.max(zMax, point.z); + let arg = { + path: path, + transform: transform.elements, + }; + let argString = JSON.stringify(arg); - dxfSection += `0 -VERTEX -8 -0 -10 -${point.x} -20 -${point.y} -30 -${point.z} -70 -32 -`; + pointcloudArgs.push(argString); } - dxfSection += `0 -SEQEND -`; + let pointcloudsArg = pointcloudArgs.join(","); - return dxfSection; - } + let elMessage = this.elContent.find("div[name=download_message]"); - static measurementSection (measurement) { - // if(measurement.points.length <= 1){ - // return ""; - // } + let error = (message) => { + elMessage.html(`
    ERROR: ${message}
    `); + }; - if (measurement.points.length === 0) { - return ''; - } else if (measurement.points.length === 1) { - return DXFExporter.measurementPointSection(measurement); - } else if (measurement.points.length >= 2) { - return DXFExporter.measurementPolylineSection(measurement); - } - } + let info = (message) => { + elMessage.html(`${message}`); + }; - static toString(measurements){ - if (!(measurements instanceof Array)) { - measurements = [measurements]; - } - measurements = measurements.filter(m => m instanceof Measure); + let handle = null; + { // START FILTER + let url = `${viewer.server}/create_regions_filter?pointclouds=[${pointcloudsArg}]®ions=[${regionsArg}]`; + + //console.log(url); - let points = measurements.filter(m => (m instanceof Measure)) - .map(m => m.points) - .reduce((a, v) => a.concat(v)) - .map(p => p.position); + info("estimating results ..."); - let min = new Vector3(Infinity, Infinity, Infinity); - let max = new Vector3(-Infinity, -Infinity, -Infinity); - for (let point of points) { - min.min(point); - max.max(point); + let response = await fetch(url); + let jsResponse = await response.json(); + //console.log(jsResponse); + + if(!jsResponse.handle){ + error(jsResponse.message); + return; + }else { + handle = jsResponse.handle; + } } - let dxfHeader = `999 -DXF created from potree -0 -SECTION -2 -HEADER -9 -$ACADVER -1 -AC1006 -9 -$INSBASE -10 -0.0 -20 -0.0 -30 -0.0 -9 -$EXTMIN -10 -${min.x} -20 -${min.y} -30 -${min.z} -9 -$EXTMAX -10 -${max.x} -20 -${max.y} -30 -${max.z} -0 -ENDSEC -`; + { // WAIT, CHECK PROGRESS, HANDLE FINISH + let url = `${viewer.server}/check_regions_filter?handle=${handle}`; - let dxfBody = `0 -SECTION -2 -ENTITIES -`; + let sleep = (function(duration){ + return new Promise( (res, rej) => { + setTimeout(() => { + res(); + }, duration); + }); + }); - for (let measurement of measurements) { - dxfBody += DXFExporter.measurementSection(measurement); - } + let handleFiltering = (jsResponse) => { + let {progress, estimate} = jsResponse; - dxfBody += `0 -ENDSEC -`; + let progressFract = progress["processed points"] / estimate.points; + let progressPercents = parseInt(progressFract * 100); - let dxf = dxfHeader + dxfBody + '0\nEOF'; + info(`progress: ${progressPercents}%`); + }; - return dxf; - } + let handleFinish = (jsResponse) => { + let message = "downloads ready:
    "; + message += ""; - this._update = () => { this.update(); }; - } + info(message); + }; - createCoordinatesTable(points){ - let table = $(` - - - - - - - -
    xyz
    - `); + let handleUnexpected = (jsResponse) => { + let message = `Unexpected Response.
    status: ${jsResponse.status}
    message: ${jsResponse.message}`; + info(message); + }; - let copyIconPath = Potree.resourcePath + '/icons/copy.svg'; + let handleError = (jsResponse) => { + let message = `ERROR: ${jsResponse.message}`; + error(message); - for (let point of points) { - let x = Utils.addCommas(point.x.toFixed(3)); - let y = Utils.addCommas(point.y.toFixed(3)); - let z = Utils.addCommas(point.z.toFixed(3)); + throw new Error(message); + }; - let row = $(` - - ${x} - ${y} - ${z} - - - - - `); + let start = Date.now(); - this.elCopy = row.find("img[name=copy]"); - this.elCopy.click( () => { - let msg = point.toArray().map(c => c.toFixed(3)).join(", "); - Utils.clipboardCopy(msg); + while(true){ + let response = await fetch(url); + let jsResponse = await response.json(); + + if(jsResponse.status === "ERROR"){ + handleError(jsResponse); + }else if(jsResponse.status === "FILTERING"){ + handleFiltering(jsResponse); + }else if(jsResponse.status === "FINISHED"){ + handleFinish(jsResponse); + + break; + }else { + handleUnexpected(jsResponse); + } - this.viewer.postMessage( - `Copied value to clipboard:
    '${msg}'`, - {duration: 3000}); - }); + let durationS = (Date.now() - start) / 1000; + let sleepAmountMS = durationS < 10 ? 100 : 1000; - table.append(row); + await sleep(sleepAmountMS); + } } - return table; - }; + } - createAttributesTable(){ - let elTable = $('
    '); + update(){ + let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); + elCoordiantesContainer.empty(); + elCoordiantesContainer.append(this.createCoordinatesTable([this.measurement.position])); - let point = this.measurement.points[0]; - - for(let attributeName of Object.keys(point)){ - if(attributeName === "position"){ - - }else if(attributeName === "rgba"){ - let color = point.rgba; - let text = color.join(', '); + { + let angles = this.measurement.rotation.toVector3(); + angles = angles.toArray(); + //angles = [angles.z, angles.x, angles.y]; + angles = angles.map(v => 180 * v / Math.PI); + angles = angles.map(a => a.toFixed(1) + '\u00B0'); - elTable.append($(` - - rgb - ${text} - - `)); - }else { - let value = point[attributeName]; - let text = value.join(', '); + let elAlpha = this.elContent.find(`#angle_cell_alpha`); + let elBetta = this.elContent.find(`#angle_cell_betta`); + let elGamma = this.elContent.find(`#angle_cell_gamma`); - elTable.append($(` - - ${attributeName} - ${text} - - `)); - } + elAlpha.html(angles[0]); + elBetta.html(angles[1]); + elGamma.html(angles[2]); } - return elTable; - } + { + let dimensions = this.measurement.scale.toArray(); + dimensions = dimensions.map(v => Utils.addCommas(v.toFixed(2))); - update(){ + let elLength = this.elContent.find(`#cell_length`); + let elWidth = this.elContent.find(`#cell_width`); + let elHeight = this.elContent.find(`#cell_height`); + + elLength.html(dimensions[0]); + elWidth.html(dimensions[1]); + elHeight.html(dimensions[2]); + } + + { + let elVolume = this.elContent.find(`#measurement_volume`); + let volume = this.measurement.getVolume(); + elVolume.html(Utils.addCommas(volume.toFixed(2))); + } + + this.elCheckClip.prop("checked", this.measurement.clip); + this.elCheckShow.prop("checked", this.measurement.visible); } }; - class DistancePanel extends MeasurePanel{ + class ProfilePanel extends MeasurePanel{ constructor(viewer, measurement, propertiesPanel){ super(viewer, measurement, propertiesPanel); @@ -74158,13 +74916,24 @@ ENDSEC

    -
    + + Width: + + +
    + +
  • + +
    +
  • + +
    + +
    - - - +
    @@ -74173,23 +74942,64 @@ ENDSEC this.elRemove = this.elContent.find("img[name=remove]"); this.elRemove.click( () => { - this.viewer.scene.removeMeasurement(measurement); + this.viewer.scene.removeProfile(measurement); }); - - this.elMakeProfile = this.elContent.find("input[name=make_profile]"); - this.elMakeProfile.click( () => { - //measurement.points; - const profile = new Profile(); - profile.name = measurement.name; - profile.width = measurement.getTotalDistance() / 50; + { // download + this.elDownloadButton = this.elContent.find(`input[name=download_profile]`); - for(const point of measurement.points){ - profile.addMarker(point.position.clone()); + if(this.propertiesPanel.viewer.server){ + this.elDownloadButton.click(() => this.download()); + } else { + this.elDownloadButton.hide(); } + } - this.viewer.scene.addProfile(profile); + { // width spinner + let elWidthSlider = this.elContent.find(`#sldProfileWidth`); + + elWidthSlider.spinner({ + min: 0, max: 10 * 1000 * 1000, step: 0.01, + numberFormat: 'n', + start: () => {}, + spin: (event, ui) => { + let value = elWidthSlider.spinner('value'); + measurement.setWidth(value); + }, + change: (event, ui) => { + let value = elWidthSlider.spinner('value'); + measurement.setWidth(value); + }, + stop: (event, ui) => { + let value = elWidthSlider.spinner('value'); + measurement.setWidth(value); + }, + incremental: (count) => { + let value = elWidthSlider.spinner('value'); + let step = elWidthSlider.spinner('option', 'step'); + + let delta = value * 0.05; + let increments = Math.max(1, parseInt(delta / step)); + return increments; + } + }); + elWidthSlider.spinner('value', measurement.getWidth()); + elWidthSlider.spinner('widget').css('width', '100%'); + + let widthListener = (event) => { + let value = elWidthSlider.spinner('value'); + if (value !== measurement.getWidth()) { + elWidthSlider.spinner('value', measurement.getWidth()); + } + }; + this.propertiesPanel.addVolatileListener(measurement, "width_changed", widthListener); + } + + let elShow2DProfile = this.elContent.find(`#show_2d_profile`); + elShow2DProfile.click(() => { + this.propertiesPanel.viewer.profileWindow.show(); + this.propertiesPanel.viewer.profileWindowController.setProfile(measurement); }); this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); @@ -74202,3438 +75012,2628 @@ ENDSEC update(){ let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); elCoordiantesContainer.empty(); - elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); + elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points)); + } - let positions = this.measurement.points.map(p => p.position); - let distances = []; - for (let i = 0; i < positions.length - 1; i++) { - let d = positions[i].distanceTo(positions[i + 1]); - distances.push(d.toFixed(3)); + async download(){ + + let profile = this.measurement; + + let regions = []; + { + let segments = profile.getSegments(); + let width = profile.width; + + for(let segment of segments){ + let start = segment.start.clone().multiply(new Vector3(1, 1, 0)); + let end = segment.end.clone().multiply(new Vector3(1, 1, 0)); + let center = new Vector3().addVectors(start, end).multiplyScalar(0.5); + + let startEndDir = new Vector3().subVectors(end, start).normalize(); + let endStartDir = new Vector3().subVectors(start, end).normalize(); + let upDir = new Vector3(0, 0, 1); + let rightDir = new Vector3().crossVectors(startEndDir, upDir); + let leftDir = new Vector3().crossVectors(endStartDir, upDir); + + console.log(leftDir); + + let right = rightDir.clone().multiplyScalar(width * 0.5).add(center); + let left = leftDir.clone().multiplyScalar(width * 0.5).add(center); + + let planes = [ + new Plane().setFromNormalAndCoplanarPoint(startEndDir, start), + new Plane().setFromNormalAndCoplanarPoint(endStartDir, end), + new Plane().setFromNormalAndCoplanarPoint(leftDir, right), + new Plane().setFromNormalAndCoplanarPoint(rightDir, left), + ]; + + let planeQueryParts = []; + for(let plane of planes){ + let part = [plane.normal.toArray(), plane.constant].join(","); + part = `[${part}]`; + planeQueryParts.push(part); + } + let region = "[" + planeQueryParts.join(",") + "]"; + regions.push(region); + } } - let totalDistance = this.measurement.getTotalDistance().toFixed(3); - let elDistanceTable = this.elContent.find(`#distances_table`); - elDistanceTable.empty(); + let regionsArg = regions.join(","); - for (let i = 0; i < distances.length; i++) { - let label = (i === 0) ? 'Distances: ' : ''; - let distance = distances[i]; - let elDistance = $(` - - ${label} - ${distance} - `); - elDistanceTable.append(elDistance); + let pointcloudArgs = []; + for(let pointcloud of this.viewer.scene.pointclouds){ + if(!pointcloud.visible){ + continue; + } + + let offset = pointcloud.pcoGeometry.offset.clone(); + let negateOffset = new Matrix4().makeTranslation(...offset.multiplyScalar(-1).toArray()); + let matrixWorld = pointcloud.matrixWorld; + + let transform = new Matrix4().multiplyMatrices(matrixWorld, negateOffset); + + let path = `${window.location.pathname}/../${pointcloud.pcoGeometry.url}`; + + let arg = { + path: path, + transform: transform.elements, + }; + let argString = JSON.stringify(arg); + + pointcloudArgs.push(argString); } + let pointcloudsArg = pointcloudArgs.join(","); - let elTotal = $(` - - Total: ${totalDistance} - `); - elDistanceTable.append(elTotal); - } - }; + let elMessage = this.elContent.find("div[name=download_message]"); + + let error = (message) => { + elMessage.html(`
    ERROR: ${message}
    `); + }; - class PointPanel extends MeasurePanel{ - constructor(viewer, measurement, propertiesPanel){ - super(viewer, measurement, propertiesPanel); + let info = (message) => { + elMessage.html(`${message}`); + }; - let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; - this.elContent = $(` -
    - -
    - + let handle = null; + { // START FILTER + let url = `${viewer.server}/create_regions_filter?pointclouds=[${pointcloudsArg}]®ions=[${regionsArg}]`; + + //console.log(url); - -
    - - - -
    -
    - `); + info("estimating results ..."); - this.elRemove = this.elContent.find("img[name=remove]"); - this.elRemove.click( () => { - this.viewer.scene.removeMeasurement(measurement); - }); + let response = await fetch(url); + let jsResponse = await response.json(); + //console.log(jsResponse); - this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); + if(!jsResponse.handle){ + error(jsResponse.message); + return; + }else { + handle = jsResponse.handle; + } + } - this.update(); - } + { // WAIT, CHECK PROGRESS, HANDLE FINISH + let url = `${viewer.server}/check_regions_filter?handle=${handle}`; - update(){ - let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); - elCoordiantesContainer.empty(); - elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); + let sleep = (function(duration){ + return new Promise( (res, rej) => { + setTimeout(() => { + res(); + }, duration); + }); + }); - let elAttributesContainer = this.elContent.find('.attributes_table_container'); - elAttributesContainer.empty(); - elAttributesContainer.append(this.createAttributesTable()); - } - }; + let handleFiltering = (jsResponse) => { + let {progress, estimate} = jsResponse; - class AreaPanel extends MeasurePanel{ - constructor(viewer, measurement, propertiesPanel){ - super(viewer, measurement, propertiesPanel); + let progressFract = progress["processed points"] / estimate.points; + let progressPercents = parseInt(progressFract * 100); - let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; - this.elContent = $(` -
    - -
    - Area: - + info(`progress: ${progressPercents}%`); + }; - -
    - - - -
    -
    - `); + let handleFinish = (jsResponse) => { + let message = "downloads ready:
    "; + message += ""; - update(){ - let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); - elCoordiantesContainer.empty(); - elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); + info(message); + }; - let elArea = this.elContent.find(`#measurement_area`); - elArea.html(this.measurement.getArea().toFixed(3)); - } - }; + let handleUnexpected = (jsResponse) => { + let message = `Unexpected Response.
    status: ${jsResponse.status}
    message: ${jsResponse.message}`; + info(message); + }; - class AnglePanel extends MeasurePanel{ - constructor(viewer, measurement, propertiesPanel){ - super(viewer, measurement, propertiesPanel); + let handleError = (jsResponse) => { + let message = `ERROR: ${jsResponse.message}`; + error(message); - let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; - this.elContent = $(` -
    - -
    - - - - - - - - - - - -
    \u03b1\u03b2\u03b3
    + throw new Error(message); + }; - -
    - - - -
    -
    - `); + let start = Date.now(); - this.elRemove = this.elContent.find("img[name=remove]"); - this.elRemove.click( () => { - this.viewer.scene.removeMeasurement(measurement); - }); + while(true){ + let response = await fetch(url); + let jsResponse = await response.json(); - this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); + if(jsResponse.status === "ERROR"){ + handleError(jsResponse); + }else if(jsResponse.status === "FILTERING"){ + handleFiltering(jsResponse); + }else if(jsResponse.status === "FINISHED"){ + handleFinish(jsResponse); - this.update(); - } + break; + }else { + handleUnexpected(jsResponse); + } - update(){ - let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); - elCoordiantesContainer.empty(); - elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); + let durationS = (Date.now() - start) / 1000; + let sleepAmountMS = durationS < 10 ? 100 : 1000; - let angles = []; - for(let i = 0; i < this.measurement.points.length; i++){ - angles.push(this.measurement.getAngle(i) * (180.0 / Math.PI)); + await sleep(sleepAmountMS); + } } - angles = angles.map(a => a.toFixed(1) + '\u00B0'); - - let elAlpha = this.elContent.find(`#angle_cell_alpha`); - let elBetta = this.elContent.find(`#angle_cell_betta`); - let elGamma = this.elContent.find(`#angle_cell_gamma`); - elAlpha.html(angles[0]); - elBetta.html(angles[1]); - elGamma.html(angles[2]); } }; - class CirclePanel extends MeasurePanel{ - constructor(viewer, measurement, propertiesPanel){ - super(viewer, measurement, propertiesPanel); + class CameraPanel{ + constructor(viewer, propertiesPanel){ + this.viewer = viewer; + this.propertiesPanel = propertiesPanel; - let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; - this.elContent = $(` -
    - -
    -
    + this._update = () => { this.update(); }; - -
    - - - -
    -
    + let copyIconPath = Potree.resourcePath + '/icons/copy.svg'; + this.elContent = $(` +
    + + + + + + + + + + + + + + + + + + + + + +
    position
    + +
    target
    + +
    +
    `); - this.elRemove = this.elContent.find("img[name=remove]"); - this.elRemove.click( () => { - this.viewer.scene.removeMeasurement(measurement); + this.elCopyPosition = this.elContent.find("img[name=copyPosition]"); + this.elCopyPosition.click( () => { + let pos = this.viewer.scene.getActiveCamera().position.toArray(); + let msg = pos.map(c => c.toFixed(3)).join(", "); + Utils.clipboardCopy(msg); + + this.viewer.postMessage( + `Copied value to clipboard:
    '${msg}'`, + {duration: 3000}); + }); + + this.elCopyTarget = this.elContent.find("img[name=copyTarget]"); + this.elCopyTarget.click( () => { + let pos = this.viewer.scene.view.getPivot().toArray(); + let msg = pos.map(c => c.toFixed(3)).join(", "); + Utils.clipboardCopy(msg); + + this.viewer.postMessage( + `Copied value to clipboard:
    '${msg}'`, + {duration: 3000}); }); - this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); + this.propertiesPanel.addVolatileListener(viewer, "camera_changed", this._update); this.update(); } update(){ - let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); - elCoordiantesContainer.empty(); - elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); - - const elInfos = this.elContent.find(`#infos_table`); - - if(this.measurement.points.length !== 3){ - elInfos.empty(); - - return; - } - - const A = this.measurement.points[0].position; - const B = this.measurement.points[1].position; - const C = this.measurement.points[2].position; + //console.log("updating camera panel"); - const center = Potree.Utils.computeCircleCenter(A, B, C); - const radius = center.distanceTo(A); - const circumference = 2 * Math.PI * radius; - - const format = (number) => { - return Potree.Utils.addCommas(number.toFixed(3)); - }; + let camera = this.viewer.scene.getActiveCamera(); + let view = this.viewer.scene.view; - - const txtCenter = `${format(center.x)} ${format(center.y)} ${format(center.z)}`; - const txtRadius = format(radius); - const txtCircumference = format(circumference); + let pos = camera.position.toArray().map(c => Utils.addCommas(c.toFixed(3))); + this.elContent.find("#camera_position_x").html(pos[0]); + this.elContent.find("#camera_position_y").html(pos[1]); + this.elContent.find("#camera_position_z").html(pos[2]); - const thStyle = `style="text-align: left"`; - const tdStyle = `style="width: 100%; padding: 5px;"`; - - elInfos.html(` - - Center: - - - - - ${txtCenter} - - - - Radius: - ${txtRadius} - - - Circumference: - ${txtCircumference} - - `); + let target = view.getPivot().toArray().map(c => Utils.addCommas(c.toFixed(3))); + this.elContent.find("#camera_target_x").html(target[0]); + this.elContent.find("#camera_target_y").html(target[1]); + this.elContent.find("#camera_target_z").html(target[2]); } }; - class HeightPanel extends MeasurePanel{ - constructor(viewer, measurement, propertiesPanel){ - super(viewer, measurement, propertiesPanel); + class AnnotationPanel{ + constructor(viewer, propertiesPanel, annotation){ + this.viewer = viewer; + this.propertiesPanel = propertiesPanel; + this.annotation = annotation; - let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; + this._update = () => { this.update(); }; + + let copyIconPath = `${Potree.resourcePath}/icons/copy.svg`; this.elContent = $(` -
    - -
    - Height:
    +
    + + + + + + + + + + + - -
    - - - -
    - - `); +
    position
    + +
    - this.elRemove = this.elContent.find("img[name=remove]"); - this.elRemove.click( () => { - this.viewer.scene.removeMeasurement(measurement); - }); +
    - this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); +
    Title
    +
    + Annotation Title +
    - this.update(); - } +
    Description
    +
    + A longer description of this annotation. + Can be multiple lines long. TODO: the user should be able + to modify title and description. +
    - update(){ - let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); - elCoordiantesContainer.empty(); - elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position))); +
    - { - let points = this.measurement.points; +
    + `); - let sorted = points.slice().sort((a, b) => a.position.z - b.position.z); - let lowPoint = sorted[0].position.clone(); - let highPoint = sorted[sorted.length - 1].position.clone(); - let min = lowPoint.z; - let max = highPoint.z; - let height = max - min; - height = height.toFixed(3); + this.elCopyPosition = this.elContent.find("img[name=copyPosition]"); + this.elCopyPosition.click( () => { + let pos = this.annotation.position.toArray(); + let msg = pos.map(c => c.toFixed(3)).join(", "); + Utils.clipboardCopy(msg); - this.elHeightLabel = this.elContent.find(`#height_label`); - this.elHeightLabel.html(`Height: ${height}`); - } - } - }; + this.viewer.postMessage( + `Copied value to clipboard:
    '${msg}'`, + {duration: 3000}); + }); - class VolumePanel extends MeasurePanel{ - constructor(viewer, measurement, propertiesPanel){ - super(viewer, measurement, propertiesPanel); + this.elTitle = this.elContent.find("#annotation_title").html(annotation.title); + this.elDescription = this.elContent.find("#annotation_description").html(annotation.description); - let copyIconPath = Potree.resourcePath + '/icons/copy.svg'; - let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; + this.elTitle[0].addEventListener("input", () => { + const title = this.elTitle.html(); + annotation.title = title; - let lblLengthText = new Map([ - [BoxVolume, "length"], - [SphereVolume, "rx"], - ]).get(measurement.constructor); + }, false); - let lblWidthText = new Map([ - [BoxVolume, "width"], - [SphereVolume, "ry"], - ]).get(measurement.constructor); + this.elDescription[0].addEventListener("input", () => { + const description = this.elDescription.html(); + annotation.description = description; + }, false); - let lblHeightText = new Map([ - [BoxVolume, "height"], - [SphereVolume, "rz"], - ]).get(measurement.constructor); + this.update(); + } - this.elContent = $(` -
    - + update(){ + const {annotation, elContent, elTitle, elDescription} = this; - - - - - - - - - - - - - -
    \u03b1\u03b2\u03b3
    - -
    + let pos = annotation.position.toArray().map(c => Utils.addCommas(c.toFixed(3))); + elContent.find("#annotation_position_x").html(pos[0]); + elContent.find("#annotation_position_y").html(pos[1]); + elContent.find("#annotation_position_z").html(pos[2]); - - - - - - - - - - - - - -
    ${lblLengthText}${lblWidthText}${lblHeightText}
    - -
    + elTitle.html(annotation.title); + elDescription.html(annotation.description); -
    - Volume: - - + } + }; + + class CameraAnimationPanel{ + constructor(viewer, propertiesPanel, animation){ + this.viewer = viewer; + this.propertiesPanel = propertiesPanel; + this.animation = animation; -
  • - -
  • + this.elContent = $(` +
    + -
  • - -
    -
  • + + + Duration: + + - -
  • - - -
  • -
    - - - -
    + Time:
    + + +
    `); - { // download - this.elDownloadButton = this.elContent.find("input[name=download_volume]"); + const elPlay = this.elContent.find("input[name=play]"); + elPlay.click( () => { + animation.play(); + }); - if(this.propertiesPanel.viewer.server){ - this.elDownloadButton.click(() => this.download()); - } else { - this.elDownloadButton.hide(); + const elSlider = this.elContent.find('#sldTime'); + elSlider.slider({ + value: 0, + min: 0, + max: 1, + step: 0.001, + slide: (event, ui) => { + animation.set(ui.value); } - } + }); - this.elCopyRotation = this.elContent.find("img[name=copyRotation]"); - this.elCopyRotation.click( () => { - let rotation = this.measurement.rotation.toArray().slice(0, 3); - let msg = rotation.map(c => c.toFixed(3)).join(", "); - Utils.clipboardCopy(msg); + let elDuration = this.elContent.find(`input[name=spnDuration]`); + elDuration.spinner({ + min: 0, max: 300, step: 0.01, + numberFormat: 'n', + start: () => {}, + spin: (event, ui) => { + let value = elDuration.spinner('value'); + animation.setDuration(value); + }, + change: (event, ui) => { + let value = elDuration.spinner('value'); + animation.setDuration(value); + }, + stop: (event, ui) => { + let value = elDuration.spinner('value'); + animation.setDuration(value); + }, + incremental: (count) => { + let value = elDuration.spinner('value'); + let step = elDuration.spinner('option', 'step'); - this.viewer.postMessage( - `Copied value to clipboard:
    '${msg}'`, - {duration: 3000}); + let delta = value * 0.05; + let increments = Math.max(1, parseInt(delta / step)); + + return increments; + } }); + elDuration.spinner('value', animation.getDuration()); + elDuration.spinner('widget').css('width', '100%'); - this.elCopyScale = this.elContent.find("img[name=copyScale]"); - this.elCopyScale.click( () => { - let scale = this.measurement.scale.toArray(); - let msg = scale.map(c => c.toFixed(3)).join(", "); - Utils.clipboardCopy(msg); + const elKeyframes = this.elContent.find("#animation_keyframes"); - this.viewer.postMessage( - `Copied value to clipboard:
    '${msg}'`, - {duration: 3000}); - }); + const updateKeyframes = () => { + elKeyframes.empty(); - this.elRemove = this.elContent.find("img[name=remove]"); - this.elRemove.click( () => { - this.viewer.scene.removeVolume(measurement); - }); + //let index = 0; - this.elContent.find("#volume_reset_orientation").click(() => { - measurement.rotation.set(0, 0, 0); - }); + // + // + // - this.elContent.find("#volume_make_uniform").click(() => { - let mean = (measurement.scale.x + measurement.scale.y + measurement.scale.z) / 3; - measurement.scale.set(mean, mean, mean); - }); + const addNewKeyframeItem = (index) => { + let elNewKeyframe = $(` +
    + + + +
    + `); - this.elCheckClip = this.elContent.find('#volume_clip'); - this.elCheckClip.click(event => { - this.measurement.clip = event.target.checked; - }); + const elAdd = elNewKeyframe.find("input[name=add]"); + elAdd.click( () => { + animation.createControlPoint(index); + }); - this.elCheckShow = this.elContent.find('#volume_show'); - this.elCheckShow.click(event => { - this.measurement.visible = event.target.checked; - }); + elKeyframes.append(elNewKeyframe); + }; - this.propertiesPanel.addVolatileListener(measurement, "position_changed", this._update); - this.propertiesPanel.addVolatileListener(measurement, "orientation_changed", this._update); - this.propertiesPanel.addVolatileListener(measurement, "scale_changed", this._update); - this.propertiesPanel.addVolatileListener(measurement, "clip_changed", this._update); + const addKeyframeItem = (index) => { + let elKeyframe = $(` +
    + + + + + + + + keyframe + + + + +
    + `); - this.update(); - } + const elAssign = elKeyframe.find("img[name=assign]"); + const elMove = elKeyframe.find("img[name=move]"); + const elDelete = elKeyframe.find("img[name=delete]"); - async download(){ + elAssign.click( () => { + const cp = animation.controlPoints[index]; - let clipBox = this.measurement; + cp.position.copy(viewer.scene.view.position); + cp.target.copy(viewer.scene.view.getPivot()); + }); - let regions = []; - //for(let clipBox of boxes){ - { - let toClip = clipBox.matrixWorld; + elMove.click( () => { + const cp = animation.controlPoints[index]; - let px = new Vector3(+0.5, 0, 0).applyMatrix4(toClip); - let nx = new Vector3(-0.5, 0, 0).applyMatrix4(toClip); - let py = new Vector3(0, +0.5, 0).applyMatrix4(toClip); - let ny = new Vector3(0, -0.5, 0).applyMatrix4(toClip); - let pz = new Vector3(0, 0, +0.5).applyMatrix4(toClip); - let nz = new Vector3(0, 0, -0.5).applyMatrix4(toClip); + viewer.scene.view.position.copy(cp.position); + viewer.scene.view.lookAt(cp.target); + }); - let pxN = new Vector3().subVectors(nx, px).normalize(); - let nxN = pxN.clone().multiplyScalar(-1); - let pyN = new Vector3().subVectors(ny, py).normalize(); - let nyN = pyN.clone().multiplyScalar(-1); - let pzN = new Vector3().subVectors(nz, pz).normalize(); - let nzN = pzN.clone().multiplyScalar(-1); + elDelete.click( () => { + const cp = animation.controlPoints[index]; + animation.removeControlPoint(cp); + }); - let planes = [ - new Plane().setFromNormalAndCoplanarPoint(pxN, px), - new Plane().setFromNormalAndCoplanarPoint(nxN, nx), - new Plane().setFromNormalAndCoplanarPoint(pyN, py), - new Plane().setFromNormalAndCoplanarPoint(nyN, ny), - new Plane().setFromNormalAndCoplanarPoint(pzN, pz), - new Plane().setFromNormalAndCoplanarPoint(nzN, nz), - ]; + elKeyframes.append(elKeyframe); + }; - let planeQueryParts = []; - for(let plane of planes){ - let part = [plane.normal.toArray(), plane.constant].join(","); - part = `[${part}]`; - planeQueryParts.push(part); - } - let region = "[" + planeQueryParts.join(",") + "]"; - regions.push(region); - } + let index = 0; - let regionsArg = regions.join(","); + addNewKeyframeItem(index); + + for(const cp of animation.controlPoints){ + + addKeyframeItem(index); + index++; + addNewKeyframeItem(index); - let pointcloudArgs = []; - for(let pointcloud of this.viewer.scene.pointclouds){ - if(!pointcloud.visible){ - continue; } + }; - let offset = pointcloud.pcoGeometry.offset.clone(); - let negateOffset = new Matrix4().makeTranslation(...offset.multiplyScalar(-1).toArray()); - let matrixWorld = pointcloud.matrixWorld; + updateKeyframes(); - let transform = new Matrix4().multiplyMatrices(matrixWorld, negateOffset); + animation.addEventListener("controlpoint_added", updateKeyframes); + animation.addEventListener("controlpoint_removed", updateKeyframes); - let path = `${window.location.pathname}/../${pointcloud.pcoGeometry.url}`; - let arg = { - path: path, - transform: transform.elements, - }; - let argString = JSON.stringify(arg); - pointcloudArgs.push(argString); + + // this._update = () => { this.update(); }; + + // this.update(); + } + + update(){ + + } + }; + + class PropertiesPanel{ + + constructor(container, viewer){ + this.container = container; + this.viewer = viewer; + this.object = null; + this.cleanupTasks = []; + this.scene = null; + } + + setScene(scene){ + this.scene = scene; + } + + set(object){ + if(this.object === object){ + return; } - let pointcloudsArg = pointcloudArgs.join(","); - let elMessage = this.elContent.find("div[name=download_message]"); - - let error = (message) => { - elMessage.html(`
    ERROR: ${message}
    `); - }; + this.object = object; + + for(let task of this.cleanupTasks){ + task(); + } + this.cleanupTasks = []; + this.container.empty(); - let info = (message) => { - elMessage.html(`${message}`); - }; + if(object instanceof PointCloudTree){ + this.setPointCloud(object); + }else if(object instanceof Measure || object instanceof Profile || object instanceof Volume){ + this.setMeasurement(object); + }else if(object instanceof Camera){ + this.setCamera(object); + }else if(object instanceof Annotation){ + this.setAnnotation(object); + }else if(object instanceof CameraAnimation){ + this.setCameraAnimation(object); + } + + } - let handle = null; - { // START FILTER - let url = `${viewer.server}/create_regions_filter?pointclouds=[${pointcloudsArg}]®ions=[${regionsArg}]`; - - //console.log(url); + // + // Used for events that should be removed when the property object changes. + // This is for listening to materials, scene, point clouds, etc. + // not required for DOM listeners, since they are automatically cleared by removing the DOM subtree. + // + addVolatileListener(target, type, callback){ + target.addEventListener(type, callback); + this.cleanupTasks.push(() => { + target.removeEventListener(type, callback); + }); + } - info("estimating results ..."); + setPointCloud(pointcloud){ - let response = await fetch(url); - let jsResponse = await response.json(); - //console.log(jsResponse); + let material = pointcloud.material; - if(!jsResponse.handle){ - error(jsResponse.message); - return; - }else { - handle = jsResponse.handle; - } - } + let panel = $(` +
    +
      - { // WAIT, CHECK PROGRESS, HANDLE FINISH - let url = `${viewer.server}/check_regions_filter?handle=${handle}`; +
    • +
      +
    • +
    • +
      +
    • - let sleep = (function(duration){ - return new Promise( (res, rej) => { - setTimeout(() => { - res(); - }, duration); - }); - }); + +
    • + + +
    • - let handleFiltering = (jsResponse) => { - let {progress, estimate} = jsResponse; + +
    • +
      + +
    • - let progressFract = progress["processed points"] / estimate.points; - let progressPercents = parseInt(progressFract * 100); +
    • + +
    • + + +
    • :
    • - info(`progress: ${progressPercents}%`); - }; +
      + Attribute +
      - let handleFinish = (jsResponse) => { - let message = "downloads ready:
      "; - message += "
        "; +
      • + +
      • - for(let i = 0; i < jsResponse.pointclouds.length; i++){ - let url = `${viewer.server}/download_regions_filter_result?handle=${handle}&index=${i}`; +
        +
        + Attribute Weights +
        - message += `
      • result_${i}.las
      • \n`; - } +
      • RGB:
      • +
      • Intensity:
      • +
      • Elevation:
      • +
      • Classification:
      • +
      • Return Number:
      • +
      • Source ID:
      • +
        - let reportURL = `${viewer.server}/download_regions_filter_report?handle=${handle}`; - message += `
      • report.json
      • \n`; - message += "
      "; +
      +
      + RGB +
      - info(message); - }; +
    • Gamma:
    • +
    • Brightness:
    • +
    • Contrast:
    • +
      - let handleUnexpected = (jsResponse) => { - let message = `Unexpected Response.
      status: ${jsResponse.status}
      message: ${jsResponse.message}`; - info(message); - }; +
      +
      + Extra Attribute +
      - let handleError = (jsResponse) => { - let message = `ERROR: ${jsResponse.message}`; - error(message); +
    • :
    • - throw new Error(message); - }; +
    • Gamma:
    • +
    • Brightness:
    • +
    • Contrast:
    • +
      + +
      +
      + MATCAP +
      - let start = Date.now(); +
    • +
      +
    • +
      - while(true){ - let response = await fetch(url); - let jsResponse = await response.json(); +
      +
      + Color +
      - if(jsResponse.status === "ERROR"){ - handleError(jsResponse); - }else if(jsResponse.status === "FILTERING"){ - handleFiltering(jsResponse); - }else if(jsResponse.status === "FINISHED"){ - handleFinish(jsResponse); + +
      - break; - }else { - handleUnexpected(jsResponse); - } - let durationS = (Date.now() - start) / 1000; - let sleepAmountMS = durationS < 10 ? 100 : 1000; +
      +
      + Elevation +
      - await sleep(sleepAmountMS); - } - } +
    • :
    • - } +
    • + + + + + +
    • - update(){ - let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); - elCoordiantesContainer.empty(); - elCoordiantesContainer.append(this.createCoordinatesTable([this.measurement.position])); +
    • + Gradient Scheme: +
      +
      +
    • +
      - { - let angles = this.measurement.rotation.toVector3(); - angles = angles.toArray(); - //angles = [angles.z, angles.x, angles.y]; - angles = angles.map(v => 180 * v / Math.PI); - angles = angles.map(a => a.toFixed(1) + '\u00B0'); +
      +
      + Transition +
      - let elAlpha = this.elContent.find(`#angle_cell_alpha`); - let elBetta = this.elContent.find(`#angle_cell_betta`); - let elGamma = this.elContent.find(`#angle_cell_gamma`); +
    • transition:
    • +
      - elAlpha.html(angles[0]); - elBetta.html(angles[1]); - elGamma.html(angles[2]); - } +
      +
      + Intensity +
      - { - let dimensions = this.measurement.scale.toArray(); - dimensions = dimensions.map(v => Utils.addCommas(v.toFixed(2))); +
    • Range:
    • +
    • Gamma:
    • +
    • Brightness:
    • +
    • Contrast:
    • +
      - let elLength = this.elContent.find(`#cell_length`); - let elWidth = this.elContent.find(`#cell_width`); - let elHeight = this.elContent.find(`#cell_height`); +
      +
      + GPS Time +
      - elLength.html(dimensions[0]); - elWidth.html(dimensions[1]); - elHeight.html(dimensions[2]); - } +
      + +
      +
      + Indices +
      +
      - { - let elVolume = this.elContent.find(`#measurement_volume`); - let volume = this.measurement.getVolume(); - elVolume.html(Utils.addCommas(volume.toFixed(2))); - } - this.elCheckClip.prop("checked", this.measurement.clip); - this.elCheckShow.prop("checked", this.measurement.visible); +
    +
    + `); - } - }; + panel.i18n(); + this.container.append(panel); - class ProfilePanel extends MeasurePanel{ - constructor(viewer, measurement, propertiesPanel){ - super(viewer, measurement, propertiesPanel); + { // POINT SIZE + let sldPointSize = panel.find(`#sldPointSize`); + let lblPointSize = panel.find(`#lblPointSize`); - let removeIconPath = Potree.resourcePath + '/icons/remove.svg'; - this.elContent = $(` -
    - -
    - - Width: - - -
    + sldPointSize.slider({ + value: material.size, + min: 0, + max: 3, + step: 0.01, + slide: function (event, ui) { material.size = ui.value; } + }); -
  • - -
    -
  • + let update = (e) => { + lblPointSize.html(material.size.toFixed(2)); + sldPointSize.slider({value: material.size}); + }; + this.addVolatileListener(material, "point_size_changed", update); + + update(); + } -
    + { // MINIMUM POINT SIZE + let sldMinPointSize = panel.find(`#sldMinPointSize`); + let lblMinPointSize = panel.find(`#lblMinPointSize`); - + sldMinPointSize.slider({ + value: material.size, + min: 0, + max: 3, + step: 0.01, + slide: function (event, ui) { material.minSize = ui.value; } + }); - -
    - - - -
    -
    - `); + let update = (e) => { + lblMinPointSize.html(material.minSize.toFixed(2)); + sldMinPointSize.slider({value: material.minSize}); + }; + this.addVolatileListener(material, "point_size_changed", update); + + update(); + } - this.elRemove = this.elContent.find("img[name=remove]"); - this.elRemove.click( () => { - this.viewer.scene.removeProfile(measurement); - }); + { // POINT SIZING + let strSizeType = Object.keys(PointSizeType)[material.pointSizeType]; - { // download - this.elDownloadButton = this.elContent.find(`input[name=download_profile]`); + let opt = panel.find(`#optPointSizing`); + opt.selectmenu(); + opt.val(strSizeType).selectmenu('refresh'); - if(this.propertiesPanel.viewer.server){ - this.elDownloadButton.click(() => this.download()); - } else { - this.elDownloadButton.hide(); - } + opt.selectmenu({ + change: (event, ui) => { + material.pointSizeType = PointSizeType[ui.item.value]; + } + }); } - { // width spinner - let elWidthSlider = this.elContent.find(`#sldProfileWidth`); + { // SHAPE + let opt = panel.find(`#optShape`); - elWidthSlider.spinner({ - min: 0, max: 10 * 1000 * 1000, step: 0.01, - numberFormat: 'n', - start: () => {}, - spin: (event, ui) => { - let value = elWidthSlider.spinner('value'); - measurement.setWidth(value); - }, + opt.selectmenu({ change: (event, ui) => { - let value = elWidthSlider.spinner('value'); - measurement.setWidth(value); - }, - stop: (event, ui) => { - let value = elWidthSlider.spinner('value'); - measurement.setWidth(value); - }, - incremental: (count) => { - let value = elWidthSlider.spinner('value'); - let step = elWidthSlider.spinner('option', 'step'); - - let delta = value * 0.05; - let increments = Math.max(1, parseInt(delta / step)); + let value = ui.item.value; - return increments; + material.shape = PointShape[value]; } }); - elWidthSlider.spinner('value', measurement.getWidth()); - elWidthSlider.spinner('widget').css('width', '100%'); - let widthListener = (event) => { - let value = elWidthSlider.spinner('value'); - if (value !== measurement.getWidth()) { - elWidthSlider.spinner('value', measurement.getWidth()); - } - }; - this.propertiesPanel.addVolatileListener(measurement, "width_changed", widthListener); - } + let update = () => { + let typename = Object.keys(PointShape)[material.shape]; - let elShow2DProfile = this.elContent.find(`#show_2d_profile`); - elShow2DProfile.click(() => { - this.propertiesPanel.viewer.profileWindow.show(); - this.propertiesPanel.viewer.profileWindowController.setProfile(measurement); - }); + opt.selectmenu().val(typename).selectmenu('refresh'); + }; + this.addVolatileListener(material, "point_shape_changed", update); - this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update); - this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update); + update(); + } - this.update(); - } + { // BACKFACE CULLING + + let opt = panel.find(`#set_backface_culling`); + opt.click(() => { + material.backfaceCulling = opt.prop("checked"); + }); + let update = () => { + let value = material.backfaceCulling; + opt.prop("checked", value); + }; + this.addVolatileListener(material, "backface_changed", update); + update(); - update(){ - let elCoordiantesContainer = this.elContent.find('.coordinates_table_container'); - elCoordiantesContainer.empty(); - elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points)); - } + let blockBackface = $('#materials_backface_container'); + blockBackface.css('display', 'none'); - async download(){ + const pointAttributes = pointcloud.pcoGeometry.pointAttributes; + const hasNormals = pointAttributes.hasNormals ? pointAttributes.hasNormals() : false; + if(hasNormals) { + blockBackface.css('display', 'block'); + } + /* + opt.checkboxradio({ + clicked: (event, ui) => { + // let value = ui.item.value; + let value = ui.item.checked; + console.log(value); + material.backfaceCulling = value; // $('#set_freeze').prop("checked"); + } + }); + */ + } - let profile = this.measurement; + { // OPACITY + let sldOpacity = panel.find(`#sldOpacity`); + let lblOpacity = panel.find(`#lblOpacity`); - let regions = []; - { - let segments = profile.getSegments(); - let width = profile.width; - - for(let segment of segments){ - let start = segment.start.clone().multiply(new Vector3(1, 1, 0)); - let end = segment.end.clone().multiply(new Vector3(1, 1, 0)); - let center = new Vector3().addVectors(start, end).multiplyScalar(0.5); - - let startEndDir = new Vector3().subVectors(end, start).normalize(); - let endStartDir = new Vector3().subVectors(start, end).normalize(); - let upDir = new Vector3(0, 0, 1); - let rightDir = new Vector3().crossVectors(startEndDir, upDir); - let leftDir = new Vector3().crossVectors(endStartDir, upDir); - - console.log(leftDir); - - let right = rightDir.clone().multiplyScalar(width * 0.5).add(center); - let left = leftDir.clone().multiplyScalar(width * 0.5).add(center); - - let planes = [ - new Plane().setFromNormalAndCoplanarPoint(startEndDir, start), - new Plane().setFromNormalAndCoplanarPoint(endStartDir, end), - new Plane().setFromNormalAndCoplanarPoint(leftDir, right), - new Plane().setFromNormalAndCoplanarPoint(rightDir, left), - ]; - - let planeQueryParts = []; - for(let plane of planes){ - let part = [plane.normal.toArray(), plane.constant].join(","); - part = `[${part}]`; - planeQueryParts.push(part); + sldOpacity.slider({ + value: material.opacity, + min: 0, + max: 1, + step: 0.001, + slide: function (event, ui) { + material.opacity = ui.value; } - let region = "[" + planeQueryParts.join(",") + "]"; - regions.push(region); - } + }); + + let update = (e) => { + lblOpacity.html(material.opacity.toFixed(2)); + sldOpacity.slider({value: material.opacity}); + }; + this.addVolatileListener(material, "opacity_changed", update); + + update(); } - let regionsArg = regions.join(","); + { - let pointcloudArgs = []; - for(let pointcloud of this.viewer.scene.pointclouds){ - if(!pointcloud.visible){ - continue; - } + const attributes = pointcloud.pcoGeometry.pointAttributes.attributes; - let offset = pointcloud.pcoGeometry.offset.clone(); - let negateOffset = new Matrix4().makeTranslation(...offset.multiplyScalar(-1).toArray()); - let matrixWorld = pointcloud.matrixWorld; + let options = []; - let transform = new Matrix4().multiplyMatrices(matrixWorld, negateOffset); + options.push(...attributes.map(a => a.name)); - let path = `${window.location.pathname}/../${pointcloud.pcoGeometry.url}`; + const intensityIndex = options.indexOf("intensity"); + if(intensityIndex >= 0){ + options.splice(intensityIndex + 1, 0, "intensity gradient"); + } - let arg = { - path: path, - transform: transform.elements, - }; - let argString = JSON.stringify(arg); + options.push( + "elevation", + "color", + 'matcap', + 'indices', + 'level of detail', + 'composite' + ); - pointcloudArgs.push(argString); - } - let pointcloudsArg = pointcloudArgs.join(","); + const blacklist = [ + "POSITION_CARTESIAN", + "position", + ]; - let elMessage = this.elContent.find("div[name=download_message]"); + options = options.filter(o => !blacklist.includes(o)); - let error = (message) => { - elMessage.html(`
    ERROR: ${message}
    `); - }; + let attributeSelection = panel.find('#optMaterial'); + for(let option of options){ + let elOption = $(``); + attributeSelection.append(elOption); + } - let info = (message) => { - elMessage.html(`${message}`); - }; + let updateMaterialPanel = (event, ui) => { + let selectedValue = attributeSelection.selectmenu().val(); + material.activeAttributeName = selectedValue; - let handle = null; - { // START FILTER - let url = `${viewer.server}/create_regions_filter?pointclouds=[${pointcloudsArg}]®ions=[${regionsArg}]`; - - //console.log(url); + let attribute = pointcloud.getAttribute(selectedValue); - info("estimating results ..."); + if(selectedValue === "intensity gradient"){ + attribute = pointcloud.getAttribute("intensity"); + } - let response = await fetch(url); - let jsResponse = await response.json(); - //console.log(jsResponse); + const isIntensity = attribute ? ["intensity", "intensity gradient"].includes(attribute.name) : false; - if(!jsResponse.handle){ - error(jsResponse.message); - return; - }else { - handle = jsResponse.handle; - } - } + if(isIntensity){ + if(pointcloud.material.intensityRange[0] === Infinity){ + pointcloud.material.intensityRange = attribute.range; + } - { // WAIT, CHECK PROGRESS, HANDLE FINISH - let url = `${viewer.server}/check_regions_filter?handle=${handle}`; + const [min, max] = attribute.range; - let sleep = (function(duration){ - return new Promise( (res, rej) => { - setTimeout(() => { - res(); - }, duration); - }); - }); + panel.find('#sldIntensityRange').slider({ + range: true, + min: min, max: max, step: 0.01, + values: [min, max], + slide: (event, ui) => { + let min = ui.values[0]; + let max = ui.values[1]; + material.intensityRange = [min, max]; + } + }); + } else if(attribute){ + const [min, max] = attribute.range; - let handleFiltering = (jsResponse) => { - let {progress, estimate} = jsResponse; + let selectedRange = material.getRange(attribute.name); - let progressFract = progress["processed points"] / estimate.points; - let progressPercents = parseInt(progressFract * 100); + if(!selectedRange){ + selectedRange = [...attribute.range]; + } - info(`progress: ${progressPercents}%`); - }; + let minMaxAreNumbers = typeof min === "number" && typeof max === "number"; - let handleFinish = (jsResponse) => { - let message = "downloads ready:
    "; - message += "
      "; + if(minMaxAreNumbers){ + panel.find('#sldExtraRange').slider({ + range: true, + min: min, + max: max, + step: 0.01, + values: selectedRange, + slide: (event, ui) => { + let [a, b] = ui.values; - for(let i = 0; i < jsResponse.pointclouds.length; i++){ - let url = `${viewer.server}/download_regions_filter_result?handle=${handle}&index=${i}`; + material.setRange(attribute.name, [a, b]); + } + }); + } - message += `
    • result_${i}.las
    • \n`; } - let reportURL = `${viewer.server}/download_regions_filter_report?handle=${handle}`; - message += `
    • report.json
    • \n`; - message += "
    "; + let blockWeights = $('#materials\\.composite_weight_container'); + let blockElevation = $('#materials\\.elevation_container'); + let blockRGB = $('#materials\\.rgb_container'); + let blockExtra = $('#materials\\.extra_container'); + let blockColor = $('#materials\\.color_container'); + let blockIntensity = $('#materials\\.intensity_container'); + let blockIndex = $('#materials\\.index_container'); + let blockTransition = $('#materials\\.transition_container'); + let blockGps = $('#materials\\.gpstime_container'); + let blockMatcap = $('#materials\\.matcap_container'); - info(message); - }; + blockIndex.css('display', 'none'); + blockIntensity.css('display', 'none'); + blockElevation.css('display', 'none'); + blockRGB.css('display', 'none'); + blockExtra.css('display', 'none'); + blockColor.css('display', 'none'); + blockWeights.css('display', 'none'); + blockTransition.css('display', 'none'); + blockMatcap.css('display', 'none'); + blockGps.css('display', 'none'); - let handleUnexpected = (jsResponse) => { - let message = `Unexpected Response.
    status: ${jsResponse.status}
    message: ${jsResponse.message}`; - info(message); + if (selectedValue === 'composite') { + blockWeights.css('display', 'block'); + blockElevation.css('display', 'block'); + blockRGB.css('display', 'block'); + blockIntensity.css('display', 'block'); + } else if (selectedValue === 'elevation') { + blockElevation.css('display', 'block'); + } else if (selectedValue === 'RGB and Elevation') { + blockRGB.css('display', 'block'); + blockElevation.css('display', 'block'); + } else if (selectedValue === 'rgba') { + blockRGB.css('display', 'block'); + } else if (selectedValue === 'color') { + blockColor.css('display', 'block'); + } else if (selectedValue === 'intensity') { + blockIntensity.css('display', 'block'); + } else if (selectedValue === 'intensity gradient') { + blockIntensity.css('display', 'block'); + } else if (selectedValue === "indices" ){ + blockIndex.css('display', 'block'); + } else if (selectedValue === "matcap" ){ + blockMatcap.css('display', 'block'); + } else if (selectedValue === "classification" ){ + // add classification color selctor? + } else if (selectedValue === "gps-time" ){ + blockGps.css('display', 'block'); + } else if(selectedValue === "number of returns"){ + + } else if(selectedValue === "return number"){ + + } else if(["source id", "point source id"].includes(selectedValue)){ + + } else { + blockExtra.css('display', 'block'); + } }; - let handleError = (jsResponse) => { - let message = `ERROR: ${jsResponse.message}`; - error(message); + attributeSelection.selectmenu({change: updateMaterialPanel}); - throw new Error(message); + let update = () => { + attributeSelection.val(material.activeAttributeName).selectmenu('refresh'); }; + this.addVolatileListener(material, "point_color_type_changed", update); + this.addVolatileListener(material, "active_attribute_changed", update); - let start = Date.now(); + update(); + updateMaterialPanel(); + } - while(true){ - let response = await fetch(url); - let jsResponse = await response.json(); + { + const schemes = Object.keys(Potree.Gradients).map(name => ({name: name, values: Gradients[name]})); + + let elSchemeContainer = panel.find("#elevation_gradient_scheme_selection"); + + for(let scheme of schemes){ + let elScheme = $(` + + + `); + + const svg = Potree.Utils.createSvgGradient(scheme.values); + svg.setAttributeNS(null, "class", `button-icon`); + + elScheme.append($(svg)); + + elScheme.click( () => { + material.gradient = Gradients[scheme.name]; + }); + + elSchemeContainer.append(elScheme); + } + } + + { + let matcaps = [ + {name: "Normals", icon: `${Potree.resourcePath}/icons/matcap/check_normal+y.jpg`}, + {name: "Basic 1", icon: `${Potree.resourcePath}/icons/matcap/basic_1.jpg`}, + {name: "Basic 2", icon: `${Potree.resourcePath}/icons/matcap/basic_2.jpg`}, + {name: "Basic Dark", icon: `${Potree.resourcePath}/icons/matcap/basic_dark.jpg`}, + {name: "Basic Side", icon: `${Potree.resourcePath}/icons/matcap/basic_side.jpg`}, + {name: "Ceramic Dark", icon: `${Potree.resourcePath}/icons/matcap/ceramic_dark.jpg`}, + {name: "Ceramic Lightbulb", icon: `${Potree.resourcePath}/icons/matcap/ceramic_lightbulb.jpg`}, + {name: "Clay Brown", icon: `${Potree.resourcePath}/icons/matcap/clay_brown.jpg`}, + {name: "Clay Muddy", icon: `${Potree.resourcePath}/icons/matcap/clay_muddy.jpg`}, + {name: "Clay Studio", icon: `${Potree.resourcePath}/icons/matcap/clay_studio.jpg`}, + {name: "Resin", icon: `${Potree.resourcePath}/icons/matcap/resin.jpg`}, + {name: "Skin", icon: `${Potree.resourcePath}/icons/matcap/skin.jpg`}, + {name: "Jade", icon: `${Potree.resourcePath}/icons/matcap/jade.jpg`}, + {name: "Metal_ Anisotropic", icon: `${Potree.resourcePath}/icons/matcap/metal_anisotropic.jpg`}, + {name: "Metal Carpaint", icon: `${Potree.resourcePath}/icons/matcap/metal_carpaint.jpg`}, + {name: "Metal Lead", icon: `${Potree.resourcePath}/icons/matcap/metal_lead.jpg`}, + {name: "Metal Shiny", icon: `${Potree.resourcePath}/icons/matcap/metal_shiny.jpg`}, + {name: "Pearl", icon: `${Potree.resourcePath}/icons/matcap/pearl.jpg`}, + {name: "Toon", icon: `${Potree.resourcePath}/icons/matcap/toon.jpg`}, + {name: "Check Rim Light", icon: `${Potree.resourcePath}/icons/matcap/check_rim_light.jpg`}, + {name: "Check Rim Dark", icon: `${Potree.resourcePath}/icons/matcap/check_rim_dark.jpg`}, + {name: "Contours 1", icon: `${Potree.resourcePath}/icons/matcap/contours_1.jpg`}, + {name: "Contours 2", icon: `${Potree.resourcePath}/icons/matcap/contours_2.jpg`}, + {name: "Contours 3", icon: `${Potree.resourcePath}/icons/matcap/contours_3.jpg`}, + {name: "Reflection Check Horizontal", icon: `${Potree.resourcePath}/icons/matcap/reflection_check_horizontal.jpg`}, + {name: "Reflection Check Vertical", icon: `${Potree.resourcePath}/icons/matcap/reflection_check_vertical.jpg`}, + ]; - if(jsResponse.status === "ERROR"){ - handleError(jsResponse); - }else if(jsResponse.status === "FILTERING"){ - handleFiltering(jsResponse); - }else if(jsResponse.status === "FINISHED"){ - handleFinish(jsResponse); + let elMatcapContainer = panel.find("#matcap_scheme_selection"); - break; - }else { - handleUnexpected(jsResponse); - } + for(let matcap of matcaps){ + let elMatcap = $(` + + `); - let durationS = (Date.now() - start) / 1000; - let sleepAmountMS = durationS < 10 ? 100 : 1000; + elMatcap.click( () => { + material.matcap = matcap.icon.substring(matcap.icon.lastIndexOf('/')); + }); - await sleep(sleepAmountMS); + elMatcapContainer.append(elMatcap); } } - } - }; + { + panel.find('#sldRGBGamma').slider({ + value: material.rgbGamma, + min: 0, max: 4, step: 0.01, + slide: (event, ui) => {material.rgbGamma = ui.value;} + }); - class CameraPanel{ - constructor(viewer, propertiesPanel){ - this.viewer = viewer; - this.propertiesPanel = propertiesPanel; + panel.find('#sldRGBContrast').slider({ + value: material.rgbContrast, + min: -1, max: 1, step: 0.01, + slide: (event, ui) => {material.rgbContrast = ui.value;} + }); - this._update = () => { this.update(); }; + panel.find('#sldRGBBrightness').slider({ + value: material.rgbBrightness, + min: -1, max: 1, step: 0.01, + slide: (event, ui) => {material.rgbBrightness = ui.value;} + }); - let copyIconPath = Potree.resourcePath + '/icons/copy.svg'; - this.elContent = $(` -
    - - - - - - - - - - - - - - - - - - - - - -
    position
    - -
    target
    - -
    -
    - `); + panel.find('#sldExtraGamma').slider({ + value: material.extraGamma, + min: 0, max: 4, step: 0.01, + slide: (event, ui) => {material.extraGamma = ui.value;} + }); - this.elCopyPosition = this.elContent.find("img[name=copyPosition]"); - this.elCopyPosition.click( () => { - let pos = this.viewer.scene.getActiveCamera().position.toArray(); - let msg = pos.map(c => c.toFixed(3)).join(", "); - Utils.clipboardCopy(msg); + panel.find('#sldExtraBrightness').slider({ + value: material.extraBrightness, + min: -1, max: 1, step: 0.01, + slide: (event, ui) => {material.extraBrightness = ui.value;} + }); - this.viewer.postMessage( - `Copied value to clipboard:
    '${msg}'`, - {duration: 3000}); - }); + panel.find('#sldExtraContrast').slider({ + value: material.extraContrast, + min: -1, max: 1, step: 0.01, + slide: (event, ui) => {material.extraContrast = ui.value;} + }); - this.elCopyTarget = this.elContent.find("img[name=copyTarget]"); - this.elCopyTarget.click( () => { - let pos = this.viewer.scene.view.getPivot().toArray(); - let msg = pos.map(c => c.toFixed(3)).join(", "); - Utils.clipboardCopy(msg); + panel.find('#sldHeightRange').slider({ + range: true, + min: 0, max: 1000, step: 0.01, + values: [0, 1000], + slide: (event, ui) => { + material.heightMin = ui.values[0]; + material.heightMax = ui.values[1]; + } + }); - this.viewer.postMessage( - `Copied value to clipboard:
    '${msg}'`, - {duration: 3000}); - }); + panel.find('#sldIntensityGamma').slider({ + value: material.intensityGamma, + min: 0, max: 4, step: 0.01, + slide: (event, ui) => {material.intensityGamma = ui.value;} + }); - this.propertiesPanel.addVolatileListener(viewer, "camera_changed", this._update); + panel.find('#sldIntensityContrast').slider({ + value: material.intensityContrast, + min: -1, max: 1, step: 0.01, + slide: (event, ui) => {material.intensityContrast = ui.value;} + }); - this.update(); - } + panel.find('#sldIntensityBrightness').slider({ + value: material.intensityBrightness, + min: -1, max: 1, step: 0.01, + slide: (event, ui) => {material.intensityBrightness = ui.value;} + }); - update(){ - //console.log("updating camera panel"); + panel.find('#sldWeightRGB').slider({ + value: material.weightRGB, + min: 0, max: 1, step: 0.01, + slide: (event, ui) => {material.weightRGB = ui.value;} + }); - let camera = this.viewer.scene.getActiveCamera(); - let view = this.viewer.scene.view; + panel.find('#sldWeightIntensity').slider({ + value: material.weightIntensity, + min: 0, max: 1, step: 0.01, + slide: (event, ui) => {material.weightIntensity = ui.value;} + }); - let pos = camera.position.toArray().map(c => Utils.addCommas(c.toFixed(3))); - this.elContent.find("#camera_position_x").html(pos[0]); - this.elContent.find("#camera_position_y").html(pos[1]); - this.elContent.find("#camera_position_z").html(pos[2]); + panel.find('#sldWeightElevation').slider({ + value: material.weightElevation, + min: 0, max: 1, step: 0.01, + slide: (event, ui) => {material.weightElevation = ui.value;} + }); - let target = view.getPivot().toArray().map(c => Utils.addCommas(c.toFixed(3))); - this.elContent.find("#camera_target_x").html(target[0]); - this.elContent.find("#camera_target_y").html(target[1]); - this.elContent.find("#camera_target_z").html(target[2]); - } - }; + panel.find('#sldWeightClassification').slider({ + value: material.weightClassification, + min: 0, max: 1, step: 0.01, + slide: (event, ui) => {material.weightClassification = ui.value;} + }); - class AnnotationPanel{ - constructor(viewer, propertiesPanel, annotation){ - this.viewer = viewer; - this.propertiesPanel = propertiesPanel; - this.annotation = annotation; + panel.find('#sldWeightReturnNumber').slider({ + value: material.weightReturnNumber, + min: 0, max: 1, step: 0.01, + slide: (event, ui) => {material.weightReturnNumber = ui.value;} + }); - this._update = () => { this.update(); }; + panel.find('#sldWeightSourceID').slider({ + value: material.weightSourceID, + min: 0, max: 1, step: 0.01, + slide: (event, ui) => {material.weightSourceID = ui.value;} + }); - let copyIconPath = `${Potree.resourcePath}/icons/copy.svg`; - this.elContent = $(` -
    - - - - - - - - - - - + panel.find(`#materials\\.color\\.picker`).spectrum({ + flat: true, + showInput: true, + preferredFormat: 'rgb', + cancelText: '', + chooseText: 'Apply', + color: `#${material.color.getHexString()}`, + move: color => { + let cRGB = color.toRgb(); + let tc = new Color().setRGB(cRGB.r / 255, cRGB.g / 255, cRGB.b / 255); + material.color = tc; + }, + change: color => { + let cRGB = color.toRgb(); + let tc = new Color().setRGB(cRGB.r / 255, cRGB.g / 255, cRGB.b / 255); + material.color = tc; + } + }); -
    position
    - -
    + this.addVolatileListener(material, "color_changed", () => { + panel.find(`#materials\\.color\\.picker`) + .spectrum('set', `#${material.color.getHexString()}`); + }); -
    + let updateHeightRange = function () { + -
    Title
    -
    - Annotation Title -
    + let aPosition = pointcloud.getAttribute("position"); -
    Description
    -
    - A longer description of this annotation. - Can be multiple lines long. TODO: the user should be able - to modify title and description. -
    + let bMin, bMax; -
    + if(aPosition){ + // for new format 2.0 and loader that contain precomputed min/max of attributes + let min = aPosition.range[0][2]; + let max = aPosition.range[1][2]; + let width = max - min; -
    - `); + bMin = min - 0.2 * width; + bMax = max + 0.2 * width; + }else { + // for format up until exlusive 2.0 + let box = [pointcloud.pcoGeometry.tightBoundingBox, pointcloud.getBoundingBoxWorld()] + .find(v => v !== undefined); - this.elCopyPosition = this.elContent.find("img[name=copyPosition]"); - this.elCopyPosition.click( () => { - let pos = this.annotation.position.toArray(); - let msg = pos.map(c => c.toFixed(3)).join(", "); - Utils.clipboardCopy(msg); + pointcloud.updateMatrixWorld(true); + box = Utils.computeTransformedBoundingBox(box, pointcloud.matrixWorld); - this.viewer.postMessage( - `Copied value to clipboard:
    '${msg}'`, - {duration: 3000}); - }); + let bWidth = box.max.z - box.min.z; + bMin = box.min.z - 0.2 * bWidth; + bMax = box.max.z + 0.2 * bWidth; + } - this.elTitle = this.elContent.find("#annotation_title").html(annotation.title); - this.elDescription = this.elContent.find("#annotation_description").html(annotation.description); + let range = material.elevationRange; - this.elTitle[0].addEventListener("input", () => { - const title = this.elTitle.html(); - annotation.title = title; + panel.find('#lblHeightRange').html(`${range[0].toFixed(2)} to ${range[1].toFixed(2)}`); + panel.find('#sldHeightRange').slider({min: bMin, max: bMax, values: range}); + }; - }, false); + let updateExtraRange = function () { - this.elDescription[0].addEventListener("input", () => { - const description = this.elDescription.html(); - annotation.description = description; - }, false); + let attributeName = material.activeAttributeName; + let attribute = pointcloud.getAttribute(attributeName); - this.update(); - } + if(attribute == null){ + return; + } + + let range = material.getRange(attributeName); - update(){ - const {annotation, elContent, elTitle, elDescription} = this; + if(range == null){ + range = attribute.range; + } - let pos = annotation.position.toArray().map(c => Utils.addCommas(c.toFixed(3))); - elContent.find("#annotation_position_x").html(pos[0]); - elContent.find("#annotation_position_y").html(pos[1]); - elContent.find("#annotation_position_z").html(pos[2]); + // currently only supporting scalar ranges. + // rgba, normals, positions, etc have vector ranges, however + let isValidRange = (typeof range[0] === "number") && (typeof range[1] === "number"); + if(!isValidRange){ + return; + } - elTitle.html(annotation.title); - elDescription.html(annotation.description); + if(range){ + let msg = `${range[0].toFixed(2)} to ${range[1].toFixed(2)}`; + panel.find('#lblExtraRange').html(msg); + }else { + panel.find("could not deduce range"); + } + }; + let updateIntensityRange = function () { + let range = material.intensityRange; - } - }; + panel.find('#lblIntensityRange').html(`${parseInt(range[0])} to ${parseInt(range[1])}`); + }; - class CameraAnimationPanel{ - constructor(viewer, propertiesPanel, animation){ - this.viewer = viewer; - this.propertiesPanel = propertiesPanel; - this.animation = animation; + { + updateHeightRange(); + panel.find(`#sldHeightRange`).slider('option', 'min'); + panel.find(`#sldHeightRange`).slider('option', 'max'); + } - this.elContent = $(` -
    - + { + let elGradientRepeat = panel.find("#gradient_repeat_option"); + elGradientRepeat.selectgroup({title: "Gradient"}); - + elGradientRepeat.find("input").click( (e) => { + this.viewer.setElevationGradientRepeat(ElevationGradientRepeat[e.target.value]); + }); - - Duration: - - + let current = Object.keys(ElevationGradientRepeat) + .filter(key => ElevationGradientRepeat[key] === this.viewer.elevationGradientRepeat); + elGradientRepeat.find(`input[value=${current}]`).trigger("click"); + } - Time:
    + let onIntensityChange = () => { + let gamma = material.intensityGamma; + let contrast = material.intensityContrast; + let brightness = material.intensityBrightness; - -
    -
    - `); + updateIntensityRange(); - const elPlay = this.elContent.find("input[name=play]"); - elPlay.click( () => { - animation.play(); - }); + panel.find('#lblIntensityGamma').html(gamma.toFixed(2)); + panel.find('#lblIntensityContrast').html(contrast.toFixed(2)); + panel.find('#lblIntensityBrightness').html(brightness.toFixed(2)); - const elSlider = this.elContent.find('#sldTime'); - elSlider.slider({ - value: 0, - min: 0, - max: 1, - step: 0.001, - slide: (event, ui) => { - animation.set(ui.value); - } - }); + panel.find('#sldIntensityGamma').slider({value: gamma}); + panel.find('#sldIntensityContrast').slider({value: contrast}); + panel.find('#sldIntensityBrightness').slider({value: brightness}); + }; - let elDuration = this.elContent.find(`input[name=spnDuration]`); - elDuration.spinner({ - min: 0, max: 300, step: 0.01, - numberFormat: 'n', - start: () => {}, - spin: (event, ui) => { - let value = elDuration.spinner('value'); - animation.setDuration(value); - }, - change: (event, ui) => { - let value = elDuration.spinner('value'); - animation.setDuration(value); - }, - stop: (event, ui) => { - let value = elDuration.spinner('value'); - animation.setDuration(value); - }, - incremental: (count) => { - let value = elDuration.spinner('value'); - let step = elDuration.spinner('option', 'step'); + let onRGBChange = () => { + let gamma = material.rgbGamma; + let contrast = material.rgbContrast; + let brightness = material.rgbBrightness; - let delta = value * 0.05; - let increments = Math.max(1, parseInt(delta / step)); + panel.find('#lblRGBGamma').html(gamma.toFixed(2)); + panel.find('#lblRGBContrast').html(contrast.toFixed(2)); + panel.find('#lblRGBBrightness').html(brightness.toFixed(2)); - return increments; - } - }); - elDuration.spinner('value', animation.getDuration()); - elDuration.spinner('widget').css('width', '100%'); + panel.find('#sldRGBGamma').slider({value: gamma}); + panel.find('#sldRGBContrast').slider({value: contrast}); + panel.find('#sldRGBBrightness').slider({value: brightness}); + }; - const elKeyframes = this.elContent.find("#animation_keyframes"); + this.addVolatileListener(material, "material_property_changed", updateExtraRange); + this.addVolatileListener(material, "material_property_changed", updateHeightRange); + this.addVolatileListener(material, "material_property_changed", onIntensityChange); + this.addVolatileListener(material, "material_property_changed", onRGBChange); - const updateKeyframes = () => { - elKeyframes.empty(); + updateExtraRange(); + updateHeightRange(); + onIntensityChange(); + onRGBChange(); + } - //let index = 0; + } - // - // - // + - const addNewKeyframeItem = (index) => { - let elNewKeyframe = $(` -
    - - - -
    - `); + setMeasurement(object){ - const elAdd = elNewKeyframe.find("input[name=add]"); - elAdd.click( () => { - animation.createControlPoint(index); - }); + let TYPE = { + DISTANCE: {panel: DistancePanel}, + AREA: {panel: AreaPanel}, + POINT: {panel: PointPanel}, + ANGLE: {panel: AnglePanel}, + HEIGHT: {panel: HeightPanel}, + PROFILE: {panel: ProfilePanel}, + VOLUME: {panel: VolumePanel}, + CIRCLE: {panel: CirclePanel}, + OTHER: {panel: PointPanel}, + }; - elKeyframes.append(elNewKeyframe); - }; + let getType = (measurement) => { + if (measurement instanceof Measure) { + if (measurement.showDistances && !measurement.showArea && !measurement.showAngles) { + return TYPE.DISTANCE; + } else if (measurement.showDistances && measurement.showArea && !measurement.showAngles) { + return TYPE.AREA; + } else if (measurement.maxMarkers === 1) { + return TYPE.POINT; + } else if (!measurement.showDistances && !measurement.showArea && measurement.showAngles) { + return TYPE.ANGLE; + } else if (measurement.showHeight) { + return TYPE.HEIGHT; + } else if (measurement.showCircle) { + return TYPE.CIRCLE; + } else { + return TYPE.OTHER; + } + } else if (measurement instanceof Profile) { + return TYPE.PROFILE; + } else if (measurement instanceof Volume) { + return TYPE.VOLUME; + } + }; + + //this.container.html("measurement"); - const addKeyframeItem = (index) => { - let elKeyframe = $(` -
    - - - - - - - - keyframe - - - - -
    - `); + let type = getType(object); + let Panel = type.panel; - const elAssign = elKeyframe.find("img[name=assign]"); - const elMove = elKeyframe.find("img[name=move]"); - const elDelete = elKeyframe.find("img[name=delete]"); + let panel = new Panel(this.viewer, object, this); + this.container.append(panel.elContent); + } - elAssign.click( () => { - const cp = animation.controlPoints[index]; + setCamera(camera){ + let panel = new CameraPanel(this.viewer, this); + this.container.append(panel.elContent); + } - cp.position.copy(viewer.scene.view.position); - cp.target.copy(viewer.scene.view.getPivot()); - }); + setAnnotation(annotation){ + let panel = new AnnotationPanel(this.viewer, this, annotation); + this.container.append(panel.elContent); + } - elMove.click( () => { - const cp = animation.controlPoints[index]; + setCameraAnimation(animation){ + let panel = new CameraAnimationPanel(this.viewer, this, animation); + this.container.append(panel.elContent); + } - viewer.scene.view.position.copy(cp.position); - viewer.scene.view.lookAt(cp.target); - }); + } - elDelete.click( () => { - const cp = animation.controlPoints[index]; - animation.removeControlPoint(cp); - }); + function addCommas(nStr){ + nStr += ''; + let x = nStr.split('.'); + let x1 = x[0]; + let x2 = x.length > 1 ? '.' + x[1] : ''; + let rgx = /(\d+)(\d{3})/; + while (rgx.test(x1)) { + x1 = x1.replace(rgx, '$1' + ',' + '$2'); + } + return x1 + x2; + }; - elKeyframes.append(elKeyframe); - }; + function format(value){ + return addCommas(value.toFixed(3)); + }; - let index = 0; + class HierarchicalSlider{ - addNewKeyframeItem(index); + constructor(params = {}){ + + this.element = document.createElement("div"); - for(const cp of animation.controlPoints){ - - addKeyframeItem(index); - index++; - addNewKeyframeItem(index); + this.labels = []; + this.sliders = []; + this.range = params.range != null ? params.range : [0, 1]; + this.slide = params.slide != null ? params.slide : null; + this.step = params.step != null ? params.step : 0.0001; - } - }; + let levels = params.levels != null ? params.levels : 1; - updateKeyframes(); + for(let level = 0; level < levels; level++){ + this.addLevel(); + } - animation.addEventListener("controlpoint_added", updateKeyframes); - animation.addEventListener("controlpoint_removed", updateKeyframes); + } + setRange(range){ + this.range = [...range]; + { // root slider + let slider = this.sliders[0]; + $(slider).slider({ + min: range[0], + max: range[1], + }); + } - // this._update = () => { this.update(); }; + for(let i = 1; i < this.sliders.length; i++){ + let parentSlider = this.sliders[i - 1]; + let slider = this.sliders[i]; - // this.update(); - } + let parentValues = $(parentSlider).slider("option", "values"); + let childRange = [...parentValues]; - update(){ + $(slider).slider({ + min: childRange[0], + max: childRange[1], + }); + } + this.updateLabels(); } - }; - - class PropertiesPanel{ - constructor(container, viewer){ - this.container = container; - this.viewer = viewer; - this.object = null; - this.cleanupTasks = []; - this.scene = null; - } + setValues(values){ + for(let slider of this.sliders){ + $(slider).slider({ + values: [...values], + }); + } - setScene(scene){ - this.scene = scene; + this.updateLabels(); } - set(object){ - if(this.object === object){ - return; - } + addLevel(){ + const elLevel = document.createElement("li"); + const elRange = document.createTextNode("Range: "); + const label = document.createElement("span"); + const slider = document.createElement("div"); - this.object = object; - - for(let task of this.cleanupTasks){ - task(); - } - this.cleanupTasks = []; - this.container.empty(); + let level = this.sliders.length; + let [min, max] = [0, 0]; - if(object instanceof PointCloudTree){ - this.setPointCloud(object); - }else if(object instanceof Measure || object instanceof Profile || object instanceof Volume){ - this.setMeasurement(object); - }else if(object instanceof Camera){ - this.setCamera(object); - }else if(object instanceof Annotation){ - this.setAnnotation(object); - }else if(object instanceof CameraAnimation){ - this.setCameraAnimation(object); + if(this.sliders.length === 0){ + [min, max] = this.range; + }else { + let parentSlider = this.sliders[this.sliders.length - 1]; + [min, max] = $(parentSlider).slider("option", "values"); } - } + $(slider).slider({ + range: true, + min: min, + max: max, + step: this.step, + values: [min, max], + slide: (event, ui) => { + + // set all descendants to same range + let levels = this.sliders.length; + for(let i = level + 1; i < levels; i++){ + let descendant = this.sliders[i]; - // - // Used for events that should be removed when the property object changes. - // This is for listening to materials, scene, point clouds, etc. - // not required for DOM listeners, since they are automatically cleared by removing the DOM subtree. - // - addVolatileListener(target, type, callback){ - target.addEventListener(type, callback); - this.cleanupTasks.push(() => { - target.removeEventListener(type, callback); - }); - } + $(descendant).slider({ + range: true, + min: ui.values[0], + max: ui.values[1], + values: [...ui.values], + }); + } - setPointCloud(pointcloud){ + if(this.slide){ + let values = [...ui.values]; - let material = pointcloud.material; + this.slide({ + target: this, + range: this.range, + values: values, + }); + } - let panel = $(` -
    -
      + this.updateLabels(); + }, + }); -
    • -
      -
    • -
    • -
      -
    • + elLevel.append(elRange, label, slider); - -
    • - - -
    • + this.sliders.push(slider); + this.labels.push(label); + this.element.append(elLevel); - -
    • -
      - -
    • + this.updateLabels(); + } -
    • - -
    • - - -
    • :
    • + removeLevel(){ -
      - Attribute -
      + } -
    • - -
    • + updateSliders(){ -
      -
      - Attribute Weights -
      + } -
    • RGB:
    • -
    • Intensity:
    • -
    • Elevation:
    • -
    • Classification:
    • -
    • Return Number:
    • -
    • Source ID:
    • -
      + updateLabels(){ -
      -
      - RGB -
      + let levels = this.sliders.length; -
    • Gamma:
    • -
    • Brightness:
    • -
    • Contrast:
    • -
      + for(let i = 0; i < levels; i++){ -
      -
      - Extra Attribute -
      + let slider = this.sliders[i]; + let label = this.labels[i]; -
    • :
    • + let [min, max] = $(slider).slider("option", "values"); + let strMin = format(min); + let strMax = format(max); + let strLabel = `${strMin} to ${strMax}`; -
    • Gamma:
    • -
    • Brightness:
    • -
    • Contrast:
    • -
      - -
      -
      - MATCAP -
      + label.innerHTML = strLabel; + } -
    • -
      -
    • -
      + } -
      -
      - Color -
      - -
      + } + class OrientedImageControls extends EventDispatcher{ + + constructor(viewer){ + super(); + + this.viewer = viewer; + this.renderer = viewer.renderer; -
      -
      - Elevation -
      + this.originalCam = viewer.scene.getActiveCamera(); + this.shearCam = viewer.scene.getActiveCamera().clone(); + this.shearCam.rotation.set(this.originalCam.rotation.toArray()); + this.shearCam.updateProjectionMatrix(); + this.shearCam.updateProjectionMatrix = () => { + return this.shearCam.projectionMatrix; + }; -
    • :
    • + this.image = null; -
    • - - - - - -
    • + this.fadeFactor = 20; + this.fovDelta = 0; -
    • - Gradient Scheme: -
      -
      -
    • -
      + this.fovMin = 0.1; + this.fovMax = 120; -
      -
      - Transition -
      + this.shear = [0, 0]; -
    • transition:
    • -
      + // const style = ``; + this.elUp = $(``); + this.elRight = $(``); + this.elDown = $(``); + this.elLeft = $(``); + this.elExit = $(``); -
      -
      - Intensity -
      + this.elExit.click( () => { + this.release(); + }); -
    • Range:
    • -
    • Gamma:
    • -
    • Brightness:
    • -
    • Contrast:
    • -
      + this.elUp.click(() => { + const fovY = viewer.getFOV(); + const top = Math.tan(MathUtils.degToRad(fovY / 2)); + this.shear[1] += 0.1 * top; + }); -
      -
      - GPS Time -
      + this.elRight.click(() => { + const fovY = viewer.getFOV(); + const top = Math.tan(MathUtils.degToRad(fovY / 2)); + this.shear[0] += 0.1 * top; + }); -
      - -
      -
      - Indices -
      -
      + this.elDown.click(() => { + const fovY = viewer.getFOV(); + const top = Math.tan(MathUtils.degToRad(fovY / 2)); + this.shear[1] -= 0.1 * top; + }); + this.elLeft.click(() => { + const fovY = viewer.getFOV(); + const top = Math.tan(MathUtils.degToRad(fovY / 2)); + this.shear[0] -= 0.1 * top; + }); -
    -
    - `); + this.scene = null; + this.sceneControls = new Scene(); - panel.i18n(); - this.container.append(panel); + let scroll = (e) => { + this.fovDelta += -e.delta * 1.0; + }; - { // POINT SIZE - let sldPointSize = panel.find(`#sldPointSize`); - let lblPointSize = panel.find(`#lblPointSize`); + this.addEventListener('mousewheel', scroll); + //this.addEventListener("mousemove", onMove); + } - sldPointSize.slider({ - value: material.size, - min: 0, - max: 3, - step: 0.01, - slide: function (event, ui) { material.size = ui.value; } - }); + hasSomethingCaptured(){ + return this.image !== null; + } - let update = (e) => { - lblPointSize.html(material.size.toFixed(2)); - sldPointSize.slider({value: material.size}); - }; - this.addVolatileListener(material, "point_size_changed", update); - - update(); + capture(image){ + if(this.hasSomethingCaptured()){ + return; } - { // MINIMUM POINT SIZE - let sldMinPointSize = panel.find(`#sldMinPointSize`); - let lblMinPointSize = panel.find(`#lblMinPointSize`); + this.image = image; - sldMinPointSize.slider({ - value: material.size, - min: 0, - max: 3, - step: 0.01, - slide: function (event, ui) { material.minSize = ui.value; } - }); + this.originalFOV = this.viewer.getFOV(); + this.originalControls = this.viewer.getControls(); - let update = (e) => { - lblMinPointSize.html(material.minSize.toFixed(2)); - sldMinPointSize.slider({value: material.minSize}); - }; - this.addVolatileListener(material, "point_size_changed", update); - - update(); - } + this.viewer.setControls(this); + this.viewer.scene.overrideCamera = this.shearCam; - { // POINT SIZING - let strSizeType = Object.keys(PointSizeType)[material.pointSizeType]; + const elCanvas = this.viewer.renderer.domElement; + const elRoot = $(elCanvas.parentElement); - let opt = panel.find(`#optPointSizing`); - opt.selectmenu(); - opt.val(strSizeType).selectmenu('refresh'); + this.shear = [0, 0]; - opt.selectmenu({ - change: (event, ui) => { - material.pointSizeType = PointSizeType[ui.item.value]; - } - }); - } - { // SHAPE - let opt = panel.find(`#optShape`); + elRoot.append(this.elUp); + elRoot.append(this.elRight); + elRoot.append(this.elDown); + elRoot.append(this.elLeft); + elRoot.append(this.elExit); + } - opt.selectmenu({ - change: (event, ui) => { - let value = ui.item.value; + release(){ + this.image = null; - material.shape = PointShape[value]; - } - }); + this.viewer.scene.overrideCamera = null; - let update = () => { - let typename = Object.keys(PointShape)[material.shape]; + this.elUp.detach(); + this.elRight.detach(); + this.elDown.detach(); + this.elLeft.detach(); + this.elExit.detach(); - opt.selectmenu().val(typename).selectmenu('refresh'); - }; - this.addVolatileListener(material, "point_shape_changed", update); + this.viewer.setFOV(this.originalFOV); + this.viewer.setControls(this.originalControls); + } - update(); - } + setScene (scene) { + this.scene = scene; + } - { // BACKFACE CULLING - - let opt = panel.find(`#set_backface_culling`); - opt.click(() => { - material.backfaceCulling = opt.prop("checked"); - }); - let update = () => { - let value = material.backfaceCulling; - opt.prop("checked", value); - }; - this.addVolatileListener(material, "backface_changed", update); - update(); + update (delta) { + // const view = this.scene.view; - let blockBackface = $('#materials_backface_container'); - blockBackface.css('display', 'none'); + // let prevTotal = this.shearCam.projectionMatrix.elements.reduce( (a, i) => a + i, 0); - const pointAttributes = pointcloud.pcoGeometry.pointAttributes; - const hasNormals = pointAttributes.hasNormals ? pointAttributes.hasNormals() : false; - if(hasNormals) { - blockBackface.css('display', 'block'); - } - /* - opt.checkboxradio({ - clicked: (event, ui) => { - // let value = ui.item.value; - let value = ui.item.checked; - console.log(value); - material.backfaceCulling = value; // $('#set_freeze').prop("checked"); - } - }); - */ - } + //const progression = Math.min(1, this.fadeFactor * delta); + //const attenuation = Math.max(0, 1 - this.fadeFactor * delta); + const progression = 1; + const attenuation = 0; - { // OPACITY - let sldOpacity = panel.find(`#sldOpacity`); - let lblOpacity = panel.find(`#lblOpacity`); + const oldFov = this.viewer.getFOV(); + let fovProgression = progression * this.fovDelta; + let newFov = oldFov * ((1 + fovProgression / 10)); - sldOpacity.slider({ - value: material.opacity, - min: 0, - max: 1, - step: 0.001, - slide: function (event, ui) { - material.opacity = ui.value; - } - }); + newFov = Math.max(this.fovMin, newFov); + newFov = Math.min(this.fovMax, newFov); - let update = (e) => { - lblOpacity.html(material.opacity.toFixed(2)); - sldOpacity.slider({value: material.opacity}); - }; - this.addVolatileListener(material, "opacity_changed", update); + let diff = newFov / oldFov; - update(); - } + const mouse = this.viewer.inputHandler.mouse; + const canvasSize = this.viewer.renderer.getSize(new Vector2()); + const uv = [ + (mouse.x / canvasSize.x), + ((canvasSize.y - mouse.y) / canvasSize.y) + ]; + + const fovY = newFov; + const aspect = canvasSize.x / canvasSize.y; + const top = Math.tan(MathUtils.degToRad(fovY / 2)); + const height = 2 * top; + const width = aspect * height; + + const shearRangeX = [ + this.shear[0] - 0.5 * width, + this.shear[0] + 0.5 * width, + ]; + + const shearRangeY = [ + this.shear[1] - 0.5 * height, + this.shear[1] + 0.5 * height, + ]; - { + const shx = (1 - uv[0]) * shearRangeX[0] + uv[0] * shearRangeX[1]; + const shy = (1 - uv[1]) * shearRangeY[0] + uv[1] * shearRangeY[1]; - const attributes = pointcloud.pcoGeometry.pointAttributes.attributes; + const shu = (1 - diff); - let options = []; + const newShear = [ + (1 - shu) * this.shear[0] + shu * shx, + (1 - shu) * this.shear[1] + shu * shy, + ]; + + this.shear = newShear; + this.viewer.setFOV(newFov); + + const {originalCam, shearCam} = this; - options.push(...attributes.map(a => a.name)); + originalCam.fov = newFov; + originalCam.updateMatrixWorld(); + originalCam.updateProjectionMatrix(); + shearCam.copy(originalCam); + shearCam.rotation.set(...originalCam.rotation.toArray()); - const intensityIndex = options.indexOf("intensity"); - if(intensityIndex >= 0){ - options.splice(intensityIndex + 1, 0, "intensity gradient"); - } + shearCam.updateMatrixWorld(); + shearCam.projectionMatrix.copy(originalCam.projectionMatrix); - options.push( - "elevation", - "color", - 'matcap', - 'indices', - 'level of detail', - 'composite' - ); + const [sx, sy] = this.shear; + const mShear = new Matrix4().set( + 1, 0, sx, 0, + 0, 1, sy, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + ); - const blacklist = [ - "POSITION_CARTESIAN", - "position", - ]; + const proj = shearCam.projectionMatrix; + proj.multiply(mShear); + shearCam.projectionMatrixInverse.copy(proj).invert(); - options = options.filter(o => !blacklist.includes(o)); + let total = shearCam.projectionMatrix.elements.reduce( (a, i) => a + i, 0); - let attributeSelection = panel.find('#optMaterial'); - for(let option of options){ - let elOption = $(``); - attributeSelection.append(elOption); - } + this.fovDelta *= attenuation; + } + }; - let updateMaterialPanel = (event, ui) => { - let selectedValue = attributeSelection.selectmenu().val(); - material.activeAttributeName = selectedValue; + // https://support.pix4d.com/hc/en-us/articles/205675256-How-are-yaw-pitch-roll-defined + // https://support.pix4d.com/hc/en-us/articles/202558969-How-are-omega-phi-kappa-defined - let attribute = pointcloud.getAttribute(selectedValue); + function createMaterial(){ - if(selectedValue === "intensity gradient"){ - attribute = pointcloud.getAttribute("intensity"); - } + let vertexShader = ` + uniform float uNear; + varying vec2 vUV; + varying vec4 vDebug; + + void main(){ + vDebug = vec4(0.0, 1.0, 0.0, 1.0); + vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0); + // make sure that this mesh is at least in front of the near plane + modelViewPosition.xyz += normalize(modelViewPosition.xyz) * uNear; + gl_Position = projectionMatrix * modelViewPosition; + vUV = uv; + } + `; - const isIntensity = attribute ? ["intensity", "intensity gradient"].includes(attribute.name) : false; + let fragmentShader = ` + uniform sampler2D tColor; + uniform float uOpacity; + varying vec2 vUV; + varying vec4 vDebug; + void main(){ + vec4 color = texture2D(tColor, vUV); + gl_FragColor = color; + gl_FragColor.a = uOpacity; + } + `; + const material = new ShaderMaterial( { + uniforms: { + // time: { value: 1.0 }, + // resolution: { value: new THREE.Vector2() } + tColor: {value: new Texture() }, + uNear: {value: 0.0}, + uOpacity: {value: 1.0}, + }, + vertexShader: vertexShader, + fragmentShader: fragmentShader, + side: DoubleSide, + } ); - if(isIntensity){ - if(pointcloud.material.intensityRange[0] === Infinity){ - pointcloud.material.intensityRange = attribute.range; - } + material.side = DoubleSide; - const [min, max] = attribute.range; + return material; + } - panel.find('#sldIntensityRange').slider({ - range: true, - min: min, max: max, step: 0.01, - values: [min, max], - slide: (event, ui) => { - let min = ui.values[0]; - let max = ui.values[1]; - material.intensityRange = [min, max]; - } - }); - } else if(attribute){ - const [min, max] = attribute.range; + const planeGeometry = new PlaneGeometry(1, 1); + const lineGeometry = new Geometry(); - let selectedRange = material.getRange(attribute.name); + lineGeometry.vertices.push( + new Vector3(-0.5, -0.5, 0), + new Vector3( 0.5, -0.5, 0), + new Vector3( 0.5, 0.5, 0), + new Vector3(-0.5, 0.5, 0), + new Vector3(-0.5, -0.5, 0), + ); - if(!selectedRange){ - selectedRange = [...attribute.range]; - } + class OrientedImage{ - let minMaxAreNumbers = typeof min === "number" && typeof max === "number"; + constructor(id){ - if(minMaxAreNumbers){ - panel.find('#sldExtraRange').slider({ - range: true, - min: min, - max: max, - step: 0.01, - values: selectedRange, - slide: (event, ui) => { - let [a, b] = ui.values; + this.id = id; + this.fov = 1.0; + this.position = new Vector3(); + this.rotation = new Vector3(); + this.width = 0; + this.height = 0; + this.fov = 1.0; - material.setRange(attribute.name, [a, b]); - } - }); - } + const material = createMaterial(); + const lineMaterial = new LineBasicMaterial( { color: 0x00ff00 } ); + this.mesh = new Mesh(planeGeometry, material); + this.line = new Line(lineGeometry, lineMaterial); + this.texture = null; - } + this.mesh.orientedImage = this; + } - let blockWeights = $('#materials\\.composite_weight_container'); - let blockElevation = $('#materials\\.elevation_container'); - let blockRGB = $('#materials\\.rgb_container'); - let blockExtra = $('#materials\\.extra_container'); - let blockColor = $('#materials\\.color_container'); - let blockIntensity = $('#materials\\.intensity_container'); - let blockIndex = $('#materials\\.index_container'); - let blockTransition = $('#materials\\.transition_container'); - let blockGps = $('#materials\\.gpstime_container'); - let blockMatcap = $('#materials\\.matcap_container'); + set(position, rotation, dimension, fov){ - blockIndex.css('display', 'none'); - blockIntensity.css('display', 'none'); - blockElevation.css('display', 'none'); - blockRGB.css('display', 'none'); - blockExtra.css('display', 'none'); - blockColor.css('display', 'none'); - blockWeights.css('display', 'none'); - blockTransition.css('display', 'none'); - blockMatcap.css('display', 'none'); - blockGps.css('display', 'none'); + let radians = rotation.map(MathUtils.degToRad); - if (selectedValue === 'composite') { - blockWeights.css('display', 'block'); - blockElevation.css('display', 'block'); - blockRGB.css('display', 'block'); - blockIntensity.css('display', 'block'); - } else if (selectedValue === 'elevation') { - blockElevation.css('display', 'block'); - } else if (selectedValue === 'RGB and Elevation') { - blockRGB.css('display', 'block'); - blockElevation.css('display', 'block'); - } else if (selectedValue === 'rgba') { - blockRGB.css('display', 'block'); - } else if (selectedValue === 'color') { - blockColor.css('display', 'block'); - } else if (selectedValue === 'intensity') { - blockIntensity.css('display', 'block'); - } else if (selectedValue === 'intensity gradient') { - blockIntensity.css('display', 'block'); - } else if (selectedValue === "indices" ){ - blockIndex.css('display', 'block'); - } else if (selectedValue === "matcap" ){ - blockMatcap.css('display', 'block'); - } else if (selectedValue === "classification" ){ - // add classification color selctor? - } else if (selectedValue === "gps-time" ){ - blockGps.css('display', 'block'); - } else if(selectedValue === "number of returns"){ - - } else if(selectedValue === "return number"){ - - } else if(["source id", "point source id"].includes(selectedValue)){ - - } else { - blockExtra.css('display', 'block'); - } - }; + this.position.set(...position); + this.mesh.position.set(...position); - attributeSelection.selectmenu({change: updateMaterialPanel}); + this.rotation.set(...radians); + this.mesh.rotation.set(...radians); - let update = () => { - attributeSelection.val(material.activeAttributeName).selectmenu('refresh'); - }; - this.addVolatileListener(material, "point_color_type_changed", update); - this.addVolatileListener(material, "active_attribute_changed", update); + [this.width, this.height] = dimension; + this.mesh.scale.set(this.width / this.height, 1, 1); - update(); - updateMaterialPanel(); - } + this.fov = fov; - { - const schemes = Object.keys(Potree.Gradients).map(name => ({name: name, values: Gradients[name]})); + this.updateTransform(); + } - let elSchemeContainer = panel.find("#elevation_gradient_scheme_selection"); + updateTransform(){ + let {mesh, line, fov} = this; - for(let scheme of schemes){ - let elScheme = $(` - - - `); + mesh.updateMatrixWorld(); + const dir = mesh.getWorldDirection(); + const alpha = MathUtils.degToRad(fov / 2); + const d = -0.5 / Math.tan(alpha); + const move = dir.clone().multiplyScalar(d); + mesh.position.add(move); - const svg = Potree.Utils.createSvgGradient(scheme.values); - svg.setAttributeNS(null, "class", `button-icon`); + line.position.copy(mesh.position); + line.scale.copy(mesh.scale); + line.rotation.copy(mesh.rotation); + } - elScheme.append($(svg)); + }; - elScheme.click( () => { - material.gradient = Gradients[scheme.name]; - }); + class OrientedImages extends EventDispatcher{ - elSchemeContainer.append(elScheme); - } - } + constructor(){ + super(); - { - let matcaps = [ - {name: "Normals", icon: `${Potree.resourcePath}/icons/matcap/check_normal+y.jpg`}, - {name: "Basic 1", icon: `${Potree.resourcePath}/icons/matcap/basic_1.jpg`}, - {name: "Basic 2", icon: `${Potree.resourcePath}/icons/matcap/basic_2.jpg`}, - {name: "Basic Dark", icon: `${Potree.resourcePath}/icons/matcap/basic_dark.jpg`}, - {name: "Basic Side", icon: `${Potree.resourcePath}/icons/matcap/basic_side.jpg`}, - {name: "Ceramic Dark", icon: `${Potree.resourcePath}/icons/matcap/ceramic_dark.jpg`}, - {name: "Ceramic Lightbulb", icon: `${Potree.resourcePath}/icons/matcap/ceramic_lightbulb.jpg`}, - {name: "Clay Brown", icon: `${Potree.resourcePath}/icons/matcap/clay_brown.jpg`}, - {name: "Clay Muddy", icon: `${Potree.resourcePath}/icons/matcap/clay_muddy.jpg`}, - {name: "Clay Studio", icon: `${Potree.resourcePath}/icons/matcap/clay_studio.jpg`}, - {name: "Resin", icon: `${Potree.resourcePath}/icons/matcap/resin.jpg`}, - {name: "Skin", icon: `${Potree.resourcePath}/icons/matcap/skin.jpg`}, - {name: "Jade", icon: `${Potree.resourcePath}/icons/matcap/jade.jpg`}, - {name: "Metal_ Anisotropic", icon: `${Potree.resourcePath}/icons/matcap/metal_anisotropic.jpg`}, - {name: "Metal Carpaint", icon: `${Potree.resourcePath}/icons/matcap/metal_carpaint.jpg`}, - {name: "Metal Lead", icon: `${Potree.resourcePath}/icons/matcap/metal_lead.jpg`}, - {name: "Metal Shiny", icon: `${Potree.resourcePath}/icons/matcap/metal_shiny.jpg`}, - {name: "Pearl", icon: `${Potree.resourcePath}/icons/matcap/pearl.jpg`}, - {name: "Toon", icon: `${Potree.resourcePath}/icons/matcap/toon.jpg`}, - {name: "Check Rim Light", icon: `${Potree.resourcePath}/icons/matcap/check_rim_light.jpg`}, - {name: "Check Rim Dark", icon: `${Potree.resourcePath}/icons/matcap/check_rim_dark.jpg`}, - {name: "Contours 1", icon: `${Potree.resourcePath}/icons/matcap/contours_1.jpg`}, - {name: "Contours 2", icon: `${Potree.resourcePath}/icons/matcap/contours_2.jpg`}, - {name: "Contours 3", icon: `${Potree.resourcePath}/icons/matcap/contours_3.jpg`}, - {name: "Reflection Check Horizontal", icon: `${Potree.resourcePath}/icons/matcap/reflection_check_horizontal.jpg`}, - {name: "Reflection Check Vertical", icon: `${Potree.resourcePath}/icons/matcap/reflection_check_vertical.jpg`}, - ]; + this.node = null; + this.cameraParams = null; + this.imageParams = null; + this.images = null; + this._visible = true; + } - let elMatcapContainer = panel.find("#matcap_scheme_selection"); + set visible(visible){ + if(this._visible === visible){ + return; + } - for(let matcap of matcaps){ - let elMatcap = $(` - - `); + for(const image of this.images){ + image.mesh.visible = visible; + image.line.visible = visible; + } - elMatcap.click( () => { - material.matcap = matcap.icon.substring(matcap.icon.lastIndexOf('/')); - }); + this._visible = visible; + this.dispatchEvent({ + type: "visibility_changed", + images: this, + }); + } - elMatcapContainer.append(elMatcap); - } - } + get visible(){ + return this._visible; + } - { - panel.find('#sldRGBGamma').slider({ - value: material.rgbGamma, - min: 0, max: 4, step: 0.01, - slide: (event, ui) => {material.rgbGamma = ui.value;} - }); - panel.find('#sldRGBContrast').slider({ - value: material.rgbContrast, - min: -1, max: 1, step: 0.01, - slide: (event, ui) => {material.rgbContrast = ui.value;} - }); + }; - panel.find('#sldRGBBrightness').slider({ - value: material.rgbBrightness, - min: -1, max: 1, step: 0.01, - slide: (event, ui) => {material.rgbBrightness = ui.value;} - }); + class OrientedImageLoader{ - panel.find('#sldExtraGamma').slider({ - value: material.extraGamma, - min: 0, max: 4, step: 0.01, - slide: (event, ui) => {material.extraGamma = ui.value;} - }); + static async loadCameraParams(path){ + const res = await fetch(path); + const text = await res.text(); - panel.find('#sldExtraBrightness').slider({ - value: material.extraBrightness, - min: -1, max: 1, step: 0.01, - slide: (event, ui) => {material.extraBrightness = ui.value;} - }); + const parser = new DOMParser(); + const doc = parser.parseFromString(text, "application/xml"); - panel.find('#sldExtraContrast').slider({ - value: material.extraContrast, - min: -1, max: 1, step: 0.01, - slide: (event, ui) => {material.extraContrast = ui.value;} - }); + const width = parseInt(doc.getElementsByTagName("width")[0].textContent); + const height = parseInt(doc.getElementsByTagName("height")[0].textContent); + const f = parseFloat(doc.getElementsByTagName("f")[0].textContent); - panel.find('#sldHeightRange').slider({ - range: true, - min: 0, max: 1000, step: 0.01, - values: [0, 1000], - slide: (event, ui) => { - material.heightMin = ui.values[0]; - material.heightMax = ui.values[1]; - } - }); + let a = (height / 2) / f; + let fov = 2 * MathUtils.radToDeg(Math.atan(a)); - panel.find('#sldIntensityGamma').slider({ - value: material.intensityGamma, - min: 0, max: 4, step: 0.01, - slide: (event, ui) => {material.intensityGamma = ui.value;} - }); + const params = { + path: path, + width: width, + height: height, + f: f, + fov: fov, + }; - panel.find('#sldIntensityContrast').slider({ - value: material.intensityContrast, - min: -1, max: 1, step: 0.01, - slide: (event, ui) => {material.intensityContrast = ui.value;} - }); + return params; + } - panel.find('#sldIntensityBrightness').slider({ - value: material.intensityBrightness, - min: -1, max: 1, step: 0.01, - slide: (event, ui) => {material.intensityBrightness = ui.value;} - }); + static async loadImageParams(path){ - panel.find('#sldWeightRGB').slider({ - value: material.weightRGB, - min: 0, max: 1, step: 0.01, - slide: (event, ui) => {material.weightRGB = ui.value;} - }); + const response = await fetch(path); + if(!response.ok){ + console.error(`failed to load ${path}`); + return; + } - panel.find('#sldWeightIntensity').slider({ - value: material.weightIntensity, - min: 0, max: 1, step: 0.01, - slide: (event, ui) => {material.weightIntensity = ui.value;} - }); + const content = await response.text(); + const lines = content.split(/\r?\n/); + const imageParams = []; - panel.find('#sldWeightElevation').slider({ - value: material.weightElevation, - min: 0, max: 1, step: 0.01, - slide: (event, ui) => {material.weightElevation = ui.value;} - }); + for(let i = 1; i < lines.length; i++){ + const line = lines[i]; - panel.find('#sldWeightClassification').slider({ - value: material.weightClassification, - min: 0, max: 1, step: 0.01, - slide: (event, ui) => {material.weightClassification = ui.value;} - }); + if(line.startsWith("#")){ + continue; + } - panel.find('#sldWeightReturnNumber').slider({ - value: material.weightReturnNumber, - min: 0, max: 1, step: 0.01, - slide: (event, ui) => {material.weightReturnNumber = ui.value;} - }); + const tokens = line.split(/\s+/); - panel.find('#sldWeightSourceID').slider({ - value: material.weightSourceID, - min: 0, max: 1, step: 0.01, - slide: (event, ui) => {material.weightSourceID = ui.value;} - }); + if(tokens.length < 6){ + continue; + } - panel.find(`#materials\\.color\\.picker`).spectrum({ - flat: true, - showInput: true, - preferredFormat: 'rgb', - cancelText: '', - chooseText: 'Apply', - color: `#${material.color.getHexString()}`, - move: color => { - let cRGB = color.toRgb(); - let tc = new Color().setRGB(cRGB.r / 255, cRGB.g / 255, cRGB.b / 255); - material.color = tc; - }, - change: color => { - let cRGB = color.toRgb(); - let tc = new Color().setRGB(cRGB.r / 255, cRGB.g / 255, cRGB.b / 255); - material.color = tc; - } - }); + const params = { + id: tokens[0], + x: Number.parseFloat(tokens[1]), + y: Number.parseFloat(tokens[2]), + z: Number.parseFloat(tokens[3]), + omega: Number.parseFloat(tokens[4]), + phi: Number.parseFloat(tokens[5]), + kappa: Number.parseFloat(tokens[6]), + }; - this.addVolatileListener(material, "color_changed", () => { - panel.find(`#materials\\.color\\.picker`) - .spectrum('set', `#${material.color.getHexString()}`); - }); + // const whitelist = ["47518.jpg"]; + // if(whitelist.includes(params.id)){ + // imageParams.push(params); + // } + imageParams.push(params); + } - let updateHeightRange = function () { - + // debug + //return [imageParams[50]]; - let aPosition = pointcloud.getAttribute("position"); + return imageParams; + } - let bMin, bMax; + static async load(cameraParamsPath, imageParamsPath, viewer){ - if(aPosition){ - // for new format 2.0 and loader that contain precomputed min/max of attributes - let min = aPosition.range[0][2]; - let max = aPosition.range[1][2]; - let width = max - min; + const tStart = performance.now(); - bMin = min - 0.2 * width; - bMax = max + 0.2 * width; - }else { - // for format up until exlusive 2.0 - let box = [pointcloud.pcoGeometry.tightBoundingBox, pointcloud.getBoundingBoxWorld()] - .find(v => v !== undefined); + const [cameraParams, imageParams] = await Promise.all([ + OrientedImageLoader.loadCameraParams(cameraParamsPath), + OrientedImageLoader.loadImageParams(imageParamsPath), + ]); - pointcloud.updateMatrixWorld(true); - box = Utils.computeTransformedBoundingBox(box, pointcloud.matrixWorld); + const orientedImageControls = new OrientedImageControls(viewer); + const raycaster = new Raycaster(); - let bWidth = box.max.z - box.min.z; - bMin = box.min.z - 0.2 * bWidth; - bMax = box.max.z + 0.2 * bWidth; - } + const tEnd = performance.now(); + console.log(tEnd - tStart); - let range = material.elevationRange; + // const sp = new THREE.PlaneGeometry(1, 1); + // const lg = new THREE.Geometry(); - panel.find('#lblHeightRange').html(`${range[0].toFixed(2)} to ${range[1].toFixed(2)}`); - panel.find('#sldHeightRange').slider({min: bMin, max: bMax, values: range}); - }; + // lg.vertices.push( + // new THREE.Vector3(-0.5, -0.5, 0), + // new THREE.Vector3( 0.5, -0.5, 0), + // new THREE.Vector3( 0.5, 0.5, 0), + // new THREE.Vector3(-0.5, 0.5, 0), + // new THREE.Vector3(-0.5, -0.5, 0), + // ); - let updateExtraRange = function () { + const {width, height} = cameraParams; + const orientedImages = []; + const sceneNode = new Object3D(); + sceneNode.name = "oriented_images"; - let attributeName = material.activeAttributeName; - let attribute = pointcloud.getAttribute(attributeName); + for(const params of imageParams){ - if(attribute == null){ - return; - } - - let range = material.getRange(attributeName); + // const material = createMaterial(); + // const lm = new THREE.LineBasicMaterial( { color: 0x00ff00 } ); + // const mesh = new THREE.Mesh(sp, material); - if(range == null){ - range = attribute.range; - } + const {x, y, z, omega, phi, kappa} = params; + // const [rx, ry, rz] = [omega, phi, kappa] + // .map(THREE.Math.degToRad); + + // mesh.position.set(x, y, z); + // mesh.scale.set(width / height, 1, 1); + // mesh.rotation.set(rx, ry, rz); + // { + // mesh.updateMatrixWorld(); + // const dir = mesh.getWorldDirection(); + // const alpha = THREE.Math.degToRad(cameraParams.fov / 2); + // const d = -0.5 / Math.tan(alpha); + // const move = dir.clone().multiplyScalar(d); + // mesh.position.add(move); + // } + // sceneNode.add(mesh); + + // const line = new THREE.Line(lg, lm); + // line.position.copy(mesh.position); + // line.scale.copy(mesh.scale); + // line.rotation.copy(mesh.rotation); + // sceneNode.add(line); + + let orientedImage = new OrientedImage(params.id); + // orientedImage.setPosition(x, y, z); + // orientedImage.setRotation(omega, phi, kappa); + // orientedImage.setDimension(width, height); + let position = [x, y, z]; + let rotation = [omega, phi, kappa]; + let dimension = [width, height]; + orientedImage.set(position, rotation, dimension, cameraParams.fov); + + sceneNode.add(orientedImage.mesh); + sceneNode.add(orientedImage.line); + + orientedImages.push(orientedImage); + } - // currently only supporting scalar ranges. - // rgba, normals, positions, etc have vector ranges, however - let isValidRange = (typeof range[0] === "number") && (typeof range[1] === "number"); - if(!isValidRange){ - return; - } + let hoveredElement = null; + let clipVolume = null; - if(range){ - let msg = `${range[0].toFixed(2)} to ${range[1].toFixed(2)}`; - panel.find('#lblExtraRange').html(msg); - }else { - panel.find("could not deduce range"); - } - }; + const onMouseMove = (evt) => { + const tStart = performance.now(); + if(hoveredElement){ + hoveredElement.line.material.color.setRGB(0, 1, 0); + } + evt.preventDefault(); - let updateIntensityRange = function () { - let range = material.intensityRange; + //var array = getMousePosition( container, evt.clientX, evt.clientY ); + const rect = viewer.renderer.domElement.getBoundingClientRect(); + const [x, y] = [evt.clientX, evt.clientY]; + const array = [ + ( x - rect.left ) / rect.width, + ( y - rect.top ) / rect.height + ]; + const onClickPosition = new Vector2(...array); + //const intersects = getIntersects(onClickPosition, scene.children); + const camera = viewer.scene.getActiveCamera(); + const mouse = new Vector3( + + ( onClickPosition.x * 2 ) - 1, + - ( onClickPosition.y * 2 ) + 1 ); + const objects = orientedImages.map(i => i.mesh); + raycaster.setFromCamera( mouse, camera ); + const intersects = raycaster.intersectObjects( objects ); + let selectionChanged = false; + + if ( intersects.length > 0){ + //console.log(intersects); + const intersection = intersects[0]; + const orientedImage = intersection.object.orientedImage; + orientedImage.line.material.color.setRGB(1, 0, 0); + selectionChanged = hoveredElement !== orientedImage; + hoveredElement = orientedImage; + }else { + hoveredElement = null; + } - panel.find('#lblIntensityRange').html(`${parseInt(range[0])} to ${parseInt(range[1])}`); - }; + let shouldRemoveClipVolume = clipVolume !== null && hoveredElement === null; + let shouldAddClipVolume = clipVolume === null && hoveredElement !== null; - { - updateHeightRange(); - panel.find(`#sldHeightRange`).slider('option', 'min'); - panel.find(`#sldHeightRange`).slider('option', 'max'); + if(clipVolume !== null && (hoveredElement === null || selectionChanged)){ + // remove existing + viewer.scene.removePolygonClipVolume(clipVolume); + clipVolume = null; + } + + if(shouldAddClipVolume || selectionChanged){ + const img = hoveredElement; + const fov = cameraParams.fov; + const aspect = cameraParams.width / cameraParams.height; + const near = 1.0; + const far = 1000 * 1000; + const camera = new PerspectiveCamera(fov, aspect, near, far); + camera.rotation.order = viewer.scene.getActiveCamera().rotation.order; + camera.rotation.copy(img.mesh.rotation); + { + const mesh = img.mesh; + const dir = mesh.getWorldDirection(); + const pos = mesh.position; + const alpha = MathUtils.degToRad(fov / 2); + const d = 0.5 / Math.tan(alpha); + const newCamPos = pos.clone().add(dir.clone().multiplyScalar(d)); + const newCamDir = pos.clone().sub(newCamPos); + const newCamTarget = new Vector3().addVectors( + newCamPos, + newCamDir.clone().multiplyScalar(viewer.getMoveSpeed())); + camera.position.copy(newCamPos); + } + let volume = new Potree.PolygonClipVolume(camera); + let m0 = new Mesh(); + let m1 = new Mesh(); + let m2 = new Mesh(); + let m3 = new Mesh(); + m0.position.set(-1, -1, 0); + m1.position.set( 1, -1, 0); + m2.position.set( 1, 1, 0); + m3.position.set(-1, 1, 0); + volume.markers.push(m0, m1, m2, m3); + volume.initialized = true; + + viewer.scene.addPolygonClipVolume(volume); + clipVolume = volume; } + const tEnd = performance.now(); + //console.log(tEnd - tStart); + }; - { - let elGradientRepeat = panel.find("#gradient_repeat_option"); - elGradientRepeat.selectgroup({title: "Gradient"}); + const moveToImage = (image) => { + console.log("move to image " + image.id); - elGradientRepeat.find("input").click( (e) => { - this.viewer.setElevationGradientRepeat(ElevationGradientRepeat[e.target.value]); - }); + const mesh = image.mesh; + const newCamPos = image.position.clone(); + const newCamTarget = mesh.position.clone(); - let current = Object.keys(ElevationGradientRepeat) - .filter(key => ElevationGradientRepeat[key] === this.viewer.elevationGradientRepeat); - elGradientRepeat.find(`input[value=${current}]`).trigger("click"); - } + viewer.scene.view.setView(newCamPos, newCamTarget, 500, () => { + orientedImageControls.capture(image); + }); - let onIntensityChange = () => { - let gamma = material.intensityGamma; - let contrast = material.intensityContrast; - let brightness = material.intensityBrightness; + if(image.texture === null){ - updateIntensityRange(); + const target = image; - panel.find('#lblIntensityGamma').html(gamma.toFixed(2)); - panel.find('#lblIntensityContrast').html(contrast.toFixed(2)); - panel.find('#lblIntensityBrightness').html(brightness.toFixed(2)); + const tmpImagePath = `${Potree.resourcePath}/images/loading.jpg`; + new TextureLoader().load(tmpImagePath, + (texture) => { + if(target.texture === null){ + target.texture = texture; + target.mesh.material.uniforms.tColor.value = texture; + mesh.material.needsUpdate = true; + } + } + ); - panel.find('#sldIntensityGamma').slider({value: gamma}); - panel.find('#sldIntensityContrast').slider({value: contrast}); - panel.find('#sldIntensityBrightness').slider({value: brightness}); - }; + const imagePath = `${imageParamsPath}/../${target.id}`; + new TextureLoader().load(imagePath, + (texture) => { + target.texture = texture; + target.mesh.material.uniforms.tColor.value = texture; + mesh.material.needsUpdate = true; + } + ); + - let onRGBChange = () => { - let gamma = material.rgbGamma; - let contrast = material.rgbContrast; - let brightness = material.rgbBrightness; + } + }; - panel.find('#lblRGBGamma').html(gamma.toFixed(2)); - panel.find('#lblRGBContrast').html(contrast.toFixed(2)); - panel.find('#lblRGBBrightness').html(brightness.toFixed(2)); + const onMouseClick = (evt) => { - panel.find('#sldRGBGamma').slider({value: gamma}); - panel.find('#sldRGBContrast').slider({value: contrast}); - panel.find('#sldRGBBrightness').slider({value: brightness}); - }; + if(orientedImageControls.hasSomethingCaptured()){ + return; + } - this.addVolatileListener(material, "material_property_changed", updateExtraRange); - this.addVolatileListener(material, "material_property_changed", updateHeightRange); - this.addVolatileListener(material, "material_property_changed", onIntensityChange); - this.addVolatileListener(material, "material_property_changed", onRGBChange); + if(hoveredElement){ + moveToImage(hoveredElement); + } + }; + viewer.renderer.domElement.addEventListener( 'mousemove', onMouseMove, false ); + viewer.renderer.domElement.addEventListener( 'mousedown', onMouseClick, false ); - updateExtraRange(); - updateHeightRange(); - onIntensityChange(); - onRGBChange(); - } + viewer.addEventListener("update", () => { + + for(const image of orientedImages){ + const world = image.mesh.matrixWorld; + const {width, height} = image; + const aspect = width / height; - } + const camera = viewer.scene.getActiveCamera(); - + const imgPos = image.mesh.getWorldPosition(new Vector3()); + const camPos = camera.position; + const d = camPos.distanceTo(imgPos); - setMeasurement(object){ + const minSize = 1; // in degrees of fov + const a = MathUtils.degToRad(minSize); + let r = d * Math.tan(a); + r = Math.max(r, 1); - let TYPE = { - DISTANCE: {panel: DistancePanel}, - AREA: {panel: AreaPanel}, - POINT: {panel: PointPanel}, - ANGLE: {panel: AnglePanel}, - HEIGHT: {panel: HeightPanel}, - PROFILE: {panel: ProfilePanel}, - VOLUME: {panel: VolumePanel}, - CIRCLE: {panel: CirclePanel}, - OTHER: {panel: PointPanel}, - }; - let getType = (measurement) => { - if (measurement instanceof Measure) { - if (measurement.showDistances && !measurement.showArea && !measurement.showAngles) { - return TYPE.DISTANCE; - } else if (measurement.showDistances && measurement.showArea && !measurement.showAngles) { - return TYPE.AREA; - } else if (measurement.maxMarkers === 1) { - return TYPE.POINT; - } else if (!measurement.showDistances && !measurement.showArea && measurement.showAngles) { - return TYPE.ANGLE; - } else if (measurement.showHeight) { - return TYPE.HEIGHT; - } else if (measurement.showCircle) { - return TYPE.CIRCLE; - } else { - return TYPE.OTHER; - } - } else if (measurement instanceof Profile) { - return TYPE.PROFILE; - } else if (measurement instanceof Volume) { - return TYPE.VOLUME; + image.mesh.scale.set(r * aspect, r, 1); + image.line.scale.set(r * aspect, r, 1); + + image.mesh.material.uniforms.uNear.value = camera.near; + } - }; - //this.container.html("measurement"); + }); - let type = getType(object); - let Panel = type.panel; + const images = new OrientedImages(); + images.node = sceneNode; + images.cameraParamsPath = cameraParamsPath; + images.imageParamsPath = imageParamsPath; + images.cameraParams = cameraParams; + images.imageParams = imageParams; + images.images = orientedImages; - let panel = new Panel(this.viewer, object, this); - this.container.append(panel.elContent); - } + Potree.debug.moveToImage = moveToImage; - setCamera(camera){ - let panel = new CameraPanel(this.viewer, this); - this.container.append(panel.elContent); + return images; } + } - setAnnotation(annotation){ - let panel = new AnnotationPanel(this.viewer, this, annotation); - this.container.append(panel.elContent); - } + let sg = new SphereGeometry(1, 8, 8); + let sgHigh = new SphereGeometry(1, 128, 128); - setCameraAnimation(animation){ - let panel = new CameraAnimationPanel(this.viewer, this, animation); - this.container.append(panel.elContent); - } + let sm = new MeshBasicMaterial({side: BackSide}); + let smHovered = new MeshBasicMaterial({side: BackSide, color: 0xff0000}); - } + let raycaster = new Raycaster(); + let currentlyHovered = null; - function addCommas(nStr){ - nStr += ''; - let x = nStr.split('.'); - let x1 = x[0]; - let x2 = x.length > 1 ? '.' + x[1] : ''; - let rgx = /(\d+)(\d{3})/; - while (rgx.test(x1)) { - x1 = x1.replace(rgx, '$1' + ',' + '$2'); - } - return x1 + x2; - }; - - function format(value){ - return addCommas(value.toFixed(3)); - }; - - class HierarchicalSlider{ - - constructor(params = {}){ - - this.element = document.createElement("div"); - - this.labels = []; - this.sliders = []; - this.range = params.range != null ? params.range : [0, 1]; - this.slide = params.slide != null ? params.slide : null; - this.step = params.step != null ? params.step : 0.0001; - - let levels = params.levels != null ? params.levels : 1; - - for(let level = 0; level < levels; level++){ - this.addLevel(); - } - - } - - setRange(range){ - this.range = [...range]; - - { // root slider - let slider = this.sliders[0]; - - $(slider).slider({ - min: range[0], - max: range[1], - }); - } - - for(let i = 1; i < this.sliders.length; i++){ - let parentSlider = this.sliders[i - 1]; - let slider = this.sliders[i]; - - let parentValues = $(parentSlider).slider("option", "values"); - let childRange = [...parentValues]; - - $(slider).slider({ - min: childRange[0], - max: childRange[1], - }); - } - - this.updateLabels(); - } - - setValues(values){ - for(let slider of this.sliders){ - $(slider).slider({ - values: [...values], - }); - } - - this.updateLabels(); - } - - addLevel(){ - const elLevel = document.createElement("li"); - const elRange = document.createTextNode("Range: "); - const label = document.createElement("span"); - const slider = document.createElement("div"); - - let level = this.sliders.length; - let [min, max] = [0, 0]; - - if(this.sliders.length === 0){ - [min, max] = this.range; - }else { - let parentSlider = this.sliders[this.sliders.length - 1]; - [min, max] = $(parentSlider).slider("option", "values"); - } - - $(slider).slider({ - range: true, - min: min, - max: max, - step: this.step, - values: [min, max], - slide: (event, ui) => { - - // set all descendants to same range - let levels = this.sliders.length; - for(let i = level + 1; i < levels; i++){ - let descendant = this.sliders[i]; - - $(descendant).slider({ - range: true, - min: ui.values[0], - max: ui.values[1], - values: [...ui.values], - }); - } - - if(this.slide){ - let values = [...ui.values]; - - this.slide({ - target: this, - range: this.range, - values: values, - }); - } - - this.updateLabels(); - }, - }); - - elLevel.append(elRange, label, slider); - - this.sliders.push(slider); - this.labels.push(label); - this.element.append(elLevel); - - this.updateLabels(); - } - - removeLevel(){ - - } - - updateSliders(){ - - } - - updateLabels(){ - - let levels = this.sliders.length; - - for(let i = 0; i < levels; i++){ - - let slider = this.sliders[i]; - let label = this.labels[i]; - - let [min, max] = $(slider).slider("option", "values"); - let strMin = format(min); - let strMax = format(max); - let strLabel = `${strMin} to ${strMax}`; - - label.innerHTML = strLabel; - } - - } - - - } + let previousView = { + controls: null, + position: null, + target: null, + }; + + class Image360{ + + constructor(file, time, longitude, latitude, altitude, course, pitch, roll){ + this.file = file; + this.time = time; + this.longitude = longitude; + this.latitude = latitude; + this.altitude = altitude; + this.course = course; + this.pitch = pitch; + this.roll = roll; + this.mesh = null; + } + }; + + class Images360 extends EventDispatcher{ - class OrientedImageControls extends EventDispatcher{ - constructor(viewer){ super(); - + this.viewer = viewer; - this.renderer = viewer.renderer; - this.originalCam = viewer.scene.getActiveCamera(); - this.shearCam = viewer.scene.getActiveCamera().clone(); - this.shearCam.rotation.set(this.originalCam.rotation.toArray()); - this.shearCam.updateProjectionMatrix(); - this.shearCam.updateProjectionMatrix = () => { - return this.shearCam.projectionMatrix; - }; + this.selectingEnabled = true; - this.image = null; + this.images = []; + this.node = new Object3D(); - this.fadeFactor = 20; - this.fovDelta = 0; + this.sphere = new Mesh(sgHigh, sm); + this.sphere.visible = false; + this.sphere.scale.set(1000, 1000, 1000); + this.node.add(this.sphere); + this._visible = true; + // this.node.add(label); + + this.focusedImage = null; + + let elUnfocus = document.createElement("input"); + elUnfocus.type = "button"; + elUnfocus.value = "unfocus"; + elUnfocus.style.position = "absolute"; + elUnfocus.style.right = "10px"; + elUnfocus.style.bottom = "10px"; + elUnfocus.style.zIndex = "10000"; + elUnfocus.style.fontSize = "2em"; + elUnfocus.addEventListener("click", () => this.unfocus()); + this.elUnfocus = elUnfocus; + + this.domRoot = viewer.renderer.domElement.parentElement; + this.domRoot.appendChild(elUnfocus); + this.elUnfocus.style.display = "none"; + + viewer.addEventListener("update", () => { + this.update(viewer); + }); + viewer.inputHandler.addInputListener(this); - this.fovMin = 0.1; - this.fovMax = 120; + this.addEventListener("mousedown", () => { + if(currentlyHovered){ + this.focus(currentlyHovered.image360); + } + }); + + }; - this.shear = [0, 0]; + set visible(visible){ + if(this._visible === visible){ + return; + } - // const style = ``; - this.elUp = $(``); - this.elRight = $(``); - this.elDown = $(``); - this.elLeft = $(``); - this.elExit = $(``); - this.elExit.click( () => { - this.release(); - }); + for(const image of this.images){ + image.mesh.visible = visible && (this.focusedImage == null); + } - this.elUp.click(() => { - const fovY = viewer.getFOV(); - const top = Math.tan(MathUtils.degToRad(fovY / 2)); - this.shear[1] += 0.1 * top; + this.sphere.visible = visible && (this.focusedImage != null); + this._visible = visible; + this.dispatchEvent({ + type: "visibility_changed", + images: this, }); + } - this.elRight.click(() => { - const fovY = viewer.getFOV(); - const top = Math.tan(MathUtils.degToRad(fovY / 2)); - this.shear[0] += 0.1 * top; - }); + get visible(){ + return this._visible; + } - this.elDown.click(() => { - const fovY = viewer.getFOV(); - const top = Math.tan(MathUtils.degToRad(fovY / 2)); - this.shear[1] -= 0.1 * top; - }); + focus(image360){ + if(this.focusedImage !== null){ + this.unfocus(); + } - this.elLeft.click(() => { - const fovY = viewer.getFOV(); - const top = Math.tan(MathUtils.degToRad(fovY / 2)); - this.shear[0] -= 0.1 * top; + previousView = { + controls: this.viewer.controls, + position: this.viewer.scene.view.position.clone(), + target: viewer.scene.view.getPivot(), + }; + + this.viewer.setControls(this.viewer.orbitControls); + this.viewer.orbitControls.doubleClockZoomEnabled = false; + + for(let image of this.images){ + image.mesh.visible = false; + } + + this.selectingEnabled = false; + + this.sphere.visible = false; + + this.load(image360).then( () => { + this.sphere.visible = true; + this.sphere.material.map = image360.texture; + this.sphere.material.needsUpdate = true; }); - this.scene = null; - this.sceneControls = new Scene(); + { // orientation + let {course, pitch, roll} = image360; + this.sphere.rotation.set( + MathUtils.degToRad(+roll + 90), + MathUtils.degToRad(-pitch), + MathUtils.degToRad(-course + 90), + "ZYX" + ); + } - let scroll = (e) => { - this.fovDelta += -e.delta * 1.0; - }; + this.sphere.position.set(...image360.position); - this.addEventListener('mousewheel', scroll); - //this.addEventListener("mousemove", onMove); - } + let target = new Vector3(...image360.position); + let dir = target.clone().sub(viewer.scene.view.position).normalize(); + let move = dir.multiplyScalar(0.000001); + let newCamPos = target.clone().sub(move); - hasSomethingCaptured(){ - return this.image !== null; + viewer.scene.view.setView( + newCamPos, + target, + 500 + ); + + this.focusedImage = image360; + + this.elUnfocus.style.display = ""; } - capture(image){ - if(this.hasSomethingCaptured()){ + unfocus(){ + this.selectingEnabled = true; + + for(let image of this.images){ + image.mesh.visible = true; + } + + let image = this.focusedImage; + + if(image === null){ return; } - this.image = image; - this.originalFOV = this.viewer.getFOV(); - this.originalControls = this.viewer.getControls(); + this.sphere.material.map = null; + this.sphere.material.needsUpdate = true; + this.sphere.visible = false; - this.viewer.setControls(this); - this.viewer.scene.overrideCamera = this.shearCam; + let pos = viewer.scene.view.position; + let target = viewer.scene.view.getPivot(); + let dir = target.clone().sub(pos).normalize(); + let move = dir.multiplyScalar(10); + let newCamPos = target.clone().sub(move); - const elCanvas = this.viewer.renderer.domElement; - const elRoot = $(elCanvas.parentElement); + viewer.orbitControls.doubleClockZoomEnabled = true; + viewer.setControls(previousView.controls); - this.shear = [0, 0]; + viewer.scene.view.setView( + previousView.position, + previousView.target, + 500 + ); - elRoot.append(this.elUp); - elRoot.append(this.elRight); - elRoot.append(this.elDown); - elRoot.append(this.elLeft); - elRoot.append(this.elExit); + this.focusedImage = null; + + this.elUnfocus.style.display = "none"; } - release(){ - this.image = null; + load(image360){ - this.viewer.scene.overrideCamera = null; + return new Promise(resolve => { + let texture = new TextureLoader().load(image360.file, resolve); + texture.wrapS = RepeatWrapping; + texture.repeat.x = -1; - this.elUp.detach(); - this.elRight.detach(); - this.elDown.detach(); - this.elLeft.detach(); - this.elExit.detach(); + image360.texture = texture; + }); - this.viewer.setFOV(this.originalFOV); - this.viewer.setControls(this.originalControls); } - setScene (scene) { - this.scene = scene; - } + handleHovering(){ + let mouse = viewer.inputHandler.mouse; + let camera = viewer.scene.getActiveCamera(); + let domElement = viewer.renderer.domElement; - update (delta) { - // const view = this.scene.view; + let ray = Potree.Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); - // let prevTotal = this.shearCam.projectionMatrix.elements.reduce( (a, i) => a + i, 0); + // let tStart = performance.now(); + raycaster.ray.copy(ray); + let intersections = raycaster.intersectObjects(this.node.children); - //const progression = Math.min(1, this.fadeFactor * delta); - //const attenuation = Math.max(0, 1 - this.fadeFactor * delta); - const progression = 1; - const attenuation = 0; + if(intersections.length === 0){ + // label.visible = false; - const oldFov = this.viewer.getFOV(); - let fovProgression = progression * this.fovDelta; - let newFov = oldFov * ((1 + fovProgression / 10)); + return; + } - newFov = Math.max(this.fovMin, newFov); - newFov = Math.min(this.fovMax, newFov); + let intersection = intersections[0]; + currentlyHovered = intersection.object; + currentlyHovered.material = smHovered; - let diff = newFov / oldFov; + //label.visible = true; + //label.setText(currentlyHovered.image360.file); + //currentlyHovered.getWorldPosition(label.position); + } - const mouse = this.viewer.inputHandler.mouse; - const canvasSize = this.viewer.renderer.getSize(new Vector2()); - const uv = [ - (mouse.x / canvasSize.x), - ((canvasSize.y - mouse.y) / canvasSize.y) - ]; + update(){ - const fovY = newFov; - const aspect = canvasSize.x / canvasSize.y; - const top = Math.tan(MathUtils.degToRad(fovY / 2)); - const height = 2 * top; - const width = aspect * height; + let {viewer} = this; - const shearRangeX = [ - this.shear[0] - 0.5 * width, - this.shear[0] + 0.5 * width, - ]; + if(currentlyHovered){ + currentlyHovered.material = sm; + currentlyHovered = null; + } - const shearRangeY = [ - this.shear[1] - 0.5 * height, - this.shear[1] + 0.5 * height, - ]; + if(this.selectingEnabled){ + this.handleHovering(); + } - const shx = (1 - uv[0]) * shearRangeX[0] + uv[0] * shearRangeX[1]; - const shy = (1 - uv[1]) * shearRangeY[0] + uv[1] * shearRangeY[1]; + } - const shu = (1 - diff); + }; - const newShear = [ - (1 - shu) * this.shear[0] + shu * shx, - (1 - shu) * this.shear[1] + shu * shy, - ]; - - this.shear = newShear; - this.viewer.setFOV(newFov); + + class Images360Loader{ + + static async load(url, viewer, params = {}){ + + if(!params.transform){ + params.transform = { + forward: a => a, + }; + } - const {originalCam, shearCam} = this; + let response = await fetch(`${url}/coordinates.txt`); + let text = await response.text(); + + let lines = text.split(/\r?\n/); + let coordinateLines = lines.slice(1); + + let images360 = new Images360(viewer); + + for(let line of coordinateLines){ + + if(line.trim().length === 0){ + continue; + } + + let tokens = line.split(/\t/); + + let [filename, time, long, lat, alt, course, pitch, roll] = tokens; + time = parseFloat(time); + long = parseFloat(long); + lat = parseFloat(lat); + alt = parseFloat(alt); + course = parseFloat(course); + pitch = parseFloat(pitch); + roll = parseFloat(roll); + + filename = filename.replace(/"/g, ""); + let file = `${url}/${filename}`; + + let image360 = new Image360(file, time, long, lat, alt, course, pitch, roll); + + let xy = params.transform.forward([long, lat]); + let position = [...xy, alt]; + image360.position = position; + + images360.images.push(image360); + } - originalCam.fov = newFov; - originalCam.updateMatrixWorld(); - originalCam.updateProjectionMatrix(); - shearCam.copy(originalCam); - shearCam.rotation.set(...originalCam.rotation.toArray()); + Images360Loader.createSceneNodes(images360, params.transform); - shearCam.updateMatrixWorld(); - shearCam.projectionMatrix.copy(originalCam.projectionMatrix); + return images360; - const [sx, sy] = this.shear; - const mShear = new Matrix4().set( - 1, 0, sx, 0, - 0, 1, sy, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - ); + } - const proj = shearCam.projectionMatrix; - proj.multiply(mShear); - shearCam.projectionMatrixInverse.copy(proj).invert(); + static createSceneNodes(images360, transform){ - let total = shearCam.projectionMatrix.elements.reduce( (a, i) => a + i, 0); + for(let image360 of images360.images){ + let {longitude, latitude, altitude} = image360; + let xy = transform.forward([longitude, latitude]); - this.fovDelta *= attenuation; + let mesh = new Mesh(sg, sm); + mesh.position.set(...xy, altitude); + mesh.scale.set(1, 1, 1); + mesh.material.transparent = true; + mesh.material.opacity = 0.75; + mesh.image360 = image360; + + { // orientation + var {course, pitch, roll} = image360; + mesh.rotation.set( + MathUtils.degToRad(+roll + 90), + MathUtils.degToRad(-pitch), + MathUtils.degToRad(-course + 90), + "ZYX" + ); + } + + images360.node.add(mesh); + + image360.mesh = mesh; + } } - }; - // https://support.pix4d.com/hc/en-us/articles/205675256-How-are-yaw-pitch-roll-defined - // https://support.pix4d.com/hc/en-us/articles/202558969-How-are-omega-phi-kappa-defined - - function createMaterial(){ - - let vertexShader = ` - uniform float uNear; - varying vec2 vUV; - varying vec4 vDebug; - - void main(){ - vDebug = vec4(0.0, 1.0, 0.0, 1.0); - vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0); - // make sure that this mesh is at least in front of the near plane - modelViewPosition.xyz += normalize(modelViewPosition.xyz) * uNear; - gl_Position = projectionMatrix * modelViewPosition; - vUV = uv; - } - `; - - let fragmentShader = ` - uniform sampler2D tColor; - uniform float uOpacity; - varying vec2 vUV; - varying vec4 vDebug; - void main(){ - vec4 color = texture2D(tColor, vUV); - gl_FragColor = color; - gl_FragColor.a = uOpacity; - } - `; - const material = new ShaderMaterial( { - uniforms: { - // time: { value: 1.0 }, - // resolution: { value: new THREE.Vector2() } - tColor: {value: new Texture() }, - uNear: {value: 0.0}, - uOpacity: {value: 1.0}, - }, - vertexShader: vertexShader, - fragmentShader: fragmentShader, - side: DoubleSide, - } ); - - material.side = DoubleSide; - - return material; - } - - const planeGeometry = new PlaneGeometry(1, 1); - const lineGeometry = new Geometry(); - - lineGeometry.vertices.push( - new Vector3(-0.5, -0.5, 0), - new Vector3( 0.5, -0.5, 0), - new Vector3( 0.5, 0.5, 0), - new Vector3(-0.5, 0.5, 0), - new Vector3(-0.5, -0.5, 0), - ); - - class OrientedImage{ - - constructor(id){ - - this.id = id; - this.fov = 1.0; - this.position = new Vector3(); - this.rotation = new Vector3(); - this.width = 0; - this.height = 0; - this.fov = 1.0; - - const material = createMaterial(); - const lineMaterial = new LineBasicMaterial( { color: 0x00ff00 } ); - this.mesh = new Mesh(planeGeometry, material); - this.line = new Line(lineGeometry, lineMaterial); - this.texture = null; - - this.mesh.orientedImage = this; - } - - set(position, rotation, dimension, fov){ - - let radians = rotation.map(MathUtils.degToRad); - - this.position.set(...position); - this.mesh.position.set(...position); - - this.rotation.set(...radians); - this.mesh.rotation.set(...radians); - - [this.width, this.height] = dimension; - this.mesh.scale.set(this.width / this.height, 1, 1); - - this.fov = fov; - - this.updateTransform(); - } - - updateTransform(){ - let {mesh, line, fov} = this; - - mesh.updateMatrixWorld(); - const dir = mesh.getWorldDirection(); - const alpha = MathUtils.degToRad(fov / 2); - const d = -0.5 / Math.tan(alpha); - const move = dir.clone().multiplyScalar(d); - mesh.position.add(move); - - line.position.copy(mesh.position); - line.scale.copy(mesh.scale); - line.rotation.copy(mesh.rotation); - } - - }; - - class OrientedImages extends EventDispatcher{ - - constructor(){ - super(); - - this.node = null; - this.cameraParams = null; - this.imageParams = null; - this.images = null; - this._visible = true; - } - - set visible(visible){ - if(this._visible === visible){ - return; - } - - for(const image of this.images){ - image.mesh.visible = visible; - image.line.visible = visible; - } - - this._visible = visible; - this.dispatchEvent({ - type: "visibility_changed", - images: this, - }); - } - - get visible(){ - return this._visible; - } - - - }; - - class OrientedImageLoader{ - - static async loadCameraParams(path){ - const res = await fetch(path); - const text = await res.text(); - - const parser = new DOMParser(); - const doc = parser.parseFromString(text, "application/xml"); - - const width = parseInt(doc.getElementsByTagName("width")[0].textContent); - const height = parseInt(doc.getElementsByTagName("height")[0].textContent); - const f = parseFloat(doc.getElementsByTagName("f")[0].textContent); - - let a = (height / 2) / f; - let fov = 2 * MathUtils.radToDeg(Math.atan(a)); - - const params = { - path: path, - width: width, - height: height, - f: f, - fov: fov, - }; - - return params; - } - - static async loadImageParams(path){ - - const response = await fetch(path); - if(!response.ok){ - console.error(`failed to load ${path}`); - return; - } - - const content = await response.text(); - const lines = content.split(/\r?\n/); - const imageParams = []; - - for(let i = 1; i < lines.length; i++){ - const line = lines[i]; - - if(line.startsWith("#")){ - continue; - } - - const tokens = line.split(/\s+/); - - if(tokens.length < 6){ - continue; - } - - const params = { - id: tokens[0], - x: Number.parseFloat(tokens[1]), - y: Number.parseFloat(tokens[2]), - z: Number.parseFloat(tokens[3]), - omega: Number.parseFloat(tokens[4]), - phi: Number.parseFloat(tokens[5]), - kappa: Number.parseFloat(tokens[6]), - }; - - // const whitelist = ["47518.jpg"]; - // if(whitelist.includes(params.id)){ - // imageParams.push(params); - // } - imageParams.push(params); - } - - // debug - //return [imageParams[50]]; - - return imageParams; - } - - static async load(cameraParamsPath, imageParamsPath, viewer){ - - const tStart = performance.now(); - - const [cameraParams, imageParams] = await Promise.all([ - OrientedImageLoader.loadCameraParams(cameraParamsPath), - OrientedImageLoader.loadImageParams(imageParamsPath), - ]); - - const orientedImageControls = new OrientedImageControls(viewer); - const raycaster = new Raycaster(); - - const tEnd = performance.now(); - console.log(tEnd - tStart); - - // const sp = new THREE.PlaneGeometry(1, 1); - // const lg = new THREE.Geometry(); - - // lg.vertices.push( - // new THREE.Vector3(-0.5, -0.5, 0), - // new THREE.Vector3( 0.5, -0.5, 0), - // new THREE.Vector3( 0.5, 0.5, 0), - // new THREE.Vector3(-0.5, 0.5, 0), - // new THREE.Vector3(-0.5, -0.5, 0), - // ); - - const {width, height} = cameraParams; - const orientedImages = []; - const sceneNode = new Object3D(); - sceneNode.name = "oriented_images"; - - for(const params of imageParams){ - - // const material = createMaterial(); - // const lm = new THREE.LineBasicMaterial( { color: 0x00ff00 } ); - // const mesh = new THREE.Mesh(sp, material); - - const {x, y, z, omega, phi, kappa} = params; - // const [rx, ry, rz] = [omega, phi, kappa] - // .map(THREE.Math.degToRad); - - // mesh.position.set(x, y, z); - // mesh.scale.set(width / height, 1, 1); - // mesh.rotation.set(rx, ry, rz); - // { - // mesh.updateMatrixWorld(); - // const dir = mesh.getWorldDirection(); - // const alpha = THREE.Math.degToRad(cameraParams.fov / 2); - // const d = -0.5 / Math.tan(alpha); - // const move = dir.clone().multiplyScalar(d); - // mesh.position.add(move); - // } - // sceneNode.add(mesh); - - // const line = new THREE.Line(lg, lm); - // line.position.copy(mesh.position); - // line.scale.copy(mesh.scale); - // line.rotation.copy(mesh.rotation); - // sceneNode.add(line); - - let orientedImage = new OrientedImage(params.id); - // orientedImage.setPosition(x, y, z); - // orientedImage.setRotation(omega, phi, kappa); - // orientedImage.setDimension(width, height); - let position = [x, y, z]; - let rotation = [omega, phi, kappa]; - let dimension = [width, height]; - orientedImage.set(position, rotation, dimension, cameraParams.fov); - - sceneNode.add(orientedImage.mesh); - sceneNode.add(orientedImage.line); - - orientedImages.push(orientedImage); - } - - let hoveredElement = null; - let clipVolume = null; - - const onMouseMove = (evt) => { - const tStart = performance.now(); - if(hoveredElement){ - hoveredElement.line.material.color.setRGB(0, 1, 0); - } - evt.preventDefault(); - - //var array = getMousePosition( container, evt.clientX, evt.clientY ); - const rect = viewer.renderer.domElement.getBoundingClientRect(); - const [x, y] = [evt.clientX, evt.clientY]; - const array = [ - ( x - rect.left ) / rect.width, - ( y - rect.top ) / rect.height - ]; - const onClickPosition = new Vector2(...array); - //const intersects = getIntersects(onClickPosition, scene.children); - const camera = viewer.scene.getActiveCamera(); - const mouse = new Vector3( - + ( onClickPosition.x * 2 ) - 1, - - ( onClickPosition.y * 2 ) + 1 ); - const objects = orientedImages.map(i => i.mesh); - raycaster.setFromCamera( mouse, camera ); - const intersects = raycaster.intersectObjects( objects ); - let selectionChanged = false; - - if ( intersects.length > 0){ - //console.log(intersects); - const intersection = intersects[0]; - const orientedImage = intersection.object.orientedImage; - orientedImage.line.material.color.setRGB(1, 0, 0); - selectionChanged = hoveredElement !== orientedImage; - hoveredElement = orientedImage; - }else { - hoveredElement = null; - } - - let shouldRemoveClipVolume = clipVolume !== null && hoveredElement === null; - let shouldAddClipVolume = clipVolume === null && hoveredElement !== null; - - if(clipVolume !== null && (hoveredElement === null || selectionChanged)){ - // remove existing - viewer.scene.removePolygonClipVolume(clipVolume); - clipVolume = null; - } - - if(shouldAddClipVolume || selectionChanged){ - const img = hoveredElement; - const fov = cameraParams.fov; - const aspect = cameraParams.width / cameraParams.height; - const near = 1.0; - const far = 1000 * 1000; - const camera = new PerspectiveCamera(fov, aspect, near, far); - camera.rotation.order = viewer.scene.getActiveCamera().rotation.order; - camera.rotation.copy(img.mesh.rotation); - { - const mesh = img.mesh; - const dir = mesh.getWorldDirection(); - const pos = mesh.position; - const alpha = MathUtils.degToRad(fov / 2); - const d = 0.5 / Math.tan(alpha); - const newCamPos = pos.clone().add(dir.clone().multiplyScalar(d)); - const newCamDir = pos.clone().sub(newCamPos); - const newCamTarget = new Vector3().addVectors( - newCamPos, - newCamDir.clone().multiplyScalar(viewer.getMoveSpeed())); - camera.position.copy(newCamPos); - } - let volume = new Potree.PolygonClipVolume(camera); - let m0 = new Mesh(); - let m1 = new Mesh(); - let m2 = new Mesh(); - let m3 = new Mesh(); - m0.position.set(-1, -1, 0); - m1.position.set( 1, -1, 0); - m2.position.set( 1, 1, 0); - m3.position.set(-1, 1, 0); - volume.markers.push(m0, m1, m2, m3); - volume.initialized = true; - - viewer.scene.addPolygonClipVolume(volume); - clipVolume = volume; - } - const tEnd = performance.now(); - //console.log(tEnd - tStart); - }; - - const moveToImage = (image) => { - console.log("move to image " + image.id); - - const mesh = image.mesh; - const newCamPos = image.position.clone(); - const newCamTarget = mesh.position.clone(); - - viewer.scene.view.setView(newCamPos, newCamTarget, 500, () => { - orientedImageControls.capture(image); - }); - - if(image.texture === null){ - - const target = image; - - const tmpImagePath = `${Potree.resourcePath}/images/loading.jpg`; - new TextureLoader().load(tmpImagePath, - (texture) => { - if(target.texture === null){ - target.texture = texture; - target.mesh.material.uniforms.tColor.value = texture; - mesh.material.needsUpdate = true; - } - } - ); - - const imagePath = `${imageParamsPath}/../${target.id}`; - new TextureLoader().load(imagePath, - (texture) => { - target.texture = texture; - target.mesh.material.uniforms.tColor.value = texture; - mesh.material.needsUpdate = true; - } - ); - - - } - }; - - const onMouseClick = (evt) => { - - if(orientedImageControls.hasSomethingCaptured()){ - return; - } - - if(hoveredElement){ - moveToImage(hoveredElement); - } - }; - viewer.renderer.domElement.addEventListener( 'mousemove', onMouseMove, false ); - viewer.renderer.domElement.addEventListener( 'mousedown', onMouseClick, false ); - - viewer.addEventListener("update", () => { - - for(const image of orientedImages){ - const world = image.mesh.matrixWorld; - const {width, height} = image; - const aspect = width / height; - - const camera = viewer.scene.getActiveCamera(); - - const imgPos = image.mesh.getWorldPosition(new Vector3()); - const camPos = camera.position; - const d = camPos.distanceTo(imgPos); - - const minSize = 1; // in degrees of fov - const a = MathUtils.degToRad(minSize); - let r = d * Math.tan(a); - r = Math.max(r, 1); - - - image.mesh.scale.set(r * aspect, r, 1); - image.line.scale.set(r * aspect, r, 1); - - image.mesh.material.uniforms.uNear.value = camera.near; - - } - - }); - - const images = new OrientedImages(); - images.node = sceneNode; - images.cameraParamsPath = cameraParamsPath; - images.imageParamsPath = imageParamsPath; - images.cameraParams = cameraParams; - images.imageParams = imageParams; - images.images = orientedImages; - - Potree.debug.moveToImage = moveToImage; - - return images; - } - } - - let sg = new SphereGeometry(1, 8, 8); - let sgHigh = new SphereGeometry(1, 128, 128); - - let sm = new MeshBasicMaterial({side: BackSide}); - let smHovered = new MeshBasicMaterial({side: BackSide, color: 0xff0000}); - - let raycaster = new Raycaster(); - let currentlyHovered = null; - - let previousView = { - controls: null, - position: null, - target: null, - }; - - class Image360{ - - constructor(file, time, longitude, latitude, altitude, course, pitch, roll){ - this.file = file; - this.time = time; - this.longitude = longitude; - this.latitude = latitude; - this.altitude = altitude; - this.course = course; - this.pitch = pitch; - this.roll = roll; - this.mesh = null; - } - }; - - class Images360 extends EventDispatcher{ - - constructor(viewer){ - super(); - - this.viewer = viewer; - - this.selectingEnabled = true; - - this.images = []; - this.node = new Object3D(); - - this.sphere = new Mesh(sgHigh, sm); - this.sphere.visible = false; - this.sphere.scale.set(1000, 1000, 1000); - this.node.add(this.sphere); - this._visible = true; - // this.node.add(label); - - this.focusedImage = null; - - let elUnfocus = document.createElement("input"); - elUnfocus.type = "button"; - elUnfocus.value = "unfocus"; - elUnfocus.style.position = "absolute"; - elUnfocus.style.right = "10px"; - elUnfocus.style.bottom = "10px"; - elUnfocus.style.zIndex = "10000"; - elUnfocus.style.fontSize = "2em"; - elUnfocus.addEventListener("click", () => this.unfocus()); - this.elUnfocus = elUnfocus; - - this.domRoot = viewer.renderer.domElement.parentElement; - this.domRoot.appendChild(elUnfocus); - this.elUnfocus.style.display = "none"; - - viewer.addEventListener("update", () => { - this.update(viewer); - }); - viewer.inputHandler.addInputListener(this); - - this.addEventListener("mousedown", () => { - if(currentlyHovered){ - this.focus(currentlyHovered.image360); - } - }); - - }; - - set visible(visible){ - if(this._visible === visible){ - return; - } - - - for(const image of this.images){ - image.mesh.visible = visible && (this.focusedImage == null); - } - - this.sphere.visible = visible && (this.focusedImage != null); - this._visible = visible; - this.dispatchEvent({ - type: "visibility_changed", - images: this, - }); - } - - get visible(){ - return this._visible; - } - - focus(image360){ - if(this.focusedImage !== null){ - this.unfocus(); - } - - previousView = { - controls: this.viewer.controls, - position: this.viewer.scene.view.position.clone(), - target: viewer.scene.view.getPivot(), - }; - - this.viewer.setControls(this.viewer.orbitControls); - this.viewer.orbitControls.doubleClockZoomEnabled = false; - - for(let image of this.images){ - image.mesh.visible = false; - } - - this.selectingEnabled = false; - - this.sphere.visible = false; - - this.load(image360).then( () => { - this.sphere.visible = true; - this.sphere.material.map = image360.texture; - this.sphere.material.needsUpdate = true; - }); - - { // orientation - let {course, pitch, roll} = image360; - this.sphere.rotation.set( - MathUtils.degToRad(+roll + 90), - MathUtils.degToRad(-pitch), - MathUtils.degToRad(-course + 90), - "ZYX" - ); - } - - this.sphere.position.set(...image360.position); - - let target = new Vector3(...image360.position); - let dir = target.clone().sub(viewer.scene.view.position).normalize(); - let move = dir.multiplyScalar(0.000001); - let newCamPos = target.clone().sub(move); - - viewer.scene.view.setView( - newCamPos, - target, - 500 - ); - - this.focusedImage = image360; - - this.elUnfocus.style.display = ""; - } - - unfocus(){ - this.selectingEnabled = true; - - for(let image of this.images){ - image.mesh.visible = true; - } - - let image = this.focusedImage; - - if(image === null){ - return; - } - - - this.sphere.material.map = null; - this.sphere.material.needsUpdate = true; - this.sphere.visible = false; - - let pos = viewer.scene.view.position; - let target = viewer.scene.view.getPivot(); - let dir = target.clone().sub(pos).normalize(); - let move = dir.multiplyScalar(10); - let newCamPos = target.clone().sub(move); - - viewer.orbitControls.doubleClockZoomEnabled = true; - viewer.setControls(previousView.controls); - - viewer.scene.view.setView( - previousView.position, - previousView.target, - 500 - ); - - - this.focusedImage = null; - - this.elUnfocus.style.display = "none"; - } - - load(image360){ - - return new Promise(resolve => { - let texture = new TextureLoader().load(image360.file, resolve); - texture.wrapS = RepeatWrapping; - texture.repeat.x = -1; - - image360.texture = texture; - }); - - } - - handleHovering(){ - let mouse = viewer.inputHandler.mouse; - let camera = viewer.scene.getActiveCamera(); - let domElement = viewer.renderer.domElement; - - let ray = Potree.Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight); - - // let tStart = performance.now(); - raycaster.ray.copy(ray); - let intersections = raycaster.intersectObjects(this.node.children); - - if(intersections.length === 0){ - // label.visible = false; - - return; - } - - let intersection = intersections[0]; - currentlyHovered = intersection.object; - currentlyHovered.material = smHovered; - - //label.visible = true; - //label.setText(currentlyHovered.image360.file); - //currentlyHovered.getWorldPosition(label.position); - } - - update(){ - - let {viewer} = this; - - if(currentlyHovered){ - currentlyHovered.material = sm; - currentlyHovered = null; - } - - if(this.selectingEnabled){ - this.handleHovering(); - } - - } - - }; - - - class Images360Loader{ - - static async load(url, viewer, params = {}){ - - if(!params.transform){ - params.transform = { - forward: a => a, - }; - } - - let response = await fetch(`${url}/coordinates.txt`); - let text = await response.text(); - - let lines = text.split(/\r?\n/); - let coordinateLines = lines.slice(1); - - let images360 = new Images360(viewer); - - for(let line of coordinateLines){ - - if(line.trim().length === 0){ - continue; - } - - let tokens = line.split(/\t/); - - let [filename, time, long, lat, alt, course, pitch, roll] = tokens; - time = parseFloat(time); - long = parseFloat(long); - lat = parseFloat(lat); - alt = parseFloat(alt); - course = parseFloat(course); - pitch = parseFloat(pitch); - roll = parseFloat(roll); - - filename = filename.replace(/"/g, ""); - let file = `${url}/${filename}`; - - let image360 = new Image360(file, time, long, lat, alt, course, pitch, roll); - - let xy = params.transform.forward([long, lat]); - let position = [...xy, alt]; - image360.position = position; - - images360.images.push(image360); - } - - Images360Loader.createSceneNodes(images360, params.transform); - - return images360; - - } - - static createSceneNodes(images360, transform){ - - for(let image360 of images360.images){ - let {longitude, latitude, altitude} = image360; - let xy = transform.forward([longitude, latitude]); - - let mesh = new Mesh(sg, sm); - mesh.position.set(...xy, altitude); - mesh.scale.set(1, 1, 1); - mesh.material.transparent = true; - mesh.material.opacity = 0.75; - mesh.image360 = image360; - - { // orientation - var {course, pitch, roll} = image360; - mesh.rotation.set( - MathUtils.degToRad(+roll + 90), - MathUtils.degToRad(-pitch), - MathUtils.degToRad(-course + 90), - "ZYX" - ); - } - - images360.node.add(mesh); - - image360.mesh = mesh; - } - } - - - + + }; // This is a generated file. Do not edit. @@ -80592,8 +80592,10 @@ ENDSEC let annotation = new Annotation({ position: [589748.270, 231444.540, 753.675], - title: "Annotation Title", - description: `Annotation Description` + title: args.title || "Annotation Title", + description: args.description || `Annotation Description`, + cameraPosition: args.cameraPosition || undefined, + cameraTarget: args.cameraTarget || undefined }); this.dispatchEvent({type: 'start_inserting_annotation', annotation: annotation}); @@ -82892,4746 +82894,4746 @@ ENDSEC lightNode.add( lightNode.target ); break; - default: - throw new Error( 'THREE.GLTFLoader: Unexpected light type: ' + lightDef.type ); + default: + throw new Error( 'THREE.GLTFLoader: Unexpected light type: ' + lightDef.type ); + + } + + // Some lights (e.g. spot) default to a position other than the origin. Reset the position + // here, because node-level parsing will only override position if explicitly specified. + lightNode.position.set( 0, 0, 0 ); + + lightNode.decay = 2; + + if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; + + lightNode.name = parser.createUniqueName( lightDef.name || ( 'light_' + lightIndex ) ); + + dependency = Promise.resolve( lightNode ); + + parser.cache.add( cacheKey, dependency ); + + return dependency; + + }; + + GLTFLightsExtension.prototype.createNodeAttachment = function ( nodeIndex ) { + + var self = this; + var parser = this.parser; + var json = parser.json; + var nodeDef = json.nodes[ nodeIndex ]; + var lightDef = ( nodeDef.extensions && nodeDef.extensions[ this.name ] ) || {}; + var lightIndex = lightDef.light; + + if ( lightIndex === undefined ) return null; + + return this._loadLight( lightIndex ).then( function ( light ) { + + return parser._getNodeRef( self.cache, lightIndex, light ); + + } ); + + }; + + /** + * Unlit Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit + */ + function GLTFMaterialsUnlitExtension() { + + this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; + + } + + GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { + + return MeshBasicMaterial; + + }; + + GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, materialDef, parser ) { + + var pending = []; + + materialParams.color = new Color( 1.0, 1.0, 1.0 ); + materialParams.opacity = 1.0; + + var metallicRoughness = materialDef.pbrMetallicRoughness; + + if ( metallicRoughness ) { + + if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { + + var array = metallicRoughness.baseColorFactor; + + materialParams.color.fromArray( array ); + materialParams.opacity = array[ 3 ]; + + } + + if ( metallicRoughness.baseColorTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); + + } + + } + + return Promise.all( pending ); + + }; + + /** + * Clearcoat Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat + */ + function GLTFMaterialsClearcoatExtension( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT; + + } + + GLTFMaterialsClearcoatExtension.prototype.getMaterialType = function ( materialIndex ) { + + var parser = this.parser; + var materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + }; + + GLTFMaterialsClearcoatExtension.prototype.extendMaterialParams = function ( materialIndex, materialParams ) { + + var parser = this.parser; + var materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + var pending = []; + + var extension = materialDef.extensions[ this.name ]; + + if ( extension.clearcoatFactor !== undefined ) { + + materialParams.clearcoat = extension.clearcoatFactor; + + } + + if ( extension.clearcoatTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) ); + + } + + if ( extension.clearcoatRoughnessFactor !== undefined ) { + + materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor; + + } + + if ( extension.clearcoatRoughnessTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) ); + + } + + if ( extension.clearcoatNormalTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) ); + + if ( extension.clearcoatNormalTexture.scale !== undefined ) { + + var scale = extension.clearcoatNormalTexture.scale; + + materialParams.clearcoatNormalScale = new Vector2( scale, scale ); + + } + + } + + return Promise.all( pending ); + + }; + + /** + * Transmission Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission + * Draft: https://github.com/KhronosGroup/glTF/pull/1698 + */ + function GLTFMaterialsTransmissionExtension( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION; + + } + + GLTFMaterialsTransmissionExtension.prototype.getMaterialType = function ( materialIndex ) { + + var parser = this.parser; + var materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + + return MeshPhysicalMaterial; + + }; + + GLTFMaterialsTransmissionExtension.prototype.extendMaterialParams = function ( materialIndex, materialParams ) { + + var parser = this.parser; + var materialDef = parser.json.materials[ materialIndex ]; + + if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + + return Promise.resolve(); + + } + + var pending = []; + + var extension = materialDef.extensions[ this.name ]; + + if ( extension.transmissionFactor !== undefined ) { + + materialParams.transmission = extension.transmissionFactor; + + } + + if ( extension.transmissionTexture !== undefined ) { + + pending.push( parser.assignTexture( materialParams, 'transmissionMap', extension.transmissionTexture ) ); + + } + + return Promise.all( pending ); + + }; + + /** + * BasisU Texture Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu + */ + function GLTFTextureBasisUExtension( parser ) { + + this.parser = parser; + this.name = EXTENSIONS.KHR_TEXTURE_BASISU; - } + } - // Some lights (e.g. spot) default to a position other than the origin. Reset the position - // here, because node-level parsing will only override position if explicitly specified. - lightNode.position.set( 0, 0, 0 ); + GLTFTextureBasisUExtension.prototype.loadTexture = function ( textureIndex ) { - lightNode.decay = 2; + var parser = this.parser; + var json = parser.json; - if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; + var textureDef = json.textures[ textureIndex ]; - lightNode.name = parser.createUniqueName( lightDef.name || ( 'light_' + lightIndex ) ); + if ( ! textureDef.extensions || ! textureDef.extensions[ this.name ] ) { - dependency = Promise.resolve( lightNode ); + return null; - parser.cache.add( cacheKey, dependency ); + } - return dependency; + var extension = textureDef.extensions[ this.name ]; + var source = json.images[ extension.source ]; + var loader = parser.options.ktx2Loader; - }; + if ( ! loader ) { - GLTFLightsExtension.prototype.createNodeAttachment = function ( nodeIndex ) { + if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { - var self = this; - var parser = this.parser; - var json = parser.json; - var nodeDef = json.nodes[ nodeIndex ]; - var lightDef = ( nodeDef.extensions && nodeDef.extensions[ this.name ] ) || {}; - var lightIndex = lightDef.light; + throw new Error( 'THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures' ); - if ( lightIndex === undefined ) return null; + } else { - return this._loadLight( lightIndex ).then( function ( light ) { + // Assumes that the extension is optional and that a fallback texture is present + return null; - return parser._getNodeRef( self.cache, lightIndex, light ); + } - } ); + } + + return parser.loadTextureImage( textureIndex, source, loader ); }; /** - * Unlit Materials Extension + * WebP Texture Extension * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp */ - function GLTFMaterialsUnlitExtension() { + function GLTFTextureWebPExtension( parser ) { - this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; + this.parser = parser; + this.name = EXTENSIONS.EXT_TEXTURE_WEBP; + this.isSupported = null; } - GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { + GLTFTextureWebPExtension.prototype.loadTexture = function ( textureIndex ) { - return MeshBasicMaterial; + var name = this.name; + var parser = this.parser; + var json = parser.json; - }; + var textureDef = json.textures[ textureIndex ]; - GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, materialDef, parser ) { + if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) { - var pending = []; + return null; - materialParams.color = new Color( 1.0, 1.0, 1.0 ); - materialParams.opacity = 1.0; + } - var metallicRoughness = materialDef.pbrMetallicRoughness; + var extension = textureDef.extensions[ name ]; + var source = json.images[ extension.source ]; + var loader = source.uri ? parser.options.manager.getHandler( source.uri ) : parser.textureLoader; - if ( metallicRoughness ) { + return this.detectSupport().then( function ( isSupported ) { - if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { + if ( isSupported ) return parser.loadTextureImage( textureIndex, source, loader ); - var array = metallicRoughness.baseColorFactor; + if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) { - materialParams.color.fromArray( array ); - materialParams.opacity = array[ 3 ]; + throw new Error( 'THREE.GLTFLoader: WebP required by asset but unsupported.' ); } - if ( metallicRoughness.baseColorTexture !== undefined ) { + // Fall back to PNG or JPEG. + return parser.loadTexture( textureIndex ); - pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); + } ); - } + }; + + GLTFTextureWebPExtension.prototype.detectSupport = function () { + + if ( ! this.isSupported ) { + + this.isSupported = new Promise( function ( resolve ) { + + var image = new Image(); + + // Lossy test image. Support for lossy images doesn't guarantee support for all + // WebP images, unfortunately. + image.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA'; + + image.onload = image.onerror = function () { + + resolve( image.height === 1 ); + + }; + + } ); } - return Promise.all( pending ); + return this.isSupported; }; /** - * Clearcoat Materials Extension - * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat - */ - function GLTFMaterialsClearcoatExtension( parser ) { + * meshopt BufferView Compression Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression + */ + function GLTFMeshoptCompression( parser ) { + this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION; this.parser = parser; - this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT; } - GLTFMaterialsClearcoatExtension.prototype.getMaterialType = function ( materialIndex ) { + GLTFMeshoptCompression.prototype.loadBufferView = function ( index ) { - var parser = this.parser; - var materialDef = parser.json.materials[ materialIndex ]; + var json = this.parser.json; + var bufferView = json.bufferViews[ index ]; - if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + if ( bufferView.extensions && bufferView.extensions[ this.name ] ) { - return MeshPhysicalMaterial; + var extensionDef = bufferView.extensions[ this.name ]; - }; + var buffer = this.parser.getDependency( 'buffer', extensionDef.buffer ); + var decoder = this.parser.options.meshoptDecoder; - GLTFMaterialsClearcoatExtension.prototype.extendMaterialParams = function ( materialIndex, materialParams ) { + if ( ! decoder || ! decoder.supported ) { - var parser = this.parser; - var materialDef = parser.json.materials[ materialIndex ]; + if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { - if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + throw new Error( 'THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files' ); - return Promise.resolve(); + } else { - } + // Assumes that the extension is optional and that fallback buffer data is present + return null; - var pending = []; + } - var extension = materialDef.extensions[ this.name ]; + } - if ( extension.clearcoatFactor !== undefined ) { + return Promise.all( [ buffer, decoder.ready ] ).then( function ( res ) { - materialParams.clearcoat = extension.clearcoatFactor; + var byteOffset = extensionDef.byteOffset || 0; + var byteLength = extensionDef.byteLength || 0; - } + var count = extensionDef.count; + var stride = extensionDef.byteStride; - if ( extension.clearcoatTexture !== undefined ) { + var result = new ArrayBuffer( count * stride ); + var source = new Uint8Array( res[ 0 ], byteOffset, byteLength ); - pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) ); + decoder.decodeGltfBuffer( new Uint8Array( result ), count, stride, source, extensionDef.mode, extensionDef.filter ); + return result; - } + } ); - if ( extension.clearcoatRoughnessFactor !== undefined ) { + } else { - materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor; + return null; } - if ( extension.clearcoatRoughnessTexture !== undefined ) { + }; - pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) ); + /* BINARY EXTENSION */ + var BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; + var BINARY_EXTENSION_HEADER_LENGTH = 12; + var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; + + function GLTFBinaryExtension( data ) { + + this.name = EXTENSIONS.KHR_BINARY_GLTF; + this.content = null; + this.body = null; + + var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); + + this.header = { + magic: LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), + version: headerView.getUint32( 4, true ), + length: headerView.getUint32( 8, true ) + }; + + if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { + + throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' ); + + } else if ( this.header.version < 2.0 ) { + + throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' ); } - if ( extension.clearcoatNormalTexture !== undefined ) { + var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); + var chunkIndex = 0; - pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) ); + while ( chunkIndex < chunkView.byteLength ) { - if ( extension.clearcoatNormalTexture.scale !== undefined ) { + var chunkLength = chunkView.getUint32( chunkIndex, true ); + chunkIndex += 4; - var scale = extension.clearcoatNormalTexture.scale; + var chunkType = chunkView.getUint32( chunkIndex, true ); + chunkIndex += 4; - materialParams.clearcoatNormalScale = new Vector2( scale, scale ); + if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { + + var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); + this.content = LoaderUtils.decodeText( contentArray ); + + } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { + + var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; + this.body = data.slice( byteOffset, byteOffset + chunkLength ); } + // Clients must ignore chunks with unknown types. + + chunkIndex += chunkLength; + } - return Promise.all( pending ); + if ( this.content === null ) { - }; + throw new Error( 'THREE.GLTFLoader: JSON content not found.' ); + + } + + } /** - * Transmission Materials Extension + * DRACO Mesh Compression Extension * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission - * Draft: https://github.com/KhronosGroup/glTF/pull/1698 + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression */ - function GLTFMaterialsTransmissionExtension( parser ) { + function GLTFDracoMeshCompressionExtension( json, dracoLoader ) { + + if ( ! dracoLoader ) { + + throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); + + } - this.parser = parser; - this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION; + this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; + this.json = json; + this.dracoLoader = dracoLoader; + this.dracoLoader.preload(); } - GLTFMaterialsTransmissionExtension.prototype.getMaterialType = function ( materialIndex ) { + GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) { - var parser = this.parser; - var materialDef = parser.json.materials[ materialIndex ]; + var json = this.json; + var dracoLoader = this.dracoLoader; + var bufferViewIndex = primitive.extensions[ this.name ].bufferView; + var gltfAttributeMap = primitive.extensions[ this.name ].attributes; + var threeAttributeMap = {}; + var attributeNormalizedMap = {}; + var attributeTypeMap = {}; - if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; + for ( var attributeName in gltfAttributeMap ) { - return MeshPhysicalMaterial; + var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); - }; + threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; - GLTFMaterialsTransmissionExtension.prototype.extendMaterialParams = function ( materialIndex, materialParams ) { + } - var parser = this.parser; - var materialDef = parser.json.materials[ materialIndex ]; + for ( attributeName in primitive.attributes ) { - if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { + var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); - return Promise.resolve(); + if ( gltfAttributeMap[ attributeName ] !== undefined ) { + + var accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; + var componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; + + attributeTypeMap[ threeAttributeName ] = componentType; + attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; + + } } - var pending = []; + return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { - var extension = materialDef.extensions[ this.name ]; + return new Promise( function ( resolve ) { - if ( extension.transmissionFactor !== undefined ) { + dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { - materialParams.transmission = extension.transmissionFactor; + for ( var attributeName in geometry.attributes ) { - } + var attribute = geometry.attributes[ attributeName ]; + var normalized = attributeNormalizedMap[ attributeName ]; - if ( extension.transmissionTexture !== undefined ) { + if ( normalized !== undefined ) attribute.normalized = normalized; - pending.push( parser.assignTexture( materialParams, 'transmissionMap', extension.transmissionTexture ) ); + } - } + resolve( geometry ); - return Promise.all( pending ); + }, threeAttributeMap, attributeTypeMap ); + + } ); + + } ); }; /** - * BasisU Texture Extension + * Texture Transform Extension * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform */ - function GLTFTextureBasisUExtension( parser ) { + function GLTFTextureTransformExtension() { - this.parser = parser; - this.name = EXTENSIONS.KHR_TEXTURE_BASISU; + this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; } - GLTFTextureBasisUExtension.prototype.loadTexture = function ( textureIndex ) { - - var parser = this.parser; - var json = parser.json; + GLTFTextureTransformExtension.prototype.extendTexture = function ( texture, transform ) { - var textureDef = json.textures[ textureIndex ]; + texture = texture.clone(); - if ( ! textureDef.extensions || ! textureDef.extensions[ this.name ] ) { + if ( transform.offset !== undefined ) { - return null; + texture.offset.fromArray( transform.offset ); } - var extension = textureDef.extensions[ this.name ]; - var source = json.images[ extension.source ]; - var loader = parser.options.ktx2Loader; + if ( transform.rotation !== undefined ) { - if ( ! loader ) { + texture.rotation = transform.rotation; - if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { + } - throw new Error( 'THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures' ); + if ( transform.scale !== undefined ) { - } else { + texture.repeat.fromArray( transform.scale ); - // Assumes that the extension is optional and that a fallback texture is present - return null; + } - } + if ( transform.texCoord !== undefined ) { + + console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' ); } - return parser.loadTextureImage( textureIndex, source, loader ); + texture.needsUpdate = true; + + return texture; }; /** - * WebP Texture Extension + * Specular-Glossiness Extension * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness */ - function GLTFTextureWebPExtension( parser ) { - - this.parser = parser; - this.name = EXTENSIONS.EXT_TEXTURE_WEBP; - this.isSupported = null; - - } - - GLTFTextureWebPExtension.prototype.loadTexture = function ( textureIndex ) { - - var name = this.name; - var parser = this.parser; - var json = parser.json; - - var textureDef = json.textures[ textureIndex ]; - - if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) { - - return null; - - } - - var extension = textureDef.extensions[ name ]; - var source = json.images[ extension.source ]; - var loader = source.uri ? parser.options.manager.getHandler( source.uri ) : parser.textureLoader; - - return this.detectSupport().then( function ( isSupported ) { - if ( isSupported ) return parser.loadTextureImage( textureIndex, source, loader ); + /** + * A sub class of StandardMaterial with some of the functionality + * changed via the `onBeforeCompile` callback + * @pailhead + */ - if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) { + function GLTFMeshStandardSGMaterial( params ) { - throw new Error( 'THREE.GLTFLoader: WebP required by asset but unsupported.' ); + MeshStandardMaterial.call( this ); - } + this.isGLTFSpecularGlossinessMaterial = true; - // Fall back to PNG or JPEG. - return parser.loadTexture( textureIndex ); + //various chunks that need replacing + var specularMapParsFragmentChunk = [ + '#ifdef USE_SPECULARMAP', + ' uniform sampler2D specularMap;', + '#endif' + ].join( '\n' ); - } ); + var glossinessMapParsFragmentChunk = [ + '#ifdef USE_GLOSSINESSMAP', + ' uniform sampler2D glossinessMap;', + '#endif' + ].join( '\n' ); - }; + var specularMapFragmentChunk = [ + 'vec3 specularFactor = specular;', + '#ifdef USE_SPECULARMAP', + ' vec4 texelSpecular = texture2D( specularMap, vUv );', + ' texelSpecular = sRGBToLinear( texelSpecular );', + ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', + ' specularFactor *= texelSpecular.rgb;', + '#endif' + ].join( '\n' ); - GLTFTextureWebPExtension.prototype.detectSupport = function () { + var glossinessMapFragmentChunk = [ + 'float glossinessFactor = glossiness;', + '#ifdef USE_GLOSSINESSMAP', + ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', + ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', + ' glossinessFactor *= texelGlossiness.a;', + '#endif' + ].join( '\n' ); - if ( ! this.isSupported ) { + var lightPhysicalFragmentChunk = [ + 'PhysicalMaterial material;', + 'material.diffuseColor = diffuseColor.rgb * ( 1. - max( specularFactor.r, max( specularFactor.g, specularFactor.b ) ) );', + 'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );', + 'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );', + 'material.specularRoughness = max( 1.0 - glossinessFactor, 0.0525 ); // 0.0525 corresponds to the base mip of a 256 cubemap.', + 'material.specularRoughness += geometryRoughness;', + 'material.specularRoughness = min( material.specularRoughness, 1.0 );', + 'material.specularColor = specularFactor;', + ].join( '\n' ); - this.isSupported = new Promise( function ( resolve ) { + var uniforms = { + specular: { value: new Color().setHex( 0xffffff ) }, + glossiness: { value: 1 }, + specularMap: { value: null }, + glossinessMap: { value: null } + }; - var image = new Image(); + this._extraUniforms = uniforms; - // Lossy test image. Support for lossy images doesn't guarantee support for all - // WebP images, unfortunately. - image.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA'; + this.onBeforeCompile = function ( shader ) { - image.onload = image.onerror = function () { + for ( var uniformName in uniforms ) { - resolve( image.height === 1 ); + shader.uniforms[ uniformName ] = uniforms[ uniformName ]; - }; + } - } ); + shader.fragmentShader = shader.fragmentShader + .replace( 'uniform float roughness;', 'uniform vec3 specular;' ) + .replace( 'uniform float metalness;', 'uniform float glossiness;' ) + .replace( '#include ', specularMapParsFragmentChunk ) + .replace( '#include ', glossinessMapParsFragmentChunk ) + .replace( '#include ', specularMapFragmentChunk ) + .replace( '#include ', glossinessMapFragmentChunk ) + .replace( '#include ', lightPhysicalFragmentChunk ); - } + }; - return this.isSupported; + Object.defineProperties( this, { - }; + specular: { + get: function () { - /** - * meshopt BufferView Compression Extension - * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression - */ - function GLTFMeshoptCompression( parser ) { + return uniforms.specular.value; - this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION; - this.parser = parser; + }, + set: function ( v ) { - } + uniforms.specular.value = v; - GLTFMeshoptCompression.prototype.loadBufferView = function ( index ) { + } + }, - var json = this.parser.json; - var bufferView = json.bufferViews[ index ]; + specularMap: { + get: function () { - if ( bufferView.extensions && bufferView.extensions[ this.name ] ) { + return uniforms.specularMap.value; - var extensionDef = bufferView.extensions[ this.name ]; + }, + set: function ( v ) { - var buffer = this.parser.getDependency( 'buffer', extensionDef.buffer ); - var decoder = this.parser.options.meshoptDecoder; + uniforms.specularMap.value = v; - if ( ! decoder || ! decoder.supported ) { + if ( v ) { - if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { + this.defines.USE_SPECULARMAP = ''; // USE_UV is set by the renderer for specular maps - throw new Error( 'THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files' ); + } else { - } else { + delete this.defines.USE_SPECULARMAP; - // Assumes that the extension is optional and that fallback buffer data is present - return null; + } } + }, - } - - return Promise.all( [ buffer, decoder.ready ] ).then( function ( res ) { + glossiness: { + get: function () { - var byteOffset = extensionDef.byteOffset || 0; - var byteLength = extensionDef.byteLength || 0; + return uniforms.glossiness.value; - var count = extensionDef.count; - var stride = extensionDef.byteStride; + }, + set: function ( v ) { - var result = new ArrayBuffer( count * stride ); - var source = new Uint8Array( res[ 0 ], byteOffset, byteLength ); + uniforms.glossiness.value = v; - decoder.decodeGltfBuffer( new Uint8Array( result ), count, stride, source, extensionDef.mode, extensionDef.filter ); - return result; + } + }, - } ); + glossinessMap: { + get: function () { - } else { + return uniforms.glossinessMap.value; - return null; + }, + set: function ( v ) { - } + uniforms.glossinessMap.value = v; - }; + if ( v ) { - /* BINARY EXTENSION */ - var BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; - var BINARY_EXTENSION_HEADER_LENGTH = 12; - var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; + this.defines.USE_GLOSSINESSMAP = ''; + this.defines.USE_UV = ''; - function GLTFBinaryExtension( data ) { + } else { - this.name = EXTENSIONS.KHR_BINARY_GLTF; - this.content = null; - this.body = null; + delete this.defines.USE_GLOSSINESSMAP; + delete this.defines.USE_UV; - var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); + } - this.header = { - magic: LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), - version: headerView.getUint32( 4, true ), - length: headerView.getUint32( 8, true ) - }; + } + } - if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { + } ); - throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' ); + delete this.metalness; + delete this.roughness; + delete this.metalnessMap; + delete this.roughnessMap; - } else if ( this.header.version < 2.0 ) { + this.setValues( params ); - throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' ); + } - } + GLTFMeshStandardSGMaterial.prototype = Object.create( MeshStandardMaterial.prototype ); + GLTFMeshStandardSGMaterial.prototype.constructor = GLTFMeshStandardSGMaterial; - var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); - var chunkIndex = 0; + GLTFMeshStandardSGMaterial.prototype.copy = function ( source ) { - while ( chunkIndex < chunkView.byteLength ) { + MeshStandardMaterial.prototype.copy.call( this, source ); + this.specularMap = source.specularMap; + this.specular.copy( source.specular ); + this.glossinessMap = source.glossinessMap; + this.glossiness = source.glossiness; + delete this.metalness; + delete this.roughness; + delete this.metalnessMap; + delete this.roughnessMap; + return this; - var chunkLength = chunkView.getUint32( chunkIndex, true ); - chunkIndex += 4; + }; - var chunkType = chunkView.getUint32( chunkIndex, true ); - chunkIndex += 4; + function GLTFMaterialsPbrSpecularGlossinessExtension() { - if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { + return { - var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); - this.content = LoaderUtils.decodeText( contentArray ); + name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, - } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { + specularGlossinessParams: [ + 'color', + 'map', + 'lightMap', + 'lightMapIntensity', + 'aoMap', + 'aoMapIntensity', + 'emissive', + 'emissiveIntensity', + 'emissiveMap', + 'bumpMap', + 'bumpScale', + 'normalMap', + 'normalMapType', + 'displacementMap', + 'displacementScale', + 'displacementBias', + 'specularMap', + 'specular', + 'glossinessMap', + 'glossiness', + 'alphaMap', + 'envMap', + 'envMapIntensity', + 'refractionRatio', + ], - var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; - this.body = data.slice( byteOffset, byteOffset + chunkLength ); + getMaterialType: function () { - } + return GLTFMeshStandardSGMaterial; - // Clients must ignore chunks with unknown types. + }, - chunkIndex += chunkLength; + extendParams: function ( materialParams, materialDef, parser ) { - } + var pbrSpecularGlossiness = materialDef.extensions[ this.name ]; - if ( this.content === null ) { + materialParams.color = new Color( 1.0, 1.0, 1.0 ); + materialParams.opacity = 1.0; - throw new Error( 'THREE.GLTFLoader: JSON content not found.' ); + var pending = []; - } + if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) { - } + var array = pbrSpecularGlossiness.diffuseFactor; - /** - * DRACO Mesh Compression Extension - * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression - */ - function GLTFDracoMeshCompressionExtension( json, dracoLoader ) { + materialParams.color.fromArray( array ); + materialParams.opacity = array[ 3 ]; - if ( ! dracoLoader ) { + } - throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); + if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) { - } + pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture ) ); - this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; - this.json = json; - this.dracoLoader = dracoLoader; - this.dracoLoader.preload(); + } - } + materialParams.emissive = new Color( 0.0, 0.0, 0.0 ); + materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; + materialParams.specular = new Color( 1.0, 1.0, 1.0 ); - GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) { + if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) { - var json = this.json; - var dracoLoader = this.dracoLoader; - var bufferViewIndex = primitive.extensions[ this.name ].bufferView; - var gltfAttributeMap = primitive.extensions[ this.name ].attributes; - var threeAttributeMap = {}; - var attributeNormalizedMap = {}; - var attributeTypeMap = {}; + materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor ); - for ( var attributeName in gltfAttributeMap ) { + } - var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); + if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) { - threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; + var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; + pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) ); + pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef ) ); - } + } - for ( attributeName in primitive.attributes ) { + return Promise.all( pending ); - var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); + }, - if ( gltfAttributeMap[ attributeName ] !== undefined ) { + createMaterial: function ( materialParams ) { - var accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; - var componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; + var material = new GLTFMeshStandardSGMaterial( materialParams ); + material.fog = true; - attributeTypeMap[ threeAttributeName ] = componentType; - attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; + material.color = materialParams.color; - } + material.map = materialParams.map === undefined ? null : materialParams.map; - } + material.lightMap = null; + material.lightMapIntensity = 1.0; - return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { + material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap; + material.aoMapIntensity = 1.0; - return new Promise( function ( resolve ) { + material.emissive = materialParams.emissive; + material.emissiveIntensity = 1.0; + material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap; - dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { + material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap; + material.bumpScale = 1; - for ( var attributeName in geometry.attributes ) { + material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap; + material.normalMapType = TangentSpaceNormalMap; - var attribute = geometry.attributes[ attributeName ]; - var normalized = attributeNormalizedMap[ attributeName ]; + if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale; - if ( normalized !== undefined ) attribute.normalized = normalized; + material.displacementMap = null; + material.displacementScale = 1; + material.displacementBias = 0; - } + material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap; + material.specular = materialParams.specular; - resolve( geometry ); + material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap; + material.glossiness = materialParams.glossiness; - }, threeAttributeMap, attributeTypeMap ); + material.alphaMap = null; - } ); + material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap; + material.envMapIntensity = 1.0; - } ); + material.refractionRatio = 0.98; - }; + return material; - /** - * Texture Transform Extension - * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform - */ - function GLTFTextureTransformExtension() { + }, - this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; + }; } - GLTFTextureTransformExtension.prototype.extendTexture = function ( texture, transform ) { + /** + * Mesh Quantization Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization + */ + function GLTFMeshQuantizationExtension() { - texture = texture.clone(); + this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; - if ( transform.offset !== undefined ) { + } - texture.offset.fromArray( transform.offset ); + /*********************************/ + /********** INTERPOLATION ********/ + /*********************************/ - } + // Spline Interpolation + // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation + function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) { - if ( transform.rotation !== undefined ) { + Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer ); - texture.rotation = transform.rotation; + } - } + GLTFCubicSplineInterpolant.prototype = Object.create( Interpolant.prototype ); + GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; - if ( transform.scale !== undefined ) { + GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function ( index ) { - texture.repeat.fromArray( transform.scale ); + // Copies a sample value to the result buffer. See description of glTF + // CUBICSPLINE values layout in interpolate_() function below. - } + var result = this.resultBuffer, + values = this.sampleValues, + valueSize = this.valueSize, + offset = index * valueSize * 3 + valueSize; - if ( transform.texCoord !== undefined ) { + for ( var i = 0; i !== valueSize; i ++ ) { - console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' ); + result[ i ] = values[ offset + i ]; } - texture.needsUpdate = true; - - return texture; + return result; }; - /** - * Specular-Glossiness Extension - * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness - */ + GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; - /** - * A sub class of StandardMaterial with some of the functionality - * changed via the `onBeforeCompile` callback - * @pailhead - */ + GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; - function GLTFMeshStandardSGMaterial( params ) { + GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) { - MeshStandardMaterial.call( this ); + var result = this.resultBuffer; + var values = this.sampleValues; + var stride = this.valueSize; - this.isGLTFSpecularGlossinessMaterial = true; + var stride2 = stride * 2; + var stride3 = stride * 3; - //various chunks that need replacing - var specularMapParsFragmentChunk = [ - '#ifdef USE_SPECULARMAP', - ' uniform sampler2D specularMap;', - '#endif' - ].join( '\n' ); + var td = t1 - t0; - var glossinessMapParsFragmentChunk = [ - '#ifdef USE_GLOSSINESSMAP', - ' uniform sampler2D glossinessMap;', - '#endif' - ].join( '\n' ); + var p = ( t - t0 ) / td; + var pp = p * p; + var ppp = pp * p; - var specularMapFragmentChunk = [ - 'vec3 specularFactor = specular;', - '#ifdef USE_SPECULARMAP', - ' vec4 texelSpecular = texture2D( specularMap, vUv );', - ' texelSpecular = sRGBToLinear( texelSpecular );', - ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', - ' specularFactor *= texelSpecular.rgb;', - '#endif' - ].join( '\n' ); + var offset1 = i1 * stride3; + var offset0 = offset1 - stride3; - var glossinessMapFragmentChunk = [ - 'float glossinessFactor = glossiness;', - '#ifdef USE_GLOSSINESSMAP', - ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', - ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', - ' glossinessFactor *= texelGlossiness.a;', - '#endif' - ].join( '\n' ); + var s2 = - 2 * ppp + 3 * pp; + var s3 = ppp - pp; + var s0 = 1 - s2; + var s1 = s3 - pp + p; - var lightPhysicalFragmentChunk = [ - 'PhysicalMaterial material;', - 'material.diffuseColor = diffuseColor.rgb * ( 1. - max( specularFactor.r, max( specularFactor.g, specularFactor.b ) ) );', - 'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );', - 'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );', - 'material.specularRoughness = max( 1.0 - glossinessFactor, 0.0525 ); // 0.0525 corresponds to the base mip of a 256 cubemap.', - 'material.specularRoughness += geometryRoughness;', - 'material.specularRoughness = min( material.specularRoughness, 1.0 );', - 'material.specularColor = specularFactor;', - ].join( '\n' ); + // Layout of keyframe output values for CUBICSPLINE animations: + // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] + for ( var i = 0; i !== stride; i ++ ) { - var uniforms = { - specular: { value: new Color().setHex( 0xffffff ) }, - glossiness: { value: 1 }, - specularMap: { value: null }, - glossinessMap: { value: null } - }; + var p0 = values[ offset0 + i + stride ]; // splineVertex_k + var m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) + var p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 + var m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) - this._extraUniforms = uniforms; + result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; - this.onBeforeCompile = function ( shader ) { + } - for ( var uniformName in uniforms ) { + return result; - shader.uniforms[ uniformName ] = uniforms[ uniformName ]; + }; - } + /*********************************/ + /********** INTERNALS ************/ + /*********************************/ - shader.fragmentShader = shader.fragmentShader - .replace( 'uniform float roughness;', 'uniform vec3 specular;' ) - .replace( 'uniform float metalness;', 'uniform float glossiness;' ) - .replace( '#include ', specularMapParsFragmentChunk ) - .replace( '#include ', glossinessMapParsFragmentChunk ) - .replace( '#include ', specularMapFragmentChunk ) - .replace( '#include ', glossinessMapFragmentChunk ) - .replace( '#include ', lightPhysicalFragmentChunk ); + /* CONSTANTS */ - }; + var WEBGL_CONSTANTS = { + FLOAT: 5126, + //FLOAT_MAT2: 35674, + FLOAT_MAT3: 35675, + FLOAT_MAT4: 35676, + FLOAT_VEC2: 35664, + FLOAT_VEC3: 35665, + FLOAT_VEC4: 35666, + LINEAR: 9729, + REPEAT: 10497, + SAMPLER_2D: 35678, + POINTS: 0, + LINES: 1, + LINE_LOOP: 2, + LINE_STRIP: 3, + TRIANGLES: 4, + TRIANGLE_STRIP: 5, + TRIANGLE_FAN: 6, + UNSIGNED_BYTE: 5121, + UNSIGNED_SHORT: 5123 + }; - Object.defineProperties( this, { + var WEBGL_COMPONENT_TYPES = { + 5120: Int8Array, + 5121: Uint8Array, + 5122: Int16Array, + 5123: Uint16Array, + 5125: Uint32Array, + 5126: Float32Array + }; - specular: { - get: function () { + var WEBGL_FILTERS = { + 9728: NearestFilter, + 9729: LinearFilter, + 9984: NearestMipmapNearestFilter, + 9985: LinearMipmapNearestFilter, + 9986: NearestMipmapLinearFilter, + 9987: LinearMipmapLinearFilter + }; - return uniforms.specular.value; + var WEBGL_WRAPPINGS = { + 33071: ClampToEdgeWrapping, + 33648: MirroredRepeatWrapping, + 10497: RepeatWrapping + }; - }, - set: function ( v ) { + var WEBGL_TYPE_SIZES = { + 'SCALAR': 1, + 'VEC2': 2, + 'VEC3': 3, + 'VEC4': 4, + 'MAT2': 4, + 'MAT3': 9, + 'MAT4': 16 + }; - uniforms.specular.value = v; + var ATTRIBUTES = { + POSITION: 'position', + NORMAL: 'normal', + TANGENT: 'tangent', + TEXCOORD_0: 'uv', + TEXCOORD_1: 'uv2', + COLOR_0: 'color', + WEIGHTS_0: 'skinWeight', + JOINTS_0: 'skinIndex', + }; - } - }, + var PATH_PROPERTIES = { + scale: 'scale', + translation: 'position', + rotation: 'quaternion', + weights: 'morphTargetInfluences' + }; - specularMap: { - get: function () { + var INTERPOLATION = { + CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each + // keyframe track will be initialized with a default interpolation type, then modified. + LINEAR: InterpolateLinear, + STEP: InterpolateDiscrete + }; - return uniforms.specularMap.value; + var ALPHA_MODES = { + OPAQUE: 'OPAQUE', + MASK: 'MASK', + BLEND: 'BLEND' + }; - }, - set: function ( v ) { + /* UTILITY FUNCTIONS */ - uniforms.specularMap.value = v; + function resolveURL( url, path ) { - if ( v ) { + // Invalid URL + if ( typeof url !== 'string' || url === '' ) return ''; - this.defines.USE_SPECULARMAP = ''; // USE_UV is set by the renderer for specular maps + // Host Relative URL + if ( /^https?:\/\//i.test( path ) && /^\//.test( url ) ) { - } else { + path = path.replace( /(^https?:\/\/[^\/]+).*/i, '$1' ); - delete this.defines.USE_SPECULARMAP; + } - } + // Absolute URL http://,https://,// + if ( /^(https?:)?\/\//i.test( url ) ) return url; - } - }, + // Data URI + if ( /^data:.*,.*$/i.test( url ) ) return url; - glossiness: { - get: function () { + // Blob URL + if ( /^blob:.*$/i.test( url ) ) return url; - return uniforms.glossiness.value; + // Relative URL + return path + url; - }, - set: function ( v ) { + } - uniforms.glossiness.value = v; + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material + */ + function createDefaultMaterial( cache ) { - } - }, + if ( cache[ 'DefaultMaterial' ] === undefined ) { - glossinessMap: { - get: function () { + cache[ 'DefaultMaterial' ] = new MeshStandardMaterial( { + color: 0xFFFFFF, + emissive: 0x000000, + metalness: 1, + roughness: 1, + transparent: false, + depthTest: true, + side: FrontSide + } ); - return uniforms.glossinessMap.value; + } - }, - set: function ( v ) { + return cache[ 'DefaultMaterial' ]; - uniforms.glossinessMap.value = v; + } - if ( v ) { + function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { - this.defines.USE_GLOSSINESSMAP = ''; - this.defines.USE_UV = ''; + // Add unknown glTF extensions to an object's userData. - } else { + for ( var name in objectDef.extensions ) { - delete this.defines.USE_GLOSSINESSMAP; - delete this.defines.USE_UV; + if ( knownExtensions[ name ] === undefined ) { - } + object.userData.gltfExtensions = object.userData.gltfExtensions || {}; + object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; - } } - } ); + } - delete this.metalness; - delete this.roughness; - delete this.metalnessMap; - delete this.roughnessMap; + } - this.setValues( params ); + /** + * @param {Object3D|Material|BufferGeometry} object + * @param {GLTF.definition} gltfDef + */ + function assignExtrasToUserData( object, gltfDef ) { - } + if ( gltfDef.extras !== undefined ) { - GLTFMeshStandardSGMaterial.prototype = Object.create( MeshStandardMaterial.prototype ); - GLTFMeshStandardSGMaterial.prototype.constructor = GLTFMeshStandardSGMaterial; + if ( typeof gltfDef.extras === 'object' ) { - GLTFMeshStandardSGMaterial.prototype.copy = function ( source ) { + Object.assign( object.userData, gltfDef.extras ); - MeshStandardMaterial.prototype.copy.call( this, source ); - this.specularMap = source.specularMap; - this.specular.copy( source.specular ); - this.glossinessMap = source.glossinessMap; - this.glossiness = source.glossiness; - delete this.metalness; - delete this.roughness; - delete this.metalnessMap; - delete this.roughnessMap; - return this; + } else { - }; + console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); - function GLTFMaterialsPbrSpecularGlossinessExtension() { + } - return { + } - name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, + } - specularGlossinessParams: [ - 'color', - 'map', - 'lightMap', - 'lightMapIntensity', - 'aoMap', - 'aoMapIntensity', - 'emissive', - 'emissiveIntensity', - 'emissiveMap', - 'bumpMap', - 'bumpScale', - 'normalMap', - 'normalMapType', - 'displacementMap', - 'displacementScale', - 'displacementBias', - 'specularMap', - 'specular', - 'glossinessMap', - 'glossiness', - 'alphaMap', - 'envMap', - 'envMapIntensity', - 'refractionRatio', - ], + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets + * + * @param {BufferGeometry} geometry + * @param {Array} targets + * @param {GLTFParser} parser + * @return {Promise} + */ + function addMorphTargets( geometry, targets, parser ) { - getMaterialType: function () { + var hasMorphPosition = false; + var hasMorphNormal = false; - return GLTFMeshStandardSGMaterial; + for ( var i = 0, il = targets.length; i < il; i ++ ) { - }, + var target = targets[ i ]; - extendParams: function ( materialParams, materialDef, parser ) { + if ( target.POSITION !== undefined ) hasMorphPosition = true; + if ( target.NORMAL !== undefined ) hasMorphNormal = true; - var pbrSpecularGlossiness = materialDef.extensions[ this.name ]; + if ( hasMorphPosition && hasMorphNormal ) break; - materialParams.color = new Color( 1.0, 1.0, 1.0 ); - materialParams.opacity = 1.0; + } - var pending = []; + if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry ); - if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) { + var pendingPositionAccessors = []; + var pendingNormalAccessors = []; - var array = pbrSpecularGlossiness.diffuseFactor; + for ( var i = 0, il = targets.length; i < il; i ++ ) { - materialParams.color.fromArray( array ); - materialParams.opacity = array[ 3 ]; + var target = targets[ i ]; - } + if ( hasMorphPosition ) { - if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) { + var pendingAccessor = target.POSITION !== undefined + ? parser.getDependency( 'accessor', target.POSITION ) + : geometry.attributes.position; - pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture ) ); + pendingPositionAccessors.push( pendingAccessor ); - } + } - materialParams.emissive = new Color( 0.0, 0.0, 0.0 ); - materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; - materialParams.specular = new Color( 1.0, 1.0, 1.0 ); + if ( hasMorphNormal ) { - if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) { + var pendingAccessor = target.NORMAL !== undefined + ? parser.getDependency( 'accessor', target.NORMAL ) + : geometry.attributes.normal; - materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor ); + pendingNormalAccessors.push( pendingAccessor ); - } + } - if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) { + } - var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; - pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) ); - pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef ) ); + return Promise.all( [ + Promise.all( pendingPositionAccessors ), + Promise.all( pendingNormalAccessors ) + ] ).then( function ( accessors ) { - } + var morphPositions = accessors[ 0 ]; + var morphNormals = accessors[ 1 ]; - return Promise.all( pending ); + if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; + if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; + geometry.morphTargetsRelative = true; - }, + return geometry; - createMaterial: function ( materialParams ) { + } ); - var material = new GLTFMeshStandardSGMaterial( materialParams ); - material.fog = true; + } - material.color = materialParams.color; + /** + * @param {Mesh} mesh + * @param {GLTF.Mesh} meshDef + */ + function updateMorphTargets( mesh, meshDef ) { - material.map = materialParams.map === undefined ? null : materialParams.map; + mesh.updateMorphTargets(); - material.lightMap = null; - material.lightMapIntensity = 1.0; + if ( meshDef.weights !== undefined ) { - material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap; - material.aoMapIntensity = 1.0; + for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) { - material.emissive = materialParams.emissive; - material.emissiveIntensity = 1.0; - material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap; + mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; - material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap; - material.bumpScale = 1; + } - material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap; - material.normalMapType = TangentSpaceNormalMap; + } - if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale; + // .extras has user-defined data, so check that .extras.targetNames is an array. + if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { - material.displacementMap = null; - material.displacementScale = 1; - material.displacementBias = 0; + var targetNames = meshDef.extras.targetNames; - material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap; - material.specular = materialParams.specular; + if ( mesh.morphTargetInfluences.length === targetNames.length ) { - material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap; - material.glossiness = materialParams.glossiness; + mesh.morphTargetDictionary = {}; - material.alphaMap = null; + for ( var i = 0, il = targetNames.length; i < il; i ++ ) { - material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap; - material.envMapIntensity = 1.0; + mesh.morphTargetDictionary[ targetNames[ i ] ] = i; - material.refractionRatio = 0.98; + } - return material; + } else { - }, + console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); - }; + } + + } } - /** - * Mesh Quantization Extension - * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization - */ - function GLTFMeshQuantizationExtension() { + function createPrimitiveKey( primitiveDef ) { - this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; + var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; + var geometryKey; - } + if ( dracoExtension ) { - /*********************************/ - /********** INTERPOLATION ********/ - /*********************************/ + geometryKey = 'draco:' + dracoExtension.bufferView + + ':' + dracoExtension.indices + + ':' + createAttributesKey( dracoExtension.attributes ); - // Spline Interpolation - // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation - function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) { + } else { - Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer ); + geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; - } + } - GLTFCubicSplineInterpolant.prototype = Object.create( Interpolant.prototype ); - GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; + return geometryKey; - GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function ( index ) { + } - // Copies a sample value to the result buffer. See description of glTF - // CUBICSPLINE values layout in interpolate_() function below. + function createAttributesKey( attributes ) { - var result = this.resultBuffer, - values = this.sampleValues, - valueSize = this.valueSize, - offset = index * valueSize * 3 + valueSize; + var attributesKey = ''; - for ( var i = 0; i !== valueSize; i ++ ) { + var keys = Object.keys( attributes ).sort(); - result[ i ] = values[ offset + i ]; + for ( var i = 0, il = keys.length; i < il; i ++ ) { + + attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; } - return result; + return attributesKey; - }; + } - GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; + /* GLTF PARSER */ - GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; + function GLTFParser( json, options ) { - GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) { + this.json = json || {}; + this.extensions = {}; + this.plugins = {}; + this.options = options || {}; - var result = this.resultBuffer; - var values = this.sampleValues; - var stride = this.valueSize; + // loader object cache + this.cache = new GLTFRegistry(); - var stride2 = stride * 2; - var stride3 = stride * 3; + // associations between Three.js objects and glTF elements + this.associations = new Map(); - var td = t1 - t0; + // BufferGeometry caching + this.primitiveCache = {}; - var p = ( t - t0 ) / td; - var pp = p * p; - var ppp = pp * p; + // Object3D instance caches + this.meshCache = { refs: {}, uses: {} }; + this.cameraCache = { refs: {}, uses: {} }; + this.lightCache = { refs: {}, uses: {} }; - var offset1 = i1 * stride3; - var offset0 = offset1 - stride3; + // Track node names, to ensure no duplicates + this.nodeNamesUsed = {}; - var s2 = - 2 * ppp + 3 * pp; - var s3 = ppp - pp; - var s0 = 1 - s2; - var s1 = s3 - pp + p; + // Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the + // expensive work of uploading a texture to the GPU off the main thread. + if ( typeof createImageBitmap !== 'undefined' && /Firefox/.test( navigator.userAgent ) === false ) { - // Layout of keyframe output values for CUBICSPLINE animations: - // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] - for ( var i = 0; i !== stride; i ++ ) { + this.textureLoader = new ImageBitmapLoader( this.options.manager ); - var p0 = values[ offset0 + i + stride ]; // splineVertex_k - var m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) - var p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 - var m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) + } else { - result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; + this.textureLoader = new TextureLoader( this.options.manager ); } - return result; - - }; + this.textureLoader.setCrossOrigin( this.options.crossOrigin ); - /*********************************/ - /********** INTERNALS ************/ - /*********************************/ + this.fileLoader = new FileLoader( this.options.manager ); + this.fileLoader.setResponseType( 'arraybuffer' ); - /* CONSTANTS */ + if ( this.options.crossOrigin === 'use-credentials' ) { - var WEBGL_CONSTANTS = { - FLOAT: 5126, - //FLOAT_MAT2: 35674, - FLOAT_MAT3: 35675, - FLOAT_MAT4: 35676, - FLOAT_VEC2: 35664, - FLOAT_VEC3: 35665, - FLOAT_VEC4: 35666, - LINEAR: 9729, - REPEAT: 10497, - SAMPLER_2D: 35678, - POINTS: 0, - LINES: 1, - LINE_LOOP: 2, - LINE_STRIP: 3, - TRIANGLES: 4, - TRIANGLE_STRIP: 5, - TRIANGLE_FAN: 6, - UNSIGNED_BYTE: 5121, - UNSIGNED_SHORT: 5123 - }; + this.fileLoader.setWithCredentials( true ); - var WEBGL_COMPONENT_TYPES = { - 5120: Int8Array, - 5121: Uint8Array, - 5122: Int16Array, - 5123: Uint16Array, - 5125: Uint32Array, - 5126: Float32Array - }; + } - var WEBGL_FILTERS = { - 9728: NearestFilter, - 9729: LinearFilter, - 9984: NearestMipmapNearestFilter, - 9985: LinearMipmapNearestFilter, - 9986: NearestMipmapLinearFilter, - 9987: LinearMipmapLinearFilter - }; + } - var WEBGL_WRAPPINGS = { - 33071: ClampToEdgeWrapping, - 33648: MirroredRepeatWrapping, - 10497: RepeatWrapping - }; + GLTFParser.prototype.setExtensions = function ( extensions ) { - var WEBGL_TYPE_SIZES = { - 'SCALAR': 1, - 'VEC2': 2, - 'VEC3': 3, - 'VEC4': 4, - 'MAT2': 4, - 'MAT3': 9, - 'MAT4': 16 - }; + this.extensions = extensions; - var ATTRIBUTES = { - POSITION: 'position', - NORMAL: 'normal', - TANGENT: 'tangent', - TEXCOORD_0: 'uv', - TEXCOORD_1: 'uv2', - COLOR_0: 'color', - WEIGHTS_0: 'skinWeight', - JOINTS_0: 'skinIndex', }; - var PATH_PROPERTIES = { - scale: 'scale', - translation: 'position', - rotation: 'quaternion', - weights: 'morphTargetInfluences' - }; + GLTFParser.prototype.setPlugins = function ( plugins ) { - var INTERPOLATION = { - CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each - // keyframe track will be initialized with a default interpolation type, then modified. - LINEAR: InterpolateLinear, - STEP: InterpolateDiscrete - }; + this.plugins = plugins; - var ALPHA_MODES = { - OPAQUE: 'OPAQUE', - MASK: 'MASK', - BLEND: 'BLEND' }; - /* UTILITY FUNCTIONS */ - - function resolveURL( url, path ) { + GLTFParser.prototype.parse = function ( onLoad, onError ) { - // Invalid URL - if ( typeof url !== 'string' || url === '' ) return ''; + var parser = this; + var json = this.json; + var extensions = this.extensions; - // Host Relative URL - if ( /^https?:\/\//i.test( path ) && /^\//.test( url ) ) { + // Clear the loader cache + this.cache.removeAll(); - path = path.replace( /(^https?:\/\/[^\/]+).*/i, '$1' ); + // Mark the special nodes/meshes in json for efficient parse + this._invokeAll( function ( ext ) { - } + return ext._markDefs && ext._markDefs(); - // Absolute URL http://,https://,// - if ( /^(https?:)?\/\//i.test( url ) ) return url; + } ); - // Data URI - if ( /^data:.*,.*$/i.test( url ) ) return url; + Promise.all( [ - // Blob URL - if ( /^blob:.*$/i.test( url ) ) return url; + this.getDependencies( 'scene' ), + this.getDependencies( 'animation' ), + this.getDependencies( 'camera' ), - // Relative URL - return path + url; + ] ).then( function ( dependencies ) { - } + var result = { + scene: dependencies[ 0 ][ json.scene || 0 ], + scenes: dependencies[ 0 ], + animations: dependencies[ 1 ], + cameras: dependencies[ 2 ], + asset: json.asset, + parser: parser, + userData: {} + }; - /** - * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material - */ - function createDefaultMaterial( cache ) { + addUnknownExtensionsToUserData( extensions, result, json ); - if ( cache[ 'DefaultMaterial' ] === undefined ) { + assignExtrasToUserData( result, json ); - cache[ 'DefaultMaterial' ] = new MeshStandardMaterial( { - color: 0xFFFFFF, - emissive: 0x000000, - metalness: 1, - roughness: 1, - transparent: false, - depthTest: true, - side: FrontSide - } ); + onLoad( result ); - } + } ).catch( onError ); - return cache[ 'DefaultMaterial' ]; + }; - } + /** + * Marks the special nodes/meshes in json for efficient parse. + */ + GLTFParser.prototype._markDefs = function () { - function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { + var nodeDefs = this.json.nodes || []; + var skinDefs = this.json.skins || []; + var meshDefs = this.json.meshes || []; - // Add unknown glTF extensions to an object's userData. + // Nothing in the node definition indicates whether it is a Bone or an + // Object3D. Use the skins' joint references to mark bones. + for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { - for ( var name in objectDef.extensions ) { + var joints = skinDefs[ skinIndex ].joints; - if ( knownExtensions[ name ] === undefined ) { + for ( var i = 0, il = joints.length; i < il; i ++ ) { - object.userData.gltfExtensions = object.userData.gltfExtensions || {}; - object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; + nodeDefs[ joints[ i ] ].isBone = true; } } - } + // Iterate over all nodes, marking references to shared resources, + // as well as skeleton joints. + for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { - /** - * @param {Object3D|Material|BufferGeometry} object - * @param {GLTF.definition} gltfDef - */ - function assignExtrasToUserData( object, gltfDef ) { + var nodeDef = nodeDefs[ nodeIndex ]; - if ( gltfDef.extras !== undefined ) { + if ( nodeDef.mesh !== undefined ) { - if ( typeof gltfDef.extras === 'object' ) { + this._addNodeRef( this.meshCache, nodeDef.mesh ); - Object.assign( object.userData, gltfDef.extras ); + // Nothing in the mesh definition indicates whether it is + // a SkinnedMesh or Mesh. Use the node's mesh reference + // to mark SkinnedMesh if node has skin. + if ( nodeDef.skin !== undefined ) { - } else { + meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; - console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); + } + + } + + if ( nodeDef.camera !== undefined ) { + + this._addNodeRef( this.cameraCache, nodeDef.camera ); } } - } + }; /** - * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets + * Counts references to shared node / Object3D resources. These resources + * can be reused, or "instantiated", at multiple nodes in the scene + * hierarchy. Mesh, Camera, and Light instances are instantiated and must + * be marked. Non-scenegraph resources (like Materials, Geometries, and + * Textures) can be reused directly and are not marked here. * - * @param {BufferGeometry} geometry - * @param {Array} targets - * @param {GLTFParser} parser - * @return {Promise} + * Example: CesiumMilkTruck sample model reuses "Wheel" meshes. */ - function addMorphTargets( geometry, targets, parser ) { - - var hasMorphPosition = false; - var hasMorphNormal = false; - - for ( var i = 0, il = targets.length; i < il; i ++ ) { + GLTFParser.prototype._addNodeRef = function ( cache, index ) { - var target = targets[ i ]; + if ( index === undefined ) return; - if ( target.POSITION !== undefined ) hasMorphPosition = true; - if ( target.NORMAL !== undefined ) hasMorphNormal = true; + if ( cache.refs[ index ] === undefined ) { - if ( hasMorphPosition && hasMorphNormal ) break; + cache.refs[ index ] = cache.uses[ index ] = 0; } - if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry ); + cache.refs[ index ] ++; - var pendingPositionAccessors = []; - var pendingNormalAccessors = []; + }; - for ( var i = 0, il = targets.length; i < il; i ++ ) { + /** Returns a reference to a shared resource, cloning it if necessary. */ + GLTFParser.prototype._getNodeRef = function ( cache, index, object ) { - var target = targets[ i ]; + if ( cache.refs[ index ] <= 1 ) return object; - if ( hasMorphPosition ) { + var ref = object.clone(); - var pendingAccessor = target.POSITION !== undefined - ? parser.getDependency( 'accessor', target.POSITION ) - : geometry.attributes.position; + ref.name += '_instance_' + ( cache.uses[ index ] ++ ); - pendingPositionAccessors.push( pendingAccessor ); + return ref; - } + }; - if ( hasMorphNormal ) { + GLTFParser.prototype._invokeOne = function ( func ) { - var pendingAccessor = target.NORMAL !== undefined - ? parser.getDependency( 'accessor', target.NORMAL ) - : geometry.attributes.normal; + var extensions = Object.values( this.plugins ); + extensions.push( this ); - pendingNormalAccessors.push( pendingAccessor ); + for ( var i = 0; i < extensions.length; i ++ ) { - } + var result = func( extensions[ i ] ); + + if ( result ) return result; } - return Promise.all( [ - Promise.all( pendingPositionAccessors ), - Promise.all( pendingNormalAccessors ) - ] ).then( function ( accessors ) { + }; - var morphPositions = accessors[ 0 ]; - var morphNormals = accessors[ 1 ]; + GLTFParser.prototype._invokeAll = function ( func ) { - if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; - if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; - geometry.morphTargetsRelative = true; + var extensions = Object.values( this.plugins ); + extensions.unshift( this ); - return geometry; + var pending = []; - } ); + for ( var i = 0; i < extensions.length; i ++ ) { - } + var result = func( extensions[ i ] ); + + if ( result ) pending.push( result ); + + } + + return pending; + + }; /** - * @param {Mesh} mesh - * @param {GLTF.Mesh} meshDef + * Requests the specified dependency asynchronously, with caching. + * @param {string} type + * @param {number} index + * @return {Promise} */ - function updateMorphTargets( mesh, meshDef ) { + GLTFParser.prototype.getDependency = function ( type, index ) { - mesh.updateMorphTargets(); + var cacheKey = type + ':' + index; + var dependency = this.cache.get( cacheKey ); - if ( meshDef.weights !== undefined ) { + if ( ! dependency ) { - for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) { + switch ( type ) { - mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; + case 'scene': + dependency = this.loadScene( index ); + break; - } + case 'node': + dependency = this.loadNode( index ); + break; - } + case 'mesh': + dependency = this._invokeOne( function ( ext ) { - // .extras has user-defined data, so check that .extras.targetNames is an array. - if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { + return ext.loadMesh && ext.loadMesh( index ); - var targetNames = meshDef.extras.targetNames; + } ); + break; - if ( mesh.morphTargetInfluences.length === targetNames.length ) { + case 'accessor': + dependency = this.loadAccessor( index ); + break; - mesh.morphTargetDictionary = {}; + case 'bufferView': + dependency = this._invokeOne( function ( ext ) { - for ( var i = 0, il = targetNames.length; i < il; i ++ ) { + return ext.loadBufferView && ext.loadBufferView( index ); - mesh.morphTargetDictionary[ targetNames[ i ] ] = i; + } ); + break; - } + case 'buffer': + dependency = this.loadBuffer( index ); + break; - } else { + case 'material': + dependency = this._invokeOne( function ( ext ) { - console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); + return ext.loadMaterial && ext.loadMaterial( index ); + + } ); + break; + + case 'texture': + dependency = this._invokeOne( function ( ext ) { + + return ext.loadTexture && ext.loadTexture( index ); + + } ); + break; + + case 'skin': + dependency = this.loadSkin( index ); + break; + + case 'animation': + dependency = this.loadAnimation( index ); + break; + + case 'camera': + dependency = this.loadCamera( index ); + break; + + default: + throw new Error( 'Unknown type: ' + type ); } + this.cache.add( cacheKey, dependency ); + } - } + return dependency; - function createPrimitiveKey( primitiveDef ) { + }; - var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; - var geometryKey; + /** + * Requests all dependencies of the specified type asynchronously, with caching. + * @param {string} type + * @return {Promise>} + */ + GLTFParser.prototype.getDependencies = function ( type ) { - if ( dracoExtension ) { + var dependencies = this.cache.get( type ); - geometryKey = 'draco:' + dracoExtension.bufferView - + ':' + dracoExtension.indices - + ':' + createAttributesKey( dracoExtension.attributes ); + if ( ! dependencies ) { - } else { + var parser = this; + var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; - geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; + dependencies = Promise.all( defs.map( function ( def, index ) { + + return parser.getDependency( type, index ); + + } ) ); + + this.cache.add( type, dependencies ); } - return geometryKey; + return dependencies; - } + }; - function createAttributesKey( attributes ) { + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views + * @param {number} bufferIndex + * @return {Promise} + */ + GLTFParser.prototype.loadBuffer = function ( bufferIndex ) { + + var bufferDef = this.json.buffers[ bufferIndex ]; + var loader = this.fileLoader; + + if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { - var attributesKey = ''; + throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); - var keys = Object.keys( attributes ).sort(); + } - for ( var i = 0, il = keys.length; i < il; i ++ ) { + // If present, GLB container is required to be the first buffer. + if ( bufferDef.uri === undefined && bufferIndex === 0 ) { - attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; + return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); } - return attributesKey; - - } + var options = this.options; - /* GLTF PARSER */ + return new Promise( function ( resolve, reject ) { - function GLTFParser( json, options ) { + loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { - this.json = json || {}; - this.extensions = {}; - this.plugins = {}; - this.options = options || {}; + reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); - // loader object cache - this.cache = new GLTFRegistry(); + } ); - // associations between Three.js objects and glTF elements - this.associations = new Map(); + } ); - // BufferGeometry caching - this.primitiveCache = {}; + }; - // Object3D instance caches - this.meshCache = { refs: {}, uses: {} }; - this.cameraCache = { refs: {}, uses: {} }; - this.lightCache = { refs: {}, uses: {} }; + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views + * @param {number} bufferViewIndex + * @return {Promise} + */ + GLTFParser.prototype.loadBufferView = function ( bufferViewIndex ) { - // Track node names, to ensure no duplicates - this.nodeNamesUsed = {}; + var bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; - // Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the - // expensive work of uploading a texture to the GPU off the main thread. - if ( typeof createImageBitmap !== 'undefined' && /Firefox/.test( navigator.userAgent ) === false ) { + return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { - this.textureLoader = new ImageBitmapLoader( this.options.manager ); + var byteLength = bufferViewDef.byteLength || 0; + var byteOffset = bufferViewDef.byteOffset || 0; + return buffer.slice( byteOffset, byteOffset + byteLength ); - } else { + } ); - this.textureLoader = new TextureLoader( this.options.manager ); + }; - } + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors + * @param {number} accessorIndex + * @return {Promise} + */ + GLTFParser.prototype.loadAccessor = function ( accessorIndex ) { - this.textureLoader.setCrossOrigin( this.options.crossOrigin ); + var parser = this; + var json = this.json; - this.fileLoader = new FileLoader( this.options.manager ); - this.fileLoader.setResponseType( 'arraybuffer' ); + var accessorDef = this.json.accessors[ accessorIndex ]; - if ( this.options.crossOrigin === 'use-credentials' ) { + if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { - this.fileLoader.setWithCredentials( true ); + // Ignore empty accessors, which may be used to declare runtime + // information about attributes coming from another source (e.g. Draco + // compression extension). + return Promise.resolve( null ); } - } + var pendingBufferViews = []; - GLTFParser.prototype.setExtensions = function ( extensions ) { + if ( accessorDef.bufferView !== undefined ) { - this.extensions = extensions; + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); - }; + } else { - GLTFParser.prototype.setPlugins = function ( plugins ) { + pendingBufferViews.push( null ); - this.plugins = plugins; + } - }; + if ( accessorDef.sparse !== undefined ) { - GLTFParser.prototype.parse = function ( onLoad, onError ) { + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); - var parser = this; - var json = this.json; - var extensions = this.extensions; + } - // Clear the loader cache - this.cache.removeAll(); + return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { - // Mark the special nodes/meshes in json for efficient parse - this._invokeAll( function ( ext ) { + var bufferView = bufferViews[ 0 ]; - return ext._markDefs && ext._markDefs(); + var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; + var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; - } ); + // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. + var elementBytes = TypedArray.BYTES_PER_ELEMENT; + var itemBytes = elementBytes * itemSize; + var byteOffset = accessorDef.byteOffset || 0; + var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; + var normalized = accessorDef.normalized === true; + var array, bufferAttribute; - Promise.all( [ + // The buffer is not interleaved if the stride is the item size in bytes. + if ( byteStride && byteStride !== itemBytes ) { - this.getDependencies( 'scene' ), - this.getDependencies( 'animation' ), - this.getDependencies( 'camera' ), + // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer + // This makes sure that IBA.count reflects accessor.count properly + var ibSlice = Math.floor( byteOffset / byteStride ); + var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; + var ib = parser.cache.get( ibCacheKey ); - ] ).then( function ( dependencies ) { + if ( ! ib ) { - var result = { - scene: dependencies[ 0 ][ json.scene || 0 ], - scenes: dependencies[ 0 ], - animations: dependencies[ 1 ], - cameras: dependencies[ 2 ], - asset: json.asset, - parser: parser, - userData: {} - }; + array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes ); - addUnknownExtensionsToUserData( extensions, result, json ); + // Integer parameters to IB/IBA are in array elements, not bytes. + ib = new InterleavedBuffer( array, byteStride / elementBytes ); - assignExtrasToUserData( result, json ); + parser.cache.add( ibCacheKey, ib ); - onLoad( result ); + } - } ).catch( onError ); + bufferAttribute = new InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized ); - }; + } else { - /** - * Marks the special nodes/meshes in json for efficient parse. - */ - GLTFParser.prototype._markDefs = function () { + if ( bufferView === null ) { - var nodeDefs = this.json.nodes || []; - var skinDefs = this.json.skins || []; - var meshDefs = this.json.meshes || []; + array = new TypedArray( accessorDef.count * itemSize ); - // Nothing in the node definition indicates whether it is a Bone or an - // Object3D. Use the skins' joint references to mark bones. - for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { + } else { - var joints = skinDefs[ skinIndex ].joints; + array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); - for ( var i = 0, il = joints.length; i < il; i ++ ) { + } - nodeDefs[ joints[ i ] ].isBone = true; + bufferAttribute = new BufferAttribute( array, itemSize, normalized ); } - } - - // Iterate over all nodes, marking references to shared resources, - // as well as skeleton joints. - for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { + // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors + if ( accessorDef.sparse !== undefined ) { - var nodeDef = nodeDefs[ nodeIndex ]; + var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; + var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; - if ( nodeDef.mesh !== undefined ) { + var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; + var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; - this._addNodeRef( this.meshCache, nodeDef.mesh ); + var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); + var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); - // Nothing in the mesh definition indicates whether it is - // a SkinnedMesh or Mesh. Use the node's mesh reference - // to mark SkinnedMesh if node has skin. - if ( nodeDef.skin !== undefined ) { + if ( bufferView !== null ) { - meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; + // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. + bufferAttribute = new BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ); } - } + for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) { - if ( nodeDef.camera !== undefined ) { + var index = sparseIndices[ i ]; - this._addNodeRef( this.cameraCache, nodeDef.camera ); + bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); + if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); + if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); + if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); + if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); + + } } - } + return bufferAttribute; + + } ); }; /** - * Counts references to shared node / Object3D resources. These resources - * can be reused, or "instantiated", at multiple nodes in the scene - * hierarchy. Mesh, Camera, and Light instances are instantiated and must - * be marked. Non-scenegraph resources (like Materials, Geometries, and - * Textures) can be reused directly and are not marked here. - * - * Example: CesiumMilkTruck sample model reuses "Wheel" meshes. + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures + * @param {number} textureIndex + * @return {Promise} */ - GLTFParser.prototype._addNodeRef = function ( cache, index ) { - - if ( index === undefined ) return; - - if ( cache.refs[ index ] === undefined ) { + GLTFParser.prototype.loadTexture = function ( textureIndex ) { - cache.refs[ index ] = cache.uses[ index ] = 0; + var parser = this; + var json = this.json; + var options = this.options; - } + var textureDef = json.textures[ textureIndex ]; - cache.refs[ index ] ++; + var textureExtensions = textureDef.extensions || {}; - }; + var source; - /** Returns a reference to a shared resource, cloning it if necessary. */ - GLTFParser.prototype._getNodeRef = function ( cache, index, object ) { + if ( textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) { - if ( cache.refs[ index ] <= 1 ) return object; + source = json.images[ textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].source ]; - var ref = object.clone(); + } else { - ref.name += '_instance_' + ( cache.uses[ index ] ++ ); + source = json.images[ textureDef.source ]; - return ref; + } - }; + var loader; - GLTFParser.prototype._invokeOne = function ( func ) { + if ( source.uri ) { - var extensions = Object.values( this.plugins ); - extensions.push( this ); + loader = options.manager.getHandler( source.uri ); - for ( var i = 0; i < extensions.length; i ++ ) { + } - var result = func( extensions[ i ] ); + if ( ! loader ) { - if ( result ) return result; + loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] + ? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader + : this.textureLoader; } + return this.loadTextureImage( textureIndex, source, loader ); + }; - GLTFParser.prototype._invokeAll = function ( func ) { + GLTFParser.prototype.loadTextureImage = function ( textureIndex, source, loader ) { - var extensions = Object.values( this.plugins ); - extensions.unshift( this ); + var parser = this; + var json = this.json; + var options = this.options; - var pending = []; + var textureDef = json.textures[ textureIndex ]; - for ( var i = 0; i < extensions.length; i ++ ) { + var URL = self.URL || self.webkitURL; - var result = func( extensions[ i ] ); + var sourceURI = source.uri; + var isObjectURL = false; + var hasAlpha = true; - if ( result ) pending.push( result ); + if ( source.mimeType === 'image/jpeg' ) hasAlpha = false; - } + if ( source.bufferView !== undefined ) { - return pending; + // Load binary image data from bufferView, if provided. - }; + sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { - /** - * Requests the specified dependency asynchronously, with caching. - * @param {string} type - * @param {number} index - * @return {Promise} - */ - GLTFParser.prototype.getDependency = function ( type, index ) { + if ( source.mimeType === 'image/png' ) { - var cacheKey = type + ':' + index; - var dependency = this.cache.get( cacheKey ); + // Inspect the PNG 'IHDR' chunk to determine whether the image could have an + // alpha channel. This check is conservative — the image could have an alpha + // channel with all values == 1, and the indexed type (colorType == 3) only + // sometimes contains alpha. + // + // https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header + var colorType = new DataView( bufferView, 25, 1 ).getUint8( 0, false ); + hasAlpha = colorType === 6 || colorType === 4 || colorType === 3; - if ( ! dependency ) { + } - switch ( type ) { + isObjectURL = true; + var blob = new Blob( [ bufferView ], { type: source.mimeType } ); + sourceURI = URL.createObjectURL( blob ); + return sourceURI; - case 'scene': - dependency = this.loadScene( index ); - break; + } ); - case 'node': - dependency = this.loadNode( index ); - break; + } - case 'mesh': - dependency = this._invokeOne( function ( ext ) { + return Promise.resolve( sourceURI ).then( function ( sourceURI ) { - return ext.loadMesh && ext.loadMesh( index ); + return new Promise( function ( resolve, reject ) { - } ); - break; + var onLoad = resolve; - case 'accessor': - dependency = this.loadAccessor( index ); - break; + if ( loader.isImageBitmapLoader === true ) { - case 'bufferView': - dependency = this._invokeOne( function ( ext ) { + onLoad = function ( imageBitmap ) { - return ext.loadBufferView && ext.loadBufferView( index ); + resolve( new CanvasTexture( imageBitmap ) ); - } ); - break; + }; - case 'buffer': - dependency = this.loadBuffer( index ); - break; + } - case 'material': - dependency = this._invokeOne( function ( ext ) { + loader.load( resolveURL( sourceURI, options.path ), onLoad, undefined, reject ); - return ext.loadMaterial && ext.loadMaterial( index ); + } ); - } ); - break; + } ).then( function ( texture ) { - case 'texture': - dependency = this._invokeOne( function ( ext ) { + // Clean up resources and configure Texture. - return ext.loadTexture && ext.loadTexture( index ); + if ( isObjectURL === true ) { - } ); - break; + URL.revokeObjectURL( sourceURI ); - case 'skin': - dependency = this.loadSkin( index ); - break; + } - case 'animation': - dependency = this.loadAnimation( index ); - break; + texture.flipY = false; - case 'camera': - dependency = this.loadCamera( index ); - break; + if ( textureDef.name ) texture.name = textureDef.name; - default: - throw new Error( 'Unknown type: ' + type ); + // When there is definitely no alpha channel in the texture, set RGBFormat to save space. + if ( ! hasAlpha ) texture.format = RGBFormat; - } + var samplers = json.samplers || {}; + var sampler = samplers[ textureDef.sampler ] || {}; - this.cache.add( cacheKey, dependency ); + texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || LinearFilter; + texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || LinearMipmapLinearFilter; + texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || RepeatWrapping; + texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || RepeatWrapping; - } + parser.associations.set( texture, { + type: 'textures', + index: textureIndex + } ); - return dependency; + return texture; + + } ); }; /** - * Requests all dependencies of the specified type asynchronously, with caching. - * @param {string} type - * @return {Promise>} + * Asynchronously assigns a texture to the given material parameters. + * @param {Object} materialParams + * @param {string} mapName + * @param {Object} mapDef + * @return {Promise} */ - GLTFParser.prototype.getDependencies = function ( type ) { + GLTFParser.prototype.assignTexture = function ( materialParams, mapName, mapDef ) { - var dependencies = this.cache.get( type ); + var parser = this; - if ( ! dependencies ) { + return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { - var parser = this; - var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; + // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured + // However, we will copy UV set 0 to UV set 1 on demand for aoMap + if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) { - dependencies = Promise.all( defs.map( function ( def, index ) { + console.warn( 'THREE.GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' ); - return parser.getDependency( type, index ); + } - } ) ); + if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { - this.cache.add( type, dependencies ); + var transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; - } + if ( transform ) { - return dependencies; + var gltfReference = parser.associations.get( texture ); + texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); + parser.associations.set( texture, gltfReference ); + + } + + } + + materialParams[ mapName ] = texture; + + } ); }; /** - * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views - * @param {number} bufferIndex - * @return {Promise} + * Assigns final material to a Mesh, Line, or Points instance. The instance + * already has a material (generated from the glTF material options alone) + * but reuse of the same glTF material may require multiple threejs materials + * to accomodate different primitive types, defines, etc. New materials will + * be created if necessary, and reused from a cache. + * @param {Object3D} mesh Mesh, Line, or Points instance. */ - GLTFParser.prototype.loadBuffer = function ( bufferIndex ) { + GLTFParser.prototype.assignFinalMaterial = function ( mesh ) { - var bufferDef = this.json.buffers[ bufferIndex ]; - var loader = this.fileLoader; + var geometry = mesh.geometry; + var material = mesh.material; - if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { + var useVertexTangents = geometry.attributes.tangent !== undefined; + var useVertexColors = geometry.attributes.color !== undefined; + var useFlatShading = geometry.attributes.normal === undefined; + var useSkinning = mesh.isSkinnedMesh === true; + var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0; + var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; - throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); + if ( mesh.isPoints ) { - } + var cacheKey = 'PointsMaterial:' + material.uuid; - // If present, GLB container is required to be the first buffer. - if ( bufferDef.uri === undefined && bufferIndex === 0 ) { + var pointsMaterial = this.cache.get( cacheKey ); - return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); + if ( ! pointsMaterial ) { - } + pointsMaterial = new PointsMaterial(); + Material.prototype.copy.call( pointsMaterial, material ); + pointsMaterial.color.copy( material.color ); + pointsMaterial.map = material.map; + pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px - var options = this.options; + this.cache.add( cacheKey, pointsMaterial ); - return new Promise( function ( resolve, reject ) { + } - loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { + material = pointsMaterial; - reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); + } else if ( mesh.isLine ) { - } ); + var cacheKey = 'LineBasicMaterial:' + material.uuid; - } ); + var lineMaterial = this.cache.get( cacheKey ); - }; + if ( ! lineMaterial ) { - /** - * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views - * @param {number} bufferViewIndex - * @return {Promise} - */ - GLTFParser.prototype.loadBufferView = function ( bufferViewIndex ) { + lineMaterial = new LineBasicMaterial(); + Material.prototype.copy.call( lineMaterial, material ); + lineMaterial.color.copy( material.color ); - var bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; + this.cache.add( cacheKey, lineMaterial ); - return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { + } + + material = lineMaterial; + + } + + // Clone the material if it will be modified + if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets ) { + + var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; + + if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; + if ( useSkinning ) cacheKey += 'skinning:'; + if ( useVertexTangents ) cacheKey += 'vertex-tangents:'; + if ( useVertexColors ) cacheKey += 'vertex-colors:'; + if ( useFlatShading ) cacheKey += 'flat-shading:'; + if ( useMorphTargets ) cacheKey += 'morph-targets:'; + if ( useMorphNormals ) cacheKey += 'morph-normals:'; - var byteLength = bufferViewDef.byteLength || 0; - var byteOffset = bufferViewDef.byteOffset || 0; - return buffer.slice( byteOffset, byteOffset + byteLength ); + var cachedMaterial = this.cache.get( cacheKey ); - } ); + if ( ! cachedMaterial ) { - }; + cachedMaterial = material.clone(); - /** - * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors - * @param {number} accessorIndex - * @return {Promise} - */ - GLTFParser.prototype.loadAccessor = function ( accessorIndex ) { + if ( useSkinning ) cachedMaterial.skinning = true; + if ( useVertexTangents ) cachedMaterial.vertexTangents = true; + if ( useVertexColors ) cachedMaterial.vertexColors = true; + if ( useFlatShading ) cachedMaterial.flatShading = true; + if ( useMorphTargets ) cachedMaterial.morphTargets = true; + if ( useMorphNormals ) cachedMaterial.morphNormals = true; - var parser = this; - var json = this.json; + this.cache.add( cacheKey, cachedMaterial ); - var accessorDef = this.json.accessors[ accessorIndex ]; + this.associations.set( cachedMaterial, this.associations.get( material ) ); - if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { + } - // Ignore empty accessors, which may be used to declare runtime - // information about attributes coming from another source (e.g. Draco - // compression extension). - return Promise.resolve( null ); + material = cachedMaterial; } - var pendingBufferViews = []; + // workarounds for mesh and geometry - if ( accessorDef.bufferView !== undefined ) { + if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) { - pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); + geometry.setAttribute( 'uv2', geometry.attributes.uv ); - } else { + } - pendingBufferViews.push( null ); + // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 + if ( material.normalScale && ! useVertexTangents ) { + + material.normalScale.y = - material.normalScale.y; } - if ( accessorDef.sparse !== undefined ) { + if ( material.clearcoatNormalScale && ! useVertexTangents ) { - pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); - pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); + material.clearcoatNormalScale.y = - material.clearcoatNormalScale.y; } - return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { + mesh.material = material; - var bufferView = bufferViews[ 0 ]; + }; - var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; - var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; + GLTFParser.prototype.getMaterialType = function ( /* materialIndex */ ) { - // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. - var elementBytes = TypedArray.BYTES_PER_ELEMENT; - var itemBytes = elementBytes * itemSize; - var byteOffset = accessorDef.byteOffset || 0; - var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; - var normalized = accessorDef.normalized === true; - var array, bufferAttribute; + return MeshStandardMaterial; - // The buffer is not interleaved if the stride is the item size in bytes. - if ( byteStride && byteStride !== itemBytes ) { + }; - // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer - // This makes sure that IBA.count reflects accessor.count properly - var ibSlice = Math.floor( byteOffset / byteStride ); - var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; - var ib = parser.cache.get( ibCacheKey ); + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials + * @param {number} materialIndex + * @return {Promise} + */ + GLTFParser.prototype.loadMaterial = function ( materialIndex ) { - if ( ! ib ) { + var parser = this; + var json = this.json; + var extensions = this.extensions; + var materialDef = json.materials[ materialIndex ]; - array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes ); + var materialType; + var materialParams = {}; + var materialExtensions = materialDef.extensions || {}; - // Integer parameters to IB/IBA are in array elements, not bytes. - ib = new InterleavedBuffer( array, byteStride / elementBytes ); + var pending = []; - parser.cache.add( ibCacheKey, ib ); + if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { - } + var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; + materialType = sgExtension.getMaterialType(); + pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); - bufferAttribute = new InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized ); + } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { - } else { + var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; + materialType = kmuExtension.getMaterialType(); + pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); - if ( bufferView === null ) { + } else { - array = new TypedArray( accessorDef.count * itemSize ); + // Specification: + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material - } else { + var metallicRoughness = materialDef.pbrMetallicRoughness || {}; - array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); + materialParams.color = new Color( 1.0, 1.0, 1.0 ); + materialParams.opacity = 1.0; - } + if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { - bufferAttribute = new BufferAttribute( array, itemSize, normalized ); + var array = metallicRoughness.baseColorFactor; - } + materialParams.color.fromArray( array ); + materialParams.opacity = array[ 3 ]; - // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors - if ( accessorDef.sparse !== undefined ) { + } - var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; - var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; + if ( metallicRoughness.baseColorTexture !== undefined ) { - var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; - var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; + pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); - var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); - var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); + } - if ( bufferView !== null ) { + materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; + materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; - // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. - bufferAttribute = new BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ); + if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { - } + pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); + pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); - for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) { + } - var index = sparseIndices[ i ]; + materialType = this._invokeOne( function ( ext ) { - bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); - if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); - if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); - if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); - if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); + return ext.getMaterialType && ext.getMaterialType( materialIndex ); - } + } ); - } + pending.push( Promise.all( this._invokeAll( function ( ext ) { - return bufferAttribute; + return ext.extendMaterialParams && ext.extendMaterialParams( materialIndex, materialParams ); - } ); + } ) ) ); - }; + } - /** - * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures - * @param {number} textureIndex - * @return {Promise} - */ - GLTFParser.prototype.loadTexture = function ( textureIndex ) { + if ( materialDef.doubleSided === true ) { - var parser = this; - var json = this.json; - var options = this.options; + materialParams.side = DoubleSide; - var textureDef = json.textures[ textureIndex ]; + } - var textureExtensions = textureDef.extensions || {}; + var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; - var source; + if ( alphaMode === ALPHA_MODES.BLEND ) { - if ( textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) { + materialParams.transparent = true; - source = json.images[ textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].source ]; + // See: https://github.com/mrdoob/three.js/issues/17706 + materialParams.depthWrite = false; } else { - source = json.images[ textureDef.source ]; - - } + materialParams.transparent = false; - var loader; + if ( alphaMode === ALPHA_MODES.MASK ) { - if ( source.uri ) { + materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; - loader = options.manager.getHandler( source.uri ); + } } - if ( ! loader ) { - - loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] - ? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader - : this.textureLoader; + if ( materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial ) { - } + pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); - return this.loadTextureImage( textureIndex, source, loader ); + materialParams.normalScale = new Vector2( 1, 1 ); - }; + if ( materialDef.normalTexture.scale !== undefined ) { - GLTFParser.prototype.loadTextureImage = function ( textureIndex, source, loader ) { + materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale ); - var parser = this; - var json = this.json; - var options = this.options; + } - var textureDef = json.textures[ textureIndex ]; + } - var URL = self.URL || self.webkitURL; + if ( materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial ) { - var sourceURI = source.uri; - var isObjectURL = false; - var hasAlpha = true; + pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); - if ( source.mimeType === 'image/jpeg' ) hasAlpha = false; + if ( materialDef.occlusionTexture.strength !== undefined ) { - if ( source.bufferView !== undefined ) { + materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; - // Load binary image data from bufferView, if provided. + } - sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { + } - if ( source.mimeType === 'image/png' ) { + if ( materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial ) { - // Inspect the PNG 'IHDR' chunk to determine whether the image could have an - // alpha channel. This check is conservative — the image could have an alpha - // channel with all values == 1, and the indexed type (colorType == 3) only - // sometimes contains alpha. - // - // https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header - var colorType = new DataView( bufferView, 25, 1 ).getUint8( 0, false ); - hasAlpha = colorType === 6 || colorType === 4 || colorType === 3; + materialParams.emissive = new Color().fromArray( materialDef.emissiveFactor ); - } + } - isObjectURL = true; - var blob = new Blob( [ bufferView ], { type: source.mimeType } ); - sourceURI = URL.createObjectURL( blob ); - return sourceURI; + if ( materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial ) { - } ); + pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) ); } - return Promise.resolve( sourceURI ).then( function ( sourceURI ) { + return Promise.all( pending ).then( function () { - return new Promise( function ( resolve, reject ) { + var material; - var onLoad = resolve; + if ( materialType === GLTFMeshStandardSGMaterial ) { - if ( loader.isImageBitmapLoader === true ) { + material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); - onLoad = function ( imageBitmap ) { + } else { - resolve( new CanvasTexture( imageBitmap ) ); + material = new materialType( materialParams ); - }; + } - } + if ( materialDef.name ) material.name = materialDef.name; - loader.load( resolveURL( sourceURI, options.path ), onLoad, undefined, reject ); + // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. + if ( material.map ) material.map.encoding = sRGBEncoding; + if ( material.emissiveMap ) material.emissiveMap.encoding = sRGBEncoding; - } ); + assignExtrasToUserData( material, materialDef ); - } ).then( function ( texture ) { + parser.associations.set( material, { type: 'materials', index: materialIndex } ); - // Clean up resources and configure Texture. + if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); - if ( isObjectURL === true ) { + return material; - URL.revokeObjectURL( sourceURI ); + } ); - } + }; - texture.flipY = false; + /** When Object3D instances are targeted by animation, they need unique names. */ + GLTFParser.prototype.createUniqueName = function ( originalName ) { - if ( textureDef.name ) texture.name = textureDef.name; + var sanitizedName = PropertyBinding.sanitizeNodeName( originalName || '' ); - // When there is definitely no alpha channel in the texture, set RGBFormat to save space. - if ( ! hasAlpha ) texture.format = RGBFormat; + var name = sanitizedName; - var samplers = json.samplers || {}; - var sampler = samplers[ textureDef.sampler ] || {}; + for ( var i = 1; this.nodeNamesUsed[ name ]; ++ i ) { - texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || LinearFilter; - texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || LinearMipmapLinearFilter; - texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || RepeatWrapping; - texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || RepeatWrapping; + name = sanitizedName + '_' + i; - parser.associations.set( texture, { - type: 'textures', - index: textureIndex - } ); + } - return texture; + this.nodeNamesUsed[ name ] = true; - } ); + return name; }; /** - * Asynchronously assigns a texture to the given material parameters. - * @param {Object} materialParams - * @param {string} mapName - * @param {Object} mapDef - * @return {Promise} + * @param {BufferGeometry} geometry + * @param {GLTF.Primitive} primitiveDef + * @param {GLTFParser} parser */ - GLTFParser.prototype.assignTexture = function ( materialParams, mapName, mapDef ) { - - var parser = this; + function computeBounds( geometry, primitiveDef, parser ) { - return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { + var attributes = primitiveDef.attributes; - // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured - // However, we will copy UV set 0 to UV set 1 on demand for aoMap - if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) { + var box = new Box3(); - console.warn( 'THREE.GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' ); + if ( attributes.POSITION !== undefined ) { - } + var accessor = parser.json.accessors[ attributes.POSITION ]; - if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { + var min = accessor.min; + var max = accessor.max; - var transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; + // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. - if ( transform ) { + if ( min !== undefined && max !== undefined ) { - var gltfReference = parser.associations.get( texture ); - texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); - parser.associations.set( texture, gltfReference ); + box.set( + new Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ), + new Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) ); - } + } else { - } + console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); - materialParams[ mapName ] = texture; + return; - } ); + } - }; + } else { - /** - * Assigns final material to a Mesh, Line, or Points instance. The instance - * already has a material (generated from the glTF material options alone) - * but reuse of the same glTF material may require multiple threejs materials - * to accomodate different primitive types, defines, etc. New materials will - * be created if necessary, and reused from a cache. - * @param {Object3D} mesh Mesh, Line, or Points instance. - */ - GLTFParser.prototype.assignFinalMaterial = function ( mesh ) { + return; - var geometry = mesh.geometry; - var material = mesh.material; + } - var useVertexTangents = geometry.attributes.tangent !== undefined; - var useVertexColors = geometry.attributes.color !== undefined; - var useFlatShading = geometry.attributes.normal === undefined; - var useSkinning = mesh.isSkinnedMesh === true; - var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0; - var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; + var targets = primitiveDef.targets; - if ( mesh.isPoints ) { + if ( targets !== undefined ) { - var cacheKey = 'PointsMaterial:' + material.uuid; + var maxDisplacement = new Vector3(); + var vector = new Vector3(); - var pointsMaterial = this.cache.get( cacheKey ); + for ( var i = 0, il = targets.length; i < il; i ++ ) { - if ( ! pointsMaterial ) { + var target = targets[ i ]; - pointsMaterial = new PointsMaterial(); - Material.prototype.copy.call( pointsMaterial, material ); - pointsMaterial.color.copy( material.color ); - pointsMaterial.map = material.map; - pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px + if ( target.POSITION !== undefined ) { - this.cache.add( cacheKey, pointsMaterial ); + var accessor = parser.json.accessors[ target.POSITION ]; + var min = accessor.min; + var max = accessor.max; - } + // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. - material = pointsMaterial; + if ( min !== undefined && max !== undefined ) { - } else if ( mesh.isLine ) { + // we need to get max of absolute components because target weight is [-1,1] + vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) ); + vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) ); + vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) ); - var cacheKey = 'LineBasicMaterial:' + material.uuid; + // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative + // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets + // are used to implement key-frame animations and as such only two are active at a time - this results in very large + // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. + maxDisplacement.max( vector ); - var lineMaterial = this.cache.get( cacheKey ); + } else { - if ( ! lineMaterial ) { + console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); - lineMaterial = new LineBasicMaterial(); - Material.prototype.copy.call( lineMaterial, material ); - lineMaterial.color.copy( material.color ); + } - this.cache.add( cacheKey, lineMaterial ); + } } - material = lineMaterial; + // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. + box.expandByVector( maxDisplacement ); } - // Clone the material if it will be modified - if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets ) { - - var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; - - if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; - if ( useSkinning ) cacheKey += 'skinning:'; - if ( useVertexTangents ) cacheKey += 'vertex-tangents:'; - if ( useVertexColors ) cacheKey += 'vertex-colors:'; - if ( useFlatShading ) cacheKey += 'flat-shading:'; - if ( useMorphTargets ) cacheKey += 'morph-targets:'; - if ( useMorphNormals ) cacheKey += 'morph-normals:'; - - var cachedMaterial = this.cache.get( cacheKey ); + geometry.boundingBox = box; - if ( ! cachedMaterial ) { + var sphere = new Sphere(); - cachedMaterial = material.clone(); + box.getCenter( sphere.center ); + sphere.radius = box.min.distanceTo( box.max ) / 2; - if ( useSkinning ) cachedMaterial.skinning = true; - if ( useVertexTangents ) cachedMaterial.vertexTangents = true; - if ( useVertexColors ) cachedMaterial.vertexColors = true; - if ( useFlatShading ) cachedMaterial.flatShading = true; - if ( useMorphTargets ) cachedMaterial.morphTargets = true; - if ( useMorphNormals ) cachedMaterial.morphNormals = true; + geometry.boundingSphere = sphere; - this.cache.add( cacheKey, cachedMaterial ); + } - this.associations.set( cachedMaterial, this.associations.get( material ) ); + /** + * @param {BufferGeometry} geometry + * @param {GLTF.Primitive} primitiveDef + * @param {GLTFParser} parser + * @return {Promise} + */ + function addPrimitiveAttributes( geometry, primitiveDef, parser ) { - } + var attributes = primitiveDef.attributes; - material = cachedMaterial; + var pending = []; - } + function assignAttributeAccessor( accessorIndex, attributeName ) { - // workarounds for mesh and geometry + return parser.getDependency( 'accessor', accessorIndex ) + .then( function ( accessor ) { - if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) { + geometry.setAttribute( attributeName, accessor ); - geometry.setAttribute( 'uv2', geometry.attributes.uv ); + } ); } - // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 - if ( material.normalScale && ! useVertexTangents ) { - - material.normalScale.y = - material.normalScale.y; + for ( var gltfAttributeName in attributes ) { - } + var threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); - if ( material.clearcoatNormalScale && ! useVertexTangents ) { + // Skip attributes already provided by e.g. Draco extension. + if ( threeAttributeName in geometry.attributes ) continue; - material.clearcoatNormalScale.y = - material.clearcoatNormalScale.y; + pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); } - mesh.material = material; - - }; - - GLTFParser.prototype.getMaterialType = function ( /* materialIndex */ ) { + if ( primitiveDef.indices !== undefined && ! geometry.index ) { - return MeshStandardMaterial; + var accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { - }; + geometry.setIndex( accessor ); - /** - * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials - * @param {number} materialIndex - * @return {Promise} - */ - GLTFParser.prototype.loadMaterial = function ( materialIndex ) { + } ); - var parser = this; - var json = this.json; - var extensions = this.extensions; - var materialDef = json.materials[ materialIndex ]; + pending.push( accessor ); - var materialType; - var materialParams = {}; - var materialExtensions = materialDef.extensions || {}; + } - var pending = []; + assignExtrasToUserData( geometry, primitiveDef ); - if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { + computeBounds( geometry, primitiveDef, parser ); - var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; - materialType = sgExtension.getMaterialType(); - pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); + return Promise.all( pending ).then( function () { - } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { + return primitiveDef.targets !== undefined + ? addMorphTargets( geometry, primitiveDef.targets, parser ) + : geometry; - var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; - materialType = kmuExtension.getMaterialType(); - pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); + } ); - } else { + } - // Specification: - // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material + /** + * @param {BufferGeometry} geometry + * @param {Number} drawMode + * @return {BufferGeometry} + */ + function toTrianglesDrawMode( geometry, drawMode ) { - var metallicRoughness = materialDef.pbrMetallicRoughness || {}; + var index = geometry.getIndex(); - materialParams.color = new Color( 1.0, 1.0, 1.0 ); - materialParams.opacity = 1.0; + // generate index if not present - if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { + if ( index === null ) { - var array = metallicRoughness.baseColorFactor; + var indices = []; - materialParams.color.fromArray( array ); - materialParams.opacity = array[ 3 ]; + var position = geometry.getAttribute( 'position' ); - } + if ( position !== undefined ) { - if ( metallicRoughness.baseColorTexture !== undefined ) { + for ( var i = 0; i < position.count; i ++ ) { - pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); + indices.push( i ); - } + } - materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; - materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; + geometry.setIndex( indices ); + index = geometry.getIndex(); - if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { + } else { - pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); - pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); + console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); + return geometry; } - materialType = this._invokeOne( function ( ext ) { - - return ext.getMaterialType && ext.getMaterialType( materialIndex ); + } - } ); + // - pending.push( Promise.all( this._invokeAll( function ( ext ) { + var numberOfTriangles = index.count - 2; + var newIndices = []; - return ext.extendMaterialParams && ext.extendMaterialParams( materialIndex, materialParams ); + if ( drawMode === TriangleFanDrawMode ) { - } ) ) ); + // gl.TRIANGLE_FAN - } + for ( var i = 1; i <= numberOfTriangles; i ++ ) { - if ( materialDef.doubleSided === true ) { + newIndices.push( index.getX( 0 ) ); + newIndices.push( index.getX( i ) ); + newIndices.push( index.getX( i + 1 ) ); - materialParams.side = DoubleSide; + } - } + } else { - var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; + // gl.TRIANGLE_STRIP - if ( alphaMode === ALPHA_MODES.BLEND ) { + for ( var i = 0; i < numberOfTriangles; i ++ ) { - materialParams.transparent = true; + if ( i % 2 === 0 ) { - // See: https://github.com/mrdoob/three.js/issues/17706 - materialParams.depthWrite = false; + newIndices.push( index.getX( i ) ); + newIndices.push( index.getX( i + 1 ) ); + newIndices.push( index.getX( i + 2 ) ); - } else { - materialParams.transparent = false; + } else { - if ( alphaMode === ALPHA_MODES.MASK ) { + newIndices.push( index.getX( i + 2 ) ); + newIndices.push( index.getX( i + 1 ) ); + newIndices.push( index.getX( i ) ); - materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; + } } } - if ( materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial ) { + if ( ( newIndices.length / 3 ) !== numberOfTriangles ) { - pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); + console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); - materialParams.normalScale = new Vector2( 1, 1 ); + } - if ( materialDef.normalTexture.scale !== undefined ) { + // build final geometry - materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale ); + var newGeometry = geometry.clone(); + newGeometry.setIndex( newIndices ); - } + return newGeometry; - } + } - if ( materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial ) { + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry + * + * Creates BufferGeometries from primitives. + * + * @param {Array} primitives + * @return {Promise>} + */ + GLTFParser.prototype.loadGeometries = function ( primitives ) { - pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); + var parser = this; + var extensions = this.extensions; + var cache = this.primitiveCache; - if ( materialDef.occlusionTexture.strength !== undefined ) { + function createDracoPrimitive( primitive ) { - materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; + return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] + .decodePrimitive( primitive, parser ) + .then( function ( geometry ) { - } + return addPrimitiveAttributes( geometry, primitive, parser ); + + } ); } - if ( materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial ) { + var pending = []; - materialParams.emissive = new Color().fromArray( materialDef.emissiveFactor ); + for ( var i = 0, il = primitives.length; i < il; i ++ ) { - } + var primitive = primitives[ i ]; + var cacheKey = createPrimitiveKey( primitive ); - if ( materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial ) { + // See if we've already created this geometry + var cached = cache[ cacheKey ]; - pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) ); + if ( cached ) { - } + // Use the cached geometry if it exists + pending.push( cached.promise ); - return Promise.all( pending ).then( function () { + } else { - var material; + var geometryPromise; - if ( materialType === GLTFMeshStandardSGMaterial ) { + if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { - material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); + // Use DRACO geometry if available + geometryPromise = createDracoPrimitive( primitive ); - } else { + } else { - material = new materialType( materialParams ); + // Otherwise create a new geometry + geometryPromise = addPrimitiveAttributes( new BufferGeometry(), primitive, parser ); - } + } - if ( materialDef.name ) material.name = materialDef.name; + // Cache this geometry + cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise }; - // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. - if ( material.map ) material.map.encoding = sRGBEncoding; - if ( material.emissiveMap ) material.emissiveMap.encoding = sRGBEncoding; + pending.push( geometryPromise ); - assignExtrasToUserData( material, materialDef ); + } - parser.associations.set( material, { type: 'materials', index: materialIndex } ); + } - if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); + return Promise.all( pending ); - return material; + }; - } ); + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes + * @param {number} meshIndex + * @return {Promise} + */ + GLTFParser.prototype.loadMesh = function ( meshIndex ) { - }; + var parser = this; + var json = this.json; + var extensions = this.extensions; - /** When Object3D instances are targeted by animation, they need unique names. */ - GLTFParser.prototype.createUniqueName = function ( originalName ) { + var meshDef = json.meshes[ meshIndex ]; + var primitives = meshDef.primitives; - var sanitizedName = PropertyBinding.sanitizeNodeName( originalName || '' ); + var pending = []; - var name = sanitizedName; + for ( var i = 0, il = primitives.length; i < il; i ++ ) { - for ( var i = 1; this.nodeNamesUsed[ name ]; ++ i ) { + var material = primitives[ i ].material === undefined + ? createDefaultMaterial( this.cache ) + : this.getDependency( 'material', primitives[ i ].material ); - name = sanitizedName + '_' + i; + pending.push( material ); } - this.nodeNamesUsed[ name ] = true; + pending.push( parser.loadGeometries( primitives ) ); - return name; + return Promise.all( pending ).then( function ( results ) { - }; + var materials = results.slice( 0, results.length - 1 ); + var geometries = results[ results.length - 1 ]; - /** - * @param {BufferGeometry} geometry - * @param {GLTF.Primitive} primitiveDef - * @param {GLTFParser} parser - */ - function computeBounds( geometry, primitiveDef, parser ) { + var meshes = []; - var attributes = primitiveDef.attributes; + for ( var i = 0, il = geometries.length; i < il; i ++ ) { - var box = new Box3(); + var geometry = geometries[ i ]; + var primitive = primitives[ i ]; - if ( attributes.POSITION !== undefined ) { + // 1. create Mesh - var accessor = parser.json.accessors[ attributes.POSITION ]; + var mesh; - var min = accessor.min; - var max = accessor.max; + var material = materials[ i ]; - // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. + if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || + primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || + primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || + primitive.mode === undefined ) { - if ( min !== undefined && max !== undefined ) { + // .isSkinnedMesh isn't in glTF spec. See ._markDefs() + mesh = meshDef.isSkinnedMesh === true + ? new SkinnedMesh( geometry, material ) + : new Mesh( geometry, material ); - box.set( - new Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ), - new Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) ); + if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) { + + // we normalize floating point skin weight array to fix malformed assets (see #15319) + // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs + mesh.normalizeSkinWeights(); + + } + + if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { + + mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode ); + + } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { + + mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleFanDrawMode ); + + } - } else { + } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { - console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); + mesh = new LineSegments( geometry, material ); - return; + } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { - } + mesh = new Line( geometry, material ); - } else { + } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { - return; + mesh = new LineLoop( geometry, material ); - } + } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { - var targets = primitiveDef.targets; + mesh = new Points( geometry, material ); - if ( targets !== undefined ) { + } else { - var maxDisplacement = new Vector3(); - var vector = new Vector3(); + throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); - for ( var i = 0, il = targets.length; i < il; i ++ ) { + } - var target = targets[ i ]; + if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { - if ( target.POSITION !== undefined ) { + updateMorphTargets( mesh, meshDef ); - var accessor = parser.json.accessors[ target.POSITION ]; - var min = accessor.min; - var max = accessor.max; + } - // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. + mesh.name = parser.createUniqueName( meshDef.name || ( 'mesh_' + meshIndex ) ); - if ( min !== undefined && max !== undefined ) { + assignExtrasToUserData( mesh, meshDef ); - // we need to get max of absolute components because target weight is [-1,1] - vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) ); - vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) ); - vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) ); + if ( primitive.extensions ) addUnknownExtensionsToUserData( extensions, mesh, primitive ); - // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative - // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets - // are used to implement key-frame animations and as such only two are active at a time - this results in very large - // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. - maxDisplacement.max( vector ); + parser.assignFinalMaterial( mesh ); - } else { + meshes.push( mesh ); - console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); + } - } + if ( meshes.length === 1 ) { - } + return meshes[ 0 ]; } - // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. - box.expandByVector( maxDisplacement ); + var group = new Group(); - } + for ( var i = 0, il = meshes.length; i < il; i ++ ) { - geometry.boundingBox = box; + group.add( meshes[ i ] ); - var sphere = new Sphere(); + } - box.getCenter( sphere.center ); - sphere.radius = box.min.distanceTo( box.max ) / 2; + return group; - geometry.boundingSphere = sphere; + } ); - } + }; /** - * @param {BufferGeometry} geometry - * @param {GLTF.Primitive} primitiveDef - * @param {GLTFParser} parser - * @return {Promise} + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras + * @param {number} cameraIndex + * @return {Promise} */ - function addPrimitiveAttributes( geometry, primitiveDef, parser ) { + GLTFParser.prototype.loadCamera = function ( cameraIndex ) { - var attributes = primitiveDef.attributes; + var camera; + var cameraDef = this.json.cameras[ cameraIndex ]; + var params = cameraDef[ cameraDef.type ]; - var pending = []; + if ( ! params ) { - function assignAttributeAccessor( accessorIndex, attributeName ) { + console.warn( 'THREE.GLTFLoader: Missing camera parameters.' ); + return; - return parser.getDependency( 'accessor', accessorIndex ) - .then( function ( accessor ) { + } - geometry.setAttribute( attributeName, accessor ); + if ( cameraDef.type === 'perspective' ) { - } ); + camera = new PerspectiveCamera( MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); - } + } else if ( cameraDef.type === 'orthographic' ) { - for ( var gltfAttributeName in attributes ) { + camera = new OrthographicCamera( - params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar ); - var threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); + } - // Skip attributes already provided by e.g. Draco extension. - if ( threeAttributeName in geometry.attributes ) continue; + if ( cameraDef.name ) camera.name = this.createUniqueName( cameraDef.name ); - pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); + assignExtrasToUserData( camera, cameraDef ); - } + return Promise.resolve( camera ); - if ( primitiveDef.indices !== undefined && ! geometry.index ) { + }; - var accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins + * @param {number} skinIndex + * @return {Promise} + */ + GLTFParser.prototype.loadSkin = function ( skinIndex ) { - geometry.setIndex( accessor ); + var skinDef = this.json.skins[ skinIndex ]; - } ); + var skinEntry = { joints: skinDef.joints }; - pending.push( accessor ); + if ( skinDef.inverseBindMatrices === undefined ) { - } + return Promise.resolve( skinEntry ); - assignExtrasToUserData( geometry, primitiveDef ); + } - computeBounds( geometry, primitiveDef, parser ); + return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { - return Promise.all( pending ).then( function () { + skinEntry.inverseBindMatrices = accessor; - return primitiveDef.targets !== undefined - ? addMorphTargets( geometry, primitiveDef.targets, parser ) - : geometry; + return skinEntry; } ); - } + }; /** - * @param {BufferGeometry} geometry - * @param {Number} drawMode - * @return {BufferGeometry} + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations + * @param {number} animationIndex + * @return {Promise} */ - function toTrianglesDrawMode( geometry, drawMode ) { + GLTFParser.prototype.loadAnimation = function ( animationIndex ) { - var index = geometry.getIndex(); + var json = this.json; - // generate index if not present + var animationDef = json.animations[ animationIndex ]; - if ( index === null ) { + var pendingNodes = []; + var pendingInputAccessors = []; + var pendingOutputAccessors = []; + var pendingSamplers = []; + var pendingTargets = []; - var indices = []; + for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) { - var position = geometry.getAttribute( 'position' ); + var channel = animationDef.channels[ i ]; + var sampler = animationDef.samplers[ channel.sampler ]; + var target = channel.target; + var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. + var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; + var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; - if ( position !== undefined ) { + pendingNodes.push( this.getDependency( 'node', name ) ); + pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); + pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); + pendingSamplers.push( sampler ); + pendingTargets.push( target ); - for ( var i = 0; i < position.count; i ++ ) { + } - indices.push( i ); + return Promise.all( [ - } + Promise.all( pendingNodes ), + Promise.all( pendingInputAccessors ), + Promise.all( pendingOutputAccessors ), + Promise.all( pendingSamplers ), + Promise.all( pendingTargets ) - geometry.setIndex( indices ); - index = geometry.getIndex(); + ] ).then( function ( dependencies ) { - } else { + var nodes = dependencies[ 0 ]; + var inputAccessors = dependencies[ 1 ]; + var outputAccessors = dependencies[ 2 ]; + var samplers = dependencies[ 3 ]; + var targets = dependencies[ 4 ]; - console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); - return geometry; + var tracks = []; - } + for ( var i = 0, il = nodes.length; i < il; i ++ ) { - } + var node = nodes[ i ]; + var inputAccessor = inputAccessors[ i ]; + var outputAccessor = outputAccessors[ i ]; + var sampler = samplers[ i ]; + var target = targets[ i ]; - // + if ( node === undefined ) continue; - var numberOfTriangles = index.count - 2; - var newIndices = []; + node.updateMatrix(); + node.matrixAutoUpdate = true; - if ( drawMode === TriangleFanDrawMode ) { + var TypedKeyframeTrack; - // gl.TRIANGLE_FAN + switch ( PATH_PROPERTIES[ target.path ] ) { - for ( var i = 1; i <= numberOfTriangles; i ++ ) { + case PATH_PROPERTIES.weights: - newIndices.push( index.getX( 0 ) ); - newIndices.push( index.getX( i ) ); - newIndices.push( index.getX( i + 1 ) ); + TypedKeyframeTrack = NumberKeyframeTrack; + break; - } + case PATH_PROPERTIES.rotation: - } else { + TypedKeyframeTrack = QuaternionKeyframeTrack; + break; - // gl.TRIANGLE_STRIP + case PATH_PROPERTIES.position: + case PATH_PROPERTIES.scale: + default: - for ( var i = 0; i < numberOfTriangles; i ++ ) { + TypedKeyframeTrack = VectorKeyframeTrack; + break; - if ( i % 2 === 0 ) { + } - newIndices.push( index.getX( i ) ); - newIndices.push( index.getX( i + 1 ) ); - newIndices.push( index.getX( i + 2 ) ); + var targetName = node.name ? node.name : node.uuid; + var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear; - } else { + var targetNames = []; - newIndices.push( index.getX( i + 2 ) ); - newIndices.push( index.getX( i + 1 ) ); - newIndices.push( index.getX( i ) ); + if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { + + // Node may be a Group (glTF mesh with several primitives) or a Mesh. + node.traverse( function ( object ) { + + if ( object.isMesh === true && object.morphTargetInfluences ) { + + targetNames.push( object.name ? object.name : object.uuid ); + + } + + } ); + + } else { + + targetNames.push( targetName ); } - } + var outputArray = outputAccessor.array; - } + if ( outputAccessor.normalized ) { - if ( ( newIndices.length / 3 ) !== numberOfTriangles ) { + var scale; - console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); + if ( outputArray.constructor === Int8Array ) { - } + scale = 1 / 127; - // build final geometry + } else if ( outputArray.constructor === Uint8Array ) { - var newGeometry = geometry.clone(); - newGeometry.setIndex( newIndices ); + scale = 1 / 255; - return newGeometry; + } else if ( outputArray.constructor == Int16Array ) { - } + scale = 1 / 32767; - /** - * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry - * - * Creates BufferGeometries from primitives. - * - * @param {Array} primitives - * @return {Promise>} - */ - GLTFParser.prototype.loadGeometries = function ( primitives ) { + } else if ( outputArray.constructor === Uint16Array ) { - var parser = this; - var extensions = this.extensions; - var cache = this.primitiveCache; + scale = 1 / 65535; - function createDracoPrimitive( primitive ) { + } else { - return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] - .decodePrimitive( primitive, parser ) - .then( function ( geometry ) { + throw new Error( 'THREE.GLTFLoader: Unsupported output accessor component type.' ); - return addPrimitiveAttributes( geometry, primitive, parser ); + } - } ); + var scaled = new Float32Array( outputArray.length ); - } + for ( var j = 0, jl = outputArray.length; j < jl; j ++ ) { - var pending = []; + scaled[ j ] = outputArray[ j ] * scale; - for ( var i = 0, il = primitives.length; i < il; i ++ ) { + } - var primitive = primitives[ i ]; - var cacheKey = createPrimitiveKey( primitive ); + outputArray = scaled; - // See if we've already created this geometry - var cached = cache[ cacheKey ]; + } - if ( cached ) { + for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) { - // Use the cached geometry if it exists - pending.push( cached.promise ); + var track = new TypedKeyframeTrack( + targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], + inputAccessor.array, + outputArray, + interpolation + ); - } else { + // Override interpolation with custom factory method. + if ( sampler.interpolation === 'CUBICSPLINE' ) { - var geometryPromise; + track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { - if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { + // A CUBICSPLINE keyframe in glTF has three output values for each input value, + // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() + // must be divided by three to get the interpolant's sampleSize argument. - // Use DRACO geometry if available - geometryPromise = createDracoPrimitive( primitive ); + return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result ); - } else { + }; - // Otherwise create a new geometry - geometryPromise = addPrimitiveAttributes( new BufferGeometry(), primitive, parser ); + // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. + track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; - } + } - // Cache this geometry - cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise }; + tracks.push( track ); - pending.push( geometryPromise ); + } } - } + var name = animationDef.name ? animationDef.name : 'animation_' + animationIndex; - return Promise.all( pending ); + return new AnimationClip( name, undefined, tracks ); + + } ); }; /** - * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes - * @param {number} meshIndex - * @return {Promise} + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy + * @param {number} nodeIndex + * @return {Promise} */ - GLTFParser.prototype.loadMesh = function ( meshIndex ) { + GLTFParser.prototype.loadNode = function ( nodeIndex ) { - var parser = this; var json = this.json; var extensions = this.extensions; + var parser = this; - var meshDef = json.meshes[ meshIndex ]; - var primitives = meshDef.primitives; + var nodeDef = json.nodes[ nodeIndex ]; - var pending = []; + // reserve node's name before its dependencies, so the root has the intended name. + var nodeName = nodeDef.name ? parser.createUniqueName( nodeDef.name ) : ''; - for ( var i = 0, il = primitives.length; i < il; i ++ ) { + return ( function () { - var material = primitives[ i ].material === undefined - ? createDefaultMaterial( this.cache ) - : this.getDependency( 'material', primitives[ i ].material ); + var pending = []; - pending.push( material ); + if ( nodeDef.mesh !== undefined ) { - } + pending.push( parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { - pending.push( parser.loadGeometries( primitives ) ); + var node = parser._getNodeRef( parser.meshCache, nodeDef.mesh, mesh ); - return Promise.all( pending ).then( function ( results ) { + // if weights are provided on the node, override weights on the mesh. + if ( nodeDef.weights !== undefined ) { - var materials = results.slice( 0, results.length - 1 ); - var geometries = results[ results.length - 1 ]; + node.traverse( function ( o ) { - var meshes = []; + if ( ! o.isMesh ) return; - for ( var i = 0, il = geometries.length; i < il; i ++ ) { + for ( var i = 0, il = nodeDef.weights.length; i < il; i ++ ) { - var geometry = geometries[ i ]; - var primitive = primitives[ i ]; + o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; - // 1. create Mesh + } - var mesh; + } ); - var material = materials[ i ]; + } - if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || - primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || - primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || - primitive.mode === undefined ) { + return node; - // .isSkinnedMesh isn't in glTF spec. See ._markDefs() - mesh = meshDef.isSkinnedMesh === true - ? new SkinnedMesh( geometry, material ) - : new Mesh( geometry, material ); + } ) ); - if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) { + } - // we normalize floating point skin weight array to fix malformed assets (see #15319) - // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs - mesh.normalizeSkinWeights(); + if ( nodeDef.camera !== undefined ) { - } + pending.push( parser.getDependency( 'camera', nodeDef.camera ).then( function ( camera ) { - if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { + return parser._getNodeRef( parser.cameraCache, nodeDef.camera, camera ); - mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode ); + } ) ); - } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { + } - mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleFanDrawMode ); + parser._invokeAll( function ( ext ) { - } + return ext.createNodeAttachment && ext.createNodeAttachment( nodeIndex ); - } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { + } ).forEach( function ( promise ) { - mesh = new LineSegments( geometry, material ); + pending.push( promise ); - } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { + } ); - mesh = new Line( geometry, material ); + return Promise.all( pending ); - } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { + }() ).then( function ( objects ) { - mesh = new LineLoop( geometry, material ); + var node; - } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { + // .isBone isn't in glTF spec. See ._markDefs + if ( nodeDef.isBone === true ) { - mesh = new Points( geometry, material ); + node = new Bone(); - } else { + } else if ( objects.length > 1 ) { - throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); + node = new Group(); - } + } else if ( objects.length === 1 ) { - if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { + node = objects[ 0 ]; - updateMorphTargets( mesh, meshDef ); + } else { - } + node = new Object3D(); - mesh.name = parser.createUniqueName( meshDef.name || ( 'mesh_' + meshIndex ) ); + } - assignExtrasToUserData( mesh, meshDef ); + if ( node !== objects[ 0 ] ) { - if ( primitive.extensions ) addUnknownExtensionsToUserData( extensions, mesh, primitive ); + for ( var i = 0, il = objects.length; i < il; i ++ ) { - parser.assignFinalMaterial( mesh ); + node.add( objects[ i ] ); - meshes.push( mesh ); + } } - if ( meshes.length === 1 ) { + if ( nodeDef.name ) { - return meshes[ 0 ]; + node.userData.name = nodeDef.name; + node.name = nodeName; } - var group = new Group(); - - for ( var i = 0, il = meshes.length; i < il; i ++ ) { - - group.add( meshes[ i ] ); + assignExtrasToUserData( node, nodeDef ); - } + if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); - return group; + if ( nodeDef.matrix !== undefined ) { - } ); + var matrix = new Matrix4(); + matrix.fromArray( nodeDef.matrix ); + node.applyMatrix4( matrix ); - }; + } else { - /** - * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras - * @param {number} cameraIndex - * @return {Promise} - */ - GLTFParser.prototype.loadCamera = function ( cameraIndex ) { + if ( nodeDef.translation !== undefined ) { - var camera; - var cameraDef = this.json.cameras[ cameraIndex ]; - var params = cameraDef[ cameraDef.type ]; + node.position.fromArray( nodeDef.translation ); - if ( ! params ) { + } - console.warn( 'THREE.GLTFLoader: Missing camera parameters.' ); - return; + if ( nodeDef.rotation !== undefined ) { - } + node.quaternion.fromArray( nodeDef.rotation ); - if ( cameraDef.type === 'perspective' ) { + } - camera = new PerspectiveCamera( MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); + if ( nodeDef.scale !== undefined ) { - } else if ( cameraDef.type === 'orthographic' ) { + node.scale.fromArray( nodeDef.scale ); - camera = new OrthographicCamera( - params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar ); + } - } + } - if ( cameraDef.name ) camera.name = this.createUniqueName( cameraDef.name ); + parser.associations.set( node, { type: 'nodes', index: nodeIndex } ); - assignExtrasToUserData( camera, cameraDef ); + return node; - return Promise.resolve( camera ); + } ); }; /** - * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins - * @param {number} skinIndex - * @return {Promise} + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes + * @param {number} sceneIndex + * @return {Promise} */ - GLTFParser.prototype.loadSkin = function ( skinIndex ) { + GLTFParser.prototype.loadScene = function () { - var skinDef = this.json.skins[ skinIndex ]; + // scene node hierachy builder - var skinEntry = { joints: skinDef.joints }; + function buildNodeHierachy( nodeId, parentObject, json, parser ) { - if ( skinDef.inverseBindMatrices === undefined ) { + var nodeDef = json.nodes[ nodeId ]; - return Promise.resolve( skinEntry ); + return parser.getDependency( 'node', nodeId ).then( function ( node ) { - } + if ( nodeDef.skin === undefined ) return node; - return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { + // build skeleton here as well - skinEntry.inverseBindMatrices = accessor; + var skinEntry; - return skinEntry; + return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) { - } ); + skinEntry = skin; - }; + var pendingJoints = []; - /** - * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations - * @param {number} animationIndex - * @return {Promise} - */ - GLTFParser.prototype.loadAnimation = function ( animationIndex ) { + for ( var i = 0, il = skinEntry.joints.length; i < il; i ++ ) { - var json = this.json; + pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) ); - var animationDef = json.animations[ animationIndex ]; + } - var pendingNodes = []; - var pendingInputAccessors = []; - var pendingOutputAccessors = []; - var pendingSamplers = []; - var pendingTargets = []; + return Promise.all( pendingJoints ); - for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) { + } ).then( function ( jointNodes ) { - var channel = animationDef.channels[ i ]; - var sampler = animationDef.samplers[ channel.sampler ]; - var target = channel.target; - var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. - var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; - var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; + node.traverse( function ( mesh ) { - pendingNodes.push( this.getDependency( 'node', name ) ); - pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); - pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); - pendingSamplers.push( sampler ); - pendingTargets.push( target ); + if ( ! mesh.isMesh ) return; - } + var bones = []; + var boneInverses = []; - return Promise.all( [ + for ( var j = 0, jl = jointNodes.length; j < jl; j ++ ) { - Promise.all( pendingNodes ), - Promise.all( pendingInputAccessors ), - Promise.all( pendingOutputAccessors ), - Promise.all( pendingSamplers ), - Promise.all( pendingTargets ) + var jointNode = jointNodes[ j ]; - ] ).then( function ( dependencies ) { + if ( jointNode ) { - var nodes = dependencies[ 0 ]; - var inputAccessors = dependencies[ 1 ]; - var outputAccessors = dependencies[ 2 ]; - var samplers = dependencies[ 3 ]; - var targets = dependencies[ 4 ]; + bones.push( jointNode ); - var tracks = []; + var mat = new Matrix4(); - for ( var i = 0, il = nodes.length; i < il; i ++ ) { + if ( skinEntry.inverseBindMatrices !== undefined ) { - var node = nodes[ i ]; - var inputAccessor = inputAccessors[ i ]; - var outputAccessor = outputAccessors[ i ]; - var sampler = samplers[ i ]; - var target = targets[ i ]; + mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); - if ( node === undefined ) continue; + } - node.updateMatrix(); - node.matrixAutoUpdate = true; + boneInverses.push( mat ); - var TypedKeyframeTrack; + } else { - switch ( PATH_PROPERTIES[ target.path ] ) { + console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] ); - case PATH_PROPERTIES.weights: + } - TypedKeyframeTrack = NumberKeyframeTrack; - break; + } - case PATH_PROPERTIES.rotation: + mesh.bind( new Skeleton( bones, boneInverses ), mesh.matrixWorld ); - TypedKeyframeTrack = QuaternionKeyframeTrack; - break; + } ); - case PATH_PROPERTIES.position: - case PATH_PROPERTIES.scale: - default: + return node; - TypedKeyframeTrack = VectorKeyframeTrack; - break; + } ); - } + } ).then( function ( node ) { - var targetName = node.name ? node.name : node.uuid; + // build node hierachy - var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear; + parentObject.add( node ); - var targetNames = []; + var pending = []; - if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { + if ( nodeDef.children ) { - // Node may be a Group (glTF mesh with several primitives) or a Mesh. - node.traverse( function ( object ) { + var children = nodeDef.children; - if ( object.isMesh === true && object.morphTargetInfluences ) { + for ( var i = 0, il = children.length; i < il; i ++ ) { - targetNames.push( object.name ? object.name : object.uuid ); + var child = children[ i ]; + pending.push( buildNodeHierachy( child, node, json, parser ) ); - } + } - } ); + } - } else { + return Promise.all( pending ); - targetNames.push( targetName ); + } ); - } + } - var outputArray = outputAccessor.array; + return function loadScene( sceneIndex ) { - if ( outputAccessor.normalized ) { + var json = this.json; + var extensions = this.extensions; + var sceneDef = this.json.scenes[ sceneIndex ]; + var parser = this; - var scale; + // Loader returns Group, not Scene. + // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 + var scene = new Group(); + if ( sceneDef.name ) scene.name = parser.createUniqueName( sceneDef.name ); - if ( outputArray.constructor === Int8Array ) { + assignExtrasToUserData( scene, sceneDef ); - scale = 1 / 127; + if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); - } else if ( outputArray.constructor === Uint8Array ) { + var nodeIds = sceneDef.nodes || []; - scale = 1 / 255; + var pending = []; - } else if ( outputArray.constructor == Int16Array ) { + for ( var i = 0, il = nodeIds.length; i < il; i ++ ) { - scale = 1 / 32767; + pending.push( buildNodeHierachy( nodeIds[ i ], scene, json, parser ) ); - } else if ( outputArray.constructor === Uint16Array ) { + } - scale = 1 / 65535; + return Promise.all( pending ).then( function () { - } else { + return scene; - throw new Error( 'THREE.GLTFLoader: Unsupported output accessor component type.' ); + } ); - } + }; - var scaled = new Float32Array( outputArray.length ); + }(); - for ( var j = 0, jl = outputArray.length; j < jl; j ++ ) { + return GLTFLoader; - scaled[ j ] = outputArray[ j ] * scale; + } )(); - } + /** + * @webxr-input-profiles/motion-controllers 1.0.0 https://github.com/immersive-web/webxr-input-profiles + */ - outputArray = scaled; + const Constants = { + Handedness: Object.freeze({ + NONE: 'none', + LEFT: 'left', + RIGHT: 'right' + }), - } + ComponentState: Object.freeze({ + DEFAULT: 'default', + TOUCHED: 'touched', + PRESSED: 'pressed' + }), - for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) { + ComponentProperty: Object.freeze({ + BUTTON: 'button', + X_AXIS: 'xAxis', + Y_AXIS: 'yAxis', + STATE: 'state' + }), - var track = new TypedKeyframeTrack( - targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], - inputAccessor.array, - outputArray, - interpolation - ); + ComponentType: Object.freeze({ + TRIGGER: 'trigger', + SQUEEZE: 'squeeze', + TOUCHPAD: 'touchpad', + THUMBSTICK: 'thumbstick', + BUTTON: 'button' + }), - // Override interpolation with custom factory method. - if ( sampler.interpolation === 'CUBICSPLINE' ) { + ButtonTouchThreshold: 0.05, - track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { + AxisTouchThreshold: 0.1, - // A CUBICSPLINE keyframe in glTF has three output values for each input value, - // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() - // must be divided by three to get the interpolant's sampleSize argument. + VisualResponseProperty: Object.freeze({ + TRANSFORM: 'transform', + VISIBILITY: 'visibility' + }) + }; + + /** + * @description Static helper function to fetch a JSON file and turn it into a JS object + * @param {string} path - Path to JSON file to be fetched + */ + async function fetchJsonFile(path) { + const response = await fetch(path); + if (!response.ok) { + throw new Error(response.statusText); + } else { + return response.json(); + } + } - return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result ); + async function fetchProfilesList(basePath) { + if (!basePath) { + throw new Error('No basePath supplied'); + } - }; + const profileListFileName = 'profilesList.json'; + const profilesList = await fetchJsonFile(`${basePath}/${profileListFileName}`); + return profilesList; + } - // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. - track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; + async function fetchProfile(xrInputSource, basePath, defaultProfile = null, getAssetPath = true) { + if (!xrInputSource) { + throw new Error('No xrInputSource supplied'); + } - } + if (!basePath) { + throw new Error('No basePath supplied'); + } - tracks.push( track ); + // Get the list of profiles + const supportedProfilesList = await fetchProfilesList(basePath); - } + // Find the relative path to the first requested profile that is recognized + let match; + xrInputSource.profiles.some((profileId) => { + const supportedProfile = supportedProfilesList[profileId]; + if (supportedProfile) { + match = { + profileId, + profilePath: `${basePath}/${supportedProfile.path}`, + deprecated: !!supportedProfile.deprecated + }; + } + return !!match; + }); - } + if (!match) { + if (!defaultProfile) { + throw new Error('No matching profile name found'); + } - var name = animationDef.name ? animationDef.name : 'animation_' + animationIndex; + const supportedProfile = supportedProfilesList[defaultProfile]; + if (!supportedProfile) { + throw new Error(`No matching profile name found and default profile "${defaultProfile}" missing.`); + } - return new AnimationClip( name, undefined, tracks ); + match = { + profileId: defaultProfile, + profilePath: `${basePath}/${supportedProfile.path}`, + deprecated: !!supportedProfile.deprecated + }; + } - } ); + const profile = await fetchJsonFile(match.profilePath); - }; + let assetPath; + if (getAssetPath) { + let layout; + if (xrInputSource.handedness === 'any') { + layout = profile.layouts[Object.keys(profile.layouts)[0]]; + } else { + layout = profile.layouts[xrInputSource.handedness]; + } + if (!layout) { + throw new Error( + `No matching handedness, ${xrInputSource.handedness}, in profile ${match.profileId}` + ); + } - /** - * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy - * @param {number} nodeIndex - * @return {Promise} - */ - GLTFParser.prototype.loadNode = function ( nodeIndex ) { + if (layout.assetPath) { + assetPath = match.profilePath.replace('profile.json', layout.assetPath); + } + } - var json = this.json; - var extensions = this.extensions; - var parser = this; + return { profile, assetPath }; + } - var nodeDef = json.nodes[ nodeIndex ]; + /** @constant {Object} */ + const defaultComponentValues = { + xAxis: 0, + yAxis: 0, + button: 0, + state: Constants.ComponentState.DEFAULT + }; - // reserve node's name before its dependencies, so the root has the intended name. - var nodeName = nodeDef.name ? parser.createUniqueName( nodeDef.name ) : ''; + /** + * @description Converts an X, Y coordinate from the range -1 to 1 (as reported by the Gamepad + * API) to the range 0 to 1 (for interpolation). Also caps the X, Y values to be bounded within + * a circle. This ensures that thumbsticks are not animated outside the bounds of their physical + * range of motion and touchpads do not report touch locations off their physical bounds. + * @param {number} x The original x coordinate in the range -1 to 1 + * @param {number} y The original y coordinate in the range -1 to 1 + */ + function normalizeAxes(x = 0, y = 0) { + let xAxis = x; + let yAxis = y; - return ( function () { + // Determine if the point is outside the bounds of the circle + // and, if so, place it on the edge of the circle + const hypotenuse = Math.sqrt((x * x) + (y * y)); + if (hypotenuse > 1) { + const theta = Math.atan2(y, x); + xAxis = Math.cos(theta); + yAxis = Math.sin(theta); + } - var pending = []; + // Scale and move the circle so values are in the interpolation range. The circle's origin moves + // from (0, 0) to (0.5, 0.5). The circle's radius scales from 1 to be 0.5. + const result = { + normalizedXAxis: (xAxis * 0.5) + 0.5, + normalizedYAxis: (yAxis * 0.5) + 0.5 + }; + return result; + } - if ( nodeDef.mesh !== undefined ) { + /** + * Contains the description of how the 3D model should visually respond to a specific user input. + * This is accomplished by initializing the object with the name of a node in the 3D model and + * property that need to be modified in response to user input, the name of the nodes representing + * the allowable range of motion, and the name of the input which triggers the change. In response + * to the named input changing, this object computes the appropriate weighting to use for + * interpolating between the range of motion nodes. + */ + class VisualResponse { + constructor(visualResponseDescription) { + this.componentProperty = visualResponseDescription.componentProperty; + this.states = visualResponseDescription.states; + this.valueNodeName = visualResponseDescription.valueNodeName; + this.valueNodeProperty = visualResponseDescription.valueNodeProperty; - pending.push( parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { + if (this.valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM) { + this.minNodeName = visualResponseDescription.minNodeName; + this.maxNodeName = visualResponseDescription.maxNodeName; + } - var node = parser._getNodeRef( parser.meshCache, nodeDef.mesh, mesh ); + // Initializes the response's current value based on default data + this.value = 0; + this.updateFromComponent(defaultComponentValues); + } - // if weights are provided on the node, override weights on the mesh. - if ( nodeDef.weights !== undefined ) { + /** + * Computes the visual response's interpolation weight based on component state + * @param {Object} componentValues - The component from which to update + * @param {number} xAxis - The reported X axis value of the component + * @param {number} yAxis - The reported Y axis value of the component + * @param {number} button - The reported value of the component's button + * @param {string} state - The component's active state + */ + updateFromComponent({ + xAxis, yAxis, button, state + }) { + const { normalizedXAxis, normalizedYAxis } = normalizeAxes(xAxis, yAxis); + switch (this.componentProperty) { + case Constants.ComponentProperty.X_AXIS: + this.value = (this.states.includes(state)) ? normalizedXAxis : 0.5; + break; + case Constants.ComponentProperty.Y_AXIS: + this.value = (this.states.includes(state)) ? normalizedYAxis : 0.5; + break; + case Constants.ComponentProperty.BUTTON: + this.value = (this.states.includes(state)) ? button : 0; + break; + case Constants.ComponentProperty.STATE: + if (this.valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY) { + this.value = (this.states.includes(state)); + } else { + this.value = this.states.includes(state) ? 1.0 : 0.0; + } + break; + default: + throw new Error(`Unexpected visualResponse componentProperty ${this.componentProperty}`); + } + } + } - node.traverse( function ( o ) { + class Component { + /** + * @param {Object} componentId - Id of the component + * @param {Object} componentDescription - Description of the component to be created + */ + constructor(componentId, componentDescription) { + if (!componentId + || !componentDescription + || !componentDescription.visualResponses + || !componentDescription.gamepadIndices + || Object.keys(componentDescription.gamepadIndices).length === 0) { + throw new Error('Invalid arguments supplied'); + } - if ( ! o.isMesh ) return; + this.id = componentId; + this.type = componentDescription.type; + this.rootNodeName = componentDescription.rootNodeName; + this.touchPointNodeName = componentDescription.touchPointNodeName; - for ( var i = 0, il = nodeDef.weights.length; i < il; i ++ ) { + // Build all the visual responses for this component + this.visualResponses = {}; + Object.keys(componentDescription.visualResponses).forEach((responseName) => { + const visualResponse = new VisualResponse(componentDescription.visualResponses[responseName]); + this.visualResponses[responseName] = visualResponse; + }); - o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; + // Set default values + this.gamepadIndices = Object.assign({}, componentDescription.gamepadIndices); - } + this.values = { + state: Constants.ComponentState.DEFAULT, + button: (this.gamepadIndices.button !== undefined) ? 0 : undefined, + xAxis: (this.gamepadIndices.xAxis !== undefined) ? 0 : undefined, + yAxis: (this.gamepadIndices.yAxis !== undefined) ? 0 : undefined + }; + } - } ); + get data() { + const data = { id: this.id, ...this.values }; + return data; + } - } + /** + * @description Poll for updated data based on current gamepad state + * @param {Object} gamepad - The gamepad object from which the component data should be polled + */ + updateFromGamepad(gamepad) { + // Set the state to default before processing other data sources + this.values.state = Constants.ComponentState.DEFAULT; - return node; + // Get and normalize button + if (this.gamepadIndices.button !== undefined + && gamepad.buttons.length > this.gamepadIndices.button) { + const gamepadButton = gamepad.buttons[this.gamepadIndices.button]; + this.values.button = gamepadButton.value; + this.values.button = (this.values.button < 0) ? 0 : this.values.button; + this.values.button = (this.values.button > 1) ? 1 : this.values.button; - } ) ); + // Set the state based on the button + if (gamepadButton.pressed || this.values.button === 1) { + this.values.state = Constants.ComponentState.PRESSED; + } else if (gamepadButton.touched || this.values.button > Constants.ButtonTouchThreshold) { + this.values.state = Constants.ComponentState.TOUCHED; + } + } + + // Get and normalize x axis value + if (this.gamepadIndices.xAxis !== undefined + && gamepad.axes.length > this.gamepadIndices.xAxis) { + this.values.xAxis = gamepad.axes[this.gamepadIndices.xAxis]; + this.values.xAxis = (this.values.xAxis < -1) ? -1 : this.values.xAxis; + this.values.xAxis = (this.values.xAxis > 1) ? 1 : this.values.xAxis; - } + // If the state is still default, check if the xAxis makes it touched + if (this.values.state === Constants.ComponentState.DEFAULT + && Math.abs(this.values.xAxis) > Constants.AxisTouchThreshold) { + this.values.state = Constants.ComponentState.TOUCHED; + } + } - if ( nodeDef.camera !== undefined ) { + // Get and normalize Y axis value + if (this.gamepadIndices.yAxis !== undefined + && gamepad.axes.length > this.gamepadIndices.yAxis) { + this.values.yAxis = gamepad.axes[this.gamepadIndices.yAxis]; + this.values.yAxis = (this.values.yAxis < -1) ? -1 : this.values.yAxis; + this.values.yAxis = (this.values.yAxis > 1) ? 1 : this.values.yAxis; - pending.push( parser.getDependency( 'camera', nodeDef.camera ).then( function ( camera ) { + // If the state is still default, check if the yAxis makes it touched + if (this.values.state === Constants.ComponentState.DEFAULT + && Math.abs(this.values.yAxis) > Constants.AxisTouchThreshold) { + this.values.state = Constants.ComponentState.TOUCHED; + } + } - return parser._getNodeRef( parser.cameraCache, nodeDef.camera, camera ); + // Update the visual response weights based on the current component data + Object.values(this.visualResponses).forEach((visualResponse) => { + visualResponse.updateFromComponent(this.values); + }); + } + } - } ) ); + /** + * @description Builds a motion controller with components and visual responses based on the + * supplied profile description. Data is polled from the xrInputSource's gamepad. + * @author Nell Waliczek / https://github.com/NellWaliczek + */ + class MotionController { + /** + * @param {Object} xrInputSource - The XRInputSource to build the MotionController around + * @param {Object} profile - The best matched profile description for the supplied xrInputSource + * @param {Object} assetUrl + */ + constructor(xrInputSource, profile, assetUrl) { + if (!xrInputSource) { + throw new Error('No xrInputSource supplied'); + } - } + if (!profile) { + throw new Error('No profile supplied'); + } - parser._invokeAll( function ( ext ) { + this.xrInputSource = xrInputSource; + this.assetUrl = assetUrl; + this.id = profile.profileId; - return ext.createNodeAttachment && ext.createNodeAttachment( nodeIndex ); + // Build child components as described in the profile description + this.layoutDescription = profile.layouts[xrInputSource.handedness]; + this.components = {}; + Object.keys(this.layoutDescription.components).forEach((componentId) => { + const componentDescription = this.layoutDescription.components[componentId]; + this.components[componentId] = new Component(componentId, componentDescription); + }); - } ).forEach( function ( promise ) { + // Initialize components based on current gamepad state + this.updateFromGamepad(); + } - pending.push( promise ); + get gripSpace() { + return this.xrInputSource.gripSpace; + } - } ); + get targetRaySpace() { + return this.xrInputSource.targetRaySpace; + } - return Promise.all( pending ); + /** + * @description Returns a subset of component data for simplified debugging + */ + get data() { + const data = []; + Object.values(this.components).forEach((component) => { + data.push(component.data); + }); + return data; + } - }() ).then( function ( objects ) { + /** + * @description Poll for updated data based on current gamepad state + */ + updateFromGamepad() { + Object.values(this.components).forEach((component) => { + component.updateFromGamepad(this.xrInputSource.gamepad); + }); + } + } - var node; + const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles'; + const DEFAULT_PROFILE = 'generic-trigger'; - // .isBone isn't in glTF spec. See ._markDefs - if ( nodeDef.isBone === true ) { + function XRControllerModel( ) { - node = new Bone(); + Object3D.call( this ); - } else if ( objects.length > 1 ) { + this.motionController = null; + this.envMap = null; - node = new Group(); + } - } else if ( objects.length === 1 ) { + XRControllerModel.prototype = Object.assign( Object.create( Object3D.prototype ), { - node = objects[ 0 ]; + constructor: XRControllerModel, - } else { + setEnvironmentMap: function ( envMap ) { - node = new Object3D(); + if ( this.envMap == envMap ) { - } + return this; - if ( node !== objects[ 0 ] ) { + } - for ( var i = 0, il = objects.length; i < il; i ++ ) { + this.envMap = envMap; + this.traverse( ( child ) => { - node.add( objects[ i ] ); + if ( child.isMesh ) { - } + child.material.envMap = this.envMap; + child.material.needsUpdate = true; } - if ( nodeDef.name ) { + } ); - node.userData.name = nodeDef.name; - node.name = nodeName; + return this; - } + }, - assignExtrasToUserData( node, nodeDef ); + /** + * Polls data from the XRInputSource and updates the model's components to match + * the real world data + */ + updateMatrixWorld: function ( force ) { - if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); + Object3D.prototype.updateMatrixWorld.call( this, force ); - if ( nodeDef.matrix !== undefined ) { + if ( ! this.motionController ) return; - var matrix = new Matrix4(); - matrix.fromArray( nodeDef.matrix ); - node.applyMatrix4( matrix ); + // Cause the MotionController to poll the Gamepad for data + this.motionController.updateFromGamepad(); - } else { + // Update the 3D model to reflect the button, thumbstick, and touchpad state + Object.values( this.motionController.components ).forEach( ( component ) => { - if ( nodeDef.translation !== undefined ) { + // Update node data based on the visual responses' current states + Object.values( component.visualResponses ).forEach( ( visualResponse ) => { - node.position.fromArray( nodeDef.translation ); + const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse; - } + // Skip if the visual response node is not found. No error is needed, + // because it will have been reported at load time. + if ( ! valueNode ) return; - if ( nodeDef.rotation !== undefined ) { + // Calculate the new properties based on the weight supplied + if ( valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY ) { - node.quaternion.fromArray( nodeDef.rotation ); + valueNode.visible = value; - } + } else if ( valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM ) { - if ( nodeDef.scale !== undefined ) { + Quaternion.slerp( + minNode.quaternion, + maxNode.quaternion, + valueNode.quaternion, + value + ); - node.scale.fromArray( nodeDef.scale ); + valueNode.position.lerpVectors( + minNode.position, + maxNode.position, + value + ); } - } + } ); - parser.associations.set( node, { type: 'nodes', index: nodeIndex } ); + } ); - return node; + } - } ); + } ); - }; + /** + * Walks the model's tree to find the nodes needed to animate the components and + * saves them to the motionContoller components for use in the frame loop. When + * touchpads are found, attaches a touch dot to them. + */ + function findNodes( motionController, scene ) { - /** - * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes - * @param {number} sceneIndex - * @return {Promise} - */ - GLTFParser.prototype.loadScene = function () { + // Loop through the components and find the nodes needed for each components' visual responses + Object.values( motionController.components ).forEach( ( component ) => { - // scene node hierachy builder + const { type, touchPointNodeName, visualResponses } = component; - function buildNodeHierachy( nodeId, parentObject, json, parser ) { + if ( type === Constants.ComponentType.TOUCHPAD ) { - var nodeDef = json.nodes[ nodeId ]; + component.touchPointNode = scene.getObjectByName( touchPointNodeName ); + if ( component.touchPointNode ) { - return parser.getDependency( 'node', nodeId ).then( function ( node ) { + // Attach a touch dot to the touchpad. + const sphereGeometry = new SphereBufferGeometry( 0.001 ); + const material = new MeshBasicMaterial( { color: 0x0000FF } ); + const sphere = new Mesh( sphereGeometry, material ); + component.touchPointNode.add( sphere ); - if ( nodeDef.skin === undefined ) return node; + } else { - // build skeleton here as well + console.warn( `Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${component.id}` ); - var skinEntry; + } - return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) { + } - skinEntry = skin; + // Loop through all the visual responses to be applied to this component + Object.values( visualResponses ).forEach( ( visualResponse ) => { - var pendingJoints = []; + const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse; - for ( var i = 0, il = skinEntry.joints.length; i < il; i ++ ) { + // If animating a transform, find the two nodes to be interpolated between. + if ( valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM ) { - pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) ); + visualResponse.minNode = scene.getObjectByName( minNodeName ); + visualResponse.maxNode = scene.getObjectByName( maxNodeName ); - } + // If the extents cannot be found, skip this animation + if ( ! visualResponse.minNode ) { - return Promise.all( pendingJoints ); + console.warn( `Could not find ${minNodeName} in the model` ); + return; - } ).then( function ( jointNodes ) { + } - node.traverse( function ( mesh ) { + if ( ! visualResponse.maxNode ) { - if ( ! mesh.isMesh ) return; + console.warn( `Could not find ${maxNodeName} in the model` ); + return; - var bones = []; - var boneInverses = []; + } - for ( var j = 0, jl = jointNodes.length; j < jl; j ++ ) { + } - var jointNode = jointNodes[ j ]; + // If the target node cannot be found, skip this animation + visualResponse.valueNode = scene.getObjectByName( valueNodeName ); + if ( ! visualResponse.valueNode ) { - if ( jointNode ) { + console.warn( `Could not find ${valueNodeName} in the model` ); - bones.push( jointNode ); + } - var mat = new Matrix4(); + } ); - if ( skinEntry.inverseBindMatrices !== undefined ) { + } ); - mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); + } - } + function addAssetSceneToControllerModel( controllerModel, scene ) { - boneInverses.push( mat ); + // Find the nodes needed for animation and cache them on the motionController. + findNodes( controllerModel.motionController, scene ); - } else { + // Apply any environment map that the mesh already has set. + if ( controllerModel.envMap ) { - console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] ); + scene.traverse( ( child ) => { - } + if ( child.isMesh ) { - } + child.material.envMap = controllerModel.envMap; + child.material.needsUpdate = true; - mesh.bind( new Skeleton( bones, boneInverses ), mesh.matrixWorld ); + } - } ); + } ); - return node; + } - } ); + // Add the glTF scene to the controllerModel. + controllerModel.add( scene ); - } ).then( function ( node ) { + } - // build node hierachy + var XRControllerModelFactory = ( function () { - parentObject.add( node ); + function XRControllerModelFactory( gltfLoader = null ) { - var pending = []; + this.gltfLoader = gltfLoader; + this.path = DEFAULT_PROFILES_PATH; + this._assetCache = {}; - if ( nodeDef.children ) { + // If a GLTFLoader wasn't supplied to the constructor create a new one. + if ( ! this.gltfLoader ) { - var children = nodeDef.children; + this.gltfLoader = new GLTFLoader(); - for ( var i = 0, il = children.length; i < il; i ++ ) { + } - var child = children[ i ]; - pending.push( buildNodeHierachy( child, node, json, parser ) ); + } - } + XRControllerModelFactory.prototype = { - } + constructor: XRControllerModelFactory, - return Promise.all( pending ); + createControllerModel: function ( controller ) { - } ); + const controllerModel = new XRControllerModel(); + let scene = null; - } + controller.addEventListener( 'connected', ( event ) => { - return function loadScene( sceneIndex ) { + const xrInputSource = event.data; - var json = this.json; - var extensions = this.extensions; - var sceneDef = this.json.scenes[ sceneIndex ]; - var parser = this; + if ( xrInputSource.targetRayMode !== 'tracked-pointer' || ! xrInputSource.gamepad ) return; - // Loader returns Group, not Scene. - // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 - var scene = new Group(); - if ( sceneDef.name ) scene.name = parser.createUniqueName( sceneDef.name ); + fetchProfile( xrInputSource, this.path, DEFAULT_PROFILE ).then( ( { profile, assetPath } ) => { - assignExtrasToUserData( scene, sceneDef ); + controllerModel.motionController = new MotionController( + xrInputSource, + profile, + assetPath + ); - if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); + const cachedAsset = this._assetCache[ controllerModel.motionController.assetUrl ]; + if ( cachedAsset ) { - var nodeIds = sceneDef.nodes || []; + scene = cachedAsset.scene.clone(); - var pending = []; + addAssetSceneToControllerModel( controllerModel, scene ); - for ( var i = 0, il = nodeIds.length; i < il; i ++ ) { + } else { - pending.push( buildNodeHierachy( nodeIds[ i ], scene, json, parser ) ); + if ( ! this.gltfLoader ) { - } + throw new Error( 'GLTFLoader not set.' ); - return Promise.all( pending ).then( function () { + } - return scene; + this.gltfLoader.setPath( '' ); + this.gltfLoader.load( controllerModel.motionController.assetUrl, ( asset ) => { - } ); + this._assetCache[ controllerModel.motionController.assetUrl ] = asset; - }; + scene = asset.scene.clone(); - }(); + addAssetSceneToControllerModel( controllerModel, scene ); - return GLTFLoader; + }, + null, + () => { - } )(); + throw new Error( `Asset ${controllerModel.motionController.assetUrl} missing or malformed.` ); - /** - * @webxr-input-profiles/motion-controllers 1.0.0 https://github.com/immersive-web/webxr-input-profiles - */ + } ); - const Constants = { - Handedness: Object.freeze({ - NONE: 'none', - LEFT: 'left', - RIGHT: 'right' - }), + } - ComponentState: Object.freeze({ - DEFAULT: 'default', - TOUCHED: 'touched', - PRESSED: 'pressed' - }), + } ).catch( ( err ) => { - ComponentProperty: Object.freeze({ - BUTTON: 'button', - X_AXIS: 'xAxis', - Y_AXIS: 'yAxis', - STATE: 'state' - }), + console.warn( err ); - ComponentType: Object.freeze({ - TRIGGER: 'trigger', - SQUEEZE: 'squeeze', - TOUCHPAD: 'touchpad', - THUMBSTICK: 'thumbstick', - BUTTON: 'button' - }), + } ); - ButtonTouchThreshold: 0.05, + } ); - AxisTouchThreshold: 0.1, + controller.addEventListener( 'disconnected', () => { - VisualResponseProperty: Object.freeze({ - TRANSFORM: 'transform', - VISIBILITY: 'visibility' - }) - }; + controllerModel.motionController = null; + controllerModel.remove( scene ); + scene = null; - /** - * @description Static helper function to fetch a JSON file and turn it into a JS object - * @param {string} path - Path to JSON file to be fetched - */ - async function fetchJsonFile(path) { - const response = await fetch(path); - if (!response.ok) { - throw new Error(response.statusText); - } else { - return response.json(); - } - } + } ); - async function fetchProfilesList(basePath) { - if (!basePath) { - throw new Error('No basePath supplied'); - } + return controllerModel; - const profileListFileName = 'profilesList.json'; - const profilesList = await fetchJsonFile(`${basePath}/${profileListFileName}`); - return profilesList; - } + } - async function fetchProfile(xrInputSource, basePath, defaultProfile = null, getAssetPath = true) { - if (!xrInputSource) { - throw new Error('No xrInputSource supplied'); - } + }; - if (!basePath) { - throw new Error('No basePath supplied'); - } + return XRControllerModelFactory; - // Get the list of profiles - const supportedProfilesList = await fetchProfilesList(basePath); + } )(); - // Find the relative path to the first requested profile that is recognized - let match; - xrInputSource.profiles.some((profileId) => { - const supportedProfile = supportedProfilesList[profileId]; - if (supportedProfile) { - match = { - profileId, - profilePath: `${basePath}/${supportedProfile.path}`, - deprecated: !!supportedProfile.deprecated - }; - } - return !!match; - }); + let fakeCam = new PerspectiveCamera(); - if (!match) { - if (!defaultProfile) { - throw new Error('No matching profile name found'); - } + function toScene(vec, ref){ + let node = ref.clone(); + node.updateMatrix(); + node.updateMatrixWorld(); - const supportedProfile = supportedProfilesList[defaultProfile]; - if (!supportedProfile) { - throw new Error(`No matching profile name found and default profile "${defaultProfile}" missing.`); - } + let result = vec.clone().applyMatrix4(node.matrix); + result.z -= 0.8 * node.scale.x; - match = { - profileId: defaultProfile, - profilePath: `${basePath}/${supportedProfile.path}`, - deprecated: !!supportedProfile.deprecated - }; - } + return result; + }; - const profile = await fetchJsonFile(match.profilePath); + function computeMove(vrControls, controller){ - let assetPath; - if (getAssetPath) { - let layout; - if (xrInputSource.handedness === 'any') { - layout = profile.layouts[Object.keys(profile.layouts)[0]]; - } else { - layout = profile.layouts[xrInputSource.handedness]; - } - if (!layout) { - throw new Error( - `No matching handedness, ${xrInputSource.handedness}, in profile ${match.profileId}` - ); - } + if(!controller || !controller.inputSource || !controller.inputSource.gamepad){ + return null; + } - if (layout.assetPath) { - assetPath = match.profilePath.replace('profile.json', layout.assetPath); - } - } + let pad = controller.inputSource.gamepad; - return { profile, assetPath }; - } + let axes = pad.axes; + // [0,1] are for touchpad, [2,3] for thumbsticks? + let y = 0; + if(axes.length === 2){ + y = axes[1]; + }else if(axes.length === 4){ + y = axes[3]; + } - /** @constant {Object} */ - const defaultComponentValues = { - xAxis: 0, - yAxis: 0, - button: 0, - state: Constants.ComponentState.DEFAULT - }; + y = Math.sign(y) * (2 * y) ** 2; - /** - * @description Converts an X, Y coordinate from the range -1 to 1 (as reported by the Gamepad - * API) to the range 0 to 1 (for interpolation). Also caps the X, Y values to be bounded within - * a circle. This ensures that thumbsticks are not animated outside the bounds of their physical - * range of motion and touchpads do not report touch locations off their physical bounds. - * @param {number} x The original x coordinate in the range -1 to 1 - * @param {number} y The original y coordinate in the range -1 to 1 - */ - function normalizeAxes(x = 0, y = 0) { - let xAxis = x; - let yAxis = y; + let maxSize = 0; + for(let pc of viewer.scene.pointclouds){ + let size = pc.boundingBox.min.distanceTo(pc.boundingBox.max); + maxSize = Math.max(maxSize, size); + } + let multiplicator = Math.pow(maxSize, 0.5) / 2; - // Determine if the point is outside the bounds of the circle - // and, if so, place it on the edge of the circle - const hypotenuse = Math.sqrt((x * x) + (y * y)); - if (hypotenuse > 1) { - const theta = Math.atan2(y, x); - xAxis = Math.cos(theta); - yAxis = Math.sin(theta); - } + let scale = vrControls.node.scale.x; + let moveSpeed = viewer.getMoveSpeed(); + let amount = multiplicator * y * (moveSpeed ** 0.5) / scale; - // Scale and move the circle so values are in the interpolation range. The circle's origin moves - // from (0, 0) to (0.5, 0.5). The circle's radius scales from 1 to be 0.5. - const result = { - normalizedXAxis: (xAxis * 0.5) + 0.5, - normalizedYAxis: (yAxis * 0.5) + 0.5 - }; - return result; - } - /** - * Contains the description of how the 3D model should visually respond to a specific user input. - * This is accomplished by initializing the object with the name of a node in the 3D model and - * property that need to be modified in response to user input, the name of the nodes representing - * the allowable range of motion, and the name of the input which triggers the change. In response - * to the named input changing, this object computes the appropriate weighting to use for - * interpolating between the range of motion nodes. - */ - class VisualResponse { - constructor(visualResponseDescription) { - this.componentProperty = visualResponseDescription.componentProperty; - this.states = visualResponseDescription.states; - this.valueNodeName = visualResponseDescription.valueNodeName; - this.valueNodeProperty = visualResponseDescription.valueNodeProperty; + let rotation = new Quaternion().setFromEuler(controller.rotation); + let dir = new Vector3(0, 0, -1); + dir.applyQuaternion(rotation); - if (this.valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM) { - this.minNodeName = visualResponseDescription.minNodeName; - this.maxNodeName = visualResponseDescription.maxNodeName; - } + let move = dir.clone().multiplyScalar(amount); - // Initializes the response's current value based on default data - this.value = 0; - this.updateFromComponent(defaultComponentValues); - } + let p1 = vrControls.toScene(controller.position); + let p2 = vrControls.toScene(controller.position.clone().add(move)); - /** - * Computes the visual response's interpolation weight based on component state - * @param {Object} componentValues - The component from which to update - * @param {number} xAxis - The reported X axis value of the component - * @param {number} yAxis - The reported Y axis value of the component - * @param {number} button - The reported value of the component's button - * @param {string} state - The component's active state - */ - updateFromComponent({ - xAxis, yAxis, button, state - }) { - const { normalizedXAxis, normalizedYAxis } = normalizeAxes(xAxis, yAxis); - switch (this.componentProperty) { - case Constants.ComponentProperty.X_AXIS: - this.value = (this.states.includes(state)) ? normalizedXAxis : 0.5; - break; - case Constants.ComponentProperty.Y_AXIS: - this.value = (this.states.includes(state)) ? normalizedYAxis : 0.5; - break; - case Constants.ComponentProperty.BUTTON: - this.value = (this.states.includes(state)) ? button : 0; - break; - case Constants.ComponentProperty.STATE: - if (this.valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY) { - this.value = (this.states.includes(state)); - } else { - this.value = this.states.includes(state) ? 1.0 : 0.0; - } - break; - default: - throw new Error(`Unexpected visualResponse componentProperty ${this.componentProperty}`); - } - } - } + move = p2.clone().sub(p1); + + return move; + }; - class Component { - /** - * @param {Object} componentId - Id of the component - * @param {Object} componentDescription - Description of the component to be created - */ - constructor(componentId, componentDescription) { - if (!componentId - || !componentDescription - || !componentDescription.visualResponses - || !componentDescription.gamepadIndices - || Object.keys(componentDescription.gamepadIndices).length === 0) { - throw new Error('Invalid arguments supplied'); - } - this.id = componentId; - this.type = componentDescription.type; - this.rootNodeName = componentDescription.rootNodeName; - this.touchPointNodeName = componentDescription.touchPointNodeName; + class FlyMode{ - // Build all the visual responses for this component - this.visualResponses = {}; - Object.keys(componentDescription.visualResponses).forEach((responseName) => { - const visualResponse = new VisualResponse(componentDescription.visualResponses[responseName]); - this.visualResponses[responseName] = visualResponse; - }); + constructor(vrControls){ + this.moveFactor = 1; + this.dbgLabel = null; + } - // Set default values - this.gamepadIndices = Object.assign({}, componentDescription.gamepadIndices); + start(vrControls){ + if(!this.dbgLabel){ + this.dbgLabel = new Potree.TextSprite("abc"); + this.dbgLabel.name = "debug label"; + vrControls.viewer.sceneVR.add(this.dbgLabel); + this.dbgLabel.visible = false; + } + } + + end(){ - this.values = { - state: Constants.ComponentState.DEFAULT, - button: (this.gamepadIndices.button !== undefined) ? 0 : undefined, - xAxis: (this.gamepadIndices.xAxis !== undefined) ? 0 : undefined, - yAxis: (this.gamepadIndices.yAxis !== undefined) ? 0 : undefined - }; - } + } - get data() { - const data = { id: this.id, ...this.values }; - return data; - } + update(vrControls, delta){ - /** - * @description Poll for updated data based on current gamepad state - * @param {Object} gamepad - The gamepad object from which the component data should be polled - */ - updateFromGamepad(gamepad) { - // Set the state to default before processing other data sources - this.values.state = Constants.ComponentState.DEFAULT; + let primary = vrControls.cPrimary; + let secondary = vrControls.cSecondary; - // Get and normalize button - if (this.gamepadIndices.button !== undefined - && gamepad.buttons.length > this.gamepadIndices.button) { - const gamepadButton = gamepad.buttons[this.gamepadIndices.button]; - this.values.button = gamepadButton.value; - this.values.button = (this.values.button < 0) ? 0 : this.values.button; - this.values.button = (this.values.button > 1) ? 1 : this.values.button; + let move1 = computeMove(vrControls, primary); + let move2 = computeMove(vrControls, secondary); - // Set the state based on the button - if (gamepadButton.pressed || this.values.button === 1) { - this.values.state = Constants.ComponentState.PRESSED; - } else if (gamepadButton.touched || this.values.button > Constants.ButtonTouchThreshold) { - this.values.state = Constants.ComponentState.TOUCHED; - } - } - // Get and normalize x axis value - if (this.gamepadIndices.xAxis !== undefined - && gamepad.axes.length > this.gamepadIndices.xAxis) { - this.values.xAxis = gamepad.axes[this.gamepadIndices.xAxis]; - this.values.xAxis = (this.values.xAxis < -1) ? -1 : this.values.xAxis; - this.values.xAxis = (this.values.xAxis > 1) ? 1 : this.values.xAxis; + if(!move1){ + move1 = new Vector3(); + } - // If the state is still default, check if the xAxis makes it touched - if (this.values.state === Constants.ComponentState.DEFAULT - && Math.abs(this.values.xAxis) > Constants.AxisTouchThreshold) { - this.values.state = Constants.ComponentState.TOUCHED; - } - } + if(!move2){ + move2 = new Vector3(); + } - // Get and normalize Y axis value - if (this.gamepadIndices.yAxis !== undefined - && gamepad.axes.length > this.gamepadIndices.yAxis) { - this.values.yAxis = gamepad.axes[this.gamepadIndices.yAxis]; - this.values.yAxis = (this.values.yAxis < -1) ? -1 : this.values.yAxis; - this.values.yAxis = (this.values.yAxis > 1) ? 1 : this.values.yAxis; + let move = move1.clone().add(move2); - // If the state is still default, check if the yAxis makes it touched - if (this.values.state === Constants.ComponentState.DEFAULT - && Math.abs(this.values.yAxis) > Constants.AxisTouchThreshold) { - this.values.state = Constants.ComponentState.TOUCHED; - } - } + move.multiplyScalar(-delta * this.moveFactor); + vrControls.node.position.add(move); + - // Update the visual response weights based on the current component data - Object.values(this.visualResponses).forEach((visualResponse) => { - visualResponse.updateFromComponent(this.values); - }); - } - } + let scale = vrControls.node.scale.x; - /** - * @description Builds a motion controller with components and visual responses based on the - * supplied profile description. Data is polled from the xrInputSource's gamepad. - * @author Nell Waliczek / https://github.com/NellWaliczek - */ - class MotionController { - /** - * @param {Object} xrInputSource - The XRInputSource to build the MotionController around - * @param {Object} profile - The best matched profile description for the supplied xrInputSource - * @param {Object} assetUrl - */ - constructor(xrInputSource, profile, assetUrl) { - if (!xrInputSource) { - throw new Error('No xrInputSource supplied'); - } + let camVR = vrControls.viewer.renderer.xr.getCamera(fakeCam); + + let vrPos = camVR.getWorldPosition(new Vector3()); + let vrDir = camVR.getWorldDirection(new Vector3()); + let vrTarget = vrPos.clone().add(vrDir.multiplyScalar(scale)); - if (!profile) { - throw new Error('No profile supplied'); - } + let scenePos = toScene(vrPos, vrControls.node); + let sceneDir = toScene(vrPos.clone().add(vrDir), vrControls.node).sub(scenePos); + sceneDir.normalize().multiplyScalar(scale); + let sceneTarget = scenePos.clone().add(sceneDir); - this.xrInputSource = xrInputSource; - this.assetUrl = assetUrl; - this.id = profile.profileId; + vrControls.viewer.scene.view.setView(scenePos, sceneTarget); - // Build child components as described in the profile description - this.layoutDescription = profile.layouts[xrInputSource.handedness]; - this.components = {}; - Object.keys(this.layoutDescription.components).forEach((componentId) => { - const componentDescription = this.layoutDescription.components[componentId]; - this.components[componentId] = new Component(componentId, componentDescription); - }); + if(Potree.debug.message){ + this.dbgLabel.visible = true; + this.dbgLabel.setText(Potree.debug.message); + this.dbgLabel.scale.set(0.1, 0.1, 0.1); + this.dbgLabel.position.copy(primary.position); + } + } + }; - // Initialize components based on current gamepad state - this.updateFromGamepad(); - } + class TranslationMode{ - get gripSpace() { - return this.xrInputSource.gripSpace; - } + constructor(){ + this.controller = null; + this.startPos = null; + this.debugLine = null; + } - get targetRaySpace() { - return this.xrInputSource.targetRaySpace; - } + start(vrControls){ + this.controller = vrControls.triggered.values().next().value; + this.startPos = vrControls.node.position.clone(); + } + + end(vrControls){ - /** - * @description Returns a subset of component data for simplified debugging - */ - get data() { - const data = []; - Object.values(this.components).forEach((component) => { - data.push(component.data); - }); - return data; - } + } - /** - * @description Poll for updated data based on current gamepad state - */ - updateFromGamepad() { - Object.values(this.components).forEach((component) => { - component.updateFromGamepad(this.xrInputSource.gamepad); - }); - } - } + update(vrControls, delta){ - const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles'; - const DEFAULT_PROFILE = 'generic-trigger'; + let start = this.controller.start.position; + let end = this.controller.position; - function XRControllerModel( ) { + start = vrControls.toScene(start); + end = vrControls.toScene(end); + + let diff = end.clone().sub(start); + diff.set(-diff.x, -diff.y, -diff.z); + + let pos = new Vector3().addVectors(this.startPos, diff); + + vrControls.node.position.copy(pos); + } + + }; + + class RotScaleMode{ - Object3D.call( this ); + constructor(){ + this.line = null; + this.startState = null; + } - this.motionController = null; - this.envMap = null; + start(vrControls){ + if(!this.line){ + this.line = Potree.Utils.debugLine( + vrControls.viewer.sceneVR, + new Vector3(0, 0, 0), + new Vector3(0, 0, 0), + 0xffff00, + ); - } + this.dbgLabel = new Potree.TextSprite("abc"); + this.dbgLabel.scale.set(0.1, 0.1, 0.1); + vrControls.viewer.sceneVR.add(this.dbgLabel); + } - XRControllerModel.prototype = Object.assign( Object.create( Object3D.prototype ), { + this.line.node.visible = true; - constructor: XRControllerModel, + this.startState = vrControls.node.clone(); + } - setEnvironmentMap: function ( envMap ) { + end(vrControls){ + this.line.node.visible = false; + this.dbgLabel.visible = false; + } - if ( this.envMap == envMap ) { + update(vrControls, delta){ - return this; + let start_c1 = vrControls.cPrimary.start.position.clone(); + let start_c2 = vrControls.cSecondary.start.position.clone(); + let start_center = start_c1.clone().add(start_c2).multiplyScalar(0.5); + let start_c1_c2 = start_c2.clone().sub(start_c1); + let end_c1 = vrControls.cPrimary.position.clone(); + let end_c2 = vrControls.cSecondary.position.clone(); + let end_center = end_c1.clone().add(end_c2).multiplyScalar(0.5); + let end_c1_c2 = end_c2.clone().sub(end_c1); - } + let d1 = start_c1_c2.length(); + let d2 = end_c1_c2.length(); - this.envMap = envMap; - this.traverse( ( child ) => { + let angleStart = new Vector2(start_c1_c2.x, start_c1_c2.z).angle(); + let angleEnd = new Vector2(end_c1_c2.x, end_c1_c2.z).angle(); + let angleDiff = angleEnd - angleStart; + + let scale = d2 / d1; - if ( child.isMesh ) { + let node = this.startState.clone(); + node.updateMatrix(); + node.matrixAutoUpdate = false; - child.material.envMap = this.envMap; - child.material.needsUpdate = true; + let mToOrigin = new Matrix4().makeTranslation(...toScene(start_center, this.startState).multiplyScalar(-1).toArray()); + let mToStart = new Matrix4().makeTranslation(...toScene(start_center, this.startState).toArray()); + let mRotate = new Matrix4().makeRotationZ(angleDiff); + let mScale = new Matrix4().makeScale(1 / scale, 1 / scale, 1 / scale); - } + node.applyMatrix4(mToOrigin); + node.applyMatrix4(mRotate); + node.applyMatrix4(mScale); + node.applyMatrix4(mToStart); - } ); + let oldScenePos = toScene(start_center, this.startState); + let newScenePos = toScene(end_center, node); + let toNew = oldScenePos.clone().sub(newScenePos); + let mToNew = new Matrix4().makeTranslation(...toNew.toArray()); + node.applyMatrix4(mToNew); - return this; + node.matrix.decompose(node.position, node.quaternion, node.scale ); - }, + vrControls.node.position.copy(node.position); + vrControls.node.quaternion.copy(node.quaternion); + vrControls.node.scale.copy(node.scale); + vrControls.node.updateMatrix(); - /** - * Polls data from the XRInputSource and updates the model's components to match - * the real world data - */ - updateMatrixWorld: function ( force ) { + { + let scale = vrControls.node.scale.x; + let camVR = vrControls.viewer.renderer.xr.getCamera(fakeCam); + + let vrPos = camVR.getWorldPosition(new Vector3()); + let vrDir = camVR.getWorldDirection(new Vector3()); + let vrTarget = vrPos.clone().add(vrDir.multiplyScalar(scale)); - Object3D.prototype.updateMatrixWorld.call( this, force ); + let scenePos = toScene(vrPos, this.startState); + let sceneDir = toScene(vrPos.clone().add(vrDir), this.startState).sub(scenePos); + sceneDir.normalize().multiplyScalar(scale); + let sceneTarget = scenePos.clone().add(sceneDir); - if ( ! this.motionController ) return; + vrControls.viewer.scene.view.setView(scenePos, sceneTarget); + vrControls.viewer.setMoveSpeed(scale); + } - // Cause the MotionController to poll the Gamepad for data - this.motionController.updateFromGamepad(); + { // update "GUI" + this.line.set(end_c1, end_c2); - // Update the 3D model to reflect the button, thumbstick, and touchpad state - Object.values( this.motionController.components ).forEach( ( component ) => { + let scale = vrControls.node.scale.x; + this.dbgLabel.visible = true; + this.dbgLabel.position.copy(end_center); + this.dbgLabel.setText(`scale: 1 : ${scale.toFixed(2)}`); + this.dbgLabel.scale.set(0.05, 0.05, 0.05); + } - // Update node data based on the visual responses' current states - Object.values( component.visualResponses ).forEach( ( visualResponse ) => { + } - const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse; + }; - // Skip if the visual response node is not found. No error is needed, - // because it will have been reported at load time. - if ( ! valueNode ) return; - // Calculate the new properties based on the weight supplied - if ( valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY ) { + class VRControls extends EventDispatcher{ - valueNode.visible = value; + constructor(viewer){ + super(viewer); - } else if ( valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM ) { + this.viewer = viewer; - Quaternion.slerp( - minNode.quaternion, - maxNode.quaternion, - valueNode.quaternion, - value - ); + viewer.addEventListener("vr_start", this.onStart.bind(this)); + viewer.addEventListener("vr_end", this.onEnd.bind(this)); - valueNode.position.lerpVectors( - minNode.position, - maxNode.position, - value - ); + this.node = new Object3D(); + this.node.up.set(0, 0, 1); + this.triggered = new Set(); - } + let xr = viewer.renderer.xr; - } ); + { // lights + + const light = new PointLight( 0xffffff, 5, 0, 1 ); + light.position.set(0, 2, 0); + this.viewer.sceneVR.add(light); + } - } ); + this.menu = null; - } + const controllerModelFactory = new XRControllerModelFactory(); - } ); + let sg = new SphereGeometry(1, 32, 32); + let sm = new MeshNormalMaterial(); - /** - * Walks the model's tree to find the nodes needed to animate the components and - * saves them to the motionContoller components for use in the frame loop. When - * touchpads are found, attaches a touch dot to them. - */ - function findNodes( motionController, scene ) { + { // setup primary controller + let controller = xr.getController(0); - // Loop through the components and find the nodes needed for each components' visual responses - Object.values( motionController.components ).forEach( ( component ) => { + let grip = xr.getControllerGrip(0); + grip.name = "grip(0)"; - const { type, touchPointNodeName, visualResponses } = component; + // ADD CONTROLLERMODEL + grip.add( controllerModelFactory.createControllerModel( grip ) ); + this.viewer.sceneVR.add(grip); - if ( type === Constants.ComponentType.TOUCHPAD ) { + // ADD SPHERE + let sphere = new Mesh(sg, sm); + sphere.scale.set(0.005, 0.005, 0.005); - component.touchPointNode = scene.getObjectByName( touchPointNodeName ); - if ( component.touchPointNode ) { + controller.add(sphere); + controller.visible = true; + this.viewer.sceneVR.add(controller); - // Attach a touch dot to the touchpad. - const sphereGeometry = new SphereBufferGeometry( 0.001 ); - const material = new MeshBasicMaterial( { color: 0x0000FF } ); - const sphere = new Mesh( sphereGeometry, material ); - component.touchPointNode.add( sphere ); + { // ADD LINE + + let lineGeometry = new LineGeometry(); - } else { + lineGeometry.setPositions([ + 0, 0, -0.15, + 0, 0, 0.05, + ]); - console.warn( `Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${component.id}` ); + let lineMaterial = new LineMaterial({ + color: 0xff0000, + linewidth: 2, + resolution: new Vector2(1000, 1000), + }); + const line = new Line2(lineGeometry, lineMaterial); + + controller.add(line); } - } - // Loop through all the visual responses to be applied to this component - Object.values( visualResponses ).forEach( ( visualResponse ) => { + controller.addEventListener( 'connected', function ( event ) { + const xrInputSource = event.data; + controller.inputSource = xrInputSource; + // initInfo(controller); + }); - const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse; + controller.addEventListener( 'selectstart', () => {this.onTriggerStart(controller);}); + controller.addEventListener( 'selectend', () => {this.onTriggerEnd(controller);}); - // If animating a transform, find the two nodes to be interpolated between. - if ( valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM ) { + this.cPrimary = controller; - visualResponse.minNode = scene.getObjectByName( minNodeName ); - visualResponse.maxNode = scene.getObjectByName( maxNodeName ); + } - // If the extents cannot be found, skip this animation - if ( ! visualResponse.minNode ) { + { // setup secondary controller + let controller = xr.getController(1); - console.warn( `Could not find ${minNodeName} in the model` ); - return; + let grip = xr.getControllerGrip(1); - } + // ADD CONTROLLER MODEL + let model = controllerModelFactory.createControllerModel( grip ); + grip.add(model); + this.viewer.sceneVR.add( grip ); - if ( ! visualResponse.maxNode ) { + // ADD SPHERE + let sphere = new Mesh(sg, sm); + sphere.scale.set(0.005, 0.005, 0.005); + controller.add(sphere); + controller.visible = true; + this.viewer.sceneVR.add(controller); - console.warn( `Could not find ${maxNodeName} in the model` ); - return; + { // ADD LINE + + let lineGeometry = new LineGeometry(); - } + lineGeometry.setPositions([ + 0, 0, -0.15, + 0, 0, 0.05, + ]); + + let lineMaterial = new LineMaterial({ + color: 0xff0000, + linewidth: 2, + resolution: new Vector2(1000, 1000), + }); + const line = new Line2(lineGeometry, lineMaterial); + + controller.add(line); } - // If the target node cannot be found, skip this animation - visualResponse.valueNode = scene.getObjectByName( valueNodeName ); - if ( ! visualResponse.valueNode ) { + controller.addEventListener( 'connected', (event) => { + const xrInputSource = event.data; + controller.inputSource = xrInputSource; + this.initMenu(controller); + }); - console.warn( `Could not find ${valueNodeName} in the model` ); + controller.addEventListener( 'selectstart', () => {this.onTriggerStart(controller);}); + controller.addEventListener( 'selectend', () => {this.onTriggerEnd(controller);}); - } + this.cSecondary = controller; + } - } ); + this.mode_fly = new FlyMode(); + this.mode_translate = new TranslationMode(); + this.mode_rotScale = new RotScaleMode(); + this.setMode(this.mode_fly); + } - } ); + createSlider(label, min, max){ - } + let sg = new SphereGeometry(1, 8, 8); + let cg = new CylinderGeometry(1, 1, 1, 8); + let matHandle = new MeshBasicMaterial({color: 0xff0000}); + let matScale = new MeshBasicMaterial({color: 0xff4444}); + let matValue = new MeshNormalMaterial(); - function addAssetSceneToControllerModel( controllerModel, scene ) { + let node = new Object3D("slider"); + let nLabel = new Potree.TextSprite(`${label}: 0`); + let nMax = new Mesh(sg, matHandle); + let nMin = new Mesh(sg, matHandle); + let nValue = new Mesh(sg, matValue); + let nScale = new Mesh(cg, matScale); - // Find the nodes needed for animation and cache them on the motionController. - findNodes( controllerModel.motionController, scene ); + nLabel.scale.set(0.2, 0.2, 0.2); + nLabel.position.set(0, 0.35, 0); - // Apply any environment map that the mesh already has set. - if ( controllerModel.envMap ) { + nMax.scale.set(0.02, 0.02, 0.02); + nMax.position.set(0, 0.25, 0); - scene.traverse( ( child ) => { + nMin.scale.set(0.02, 0.02, 0.02); + nMin.position.set(0, -0.25, 0); - if ( child.isMesh ) { + nValue.scale.set(0.02, 0.02, 0.02); + nValue.position.set(0, 0, 0); - child.material.envMap = controllerModel.envMap; - child.material.needsUpdate = true; + nScale.scale.set(0.005, 0.5, 0.005); - } + node.add(nLabel); + node.add(nMax); + node.add(nMin); + node.add(nValue); + node.add(nScale); - } ); + return node; + } + createInfo(){ + + let texture = new TextureLoader().load(`${Potree.resourcePath}/images/vr_controller_help.jpg`); + let plane = new PlaneBufferGeometry(1, 1, 1, 1); + let infoMaterial = new MeshBasicMaterial({map: texture}); + let infoNode = new Mesh(plane, infoMaterial); + + return infoNode; } - // Add the glTF scene to the controllerModel. - controllerModel.add( scene ); + initMenu(controller){ - } + if(this.menu){ + return; + } - var XRControllerModelFactory = ( function () { + let node = new Object3D("vr menu"); - function XRControllerModelFactory( gltfLoader = null ) { + // let nSlider = this.createSlider("speed", 0, 1); + // let nInfo = this.createInfo(); - this.gltfLoader = gltfLoader; - this.path = DEFAULT_PROFILES_PATH; - this._assetCache = {}; + // // node.add(nSlider); + // node.add(nInfo); - // If a GLTFLoader wasn't supplied to the constructor create a new one. - if ( ! this.gltfLoader ) { + // { + // node.rotation.set(-1.5, 0, 0) + // node.scale.set(0.3, 0.3, 0.3); + // node.position.set(-0.2, -0.002, -0.1) - this.gltfLoader = new GLTFLoader(); + // // nInfo.position.set(0.5, 0, 0); + // nInfo.scale.set(0.8, 0.6, 0); - } + // // controller.add(node); + // } + + // node.position.set(-0.3, 1.2, 0.2); + // node.scale.set(0.3, 0.2, 0.3); + // node.lookAt(new THREE.Vector3(0, 1.5, 0.1)); + + // this.viewer.sceneVR.add(node); + + this.menu = node; + + // window.vrSlider = nSlider; + window.vrMenu = node; } - XRControllerModelFactory.prototype = { - constructor: XRControllerModelFactory, + toScene(vec){ + let camVR = this.getCamera(); - createControllerModel: function ( controller ) { + let mat = camVR.matrixWorld; + let result = vec.clone().applyMatrix4(mat); - const controllerModel = new XRControllerModel(); - let scene = null; + return result; + } - controller.addEventListener( 'connected', ( event ) => { + toVR(vec){ + let camVR = this.getCamera(); - const xrInputSource = event.data; + let mat = camVR.matrixWorld.clone(); + mat.invert(); + let result = vec.clone().applyMatrix4(mat); - if ( xrInputSource.targetRayMode !== 'tracked-pointer' || ! xrInputSource.gamepad ) return; + return result; + } - fetchProfile( xrInputSource, this.path, DEFAULT_PROFILE ).then( ( { profile, assetPath } ) => { + setMode(mode){ - controllerModel.motionController = new MotionController( - xrInputSource, - profile, - assetPath - ); + if(this.mode === mode){ + return; + } - const cachedAsset = this._assetCache[ controllerModel.motionController.assetUrl ]; - if ( cachedAsset ) { + if(this.mode){ + this.mode.end(this); + } - scene = cachedAsset.scene.clone(); + for(let controller of [this.cPrimary, this.cSecondary]){ - addAssetSceneToControllerModel( controllerModel, scene ); + let start = { + position: controller.position.clone(), + rotation: controller.rotation.clone(), + }; - } else { + controller.start = start; + } + + this.mode = mode; + this.mode.start(this); + } - if ( ! this.gltfLoader ) { + onTriggerStart(controller){ + this.triggered.add(controller); - throw new Error( 'GLTFLoader not set.' ); + if(this.triggered.size === 0){ + this.setMode(this.mode_fly); + }else if(this.triggered.size === 1){ + this.setMode(this.mode_translate); + }else if(this.triggered.size === 2){ + this.setMode(this.mode_rotScale); + } + } - } + onTriggerEnd(controller){ + this.triggered.delete(controller); - this.gltfLoader.setPath( '' ); - this.gltfLoader.load( controllerModel.motionController.assetUrl, ( asset ) => { + if(this.triggered.size === 0){ + this.setMode(this.mode_fly); + }else if(this.triggered.size === 1){ + this.setMode(this.mode_translate); + }else if(this.triggered.size === 2){ + this.setMode(this.mode_rotScale); + } + } - this._assetCache[ controllerModel.motionController.assetUrl ] = asset; + onStart(){ - scene = asset.scene.clone(); + let position = this.viewer.scene.view.position.clone(); + let direction = this.viewer.scene.view.direction; + direction.multiplyScalar(-1); - addAssetSceneToControllerModel( controllerModel, scene ); + let target = position.clone().add(direction); + target.z = position.z; - }, - null, - () => { + let scale = this.viewer.getMoveSpeed(); - throw new Error( `Asset ${controllerModel.motionController.assetUrl} missing or malformed.` ); + this.node.position.copy(position); + this.node.lookAt(target); + this.node.scale.set(scale, scale, scale); + this.node.updateMatrix(); + this.node.updateMatrixWorld(); + } - } ); + onEnd(){ + + } - } - } ).catch( ( err ) => { + setScene(scene){ + this.scene = scene; + } - console.warn( err ); + getCamera(){ + let reference = this.viewer.scene.getActiveCamera(); + let camera = new PerspectiveCamera(); - } ); + // let scale = this.node.scale.x; + let scale = this.viewer.getMoveSpeed(); + //camera.near = 0.01 / scale; + camera.near = 0.1; + camera.far = 1000; + // camera.near = reference.near / scale; + // camera.far = reference.far / scale; + camera.up.set(0, 0, 1); + camera.lookAt(new Vector3(0, -1, 0)); + camera.updateMatrix(); + camera.updateMatrixWorld(); - } ); + camera.position.copy(this.node.position); + camera.rotation.copy(this.node.rotation); + camera.scale.set(scale, scale, scale); + camera.updateMatrix(); + camera.updateMatrixWorld(); + camera.matrixAutoUpdate = false; + camera.parent = camera; - controller.addEventListener( 'disconnected', () => { + return camera; + } - controllerModel.motionController = null; - controllerModel.remove( scene ); - scene = null; + update(delta){ - } ); + - return controllerModel; + // if(this.mode === this.mode_fly){ + // let ray = new THREE.Ray(origin, direction); + + // for(let object of this.selectables){ - } + // if(object.intersectsRay(ray)){ + // object.onHit(ray); + // } - }; + // } - return XRControllerModelFactory; + // } - } )(); + this.mode.update(this, delta); + + - let fakeCam = new PerspectiveCamera(); - - function toScene(vec, ref){ - let node = ref.clone(); - node.updateMatrix(); - node.updateMatrixWorld(); - - let result = vec.clone().applyMatrix4(node.matrix); - result.z -= 0.8 * node.scale.x; - - return result; - }; - - function computeMove(vrControls, controller){ - - if(!controller || !controller.inputSource || !controller.inputSource.gamepad){ - return null; - } - - let pad = controller.inputSource.gamepad; - - let axes = pad.axes; - // [0,1] are for touchpad, [2,3] for thumbsticks? - let y = 0; - if(axes.length === 2){ - y = axes[1]; - }else if(axes.length === 4){ - y = axes[3]; - } - - y = Math.sign(y) * (2 * y) ** 2; - - let maxSize = 0; - for(let pc of viewer.scene.pointclouds){ - let size = pc.boundingBox.min.distanceTo(pc.boundingBox.max); - maxSize = Math.max(maxSize, size); - } - let multiplicator = Math.pow(maxSize, 0.5) / 2; - - let scale = vrControls.node.scale.x; - let moveSpeed = viewer.getMoveSpeed(); - let amount = multiplicator * y * (moveSpeed ** 0.5) / scale; - - - let rotation = new Quaternion().setFromEuler(controller.rotation); - let dir = new Vector3(0, 0, -1); - dir.applyQuaternion(rotation); - - let move = dir.clone().multiplyScalar(amount); - - let p1 = vrControls.toScene(controller.position); - let p2 = vrControls.toScene(controller.position.clone().add(move)); - - move = p2.clone().sub(p1); - - return move; - }; - - - class FlyMode{ - - constructor(vrControls){ - this.moveFactor = 1; - this.dbgLabel = null; - } - - start(vrControls){ - if(!this.dbgLabel){ - this.dbgLabel = new Potree.TextSprite("abc"); - this.dbgLabel.name = "debug label"; - vrControls.viewer.sceneVR.add(this.dbgLabel); - this.dbgLabel.visible = false; - } - } - - end(){ - - } - - update(vrControls, delta){ - - let primary = vrControls.cPrimary; - let secondary = vrControls.cSecondary; - - let move1 = computeMove(vrControls, primary); - let move2 = computeMove(vrControls, secondary); - - - if(!move1){ - move1 = new Vector3(); - } - - if(!move2){ - move2 = new Vector3(); - } - - let move = move1.clone().add(move2); - - move.multiplyScalar(-delta * this.moveFactor); - vrControls.node.position.add(move); - - - let scale = vrControls.node.scale.x; - - let camVR = vrControls.viewer.renderer.xr.getCamera(fakeCam); - - let vrPos = camVR.getWorldPosition(new Vector3()); - let vrDir = camVR.getWorldDirection(new Vector3()); - let vrTarget = vrPos.clone().add(vrDir.multiplyScalar(scale)); - - let scenePos = toScene(vrPos, vrControls.node); - let sceneDir = toScene(vrPos.clone().add(vrDir), vrControls.node).sub(scenePos); - sceneDir.normalize().multiplyScalar(scale); - let sceneTarget = scenePos.clone().add(sceneDir); - - vrControls.viewer.scene.view.setView(scenePos, sceneTarget); - - if(Potree.debug.message){ - this.dbgLabel.visible = true; - this.dbgLabel.setText(Potree.debug.message); - this.dbgLabel.scale.set(0.1, 0.1, 0.1); - this.dbgLabel.position.copy(primary.position); - } - } - }; - - class TranslationMode{ - - constructor(){ - this.controller = null; - this.startPos = null; - this.debugLine = null; - } - - start(vrControls){ - this.controller = vrControls.triggered.values().next().value; - this.startPos = vrControls.node.position.clone(); - } - - end(vrControls){ - - } - - update(vrControls, delta){ - - let start = this.controller.start.position; - let end = this.controller.position; - - start = vrControls.toScene(start); - end = vrControls.toScene(end); - - let diff = end.clone().sub(start); - diff.set(-diff.x, -diff.y, -diff.z); - - let pos = new Vector3().addVectors(this.startPos, diff); - - vrControls.node.position.copy(pos); - } - - }; - - class RotScaleMode{ - - constructor(){ - this.line = null; - this.startState = null; - } - - start(vrControls){ - if(!this.line){ - this.line = Potree.Utils.debugLine( - vrControls.viewer.sceneVR, - new Vector3(0, 0, 0), - new Vector3(0, 0, 0), - 0xffff00, - ); - - this.dbgLabel = new Potree.TextSprite("abc"); - this.dbgLabel.scale.set(0.1, 0.1, 0.1); - vrControls.viewer.sceneVR.add(this.dbgLabel); - } - - this.line.node.visible = true; - - this.startState = vrControls.node.clone(); - } - - end(vrControls){ - this.line.node.visible = false; - this.dbgLabel.visible = false; - } - - update(vrControls, delta){ - - let start_c1 = vrControls.cPrimary.start.position.clone(); - let start_c2 = vrControls.cSecondary.start.position.clone(); - let start_center = start_c1.clone().add(start_c2).multiplyScalar(0.5); - let start_c1_c2 = start_c2.clone().sub(start_c1); - let end_c1 = vrControls.cPrimary.position.clone(); - let end_c2 = vrControls.cSecondary.position.clone(); - let end_center = end_c1.clone().add(end_c2).multiplyScalar(0.5); - let end_c1_c2 = end_c2.clone().sub(end_c1); - - let d1 = start_c1_c2.length(); - let d2 = end_c1_c2.length(); - - let angleStart = new Vector2(start_c1_c2.x, start_c1_c2.z).angle(); - let angleEnd = new Vector2(end_c1_c2.x, end_c1_c2.z).angle(); - let angleDiff = angleEnd - angleStart; - - let scale = d2 / d1; - - let node = this.startState.clone(); - node.updateMatrix(); - node.matrixAutoUpdate = false; - - let mToOrigin = new Matrix4().makeTranslation(...toScene(start_center, this.startState).multiplyScalar(-1).toArray()); - let mToStart = new Matrix4().makeTranslation(...toScene(start_center, this.startState).toArray()); - let mRotate = new Matrix4().makeRotationZ(angleDiff); - let mScale = new Matrix4().makeScale(1 / scale, 1 / scale, 1 / scale); - - node.applyMatrix4(mToOrigin); - node.applyMatrix4(mRotate); - node.applyMatrix4(mScale); - node.applyMatrix4(mToStart); - - let oldScenePos = toScene(start_center, this.startState); - let newScenePos = toScene(end_center, node); - let toNew = oldScenePos.clone().sub(newScenePos); - let mToNew = new Matrix4().makeTranslation(...toNew.toArray()); - node.applyMatrix4(mToNew); - - node.matrix.decompose(node.position, node.quaternion, node.scale ); - - vrControls.node.position.copy(node.position); - vrControls.node.quaternion.copy(node.quaternion); - vrControls.node.scale.copy(node.scale); - vrControls.node.updateMatrix(); - - { - let scale = vrControls.node.scale.x; - let camVR = vrControls.viewer.renderer.xr.getCamera(fakeCam); - - let vrPos = camVR.getWorldPosition(new Vector3()); - let vrDir = camVR.getWorldDirection(new Vector3()); - let vrTarget = vrPos.clone().add(vrDir.multiplyScalar(scale)); - - let scenePos = toScene(vrPos, this.startState); - let sceneDir = toScene(vrPos.clone().add(vrDir), this.startState).sub(scenePos); - sceneDir.normalize().multiplyScalar(scale); - let sceneTarget = scenePos.clone().add(sceneDir); - - vrControls.viewer.scene.view.setView(scenePos, sceneTarget); - vrControls.viewer.setMoveSpeed(scale); - } - - { // update "GUI" - this.line.set(end_c1, end_c2); - - let scale = vrControls.node.scale.x; - this.dbgLabel.visible = true; - this.dbgLabel.position.copy(end_center); - this.dbgLabel.setText(`scale: 1 : ${scale.toFixed(2)}`); - this.dbgLabel.scale.set(0.05, 0.05, 0.05); - } - - } - - }; - - - class VRControls extends EventDispatcher{ - - constructor(viewer){ - super(viewer); - - this.viewer = viewer; - - viewer.addEventListener("vr_start", this.onStart.bind(this)); - viewer.addEventListener("vr_end", this.onEnd.bind(this)); - - this.node = new Object3D(); - this.node.up.set(0, 0, 1); - this.triggered = new Set(); - - let xr = viewer.renderer.xr; - - { // lights - - const light = new PointLight( 0xffffff, 5, 0, 1 ); - light.position.set(0, 2, 0); - this.viewer.sceneVR.add(light); - } - - this.menu = null; - - const controllerModelFactory = new XRControllerModelFactory(); - - let sg = new SphereGeometry(1, 32, 32); - let sm = new MeshNormalMaterial(); - - { // setup primary controller - let controller = xr.getController(0); - - let grip = xr.getControllerGrip(0); - grip.name = "grip(0)"; - - // ADD CONTROLLERMODEL - grip.add( controllerModelFactory.createControllerModel( grip ) ); - this.viewer.sceneVR.add(grip); - - // ADD SPHERE - let sphere = new Mesh(sg, sm); - sphere.scale.set(0.005, 0.005, 0.005); - - controller.add(sphere); - controller.visible = true; - this.viewer.sceneVR.add(controller); - - { // ADD LINE - - let lineGeometry = new LineGeometry(); - - lineGeometry.setPositions([ - 0, 0, -0.15, - 0, 0, 0.05, - ]); - - let lineMaterial = new LineMaterial({ - color: 0xff0000, - linewidth: 2, - resolution: new Vector2(1000, 1000), - }); - - const line = new Line2(lineGeometry, lineMaterial); - - controller.add(line); - } - - - controller.addEventListener( 'connected', function ( event ) { - const xrInputSource = event.data; - controller.inputSource = xrInputSource; - // initInfo(controller); - }); - - controller.addEventListener( 'selectstart', () => {this.onTriggerStart(controller);}); - controller.addEventListener( 'selectend', () => {this.onTriggerEnd(controller);}); - - this.cPrimary = controller; - - } - - { // setup secondary controller - let controller = xr.getController(1); - - let grip = xr.getControllerGrip(1); - - // ADD CONTROLLER MODEL - let model = controllerModelFactory.createControllerModel( grip ); - grip.add(model); - this.viewer.sceneVR.add( grip ); - - // ADD SPHERE - let sphere = new Mesh(sg, sm); - sphere.scale.set(0.005, 0.005, 0.005); - controller.add(sphere); - controller.visible = true; - this.viewer.sceneVR.add(controller); - - { // ADD LINE - - let lineGeometry = new LineGeometry(); - - lineGeometry.setPositions([ - 0, 0, -0.15, - 0, 0, 0.05, - ]); - - let lineMaterial = new LineMaterial({ - color: 0xff0000, - linewidth: 2, - resolution: new Vector2(1000, 1000), - }); - - const line = new Line2(lineGeometry, lineMaterial); - - controller.add(line); - } - - controller.addEventListener( 'connected', (event) => { - const xrInputSource = event.data; - controller.inputSource = xrInputSource; - this.initMenu(controller); - }); - - controller.addEventListener( 'selectstart', () => {this.onTriggerStart(controller);}); - controller.addEventListener( 'selectend', () => {this.onTriggerEnd(controller);}); - - this.cSecondary = controller; - } - - this.mode_fly = new FlyMode(); - this.mode_translate = new TranslationMode(); - this.mode_rotScale = new RotScaleMode(); - this.setMode(this.mode_fly); - } - - createSlider(label, min, max){ - - let sg = new SphereGeometry(1, 8, 8); - let cg = new CylinderGeometry(1, 1, 1, 8); - let matHandle = new MeshBasicMaterial({color: 0xff0000}); - let matScale = new MeshBasicMaterial({color: 0xff4444}); - let matValue = new MeshNormalMaterial(); - - let node = new Object3D("slider"); - let nLabel = new Potree.TextSprite(`${label}: 0`); - let nMax = new Mesh(sg, matHandle); - let nMin = new Mesh(sg, matHandle); - let nValue = new Mesh(sg, matValue); - let nScale = new Mesh(cg, matScale); - - nLabel.scale.set(0.2, 0.2, 0.2); - nLabel.position.set(0, 0.35, 0); - - nMax.scale.set(0.02, 0.02, 0.02); - nMax.position.set(0, 0.25, 0); - - nMin.scale.set(0.02, 0.02, 0.02); - nMin.position.set(0, -0.25, 0); - - nValue.scale.set(0.02, 0.02, 0.02); - nValue.position.set(0, 0, 0); - - nScale.scale.set(0.005, 0.5, 0.005); - - node.add(nLabel); - node.add(nMax); - node.add(nMin); - node.add(nValue); - node.add(nScale); - - return node; - } - - createInfo(){ - - let texture = new TextureLoader().load(`${Potree.resourcePath}/images/vr_controller_help.jpg`); - let plane = new PlaneBufferGeometry(1, 1, 1, 1); - let infoMaterial = new MeshBasicMaterial({map: texture}); - let infoNode = new Mesh(plane, infoMaterial); - - return infoNode; - } - - initMenu(controller){ - - if(this.menu){ - return; - } - - let node = new Object3D("vr menu"); - - // let nSlider = this.createSlider("speed", 0, 1); - // let nInfo = this.createInfo(); - - // // node.add(nSlider); - // node.add(nInfo); - - // { - // node.rotation.set(-1.5, 0, 0) - // node.scale.set(0.3, 0.3, 0.3); - // node.position.set(-0.2, -0.002, -0.1) - - // // nInfo.position.set(0.5, 0, 0); - // nInfo.scale.set(0.8, 0.6, 0); - - // // controller.add(node); - // } - - // node.position.set(-0.3, 1.2, 0.2); - // node.scale.set(0.3, 0.2, 0.3); - // node.lookAt(new THREE.Vector3(0, 1.5, 0.1)); - - // this.viewer.sceneVR.add(node); - - this.menu = node; - - // window.vrSlider = nSlider; - window.vrMenu = node; - - } - - - toScene(vec){ - let camVR = this.getCamera(); - - let mat = camVR.matrixWorld; - let result = vec.clone().applyMatrix4(mat); - - return result; - } - - toVR(vec){ - let camVR = this.getCamera(); - - let mat = camVR.matrixWorld.clone(); - mat.invert(); - let result = vec.clone().applyMatrix4(mat); - - return result; - } - - setMode(mode){ - - if(this.mode === mode){ - return; - } - - if(this.mode){ - this.mode.end(this); - } - - for(let controller of [this.cPrimary, this.cSecondary]){ - - let start = { - position: controller.position.clone(), - rotation: controller.rotation.clone(), - }; - - controller.start = start; - } - - this.mode = mode; - this.mode.start(this); - } - - onTriggerStart(controller){ - this.triggered.add(controller); - - if(this.triggered.size === 0){ - this.setMode(this.mode_fly); - }else if(this.triggered.size === 1){ - this.setMode(this.mode_translate); - }else if(this.triggered.size === 2){ - this.setMode(this.mode_rotScale); - } - } - - onTriggerEnd(controller){ - this.triggered.delete(controller); - - if(this.triggered.size === 0){ - this.setMode(this.mode_fly); - }else if(this.triggered.size === 1){ - this.setMode(this.mode_translate); - }else if(this.triggered.size === 2){ - this.setMode(this.mode_rotScale); - } - } - - onStart(){ - - let position = this.viewer.scene.view.position.clone(); - let direction = this.viewer.scene.view.direction; - direction.multiplyScalar(-1); - - let target = position.clone().add(direction); - target.z = position.z; - - let scale = this.viewer.getMoveSpeed(); - - this.node.position.copy(position); - this.node.lookAt(target); - this.node.scale.set(scale, scale, scale); - this.node.updateMatrix(); - this.node.updateMatrixWorld(); - } - - onEnd(){ - - } - - - setScene(scene){ - this.scene = scene; - } - - getCamera(){ - let reference = this.viewer.scene.getActiveCamera(); - let camera = new PerspectiveCamera(); - - // let scale = this.node.scale.x; - let scale = this.viewer.getMoveSpeed(); - //camera.near = 0.01 / scale; - camera.near = 0.1; - camera.far = 1000; - // camera.near = reference.near / scale; - // camera.far = reference.far / scale; - camera.up.set(0, 0, 1); - camera.lookAt(new Vector3(0, -1, 0)); - camera.updateMatrix(); - camera.updateMatrixWorld(); - - camera.position.copy(this.node.position); - camera.rotation.copy(this.node.rotation); - camera.scale.set(scale, scale, scale); - camera.updateMatrix(); - camera.updateMatrixWorld(); - camera.matrixAutoUpdate = false; - camera.parent = camera; - - return camera; - } - - update(delta){ - - - - // if(this.mode === this.mode_fly){ - // let ray = new THREE.Ray(origin, direction); - - // for(let object of this.selectables){ - - // if(object.intersectsRay(ray)){ - // object.onHit(ray); - // } - - // } - - // } - - this.mode.update(this, delta); - - - - } + } }; // Adapted from three.js VRButton