@sauskylark/potree
Version:
WebGL point cloud viewer
388 lines (307 loc) • 11.8 kB
JavaScript
import * as THREE from "../../../libs/three.js/build/three.module.js";
import {Utils} from "../../utils.js";
import {Volume, BoxVolume, SphereVolume} from "../../utils/Volume.js";
import {MeasurePanel} from "./MeasurePanel.js";
export class VolumePanel extends MeasurePanel{
constructor(viewer, measurement, propertiesPanel){
super(viewer, measurement, propertiesPanel);
let copyIconPath = Potree.resourcePath + '/icons/copy.svg';
let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
let lblLengthText = new Map([
[BoxVolume, "length"],
[SphereVolume, "rx"],
]).get(measurement.constructor);
let lblWidthText = new Map([
[BoxVolume, "width"],
[SphereVolume, "ry"],
]).get(measurement.constructor);
let lblHeightText = new Map([
[BoxVolume, "height"],
[SphereVolume, "rz"],
]).get(measurement.constructor);
this.elContent = $(`
<div class="measurement_content selectable">
<span class="coordinates_table_container"></span>
<table class="measurement_value_table">
<tr>
<th>\u03b1</th>
<th>\u03b2</th>
<th>\u03b3</th>
<th></th>
</tr>
<tr>
<td align="center" id="angle_cell_alpha" style="width: 33%"></td>
<td align="center" id="angle_cell_betta" style="width: 33%"></td>
<td align="center" id="angle_cell_gamma" style="width: 33%"></td>
<td align="right" style="width: 25%">
<img name="copyRotation" title="copy" class="button-icon" src="${copyIconPath}" style="width: 16px; height: 16px"/>
</td>
</tr>
</table>
<table class="measurement_value_table">
<tr>
<th>${lblLengthText}</th>
<th>${lblWidthText}</th>
<th>${lblHeightText}</th>
<th></th>
</tr>
<tr>
<td align="center" id="cell_length" style="width: 33%"></td>
<td align="center" id="cell_width" style="width: 33%"></td>
<td align="center" id="cell_height" style="width: 33%"></td>
<td align="right" style="width: 25%">
<img name="copyScale" title="copy" class="button-icon" src="${copyIconPath}" style="width: 16px; height: 16px"/>
</td>
</tr>
</table>
<br>
<span style="font-weight: bold">Volume: </span>
<span id="measurement_volume"></span>
<!--
<li>
<label style="whitespace: nowrap">
<input id="volume_show" type="checkbox"/>
<span>show volume</span>
</label>
</li>-->
<li>
<label style="whitespace: nowrap">
<input id="volume_clip" type="checkbox"/>
<span>make clip volume</span>
</label>
</li>
<li style="margin-top: 10px">
<input name="download_volume" type="button" value="prepare download" style="width: 100%" />
<div name="download_message"></div>
</li>
<!-- ACTIONS -->
<li style="display: grid; grid-template-columns: auto auto; grid-column-gap: 5px; margin-top: 10px">
<input id="volume_reset_orientation" type="button" value="reset orientation"/>
<input id="volume_make_uniform" type="button" value="make uniform"/>
</li>
<div style="display: flex; margin-top: 12px">
<span></span>
<span style="flex-grow: 1"></span>
<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
</div>
</div>
`);
{ // download
this.elDownloadButton = this.elContent.find("input[name=download_volume]");
if(this.propertiesPanel.viewer.server){
this.elDownloadButton.click(() => this.download());
} else {
this.elDownloadButton.hide();
}
}
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);
this.viewer.postMessage(
`Copied value to clipboard: <br>'${msg}'`,
{duration: 3000});
});
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);
this.viewer.postMessage(
`Copied value to clipboard: <br>'${msg}'`,
{duration: 3000});
});
this.elRemove = this.elContent.find("img[name=remove]");
this.elRemove.click( () => {
this.viewer.scene.removeVolume(measurement);
});
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);
});
this.elCheckClip = this.elContent.find('#volume_clip');
this.elCheckClip.click(event => {
this.measurement.clip = event.target.checked;
});
this.elCheckShow = this.elContent.find('#volume_show');
this.elCheckShow.click(event => {
this.measurement.visible = event.target.checked;
});
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);
this.update();
}
async download(){
let clipBox = this.measurement;
let regions = [];
//for(let clipBox of boxes){
{
let toClip = clipBox.matrixWorld;
let px = new THREE.Vector3(+0.5, 0, 0).applyMatrix4(toClip);
let nx = new THREE.Vector3(-0.5, 0, 0).applyMatrix4(toClip);
let py = new THREE.Vector3(0, +0.5, 0).applyMatrix4(toClip);
let ny = new THREE.Vector3(0, -0.5, 0).applyMatrix4(toClip);
let pz = new THREE.Vector3(0, 0, +0.5).applyMatrix4(toClip);
let nz = new THREE.Vector3(0, 0, -0.5).applyMatrix4(toClip);
let pxN = new THREE.Vector3().subVectors(nx, px).normalize();
let nxN = pxN.clone().multiplyScalar(-1);
let pyN = new THREE.Vector3().subVectors(ny, py).normalize();
let nyN = pyN.clone().multiplyScalar(-1);
let pzN = new THREE.Vector3().subVectors(nz, pz).normalize();
let nzN = pzN.clone().multiplyScalar(-1);
let planes = [
new THREE.Plane().setFromNormalAndCoplanarPoint(pxN, px),
new THREE.Plane().setFromNormalAndCoplanarPoint(nxN, nx),
new THREE.Plane().setFromNormalAndCoplanarPoint(pyN, py),
new THREE.Plane().setFromNormalAndCoplanarPoint(nyN, ny),
new THREE.Plane().setFromNormalAndCoplanarPoint(pzN, pz),
new THREE.Plane().setFromNormalAndCoplanarPoint(nzN, nz),
];
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 regionsArg = regions.join(",");
let pointcloudArgs = [];
for(let pointcloud of this.viewer.scene.pointclouds){
if(!pointcloud.visible){
continue;
}
let offset = pointcloud.pcoGeometry.offset.clone();
let negateOffset = new THREE.Matrix4().makeTranslation(...offset.multiplyScalar(-1).toArray());
let matrixWorld = pointcloud.matrixWorld;
let transform = new THREE.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 elMessage = this.elContent.find("div[name=download_message]");
let error = (message) => {
elMessage.html(`<div style="color: #ff0000">ERROR: ${message}</div>`);
};
let info = (message) => {
elMessage.html(`${message}`);
};
let handle = null;
{ // START FILTER
let url = `${viewer.server}/create_regions_filter?pointclouds=[${pointcloudsArg}]®ions=[${regionsArg}]`;
//console.log(url);
info("estimating results ...");
let response = await fetch(url);
let jsResponse = await response.json();
//console.log(jsResponse);
if(!jsResponse.handle){
error(jsResponse.message);
return;
}else{
handle = jsResponse.handle;
}
}
{ // WAIT, CHECK PROGRESS, HANDLE FINISH
let url = `${viewer.server}/check_regions_filter?handle=${handle}`;
let sleep = (function(duration){
return new Promise( (res, rej) => {
setTimeout(() => {
res();
}, duration);
});
});
let handleFiltering = (jsResponse) => {
let {progress, estimate} = jsResponse;
let progressFract = progress["processed points"] / estimate.points;
let progressPercents = parseInt(progressFract * 100);
info(`progress: ${progressPercents}%`);
};
let handleFinish = (jsResponse) => {
let message = "downloads ready: <br>";
message += "<ul>";
for(let i = 0; i < jsResponse.pointclouds.length; i++){
let url = `${viewer.server}/download_regions_filter_result?handle=${handle}&index=${i}`;
message += `<li><a href="${url}">result_${i}.las</a> </li>\n`;
}
let reportURL = `${viewer.server}/download_regions_filter_report?handle=${handle}`;
message += `<li> <a href="${reportURL}">report.json</a> </li>\n`;
message += "</ul>";
info(message);
};
let handleUnexpected = (jsResponse) => {
let message = `Unexpected Response. <br>status: ${jsResponse.status} <br>message: ${jsResponse.message}`;
info(message);
};
let handleError = (jsResponse) => {
let message = `ERROR: ${jsResponse.message}`;
error(message);
throw new Error(message);
};
let start = Date.now();
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);
}
let durationS = (Date.now() - start) / 1000;
let sleepAmountMS = durationS < 10 ? 100 : 1000;
await sleep(sleepAmountMS);
}
}
}
update(){
let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
elCoordiantesContainer.empty();
elCoordiantesContainer.append(this.createCoordinatesTable([this.measurement.position]));
{
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');
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]);
}
{
let dimensions = this.measurement.scale.toArray();
dimensions = dimensions.map(v => Utils.addCommas(v.toFixed(2)));
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);
}
};