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 += `
+
+ x
+ ${values[0]}
+
+
+ y
+ ${values[1]}
+
+
+ z
+ ${values[2]}
+ `;
+ } else if (attributeName === 'rgba') {
+ html += `
+
+ ${attributeName}
+ ${value.join(', ')}
+ `;
+ } else if (attributeName === 'normal') {
+ continue;
+ } else if (attributeName === 'mileage') {
+ html += `
+
+ ${attributeName}
+ ${value.toFixed(3)}
+ `;
+ } else {
+ html += `
+
+ ${attributeName}
+ ${transform(value)}
+ `;
+ }
+ }
+ html += '
';
+ 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 += `
-
- x
- ${values[0]}
-
-
- y
- ${values[1]}
-
-
- z
- ${values[2]}
- `;
- } else if (attributeName === 'rgba') {
- html += `
-
- ${attributeName}
- ${value.join(', ')}
- `;
- } else if (attributeName === 'normal') {
- continue;
- } else if (attributeName === 'mileage') {
- html += `
-
- ${attributeName}
- ${value.toFixed(3)}
- `;
- } else {
- html += `
-
- ${attributeName}
- ${transform(value)}
- `;
- }
+ 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 += '
';
- 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 = $(`
+
+ `);
- 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) {
+
+
+
+ make clip volume
+
+
- }
- },
- '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 += "";
- }
+ for(let i = 0; i < jsResponse.pointclouds.length; i++){
+ let url = `${viewer.server}/download_regions_filter_result?handle=${handle}&index=${i}`;
- class MeasurePanel{
+ message += `result_${i}.las \n`;
+ }
- constructor(viewer, measurement, propertiesPanel){
- this.viewer = viewer;
- this.measurement = measurement;
- this.propertiesPanel = propertiesPanel;
+ let reportURL = `${viewer.server}/download_regions_filter_report?handle=${handle}`;
+ message += ` report.json \n`;
+ message += " ";
- this._update = () => { this.update(); };
- }
+ info(message);
+ };
- createCoordinatesTable(points){
- let table = $(`
-
- `);
+ 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 += "
";
- this.elRemove = this.elContent.find("img[name=remove]");
- this.elRemove.click( () => {
- this.viewer.scene.removeMeasurement(measurement);
- });
+ for(let i = 0; i < jsResponse.pointclouds.length; i++){
+ let url = `${viewer.server}/download_regions_filter_result?handle=${handle}&index=${i}`;
- this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update);
- this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update);
- this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update);
+ message += `result_${i}.las \n`;
+ }
- this.update();
- }
+ let reportURL = `${viewer.server}/download_regions_filter_report?handle=${handle}`;
+ message += ` report.json \n`;
+ 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;
-
-
-
- make clip volume
-
-
+ this.elContent = $(`
+
`);
- { // 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 = $(`
+
+
+
+ `);
- }
- };
+ 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 = $(`
${option} `);
+ 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 = $(`
-
-
-
- position
-
-
-
-
-
-
-
-
-
-
+ 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;
+ }
+ });
-
+ 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 = $(`
-
- `);
+ 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.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 = $(`
${option} `);
- 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