src/plugins/ZonesPlugin/ZonesPlugin.js
import {Plugin} from "../../viewer/Plugin.js";
import {Component} from "../../viewer/scene/Component.js";
import {buildBoxGeometry} from "../../viewer/scene/geometry/builders/buildBoxGeometry.js";
import {ReadableGeometry} from "../../viewer/scene/geometry/ReadableGeometry.js";
import {Marker} from "../../viewer/scene/marker/Marker.js";
import {PhongMaterial} from "../../viewer/scene/materials/PhongMaterial.js";
import {math} from "../../viewer/scene/math/math.js";
import {Mesh} from "../../viewer/scene/mesh/Mesh.js";
import {Dot} from "../lib/html/Dot.js";
import {activateDraggableDots, Dot3D, touchPointSelector, transformToNode} from "../../../src/plugins/lib/ui/index.js";
const hex2rgb = function(color) {
const rgb = idx => parseInt(color.substr(idx + 1, 2), 16) / 255;
return [ rgb(0), rgb(2), rgb(4) ];
};
const triangulateEarClipping = function(planeCoords) {
const polygonVertices = [ ];
for (let i = 0; i < planeCoords.length; ++i)
polygonVertices.push(i);
const isCCW = (function() {
const ba = math.vec2();
const bc = math.vec2();
let anglesSum = 0;
const angles = [ ];
for (let i = 0; i < polygonVertices.length; ++i)
{
const a = planeCoords[polygonVertices[i]];
const b = planeCoords[polygonVertices[(i + 1) % polygonVertices.length]];
const c = planeCoords[polygonVertices[(i + 2) % polygonVertices.length]];
math.subVec2(a, b, ba);
math.subVec2(c, b, bc);
const theta = math.dotVec2(ba, bc) / Math.sqrt(math.sqLenVec2(ba) * math.sqLenVec2(bc));
const angle = Math.acos(Math.max(-1, Math.min(theta, 1)));
const convex = (ba[0] * bc[1] - ba[1] * bc[0]) >= 0;
anglesSum += convex ? angle : (2 * Math.PI - angle);
}
return anglesSum < (polygonVertices.length * Math.PI);
})();
const pointInTriangle = (function() {
const sign = (p1, p2, p3) => {
return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]);
};
return (pt, v1, v2, v3) => {
const d1 = sign(pt, v1, v2);
const d2 = sign(pt, v2, v3);
const d3 = sign(pt, v3, v1);
const has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0);
const has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0);
return !(has_neg && has_pos);
};
})();
const baseTriangles = [ ];
const vertices = (isCCW ? polygonVertices : polygonVertices.slice(0).reverse()).map(i => ({ idx: i }));
vertices.forEach((v, i) => {
v.prev = vertices[(i - 1 + vertices.length) % vertices.length];
v.next = vertices[(i + 1) % vertices.length];
});
const ba = math.vec2();
const bc = math.vec2();
while (vertices.length > 2) {
let earIdx = 0;
while (true) {
if (earIdx >= vertices.length)
{
throw `isCCW = ${isCCW}; earIdx = ${earIdx}; len = ${vertices.length}`;
}
const v = vertices[earIdx];
const a = planeCoords[v.prev.idx];
const b = planeCoords[v.idx];
const c = planeCoords[v.next.idx];
math.subVec2(a, b, ba);
math.subVec2(c, b, bc);
if (((ba[0] * bc[1] - ba[1] * bc[0]) >= 0) // a convex vertex
&&
vertices.every( // no other vertices inside
vv => ((vv === v)
||
(vv === v.prev)
||
(vv === v.next)
||
!pointInTriangle(planeCoords[vv.idx], a, b, c))))
break;
++earIdx;
}
const ear = vertices[earIdx];
vertices.splice(earIdx, 1);
baseTriangles.push([ ear.idx, ear.next.idx, ear.prev.idx ]);
const prev = ear.prev;
prev.next = ear.next;
const next = ear.next;
next.prev = ear.prev;
}
return [ planeCoords, baseTriangles, isCCW ];
};
const marker3D = function(scene, color) {
const canvas = scene.canvas.canvas;
const markerParent = canvas.parentNode;
const markerDiv = document.createElement("div");
markerParent.insertBefore(markerDiv, canvas);
let size = 5;
markerDiv.style.background = color;
markerDiv.style.border = "2px solid white";
markerDiv.style.margin = "0 0";
markerDiv.style.zIndex = "100";
markerDiv.style.position = "absolute";
markerDiv.style.pointerEvents = "none";
markerDiv.style.display = "none";
const marker = new Marker(scene, {});
const px = x => x + "px";
const update = function() {
const pos = marker.canvasPos.slice();
transformToNode(canvas, markerParent, pos);
markerDiv.style.left = px(pos[0] - 3 - size / 2);
markerDiv.style.top = px(pos[1] - 3 - size / 2);
markerDiv.style.borderRadius = px(size * 2);
markerDiv.style.width = px(size);
markerDiv.style.height = px(size);
};
const onViewMatrix = scene.camera.on("viewMatrix", update);
const onProjMatrix = scene.camera.on("projMatrix", update);
return {
update: function(worldPos) {
if (worldPos)
{
marker.worldPos = worldPos;
update();
}
markerDiv.style.display = worldPos ? "" : "none";
},
setHighlighted: function(h) {
size = h ? 10 : 5;
update();
},
getCanvasPos: () => marker.canvasPos,
getWorldPos: () => marker.worldPos,
destroy: function() {
markerDiv.parentNode.removeChild(markerDiv);
scene.camera.off(onViewMatrix);
scene.camera.off(onProjMatrix);
marker.destroy();
}
};
};
import {Wire} from "../lib/html/Wire.js";
const wire3D = function(scene, color, startWorldPos) {
const canvas = scene.canvas.canvas;
const startMarker = new Marker(scene, {});
startMarker.worldPos = startWorldPos;
const endMarker = new Marker(scene, {});
const wireParent = canvas.ownerDocument.body;
const wire = new Wire(wireParent, {
color: color,
thickness: 1,
thicknessClickable: 6
});
wire.setVisible(false);
const updatePos = function() {
const p0 = startMarker.canvasPos.slice();
const p1 = endMarker.canvasPos.slice();
transformToNode(canvas, wireParent, p0);
transformToNode(canvas, wireParent, p1);
wire.setStartAndEnd(p0[0], p0[1], p1[0], p1[1]);
};
const onViewMatrix = scene.camera.on("viewMatrix", updatePos);
const onProjMatrix = scene.camera.on("projMatrix", updatePos);
return {
update: function(endWorldPos) {
if (endWorldPos)
{
endMarker.worldPos = endWorldPos;
updatePos();
}
wire.setVisible(!!endWorldPos);
},
destroy: function() {
scene.camera.off(onViewMatrix);
scene.camera.off(onProjMatrix);
startMarker.destroy();
endMarker.destroy();
wire.destroy();
}
};
};
const basePolygon3D = function(scene, color, alpha) {
let mesh = null;
const updateBase = points => {
if (points)
{
if (mesh)
{
mesh.destroy();
}
try {
const [ baseVertices, baseTriangles ] = triangulateEarClipping(points.map(p => [ p[0], p[2] ]));
const positions = [ ].concat(...baseVertices.map(p => [p[0], points[0][1], p[1]])); // To convert from Float64Array into an Array
const ind = [ ].concat(...baseTriangles);
mesh = new Mesh(scene, {
pickable: false, // otherwise there's a WebGL error inside PickMeshRenderer.prototype.drawMesh
geometry: new ReadableGeometry(
scene,
{
positions: positions,
indices: ind,
normals: math.buildNormals(positions, ind)
}),
material: new PhongMaterial(scene, {
alpha: (alpha !== undefined) ? alpha : 0.5,
backfaces: true,
diffuse: hex2rgb(color)
})
});
} catch (e) {
mesh = null;
}
}
if (mesh)
{
mesh.visible = !!points;
}
};
updateBase(null);
return {
updateBase: updateBase,
destroy: () => mesh && mesh.destroy()
};
};
const startAARectCreateUI = function(scene, markersColor, pointerLens, select3dPoint, withPoints, onPointsSelected) {
const marker1 = marker3D(scene, markersColor);
const marker2 = marker3D(scene, markersColor);
const updatePointerLens = (pointerLens
? function(canvasPos) {
pointerLens.visible = !! canvasPos;
if (canvasPos)
{
pointerLens.canvasPos = canvasPos;
}
}
: () => { });
let deactivatePointSelection = select3dPoint(
() => {
updatePointerLens(null);
marker1.update(null);
},
(canvasPos, worldPos) => {
updatePointerLens(canvasPos);
marker1.update(worldPos);
},
function(point1CanvasPos, point1WorldPos) {
marker1.update(point1WorldPos);
deactivatePointSelection = select3dPoint(
function() {
updatePointerLens(null);
marker2.update(null);
withPoints(null);
},
function(canvasPos, point2WorldPos) {
updatePointerLens(canvasPos);
marker2.update(point2WorldPos);
withPoints((math.distVec3(point1WorldPos, point2WorldPos) > 0.01)
&&
[ point1WorldPos, point2WorldPos ]);
},
function(point2CanvasPos, point2WorldPos) {
// `marker2.update' makes sure marker's position has been updated from its default [0,0,0]
// This works around an unidentified bug somewhere around OcclusionLayer, that causes error
// [.WebGL-0x13400c47e00] GL_INVALID_OPERATION: Vertex buffer is not big enough for the draw call
marker2.update(point2WorldPos);
marker1.destroy();
marker2.destroy();
updatePointerLens(null);
onPointsSelected([ point1WorldPos, point2WorldPos ]);
});
});
return {
deactivate: function() {
deactivatePointSelection();
marker1.destroy();
marker2.destroy();
updatePointerLens(null);
}
};
};
const mousePointSelector = function(viewer, ray2WorldPos) {
return function(onCancel, onChange, onCommit) {
const scene = viewer.scene;
const canvas = scene.canvas.canvas;
const moveTolerance = 20;
const copyCanvasPos = (event, vec2) => {
vec2[0] = event.clientX;
vec2[1] = event.clientY;
transformToNode(canvas.ownerDocument.documentElement, canvas, vec2);
return vec2;
};
const pickWorldPos = canvasPos => {
const origin = math.vec3();
const direction = math.vec3();
math.canvasPosToWorldRay(canvas, scene.camera.viewMatrix, scene.camera.projMatrix, scene.camera.projection, canvasPos, origin, direction);
return ray2WorldPos(origin, direction);
};
let buttonDown = false;
const resetAction = function() {
buttonDown = false;
};
const cleanup = function() {
resetAction();
canvas.removeEventListener("mousedown", onMouseDown);
canvas.removeEventListener("mousemove", onMouseMove);
viewer.cameraControl.off(onCameraControlRayMove);
canvas.removeEventListener("mouseup", onMouseUp);
};
const startCanvasPos = math.vec2();
const onMouseDown = function(event) {
if (event.which === 1)
{
copyCanvasPos(event, startCanvasPos);
buttonDown = true;
}
};
canvas.addEventListener("mousedown", onMouseDown);
const onMouseMove = function(event) {
const canvasPos = copyCanvasPos(event, math.vec2());
if (buttonDown && math.distVec2(startCanvasPos, canvasPos) > moveTolerance)
{
resetAction();
onCancel();
}
};
canvas.addEventListener("mousemove", onMouseMove);
const onCameraControlRayMove = viewer.cameraControl.on(
"rayMove",
event => {
const canvasPos = event.canvasPos;
onChange(canvasPos, pickWorldPos(canvasPos));
});
const onMouseUp = function(event) {
if ((event.which === 1) && buttonDown)
{
cleanup();
const canvasPos = copyCanvasPos(event, math.vec2());
onCommit(canvasPos, pickWorldPos(canvasPos));
}
};
canvas.addEventListener("mouseup", onMouseUp);
return cleanup;
};
};
const planeIntersect = function(p0, n, origin, direction) {
const t = - (math.dotVec3(origin, n) - p0) / math.dotVec3(direction, n);
if (false) // (t < 0)
{
return false;
}
else
{
const worldPos = math.vec3();
math.mulVec3Scalar(direction, t, worldPos);
math.addVec3(origin, worldPos, worldPos);
return worldPos;
}
};
/**
* @desc Renders a transparent box between two 3D points.
*
* See {@link ZonesPlugin} for more info.
*/
class Zone extends Component {
/**
* @private
*/
constructor(plugin, cfg = {}) {
super(plugin.viewer.scene, cfg);
/**
* The {@link ZonesPlugin} that owns this Zone.
* @type {ZonesPlugin}
*/
this.plugin = plugin;
this._container = cfg.container;
if (!this._container) {
throw "config missing: container";
}
this._eventSubs = {};
const scene = this.plugin.viewer.scene;
this._geometry = cfg.geometry;
const onMouseOver = cfg.onMouseOver ? (event) => {
cfg.onMouseOver(event, this);
this.plugin.viewer.scene.canvas.canvas.dispatchEvent(new MouseEvent('mouseover', event));
} : null;
const onMouseLeave = cfg.onMouseLeave ? (event) => {
cfg.onMouseLeave(event, this);
this.plugin.viewer.scene.canvas.canvas.dispatchEvent(new MouseEvent('mouseleave', event));
} : null;
const onMouseDown = (event) => {
this.plugin.viewer.scene.canvas.canvas.dispatchEvent(new MouseEvent('mousedown', event));
} ;
const onMouseUp = (event) => {
this.plugin.viewer.scene.canvas.canvas.dispatchEvent(new MouseEvent('mouseup', event));
};
const onMouseMove = (event) => {
this.plugin.viewer.scene.canvas.canvas.dispatchEvent(new MouseEvent('mousemove', event));
};
const onContextMenu = cfg.onContextMenu ? (event) => {
cfg.onContextMenu(event, this);
} : null;
const onMouseWheel = (event) => {
this.plugin.viewer.scene.canvas.canvas.dispatchEvent(new WheelEvent('wheel', event));
};
this._alpha = (("alpha" in cfg) && (cfg.alpha !== undefined)) ? cfg.alpha : 0.5;
this.color = cfg.color;
this._visible = true;
this._rebuildMesh();
}
_rebuildMesh() {
const scene = this.plugin.viewer.scene;
const planeCoords = this._geometry.planeCoordinates.slice();
const downward = this._geometry.height < 0;
const altitude = this._geometry.altitude + (downward ? this._geometry.height : 0);
const height = this._geometry.height * (downward ? -1 : 1);
const [ baseVertices, baseTriangles, isCCW ] = triangulateEarClipping(planeCoords); // TODO: prevent crossing edges
const pos = [ ];
const ind = [ ];
const addPlane = (isCeiling) => {
const baseIdx = pos.length;
for (let c of baseVertices) {
pos.push([ c[0], altitude + (isCeiling ? height : 0), c[1] ]);
}
for (let t of baseTriangles) {
ind.push(...(isCeiling ? t : t.slice(0).reverse()).map(i => i + baseIdx));
}
};
addPlane(false); // floor
addPlane(true); // ceiling
// sides
for (let i = 0; i < baseVertices.length; ++i) {
const a = baseVertices[i];
const b = baseVertices[(baseVertices.length + i + (isCCW ? 1 : -1)) % baseVertices.length];
const f = altitude;
const c = altitude + height;
const baseIdx = pos.length;
pos.push(
[ a[0], f, a[1] ],
[ b[0], f, b[1] ],
[ b[0], c, b[1] ],
[ a[0], c, a[1] ]
);
ind.push(...[ 0, 1, 2, 0, 2, 3 ].map(i => i + baseIdx));
}
if (this._zoneMesh) {
this._zoneMesh.destroy();
}
const positions = [].concat(...pos);
this._zoneMesh = new Mesh(scene, {
edges: this._edges,
geometry: new ReadableGeometry(
scene,
{
positions: positions,
indices: ind,
normals: math.buildNormals(positions, ind)
}),
material: new PhongMaterial(scene, {
alpha: this._alpha,
backfaces: true,
diffuse: hex2rgb(this._color)
}),
visible: this._visible
});
this._zoneMesh.highlighted = this._highlighted;
this._zoneMesh.zone = this;
{
const u = math.vec2();
const v = math.vec2();
let baseArea = 0;
for (let t of baseTriangles) {
const p0 = baseVertices[t[0]];
const p1 = baseVertices[t[1]];
const p2 = baseVertices[t[2]];
math.subVec2(p1, p0, u);
math.subVec2(p2, p0, v);
baseArea += Math.abs(u[0] * v[1] - u[1] * v[0]);
}
this._baseArea = baseArea / 2;
}
this._metrics = null;
const min = idx => Math.min(...pos.map(p => p[idx]));
const max = idx => Math.max(...pos.map(p => p[idx]));
const xmin = min(0);
const ymin = min(1);
const zmin = min(2);
const xmax = max(0);
const ymax = max(1);
const zmax = max(2);
this._center = math.vec3([ (xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2 ]);
}
get baseArea() {
return this._baseArea;
}
get area() {
return this._getMetrics().area;
}
get volume() {
return this._getMetrics().volume;
}
_getMetrics() {
if (this._metrics === null) {
// Sum the volume of tetrahedrons formed by the origin and face triangles
let volume = 0;
let area = 0;
const geo = this._zoneMesh.geometry;
const pts = [ math.vec3(), math.vec3(), math.vec3() ];
const tmpVec3 = math.vec3();
for (let i = 0; i < geo.indices.length; i += 3) {
for (let off = 0; off < 3; ++off) {
const p = pts[off];
const pIdx = 3 * geo.indices[i + off];
for (let c = 0; c < 3; ++c) {
p[c] = geo.positions[pIdx + c];
}
}
volume += math.dotVec3(pts[0], math.cross3Vec3(pts[1], pts[2], tmpVec3));
math.subVec3(pts[1], pts[0], pts[1]);
math.subVec3(pts[2], pts[0], pts[2]);
area += math.lenVec3(math.cross3Vec3(pts[1], pts[2], tmpVec3));
}
this._metrics = {
area: area / 2,
volume: volume / 6
};
}
return this._metrics;
}
sectionedAverage(sectionPlanes) {
const planeCoords = this._geometry.planeCoordinates.slice();
let faces = [ ];
{
const h = this._geometry.height;
const a = this._geometry.altitude;
const c = a + Math.max(0, h);
const f = a + Math.min(0, h);
const addPlane = (isCeiling) => {
const face = planeCoords.map(p => [ p[0], isCeiling ? c : f, p[1] ]);
faces.push(isCeiling ? face : face.slice(0).reverse());
};
addPlane(true); // ceiling
addPlane(false); // floor
// sides
const p = (idx, y) => [ planeCoords[idx][0], y, planeCoords[idx][1] ];
for (let i = 0; i < planeCoords.length; ++i)
{
const j = (i + 1) % planeCoords.length;
faces.push([ p(i, f), p(j, f), p(j, c), p(i, c) ]);
}
}
for (const s of sectionPlanes)
{
const dir = s.dir;
const dist = s.dist;
const newFaces = [ ];
for (const face of faces)
{
const EPSILON = 1e-5;
const COPLANAR = 0;
const FRONT = 1;
const BACK = 2;
const SPANNING = 3;
// Classify each point as well as the entire polygon into one of the above four classes.
let polygonType = 0;
const types = [ ];
for (let i = 0; i < face.length; i++) {
const t = math.dotVec3(dir, face[i]) + dist;
const type = (t < -EPSILON) ? BACK : (t > EPSILON) ? FRONT : COPLANAR;
polygonType |= type;
types.push(type);
}
// Put the polygon in the correct list, splitting it when necessary.
switch (polygonType) {
case COPLANAR:
newFaces.push(face);
break;
case FRONT:
newFaces.push(face);
break;
case BACK:
break;
case SPANNING:
const f = [ ];
for (let i = 0; i < face.length; i++)
{
var j = (i + 1) % face.length;
const ti = types[i];
const tj = types[j];
const vi = face[i];
const vj = face[j];
if (ti !== BACK)
{
f.push(vi);
}
if ((ti | tj) === SPANNING)
{
const diff = math.vec3();
math.subVec3(vj, vi, diff);
const t = - (dist + math.dotVec3(dir, vi)) / math.dotVec3(dir, diff);
const v = [0,0,0];
math.lerpVec3(t, 0, 1, vi, vj, v);
f.push(v);
}
}
if (f.length >= 3)
{
newFaces.push(f);
}
break;
}
}
faces = newFaces;
}
if (faces.length === 0)
{
return null;
}
else
{
const avg = math.vec3([ 0, 0, 0 ]);
const unique = new Set();
for (const f of faces)
{
for (const p of f)
{
const id = p.map(x => x.toFixed(3)).join(":");
if (! (unique.has(id)))
{
unique.add(id);
math.addVec3(avg, p, avg);
}
}
}
math.mulVec3Scalar(avg, 1 / unique.size, avg);
return avg;
}
}
get center() {
return this._center;
}
get altitude() {
return this._geometry.altitude;
}
set altitude(value) {
this._geometry.altitude = value;
this._rebuildMesh();
}
get height() {
return this._geometry.height;
}
set height(value) {
this._geometry.height = value;
this._rebuildMesh();
}
get highlighted() {
return this._highlighted;
}
set highlighted(value)
{
this._highlighted = value;
if (this._zoneMesh) {
this._zoneMesh.highlighted = value;
}
}
set color(value) {
this._color = value;
if (this._zoneMesh) {
this._zoneMesh.material.diffuse = hex2rgb(this._color);
}
}
get color() {
return this._color;
}
set alpha(value) {
this._alpha = value;
if (this._zoneMesh) {
this._zoneMesh.material.alpha = this._alpha;
}
}
get alpha() {
return this._alpha;
}
get edges() {
return this._edges;
}
set edges(edges) {
this._edges = edges;
if (this._zoneMesh) {
this._zoneMesh.edges = this._edges;
}
}
/**
* Sets whether this Zone is visible or not.
*
* @type {Boolean}
*/
set visible(value) {
this._visible = !!value;
this._zoneMesh.visible = this._visible;
this._needUpdate();
}
/**
* Gets whether this Zone is visible or not.
*
* @type {Boolean}
*/
get visible() {
return this._visible;
}
/**
* Gets this Zone as JSON.
*
* @returns {JSON}
*/
getJSON() {
return {
id: this.id,
geometry: this._geometry,
alpha: this._alpha,
color: this._color
};
}
duplicate() {
return this.plugin.createZone(
{
id: math.createUUID(),
geometry: {
planeCoordinates: this._geometry.planeCoordinates.map(c => c.slice()),
altitude: this._geometry.altitude,
height: this._geometry.height
},
alpha: this._alpha,
color: this._color
});
}
/**
* @private
*/
destroy() {
this._zoneMesh.destroy();
super.destroy();
}
}
const createAAZoneFromPoints = function(pos1, pos2, zoneAltitude, zoneHeight, zoneColor, zoneAlpha, zonesPlugin) {
const min = (idx) => Math.min(pos1[idx], pos2[idx]);
const max = (idx) => Math.max(pos1[idx], pos2[idx]);
const xmin = min(0);
const zmin = min(2);
const xmax = max(0);
const zmax = max(2);
return zonesPlugin.createZone(
{
id: math.createUUID(),
geometry: {
planeCoordinates: [
[ xmin, zmax ],
[ xmax, zmax ],
[ xmax, zmin ],
[ xmin, zmin ]
],
altitude: zoneAltitude,
height: zoneHeight
},
alpha: zoneAlpha,
color: zoneColor
});
};
/**
* Creates {@link Zone}s in a {@link ZonesPlugin} from mouse input.
*
* ## Usage
*
* [[Run example](/examples/measurement/#distance_createWithMouse_snapping)]
*
* ````javascript
* import {Viewer, XKTLoaderPlugin, ZonesPlugin, ZonesMouseControl} from "xeokit-sdk.es.js";
*
* const viewer = new Viewer({
* canvasId: "myCanvas",
* });
*
* viewer.camera.eye = [-3.93, 2.85, 27.01];
* viewer.camera.look = [4.40, 3.72, 8.89];
* viewer.camera.up = [-0.01, 0.99, 0.039];
*
* const xktLoader = new XKTLoaderPlugin(viewer);
*
* const sceneModel = xktLoader.load({
* id: "myModel",
* src: "Duplex.xkt"
* });
*
* const zones = new ZonesPlugin(viewer);
*
* const zonesControl = new ZonesMouseControl(Zones)
* ````
*/
class ZonesAAZoneControl extends Component {
/**
* Creates a ZonesMouseControl bound to the given ZonesPlugin.
*
* @param {ZonesPlugin} zonesPlugin The ZonesPlugin to control.
* @param [cfg] Configuration
* @param {PointerLens} [cfg.pointerLens] A PointerLens to use to provide a magnified view of the cursor when snapping is enabled.
*/
constructor(zonesPlugin, cfg, createSelect3dPoint) {
super(zonesPlugin.viewer.scene);
this.zonesPlugin = zonesPlugin;
this.pointerLens = cfg.pointerLens;
this.createSelect3dPoint = createSelect3dPoint;
this._deactivate = null;
}
get active() {
return !! this._deactivate;
}
activate(zoneAltitude, zoneHeight, zoneColor, zoneAlpha) {
if (this._deactivate) {
return;
}
if (typeof(zoneAltitude) === "object" && (zoneAltitude !== null)) {
const params = zoneAltitude;
const param = (name, defaultValue) => {
if (name in params) {
return params[name];
} else if (defaultValue !== undefined) {
return defaultValue;
} else {
throw "config missing: " + name;
}
};
zoneAltitude = param("altitude");
zoneHeight = param("height");
zoneColor = param("color", "#008000");
zoneAlpha = param("alpha", 0.5);
}
const zonesPlugin = this.zonesPlugin;
const viewer = zonesPlugin.viewer;
const scene = viewer.scene;
const self = this;
const select3dPoint = this.createSelect3dPoint(viewer, (origin, direction) => planeIntersect(zoneAltitude, math.vec3([ 0, 1, 0 ]), origin, direction));
(function rec() {
const basePolygon = basePolygon3D(scene, zoneColor, zoneAlpha);
const deactivate = startAARectCreateUI(
scene, zoneColor, self.pointerLens, select3dPoint,
points => {
if (points) {
const p0 = points[0];
const p1 = points[1];
const min = (idx) => Math.min(p0[idx], p1[idx]);
const max = (idx) => Math.max(p0[idx], p1[idx]);
const xmin = min(0);
const ymin = min(1);
const zmin = min(2);
const xmax = max(0);
const ymax = max(1);
const zmax = max(2);
basePolygon.updateBase([ [ xmin, ymin, zmax ], [ xmax, ymin, zmax ],
[ xmax, ymin, zmin ], [ xmin, ymin, zmin ] ]);
} else {
basePolygon.updateBase(null);
}
},
points => {
basePolygon.destroy();
const zone = createAAZoneFromPoints(points[0], points[1], zoneAltitude, zoneHeight, zoneColor, zoneAlpha, zonesPlugin);
let reactivate = true;
self._deactivate = () => { reactivate = false; };
self.fire("zoneEnd", zone);
if (reactivate)
{
rec();
}
}).deactivate;
self._deactivate = () => {
deactivate();
basePolygon.destroy();
};
})();
}
deactivate() {
if (this._deactivate)
{
this._deactivate();
this._deactivate = null;
}
}
/**
* Destroys this ZonesMouseControl.
*
* Destroys any {@link Zone} under construction by this ZonesMouseControl.
*/
destroy() {
this.deactivate();
super.destroy();
}
}
export class ZonesMouseControl extends ZonesAAZoneControl {
constructor(zonesPlugin, cfg = {}) {
super(
zonesPlugin,
cfg,
(viewer, ray2WorldPos) => mousePointSelector(viewer, ray2WorldPos));
}
}
import {PointerCircle} from "../../extras/PointerCircle/PointerCircle.js";
export class ZonesTouchControl extends ZonesAAZoneControl {
constructor(zonesPlugin, cfg = {}) {
const pointerCircle = new PointerCircle(zonesPlugin.viewer);
super(
zonesPlugin,
cfg,
(viewer, ray2WorldPos) => touchPointSelector(viewer, pointerCircle, ray2WorldPos));
this.pointerCircle = pointerCircle;
}
destroy() {
this.pointerCircle.destroy();
super.destroy();
}
}
/**
* ZonesPlugin documentation to be added, mostly compatible with DistanceMeasurementsPlugin.
*/
export class ZonesPlugin extends Plugin {
/**
* @constructor
* @param {Viewer} viewer The Viewer.
* @param {Object} [cfg] Plugin configuration.
* @param {String} [cfg.id="Zones"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
* @param {HTMLElement} [cfg.container] Container DOM element for markers and labels. Defaults to ````document.body````.
* @param {string} [cfg.defaultColor=#00BBFF] The default color of the length dots, wire and label.
* @param {number} [cfg.zIndex] If set, the wires, dots and labels will have this zIndex (+1 for dots and +2 for labels).
* @param {PointerCircle} [cfg.pointerLens] A PointerLens to help the user position the pointer. This can be shared with other plugins.
*/
constructor(viewer, cfg = {}) {
super("Zones", viewer);
this._pointerLens = cfg.pointerLens;
this._container = cfg.container || document.body;
this._zones = [ ];
this.defaultColor = cfg.defaultColor !== undefined ? cfg.defaultColor : "#00BBFF";
this.zIndex = cfg.zIndex || 10000;
this._onMouseOver = (event, zone) => {
this.fire("mouseOver", {
plugin: this,
zone,
event
});
};
this._onMouseLeave = (event, zone) => {
this.fire("mouseLeave", {
plugin: this,
zone,
event
});
};
this._onContextMenu = (event, zone) => {
this.fire("contextMenu", {
plugin: this,
zone,
event
});
};
}
/**
* Creates a {@link Zone}.
*
* The Zone is then registered by {@link Zone#id} in {@link ZonesPlugin#zones}.
*
* @param {Object} params {@link Zone} configuration.
* @param {String} params.id Unique ID to assign to {@link Zone#id}. The Zone will be registered by this in {@link ZonesPlugin#zones} and {@link Scene.components}. Must be unique among all components in the {@link Viewer}.
* @param {Number[]} params.origin.worldPos Origin World-space 3D position.
* @param {Entity} params.origin.entity Origin Entity.
* @param {Number[]} params.target.worldPos Target World-space 3D position.
* @param {Entity} params.target.entity Target Entity.
* @param {string} [params.color] The color of the length dot, wire and label.
* @returns {Zone} The new {@link Zone}.
*/
createZone(params = {}) {
if (this.viewer.scene.components[params.id]) {
this.error("Viewer scene component with this ID already exists: " + params.id);
delete params.id;
}
const zone = new Zone(this, {
id: params.id,
plugin: this,
container: this._container,
geometry: params.geometry,
alpha: params.alpha,
color: params.color,
onMouseOver: this._onMouseOver,
onMouseLeave: this._onMouseLeave,
onContextMenu: this._onContextMenu
});
this._zones.push(zone);
zone.on("destroyed", () => {
const idx = this._zones.indexOf(zone);
if (idx >= 0) {
this._zones.splice(idx, 1);
}
});
this.fire("zoneCreated", zone);
return zone;
}
/**
* Gets the existing {@link Zone}s, each mapped to its {@link Zone#id}.
*
* @type {{String:Zone}}
*/
get zones() {
return this._zones;
}
/**
* Destroys this ZonesPlugin.
*
* Destroys all {@link Zone}s first.
*/
destroy() {
super.destroy();
}
}
const startPolysurfaceZoneCreateUI = function(scene, zoneAltitude, zoneHeight, zoneColor, zoneAlpha, pointerLens, zonesPlugin, select3dPoint, onZoneCreated) {
const updatePointerLens = (pointerLens
? function(canvasPos) {
pointerLens.visible = !! canvasPos;
if (canvasPos)
{
pointerLens.canvasPos = canvasPos;
}
}
: () => { });
let deactivatePointSelection;
const cleanups = [ () => updatePointerLens(null) ];
const basePolygon = basePolygon3D(scene, zoneColor, zoneAlpha);
cleanups.push(() => basePolygon.destroy());
(function selectNextPoint(markers) {
const marker = marker3D(scene, zoneColor);
const wire = (markers.length > 0) && wire3D(scene, zoneColor, markers[markers.length - 1].getWorldPos());
cleanups.push(() => {
marker.destroy();
wire && wire.destroy();
});
const firstMarker = (markers.length > 0) && markers[0];
const getSnappedFirst = function(canvasPos) {
const firstCanvasPos = firstMarker && firstMarker.getCanvasPos();
const snapToFirst = firstCanvasPos && (math.distVec2(firstCanvasPos, canvasPos) < 10);
return snapToFirst && { canvasPos: firstCanvasPos, worldPos: firstMarker.getWorldPos() };
};
const lastSegmentIntersects = (function() {
const onSegment = (p, q, r) => ((q[0] <= Math.max(p[0], r[0])) &&
(q[0] >= Math.min(p[0], r[0])) &&
(q[1] <= Math.max(p[1], r[1])) &&
(q[1] >= Math.min(p[1], r[1])));
const orient = (p, q, r) => {
const val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
// collinear
// clockwise
// counterclockwise
return ((val === 0) ? 0 : ((val > 0) ? 1 : 2));
};
return function(pos2D, excludeFirstSegment) {
const a = pos2D[pos2D.length - 2];
const b = pos2D[pos2D.length - 1];
for (let i = excludeFirstSegment ? 1 : 0; i < pos2D.length - 2 - 1; ++i)
{
const c = pos2D[i];
const d = pos2D[i + 1];
const o1 = orient(a, b, c);
const o2 = orient(a, b, d);
const o3 = orient(c, d, a);
const o4 = orient(c, d, b);
if (((o1 !== o2) && (o3 !== o4)) || // General case
((o1 === 0) && onSegment(a, c, b)) || // a, b and c are collinear and c lies on segment ab
((o2 === 0) && onSegment(a, d, b)) || // a, b and d are collinear and d lies on segment ab
((o3 === 0) && onSegment(c, a, d)) || // c, d and a are collinear and a lies on segment cd
((o4 === 0) && onSegment(c, b, d))) // c, d and b are collinear and b lies on segment cd
{
return true;
}
}
return false;
};
})();
deactivatePointSelection = select3dPoint(
() => {
updatePointerLens(null);
marker.update(null);
wire && wire.update(null);
basePolygon.updateBase((markers.length > 2) ? markers.map(m => m.getWorldPos()) : null);
},
(canvasPos, worldPos) => {
const snappedFirst = (markers.length > 2) && getSnappedFirst(canvasPos);
firstMarker && firstMarker.setHighlighted(!! snappedFirst);
updatePointerLens(snappedFirst ? snappedFirst.canvasPos : canvasPos);
marker.update((! snappedFirst) && worldPos);
wire && wire.update(snappedFirst ? snappedFirst.worldPos : worldPos);
if ((markers.length >= 2))
{
const pos = markers.map(m => m.getWorldPos()).concat(snappedFirst ? [] : [worldPos]);
const inter = lastSegmentIntersects(pos.map(p => [ p[0], p[2] ]), snappedFirst);
basePolygon.updateBase(inter ? null : pos);
}
else
basePolygon.updateBase(null);
},
function(canvasPos, worldPos) {
const snappedFirst = (markers.length > 2) && getSnappedFirst(canvasPos);
const pos = markers.map(m => m.getWorldPos()).concat(snappedFirst ? [] : [worldPos]);
basePolygon.updateBase(pos);
const pos2D = pos.map(p => [ p[0], p[2] ]);
if ((markers.length > 2) && lastSegmentIntersects(pos2D, snappedFirst))
{
cleanups.pop()();
selectNextPoint(markers);
}
else if (snappedFirst)
{
// `marker2.update' makes sure marker's position has been updated from its default [0,0,0]
// This works around an unidentified bug somewhere around OcclusionLayer, that causes error
// [.WebGL-0x13400c47e00] GL_INVALID_OPERATION: Vertex buffer is not big enough for the draw call
marker.update(worldPos);
cleanups.forEach(c => c());
onZoneCreated(
zonesPlugin.createZone(
{
id: math.createUUID(),
geometry: {
planeCoordinates: pos2D,
altitude: zoneAltitude,
height: zoneHeight
},
alpha: zoneAlpha,
color: zoneColor
}));
}
else
{
marker.update(worldPos);
wire && wire.update(worldPos);
selectNextPoint(markers.concat(marker));
}
});
})([ ], null);
return {
closeSurface: function() {
throw "TODO";
},
deactivate: function() {
deactivatePointSelection();
cleanups.forEach(c => c());
}
};
};
export class ZonesPolysurfaceMouseControl extends Component {
constructor(zonesPlugin, cfg = {}) {
super(zonesPlugin.viewer.scene);
this.zonesPlugin = zonesPlugin;
this.pointerLens = cfg.pointerLens;
this._action = null;
}
get active() {
return !! this._action;
}
activate(zoneAltitude, zoneHeight, zoneColor, zoneAlpha) {
if (typeof(zoneAltitude) === "object" && (zoneAltitude !== null)) {
const params = zoneAltitude;
const param = (name, defaultValue) => {
if (name in params) {
return params[name];
} else if (defaultValue !== undefined) {
return defaultValue;
} else {
throw "config missing: " + name;
}
};
zoneAltitude = param("altitude");
zoneHeight = param("height");
zoneColor = param("color", "#008000");
zoneAlpha = param("alpha", 0.5);
}
if (this._action) {
return;
}
const zonesPlugin = this.zonesPlugin;
const viewer = zonesPlugin.viewer;
const scene = viewer.scene;
const self = this;
const select3dPoint = mousePointSelector(
viewer,
function(origin, direction) {
return planeIntersect(zoneAltitude, math.vec3([ 0, 1, 0 ]), origin, direction);
});
(function rec() {
self._action = startPolysurfaceZoneCreateUI(
scene, zoneAltitude, zoneHeight, zoneColor, zoneAlpha, self.pointerLens, zonesPlugin, select3dPoint,
zone => {
let reactivate = true;
self._action = { deactivate: () => { reactivate = false; } };
self.fire("zoneEnd", zone);
if (reactivate)
{
rec();
}
});
})();
}
deactivate() {
if (this._action)
{
this._action.deactivate();
this._action = null;
}
}
destroy() {
this.deactivate();
super.destroy();
}
}
export class ZonesPolysurfaceTouchControl extends Component {
constructor(zonesPlugin, cfg = {}) {
super(zonesPlugin.viewer.scene);
this.zonesPlugin = zonesPlugin;
this.pointerLens = cfg.pointerLens;
this.pointerCircle = new PointerCircle(zonesPlugin.viewer);
this._action = null;
}
get active() {
return !! this._action;
}
activate(zoneAltitude, zoneHeight, zoneColor, zoneAlpha) {
if (typeof(zoneAltitude) === "object" && (zoneAltitude !== null)) {
const params = zoneAltitude;
const param = (name, defaultValue) => {
if (name in params) {
return params[name];
} else if (defaultValue !== undefined) {
return defaultValue;
} else {
throw "config missing: " + name;
}
};
zoneAltitude = param("altitude");
zoneHeight = param("height");
zoneColor = param("color", "#008000");
zoneAlpha = param("alpha", 0.5);
}
if (this._action) {
return;
}
const zonesPlugin = this.zonesPlugin;
const viewer = zonesPlugin.viewer;
const scene = viewer.scene;
const self = this;
const select3dPoint = touchPointSelector(
viewer,
this.pointerCircle,
function(origin, direction) {
return planeIntersect(zoneAltitude, math.vec3([ 0, 1, 0 ]), origin, direction);
});
(function rec() {
self._action = startPolysurfaceZoneCreateUI(
scene, zoneAltitude, zoneHeight, zoneColor, zoneAlpha, self.pointerLens, zonesPlugin, select3dPoint,
zone => {
let reactivate = true;
self._action = { deactivate: () => { reactivate = false; } };
self.fire("zoneEnd", zone);
if (reactivate)
{
rec();
}
});
})();
}
deactivate() {
if (this._action)
{
this._action.deactivate();
this._action = null;
}
}
destroy() {
this.deactivate();
super.destroy();
}
}
export class ZoneEditControl extends Component {
constructor(zone, cfg, handleMouseEvents, handleTouchEvents) {
const viewer = zone.plugin.viewer;
const scene = viewer.scene;
super(scene);
const altitude = zone._geometry.altitude;
const dots = zone._geometry.planeCoordinates.map(planeCoord => {
const dotParent = scene.canvas.canvas.ownerDocument.body;
const dot = new Dot3D(scene, {}, dotParent, { fillColor: zone._color });
dot.worldPos = math.vec3([ planeCoord[0], altitude, planeCoord[1] ]);
dot.on("worldPos", function() {
planeCoord[0] = dot.worldPos[0];
planeCoord[1] = dot.worldPos[2];
try {
zone._rebuildMesh();
} catch (e) {
if (zone._zoneMesh) {
zone._zoneMesh.destroy();
zone._zoneMesh = null;
}
}
});
return dot;
});
const cleanupDrag = activateDraggableDots({
viewer: viewer,
handleMouseEvents: handleMouseEvents,
handleTouchEvents: handleTouchEvents,
pointerLens: cfg && cfg.pointerLens,
dots: dots,
ray2WorldPos: (orig, dir) => planeIntersect(altitude, math.vec3([ 0, 1, 0 ]), orig, dir),
onEnd: (initPos, dot) => {
if (zone._zoneMesh)
{
this.fire("edited");
}
return !! zone._zoneMesh;
}
});
const cleanup = function() {
cleanupDrag();
dots.forEach(d => d.destroy());
};
const destroyCb = zone.on("destroyed", cleanup);
this._deactivate = function() {
zone.off("destroyed", destroyCb);
cleanup();
};
}
deactivate() {
this._deactivate();
super.destroy();
}
}
export class ZoneEditMouseControl extends ZoneEditControl {
constructor(zone, cfg) {
super(zone, cfg, true, false);
}
}
export class ZoneEditTouchControl extends ZoneEditControl {
constructor(zone, cfg) {
super(zone, cfg, false, true);
}
}
export class ZoneTranslateControl extends Component {
constructor(zone, cfg, handleMouseEvents, handleTouchEvents) {
const viewer = zone.plugin.viewer;
const scene = viewer.scene;
const canvas = scene.canvas.canvas;
super(scene);
const self = this;
const altitude = zone._geometry.altitude;
const pointerLens = cfg && cfg.pointerLens;
const updatePointerLens = (pointerLens
? function(canvasPos) {
pointerLens.visible = !! canvasPos;
if (canvasPos)
{
pointerLens.canvasPos = canvasPos;
}
}
: () => { });
const ray2WorldPos = (orig, dir) => planeIntersect(altitude, math.vec3([ 0, 1, 0 ]), orig, dir);
const pickWorldPos = canvasPos => {
const origin = math.vec3();
const direction = math.vec3();
math.canvasPosToWorldRay(canvas, scene.camera.viewMatrix, scene.camera.projMatrix, scene.camera.projection, canvasPos, origin, direction);
return ray2WorldPos(origin, direction);
};
const copyCanvasPos = (event, vec2) => {
vec2[0] = event.clientX;
vec2[1] = event.clientY;
transformToNode(canvas.ownerDocument.documentElement, canvas, vec2);
return vec2;
};
const canvasHandle = function(type, cb) {
const callback = event => {
event.preventDefault();
cb(event);
};
canvas.addEventListener(type, callback);
return () => canvas.removeEventListener(type, callback);
};
let cleanupCurrentDrag = () => { };
const startDrag = function(event, onMoveType, onEndType, matchesEvent) {
const e = matchesEvent(event);
const canvasPos = copyCanvasPos(e, math.vec2());
const pickRecord = viewer.scene.pick({ canvasPos: canvasPos, includeEntities: [ zone._zoneMesh.id ] });
const pickZone = pickRecord && pickRecord.entity && pickRecord.entity.zone;
if (pickZone === zone)
{
cleanupCurrentDrag();
canvas.style.cursor = "move";
viewer.cameraControl.active = false;
const onChange = (function() {
const initCoords = zone._geometry.planeCoordinates.map(c => c.slice());
const initWorldPos = pickWorldPos(canvasPos);
const initDragCoord = math.vec2([ initWorldPos[0], initWorldPos[2] ]);
const dPos = math.vec2();
return function(canvasPos) {
const worldPos = pickWorldPos(canvasPos);
dPos[0] = worldPos[0];
dPos[1] = worldPos[2];
math.subVec2(initDragCoord, dPos, dPos);
zone._geometry.planeCoordinates.forEach((planeCoord, idx) => {
math.subVec2(initCoords[idx], dPos, planeCoord);
});
try {
zone._rebuildMesh();
} catch (e) {
if (zone._zoneMesh) {
zone._zoneMesh.destroy();
zone._zoneMesh = null;
}
}
};
})();
const cleanupMove = canvasHandle(
onMoveType,
function(event) {
const e = matchesEvent(event);
if (e)
{
const canvasPos = copyCanvasPos(e, math.vec2());
onChange(canvasPos);
updatePointerLens(canvasPos);
}
});
const cleanupEnd = canvasHandle(
onEndType,
function(event) {
const e = matchesEvent(event);
if (e)
{
const canvasPos = copyCanvasPos(e, math.vec2());
onChange(canvasPos);
updatePointerLens(null);
cleanupCurrentDrag();
self.fire("translated");
}
});
cleanupCurrentDrag = function() {
cleanupCurrentDrag = () => { };
canvas.style.cursor = "default";
viewer.cameraControl.active = true;
cleanupMove();
cleanupEnd();
};
}
};
const startDragCbs = [ ];
if (handleMouseEvents) {
startDragCbs.push(
canvasHandle("mousedown", event => {
if (event.which === 1) {
startDrag(
event,
"mousemove",
"mouseup",
event => (event.which === 1) && event);
}
}));
}
if (handleTouchEvents) {
startDragCbs.push(
canvasHandle("touchstart", event => {
if (event.touches.length === 1) {
const touchStartId = event.touches[0].identifier;
startDrag(
event,
"touchmove",
"touchend",
event => [...event.changedTouches].find(e => e.identifier === touchStartId));
}
}));
}
const cleanup = function() {
cleanupCurrentDrag();
startDragCbs.forEach(cb => cb());
updatePointerLens(null);
};
const destroyCb = zone.on("destroyed", cleanup);
this._deactivate = function() {
zone.off("destroyed", destroyCb);
cleanup();
};
}
deactivate() {
this._deactivate();
super.destroy();
}
}
export class ZoneTranslateMouseControl extends ZoneTranslateControl {
constructor(zone, cfg) {
super(zone, cfg, true, false);
}
}
export class ZoneTranslateTouchControl extends ZoneTranslateControl {
constructor(zone, cfg) {
super(zone, cfg, false, true);
}
}