diff --git a/examples/slicing/SectionCaps.html b/examples/slicing/SectionCaps.html
new file mode 100644
index 0000000000..050be3744c
--- /dev/null
+++ b/examples/slicing/SectionCaps.html
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+ xeokit Example
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html b/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html
new file mode 100644
index 0000000000..178905ce40
--- /dev/null
+++ b/examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+ xeokit Example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/plugins/CityJSONLoaderPlugin/CityJSONLoaderPlugin.js b/src/plugins/CityJSONLoaderPlugin/CityJSONLoaderPlugin.js
index 41ee7e119c..945a28aa45 100644
--- a/src/plugins/CityJSONLoaderPlugin/CityJSONLoaderPlugin.js
+++ b/src/plugins/CityJSONLoaderPlugin/CityJSONLoaderPlugin.js
@@ -1,7 +1,7 @@
import {math, Plugin, SceneModel, utils} from "../../viewer/index.js";
import {CityJSONDefaultDataSource} from "./CityJSONDefaultDataSource.js";
-import {earcut} from '../lib/earcut.js';
+import earcut from '../../viewer/scene/libs/earcut.js';
const tempVec2a = math.vec2();
const tempVec3a = math.vec3();
diff --git a/src/plugins/lib/earcut.js b/src/viewer/scene/libs/earcut.js
similarity index 61%
rename from src/plugins/lib/earcut.js
rename to src/viewer/scene/libs/earcut.js
index 4d1aefeca2..ff75641be5 100644
--- a/src/plugins/lib/earcut.js
+++ b/src/viewer/scene/libs/earcut.js
@@ -1,27 +1,27 @@
-/** @private */
-function earcut(data, holeIndices, dim) {
- dim = dim || 2;
+export default function earcut(data, holeIndices, dim = 2) {
- var hasHoles = holeIndices && holeIndices.length,
- outerLen = hasHoles ? holeIndices[0] * dim : data.length,
- outerNode = linkedList(data, 0, outerLen, dim, true),
- triangles = [];
+ const hasHoles = holeIndices && holeIndices.length;
+ const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
+ let outerNode = linkedList(data, 0, outerLen, dim, true);
+ const triangles = [];
if (!outerNode || outerNode.next === outerNode.prev) return triangles;
- var minX, minY, maxX, maxY, x, y, invSize;
+ let minX, minY, invSize;
if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
if (data.length > 80 * dim) {
- minX = maxX = data[0];
- minY = maxY = data[1];
-
- for (var i = dim; i < outerLen; i += dim) {
- x = data[i];
- y = data[i + 1];
+ minX = Infinity;
+ minY = Infinity;
+ let maxX = -Infinity;
+ let maxY = -Infinity;
+
+ for (let i = dim; i < outerLen; i += dim) {
+ const x = data[i];
+ const y = data[i + 1];
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
@@ -30,22 +30,22 @@ function earcut(data, holeIndices, dim) {
// minX, minY and invSize are later used to transform coords into integers for z-order calculation
invSize = Math.max(maxX - minX, maxY - minY);
- invSize = invSize !== 0 ? 1 / invSize : 0;
+ invSize = invSize !== 0 ? 32767 / invSize : 0;
}
- earcutLinked(outerNode, triangles, dim, minX, minY, invSize);
+ earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);
return triangles;
}
// create a circular doubly linked list from polygon points in the specified winding order
function linkedList(data, start, end, dim, clockwise) {
- var i, last;
+ let last;
if (clockwise === (signedArea(data, start, end, dim) > 0)) {
- for (i = start; i < end; i += dim) last = insertNode(i, data[i], data[i + 1], last);
+ for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
} else {
- for (i = end - dim; i >= start; i -= dim) last = insertNode(i, data[i], data[i + 1], last);
+ for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
}
if (last && equals(last, last.next)) {
@@ -61,7 +61,7 @@ function filterPoints(start, end) {
if (!start) return start;
if (!end) end = start;
- var p = start,
+ let p = start,
again;
do {
again = false;
@@ -87,19 +87,15 @@ function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
// interlink polygon nodes in z-order
if (!pass && invSize) indexCurve(ear, minX, minY, invSize);
- var stop = ear,
- prev, next;
+ let stop = ear;
// iterate through ears, slicing them one by one
while (ear.prev !== ear.next) {
- prev = ear.prev;
- next = ear.next;
+ const prev = ear.prev;
+ const next = ear.next;
if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
- // cut off the triangle
- triangles.push(prev.i / dim);
- triangles.push(ear.i / dim);
- triangles.push(next.i / dim);
+ triangles.push(prev.i, ear.i, next.i); // cut off the triangle
removeNode(ear);
@@ -118,12 +114,12 @@ function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
if (!pass) {
earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
- // if this didn't work, try curing all small self-intersections locally
+ // if this didn't work, try curing all small self-intersections locally
} else if (pass === 1) {
- ear = cureLocalIntersections(filterPoints(ear), triangles, dim);
+ ear = cureLocalIntersections(filterPoints(ear), triangles);
earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
- // as a last resort, try splitting the remaining polygon into two
+ // as a last resort, try splitting the remaining polygon into two
} else if (pass === 2) {
splitEarcut(ear, triangles, dim, minX, minY, invSize);
}
@@ -135,17 +131,25 @@ function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
// check whether a polygon node forms a valid ear with adjacent nodes
function isEar(ear) {
- var a = ear.prev,
+ const a = ear.prev,
b = ear,
c = ear.next;
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// now make sure we don't have other points inside the potential ear
- var p = ear.next.next;
-
- while (p !== ear.prev) {
- if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
+ const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
+
+ // triangle bbox
+ const x0 = Math.min(ax, bx, cx),
+ y0 = Math.min(ay, by, cy),
+ x1 = Math.max(ax, bx, cx),
+ y1 = Math.max(ay, by, cy);
+
+ let p = c.next;
+ while (p !== a) {
+ if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
+ pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
area(p.prev, p, p.next) >= 0) return false;
p = p.next;
}
@@ -154,51 +158,49 @@ function isEar(ear) {
}
function isEarHashed(ear, minX, minY, invSize) {
- var a = ear.prev,
+ const a = ear.prev,
b = ear,
c = ear.next;
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
- // triangle bbox; min & max are calculated like this for speed
- var minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x),
- minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y),
- maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x),
- maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y);
+ const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
+
+ // triangle bbox
+ const x0 = Math.min(ax, bx, cx),
+ y0 = Math.min(ay, by, cy),
+ x1 = Math.max(ax, bx, cx),
+ y1 = Math.max(ay, by, cy);
// z-order range for the current triangle bbox;
- var minZ = zOrder(minTX, minTY, minX, minY, invSize),
- maxZ = zOrder(maxTX, maxTY, minX, minY, invSize);
+ const minZ = zOrder(x0, y0, minX, minY, invSize),
+ maxZ = zOrder(x1, y1, minX, minY, invSize);
- var p = ear.prevZ,
+ let p = ear.prevZ,
n = ear.nextZ;
// look for points inside the triangle in both directions
while (p && p.z >= minZ && n && n.z <= maxZ) {
- if (p !== ear.prev && p !== ear.next &&
- pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
- area(p.prev, p, p.next) >= 0) return false;
+ if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
+ pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
- if (n !== ear.prev && n !== ear.next &&
- pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
- area(n.prev, n, n.next) >= 0) return false;
+ if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
+ pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}
// look for remaining points in decreasing z-order
while (p && p.z >= minZ) {
- if (p !== ear.prev && p !== ear.next &&
- pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
- area(p.prev, p, p.next) >= 0) return false;
+ if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
+ pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
}
// look for remaining points in increasing z-order
while (n && n.z <= maxZ) {
- if (n !== ear.prev && n !== ear.next &&
- pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
- area(n.prev, n, n.next) >= 0) return false;
+ if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
+ pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}
@@ -206,17 +208,15 @@ function isEarHashed(ear, minX, minY, invSize) {
}
// go through all polygon nodes and cure small local self-intersections
-function cureLocalIntersections(start, triangles, dim) {
- var p = start;
+function cureLocalIntersections(start, triangles) {
+ let p = start;
do {
- var a = p.prev,
+ const a = p.prev,
b = p.next.next;
if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
- triangles.push(a.i / dim);
- triangles.push(p.i / dim);
- triangles.push(b.i / dim);
+ triangles.push(a.i, p.i, b.i);
// remove two nodes involved
removeNode(p);
@@ -233,21 +233,21 @@ function cureLocalIntersections(start, triangles, dim) {
// try splitting polygon into two and triangulate them independently
function splitEarcut(start, triangles, dim, minX, minY, invSize) {
// look for a valid diagonal that divides the polygon into two
- var a = start;
+ let a = start;
do {
- var b = a.next.next;
+ let b = a.next.next;
while (b !== a.prev) {
if (a.i !== b.i && isValidDiagonal(a, b)) {
// split the polygon in two by the diagonal
- var c = splitPolygon(a, b);
+ let c = splitPolygon(a, b);
// filter colinear points around the cuts
a = filterPoints(a, a.next);
c = filterPoints(c, c.next);
// run earcut on each half
- earcutLinked(a, triangles, dim, minX, minY, invSize);
- earcutLinked(c, triangles, dim, minX, minY, invSize);
+ earcutLinked(a, triangles, dim, minX, minY, invSize, 0);
+ earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
return;
}
b = b.next;
@@ -258,64 +258,75 @@ function splitEarcut(start, triangles, dim, minX, minY, invSize) {
// link every hole into the outer loop, producing a single-ring polygon without holes
function eliminateHoles(data, holeIndices, outerNode, dim) {
- var queue = [],
- i, len, start, end, list;
+ const queue = [];
- for (i = 0, len = holeIndices.length; i < len; i++) {
- start = holeIndices[i] * dim;
- end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
- list = linkedList(data, start, end, dim, false);
+ for (let i = 0, len = holeIndices.length; i < len; i++) {
+ const start = holeIndices[i] * dim;
+ const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
+ const list = linkedList(data, start, end, dim, false);
if (list === list.next) list.steiner = true;
queue.push(getLeftmost(list));
}
- queue.sort(compareX);
+ queue.sort(compareXYSlope);
// process holes from left to right
- for (i = 0; i < queue.length; i++) {
- eliminateHole(queue[i], outerNode);
- outerNode = filterPoints(outerNode, outerNode.next);
+ for (let i = 0; i < queue.length; i++) {
+ outerNode = eliminateHole(queue[i], outerNode);
}
return outerNode;
}
-function compareX(a, b) {
- return a.x - b.x;
+function compareXYSlope(a, b) {
+ let result = a.x - b.x;
+ // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
+ // the bridge to the outer shell is always the point that they meet at.
+ if (result === 0) {
+ result = a.y - b.y;
+ if (result === 0) {
+ const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
+ const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
+ result = aSlope - bSlope;
+ }
+ }
+ return result;
}
// find a bridge between vertices that connects hole with an outer ring and and link it
function eliminateHole(hole, outerNode) {
- outerNode = findHoleBridge(hole, outerNode);
- if (outerNode) {
- var b = splitPolygon(outerNode, hole);
-
- // filter collinear points around the cuts
- filterPoints(outerNode, outerNode.next);
- filterPoints(b, b.next);
+ const bridge = findHoleBridge(hole, outerNode);
+ if (!bridge) {
+ return outerNode;
}
+
+ const bridgeReverse = splitPolygon(bridge, hole);
+
+ // filter collinear points around the cuts
+ filterPoints(bridgeReverse, bridgeReverse.next);
+ return filterPoints(bridge, bridge.next);
}
// David Eberly's algorithm for finding a bridge between hole and outer polygon
function findHoleBridge(hole, outerNode) {
- var p = outerNode,
- hx = hole.x,
- hy = hole.y,
- qx = -Infinity,
- m;
+ let p = outerNode;
+ const hx = hole.x;
+ const hy = hole.y;
+ let qx = -Infinity;
+ let m;
// find a segment intersected by a ray from the hole's leftmost point to the left;
// segment's endpoint with lesser x will be potential connection point
+ // unless they intersect at a vertex, then choose the vertex
+ if (equals(hole, p)) return p;
do {
- if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
- var x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
+ if (equals(hole, p.next)) return p.next;
+ else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
+ const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
if (x <= hx && x > qx) {
qx = x;
- if (x === hx) {
- if (hy === p.y) return p;
- if (hy === p.next.y) return p.next;
- }
m = p.x < p.next.x ? p : p.next;
+ if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
}
}
p = p.next;
@@ -323,25 +334,22 @@ function findHoleBridge(hole, outerNode) {
if (!m) return null;
- if (hx === qx) return m; // hole touches outer segment; pick leftmost endpoint
-
// look for points inside the triangle of hole point, segment intersection and endpoint;
// if there are no points found, we have a valid connection;
// otherwise choose the point of the minimum angle with the ray as connection point
- var stop = m,
- mx = m.x,
- my = m.y,
- tanMin = Infinity,
- tan;
+ const stop = m;
+ const mx = m.x;
+ const my = m.y;
+ let tanMin = Infinity;
p = m;
do {
if (hx >= p.x && p.x >= mx && hx !== p.x &&
- pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
+ pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
- tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
+ const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
if (locallyInside(p, hole) &&
(tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
@@ -363,9 +371,9 @@ function sectorContainsSector(m, p) {
// interlink polygon nodes in z-order
function indexCurve(start, minX, minY, invSize) {
- var p = start;
+ let p = start;
do {
- if (p.z === null) p.z = zOrder(p.x, p.y, minX, minY, invSize);
+ if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
p.prevZ = p.prev;
p.nextZ = p.next;
p = p.next;
@@ -380,25 +388,26 @@ function indexCurve(start, minX, minY, invSize) {
// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
function sortLinked(list) {
- var i, p, q, e, tail, numMerges, pSize, qSize,
- inSize = 1;
+ let numMerges;
+ let inSize = 1;
do {
- p = list;
+ let p = list;
+ let e;
list = null;
- tail = null;
+ let tail = null;
numMerges = 0;
while (p) {
numMerges++;
- q = p;
- pSize = 0;
- for (i = 0; i < inSize; i++) {
+ let q = p;
+ let pSize = 0;
+ for (let i = 0; i < inSize; i++) {
pSize++;
q = q.nextZ;
if (!q) break;
}
- qSize = inSize;
+ let qSize = inSize;
while (pSize > 0 || (qSize > 0 && q)) {
@@ -433,8 +442,8 @@ function sortLinked(list) {
// z-order of a point given coords and inverse of the longer side of data bbox
function zOrder(x, y, minX, minY, invSize) {
// coords are transformed into non-negative 15-bit integer range
- x = 32767 * (x - minX) * invSize;
- y = 32767 * (y - minY) * invSize;
+ x = (x - minX) * invSize | 0;
+ y = (y - minY) * invSize | 0;
x = (x | (x << 8)) & 0x00FF00FF;
x = (x | (x << 4)) & 0x0F0F0F0F;
@@ -451,7 +460,7 @@ function zOrder(x, y, minX, minY, invSize) {
// find the leftmost node of a polygon ring
function getLeftmost(start) {
- var p = start,
+ let p = start,
leftmost = start;
do {
if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
@@ -463,15 +472,20 @@ function getLeftmost(start) {
// check if a point lies within a convex triangle
function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
- return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
- (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
- (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
+ return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
+ (ax - px) * (by - py) >= (bx - px) * (ay - py) &&
+ (bx - px) * (cy - py) >= (cx - px) * (by - py);
+}
+
+// check if a point lies within a convex triangle but false if its equal to the first point of the triangle
+function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
+ return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
}
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
function isValidDiagonal(a, b) {
return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones't intersect other edges
- (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
+ (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
(area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
}
@@ -488,10 +502,10 @@ function equals(p1, p2) {
// check if two segments intersect
function intersects(p1, q1, p2, q2) {
- var o1 = sign(area(p1, q1, p2));
- var o2 = sign(area(p1, q1, q2));
- var o3 = sign(area(p2, q2, p1));
- var o4 = sign(area(p2, q2, q1));
+ const o1 = sign(area(p1, q1, p2));
+ const o2 = sign(area(p1, q1, q2));
+ const o3 = sign(area(p2, q2, p1));
+ const o4 = sign(area(p2, q2, q1));
if (o1 !== o2 && o3 !== o4) return true; // general case
@@ -514,10 +528,10 @@ function sign(num) {
// check if a polygon diagonal intersects any polygon segments
function intersectsPolygon(a, b) {
- var p = a;
+ let p = a;
do {
if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
- intersects(p, p.next, a, b)) return true;
+ intersects(p, p.next, a, b)) return true;
p = p.next;
} while (p !== a);
@@ -533,13 +547,13 @@ function locallyInside(a, b) {
// check if the middle point of a polygon diagonal is inside the polygon
function middleInside(a, b) {
- var p = a,
- inside = false,
- px = (a.x + b.x) / 2,
- py = (a.y + b.y) / 2;
+ let p = a;
+ let inside = false;
+ const px = (a.x + b.x) / 2;
+ const py = (a.y + b.y) / 2;
do {
if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
- (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
+ (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
inside = !inside;
p = p.next;
} while (p !== a);
@@ -550,8 +564,8 @@ function middleInside(a, b) {
// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
// if one belongs to the outer ring and another to a hole, it merges it into a single ring
function splitPolygon(a, b) {
- var a2 = new Node(a.i, a.x, a.y),
- b2 = new Node(b.i, b.x, b.y),
+ const a2 = createNode(a.i, a.x, a.y),
+ b2 = createNode(b.i, b.x, b.y),
an = a.next,
bp = b.prev;
@@ -572,7 +586,7 @@ function splitPolygon(a, b) {
// create a node and optionally link it with previous one (in a circular doubly linked list)
function insertNode(i, x, y, last) {
- var p = new Node(i, x, y);
+ const p = createNode(i, x, y);
if (!last) {
p.prev = p;
@@ -595,49 +609,39 @@ function removeNode(p) {
if (p.nextZ) p.nextZ.prevZ = p.prevZ;
}
-function Node(i, x, y) {
- // vertex index in coordinates array
- this.i = i;
-
- // vertex coordinates
- this.x = x;
- this.y = y;
-
- // previous and next vertex nodes in a polygon ring
- this.prev = null;
- this.next = null;
-
- // z-order curve value
- this.z = null;
-
- // previous and next nodes in z-order
- this.prevZ = null;
- this.nextZ = null;
-
- // indicates whether this is a steiner point
- this.steiner = false;
+function createNode(i, x, y) {
+ return {
+ i, // vertex index in coordinates array
+ x, y, // vertex coordinates
+ prev: null, // previous and next vertex nodes in a polygon ring
+ next: null,
+ z: 0, // z-order curve value
+ prevZ: null, // previous and next nodes in z-order
+ nextZ: null,
+ steiner: false // indicates whether this is a steiner point
+ };
}
// return a percentage difference between the polygon area and its triangulation area;
// used to verify correctness of triangulation
-earcut.deviation = function (data, holeIndices, dim, triangles) {
- var hasHoles = holeIndices && holeIndices.length;
- var outerLen = hasHoles ? holeIndices[0] * dim : data.length;
+export function deviation(data, holeIndices, dim, triangles) {
+ const hasHoles = holeIndices && holeIndices.length;
+ const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
- var polygonArea = Math.abs(signedArea(data, 0, outerLen, dim));
+ let polygonArea = Math.abs(signedArea(data, 0, outerLen, dim));
if (hasHoles) {
- for (var i = 0, len = holeIndices.length; i < len; i++) {
- var start = holeIndices[i] * dim;
- var end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
+ for (let i = 0, len = holeIndices.length; i < len; i++) {
+ const start = holeIndices[i] * dim;
+ const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
polygonArea -= Math.abs(signedArea(data, start, end, dim));
}
}
- var trianglesArea = 0;
- for (i = 0; i < triangles.length; i += 3) {
- var a = triangles[i] * dim;
- var b = triangles[i + 1] * dim;
- var c = triangles[i + 2] * dim;
+ let trianglesArea = 0;
+ for (let i = 0; i < triangles.length; i += 3) {
+ const a = triangles[i] * dim;
+ const b = triangles[i + 1] * dim;
+ const c = triangles[i + 2] * dim;
trianglesArea += Math.abs(
(data[a] - data[c]) * (data[b + 1] - data[a + 1]) -
(data[a] - data[b]) * (data[c + 1] - data[a + 1]));
@@ -645,11 +649,11 @@ earcut.deviation = function (data, holeIndices, dim, triangles) {
return polygonArea === 0 && trianglesArea === 0 ? 0 :
Math.abs((trianglesArea - polygonArea) / polygonArea);
-};
+}
function signedArea(data, start, end, dim) {
- var sum = 0;
- for (var i = start, j = end - dim; i < end; i += dim) {
+ let sum = 0;
+ for (let i = start, j = end - dim; i < end; i += dim) {
sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
j = i;
}
@@ -657,21 +661,22 @@ function signedArea(data, start, end, dim) {
}
// turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
-earcut.flatten = function (data) {
- var dim = data[0][0].length,
- result = {vertices: [], holes: [], dimensions: dim},
- holeIndex = 0;
-
- for (var i = 0; i < data.length; i++) {
- for (var j = 0; j < data[i].length; j++) {
- for (var d = 0; d < dim; d++) result.vertices.push(data[i][j][d]);
+export function flatten(data) {
+ const vertices = [];
+ const holes = [];
+ const dimensions = data[0][0].length;
+ let holeIndex = 0;
+ let prevLen = 0;
+
+ for (const ring of data) {
+ for (const p of ring) {
+ for (let d = 0; d < dimensions; d++) vertices.push(p[d]);
}
- if (i > 0) {
- holeIndex += data[i - 1].length;
- result.holes.push(holeIndex);
+ if (prevLen) {
+ holeIndex += prevLen;
+ holes.push(holeIndex);
}
+ prevLen = ring.length;
}
- return result;
-};
-
-export {earcut};
\ No newline at end of file
+ return {vertices, holes, dimensions};
+}
diff --git a/src/viewer/scene/model/SceneModelEntity.js b/src/viewer/scene/model/SceneModelEntity.js
index 1057adff4e..5497b0284c 100644
--- a/src/viewer/scene/model/SceneModelEntity.js
+++ b/src/viewer/scene/model/SceneModelEntity.js
@@ -1,5 +1,6 @@
import {ENTITY_FLAGS} from './ENTITY_FLAGS.js';
import {math} from "../math/math.js";
+import { Material } from '../materials/Material.js';
const tempFloatRGB = new Float32Array([0, 0, 0]);
const tempIntRGB = new Uint16Array([0, 0, 0]);
@@ -48,6 +49,7 @@ export class SceneModelEntity {
this.meshes = meshes;
this._numPrimitives = 0;
+ this._capMaterial = null;
for (let i = 0, len = this.meshes.length; i < len; i++) { // TODO: tidier way? Refactor?
const mesh = this.meshes[i];
@@ -649,6 +651,29 @@ export class SceneModelEntity {
return this.model.saoEnabled;
}
+ /**
+ * Sets the SceneModelEntity's capMaterial that will be used on the caps generated when this entity is sliced
+ *
+ * Default value is ````null````.
+ *
+ * @type {Material}
+ */
+ set capMaterial(value) {
+ this._capMaterial = value instanceof Material ? value : null;
+ this.scene._capMaterialUpdated(this.id, this.model.id);
+ }
+
+ /**
+ * Gets the SceneModelEntity's capMaterial.
+ *
+ * Default value is ````null````.
+ *
+ * @type {Material}
+ */
+ get capMaterial() {
+ return this._capMaterial;
+ }
+
getEachVertex(callback) {
for (let i = 0, len = this.meshes.length; i < len; i++) {
this.meshes[i].getEachVertex(callback)
diff --git a/src/viewer/scene/scene/Scene.js b/src/viewer/scene/scene/Scene.js
index d2745e71c7..4cc22fe701 100644
--- a/src/viewer/scene/scene/Scene.js
+++ b/src/viewer/scene/scene/Scene.js
@@ -19,6 +19,7 @@ import {SAO} from "../postfx/SAO.js";
import {CrossSections} from "../postfx/CrossSections.js";
import {PointsMaterial} from "../materials/PointsMaterial.js";
import {LinesMaterial} from "../materials/LinesMaterial.js";
+import {SectionCaps} from '../sectionCaps/SectionCaps.js';
// Enables runtime check for redundant calls to object state update methods, eg. Scene#_objectVisibilityUpdated
const ASSERT_OBJECT_STATE_UPDATE = false;
@@ -887,6 +888,8 @@ class Scene extends Component {
dontClear: true // Never destroy this component with Scene#clear();
});
+ this._sectionCaps = new SectionCaps(this);
+
// Default lights
new AmbientLight(this, {
@@ -988,6 +991,10 @@ class Scene extends Component {
// Scene. Violates Hollywood Principle, where we could just filter on type in _addComponent,
// but this is faster than checking the type of each component in such a filter.
+ _capMaterialUpdated(entityId, modelId) {
+ this._sectionCaps._onCapMaterialUpdated(entityId, modelId);
+ }
+
_sectionPlaneCreated(sectionPlane) {
this.sectionPlanes[sectionPlane.id] = sectionPlane;
this.scene._sectionPlanesState.addSectionPlane(sectionPlane._state);
@@ -2796,6 +2803,9 @@ class Scene extends Component {
}
}
+ //destroy section caps separately because it's not a component
+ this._sectionCaps.destroy();
+
this.canvas.gl = null;
// Memory leak prevention
@@ -2818,6 +2828,7 @@ class Scene extends Component {
this._highlightedObjectIds = null;
this._selectedObjectIds = null;
this._colorizedObjectIds = null;
+ this._sectionCaps = null;
this.types = null;
this.components = null;
this.canvas = null;
diff --git a/src/viewer/scene/sectionCaps/SectionCaps.js b/src/viewer/scene/sectionCaps/SectionCaps.js
new file mode 100644
index 0000000000..7707460196
--- /dev/null
+++ b/src/viewer/scene/sectionCaps/SectionCaps.js
@@ -0,0 +1,810 @@
+import { math } from "../math/math.js";
+import { Mesh } from "../mesh/Mesh.js";
+import { ReadableGeometry } from "../geometry/ReadableGeometry.js";
+import { buildLineGeometry } from "../geometry/index.js";
+import { PhongMaterial } from "../materials/PhongMaterial.js";
+import earcut from '../libs/earcut.js';
+
+const epsilon = 1e-6;
+const worldUp = [0, 1, 0];
+const worldRight = [1, 0, 0];
+const tempVec3a = math.vec3();
+const tempVec3b = math.vec3();
+const tempVec3c = math.vec3();
+const tempVec3d = math.vec3();
+
+function pointsEqual(p1, p2) {
+ return (
+ Math.abs(p1[0] - p2[0]) < epsilon &&
+ Math.abs(p1[1] - p2[1]) < epsilon &&
+ Math.abs(p1[2] - p2[2]) < epsilon
+ );
+}
+
+class SectionCaps {
+ /**
+ * @constructor
+ */
+ constructor(scene) {
+ this.scene = scene;
+ if (!this.scene.readableGeometryEnabled) {
+ console.log('SectionCapsPlugin only works when readable geometry is enable on the viewer.');
+ return;
+ }
+ this._sectionPlanes = [];
+ this._sceneModel = [];
+ this._verticesMap = {};
+ this._indicesMap = {};
+ this._dirtyMap = {};
+ this._prevIntersectionModelsMap = {};
+ this._sectionPlaneTimeout = null;
+ this._updateTimeout = null;
+
+ this._onSectionPlaneCreated = this.scene.on('sectionPlaneCreated', (sectionPlane) => {
+
+ const onSectionPlaneUpdated = () => {
+ this._setAllDirty(true);
+ this._update();
+ }
+ this._sectionPlanes.push(sectionPlane);
+ sectionPlane.on('pos', onSectionPlaneUpdated);
+ sectionPlane.on('dir', onSectionPlaneUpdated);
+ sectionPlane.once('destroyed', ((sectionPlane) => {
+ const sectionPlaneId = sectionPlane.id;
+ if (sectionPlaneId) {
+ this._sectionPlanes = this._sectionPlanes.filter((sectionPlane) => sectionPlane.id !== sectionPlaneId);
+ this._update();
+ }
+ }).bind(this));
+ })
+
+ this._onTick = this.scene.on("tick", () => {
+ if(Object.keys(this._verticesMap).length === Object.keys(this.scene.models).length) return;
+ for(const key in this._verticesMap) {
+ if(!this.scene.models[key]){
+ delete this._verticesMap[key];
+ delete this._indicesMap[key];
+ }
+ }
+ this._update();
+ })
+
+ }
+
+ _onCapMaterialUpdated(entityId, modelId) {
+ if(!this.scene.readableGeometryEnabled) return;
+ if(!this._verticesMap[modelId]){
+ this._verticesMap[modelId] = new Map();
+ this._indicesMap[modelId] = new Map();
+ this._dirtyMap[modelId] = new Map();
+ }
+ if(!this._verticesMap[modelId].has(entityId)) {
+ const {vertices, indices} = this._getEntityVI(this.scene.models[modelId].objects[entityId]);
+ this._verticesMap[modelId].set(entityId, vertices);
+ this._indicesMap[modelId].set(entityId, indices);
+ }
+ this._dirtyMap[modelId].set(entityId, true);
+ this._update();
+ }
+
+ _update() {
+ clearTimeout(this._updateTimeout);
+ this._deletePreviousModels();
+ this._updateTimeout = setTimeout(() => {
+ clearTimeout(this._updateTimeout);
+ const sceneModels = Object.keys(this.scene.models).map((key) => this.scene.models[key]);
+ this._addHatches(sceneModels, this._sectionPlanes);
+ this._setAllDirty(false);
+ }, 100);
+ }
+
+ _setAllDirty(value) {
+ for(const key in this._dirty) {
+ this._dirtyMap[key].forEach((_, key2) => this._dirtyMap[key].set(key2, value));
+ }
+ }
+
+ _addHatches(sceneModels, planes) {
+
+ if (planes.length <= 0) return;
+
+ planes.forEach((plane) => {
+ sceneModels.forEach((sceneModel) => {
+ this._generateHatchesForModel(sceneModel, plane);
+ })
+ })
+
+ }
+
+ _generateHatchesForModel(sceneModel, plane) {
+ //we create a plane equation that will be used to slice through each triangle
+ const planeEquation = this._createPlaneEquation(plane.pos, plane.dir);
+
+ if(!this._doesPlaneIntersectBoundingBox(sceneModel.aabb, planeEquation)) return;
+
+ //we get the vertices and indices of each object in the scene model
+ //the vertices and indices are structured in the same way as objects in the scene model
+ if(this._verticesMap[sceneModel.id].size <=0 ) return;
+
+ //we calculate the segments by intersecting plane each triangle
+ const unsortedSegmentsMap = this._getIntersectionSegments(sceneModel, this._verticesMap[sceneModel.id], this._indicesMap[sceneModel.id], this._dirtyMap[sceneModel.id], planeEquation);
+ const sortedSegmentsMap = this._sortAndCombineSegments(unsortedSegmentsMap);
+ const projectedSegmentsMap = this._projectSegments(sortedSegmentsMap, plane.dir);
+ const capsMap = this._createCaps(projectedSegmentsMap, plane);
+ const geometriesMap = this._convertCapsToGeometry(capsMap);
+
+ if(!this._prevIntersectionModelsMap[sceneModel.id])
+ this._prevIntersectionModelsMap[sceneModel.id] = new Map();
+
+ // Cache plane direction values
+ const offsetX = plane.dir[0] * 0.001;
+ const offsetY = plane.dir[1] * 0.001;
+ const offsetZ = plane.dir[2] * 0.001;
+
+ geometriesMap.forEach((value, key) => {
+ const meshArray = new Array(value.size); // Pre-allocate array with known size
+ let meshIndex = 0;
+
+ value.forEach((geometry, index) => {
+ const vertices = geometry.positions;
+ const indices = geometry.indices;
+ const verticesLength = vertices.length;
+
+ for (let i = 0; i < verticesLength; i += 3) {
+ vertices[i] += offsetX;
+ vertices[i + 1] += offsetY;
+ vertices[i + 2] += offsetZ;
+ }
+
+ // Build normals and UVs in parallel if possible
+ const meshNormals = math.buildNormals(vertices, indices);
+ const uvs = this._createUVs(vertices, plane);
+
+ // Create mesh with transformed vertices
+ meshArray[meshIndex++] = new Mesh(this.scene, {
+ id: `${plane.id}-${key}-${index}`,
+ geometry: new ReadableGeometry(this.scene, {
+ primitive: 'triangles',
+ positions: vertices, // Only copy what we need
+ indices,
+ normals: meshNormals,
+ uv: uvs
+ }),
+ position: [0, 0, 0],
+ rotation: [0, 0, 0],
+ material: sceneModel.objects[key].capMaterial
+ });
+ })
+ if(this._prevIntersectionModelsMap[sceneModel.id].has(key)) {
+ this._prevIntersectionModelsMap[sceneModel.id].get(key).push(...meshArray)
+ }
+ else
+ this._prevIntersectionModelsMap[sceneModel.id].set(key, meshArray);
+ })
+ }
+
+ _createPlaneEquation(position, normal) {
+ const A = normal[0];
+ const B = normal[1];
+ const C = normal[2];
+ const D = -(A * position[0] + B * position[1] + C * position[2]);
+
+ return { A, B, C, D }; // Return the plane equation
+ }
+
+ _getVI(sceneModel) {
+ const objects = {};
+ for (const key in sceneModel.objects) {
+
+ const object = sceneModel.objects[key];
+ const isSolid = object.meshes[0].layer.solid !== false;
+ if (isSolid && object.capMaterial) {
+ objects[key] = sceneModel.objects[key];
+ }
+ }
+
+ let cloneModel = {
+ ...sceneModel,
+ objects: objects
+ }
+
+ return this._getVerticesAndIndices(cloneModel);
+ }
+
+ _getEntityVI(entity) {
+ const vertices = [];
+ const indices = [];
+ const isSolid = entity.meshes[0].layer.solid !== false;
+ if(isSolid && entity.capMaterial) {
+
+ if (entity.meshes.length > 1) {
+ let index = 0;
+ entity.meshes.forEach((mesh) => {
+ if (mesh.layer.solid) {
+ vertices.push([]);
+ indices.push([]);
+ mesh.getEachVertex((v) => {
+ vertices[index].push(v[0], v[1], v[2]);
+ })
+ mesh.getEachIndex((_indices) => {
+ indices[index].push(_indices);
+ })
+ index++;
+ }
+
+ })
+ }
+ else {
+ entity.getEachVertex((_vertices) => {
+ vertices.push(_vertices[0], _vertices[1], _vertices[2]);
+ })
+ entity.getEachIndex((_indices) => {
+ indices.push(_indices);
+ })
+ }
+ }
+ return { vertices, indices };
+ }
+
+ _getIntersectionSegments(sceneModel, vertices, indices, dirty, planeEquation) {
+ const unsortedSegments = new Map();
+ const objects = sceneModel.objects;
+ // Preallocate arrays for triangle vertices to avoid repeated allocation
+ const triangle = [
+ new Float32Array(3),
+ new Float32Array(3),
+ new Float32Array(3)
+ ];
+
+ vertices.forEach((value, key) => {
+ if (!dirty.get(key)) {
+ return;
+ }
+
+ if(!this._doesPlaneIntersectBoundingBox(objects[key].aabb, planeEquation)) return;
+
+ const segment = this._processModel(value, indices.get(key), planeEquation, triangle);
+ if (segment.length > 0) {
+ unsortedSegments.set(key, segment);
+ }
+ })
+
+ return unsortedSegments;
+ }
+
+ _doesPlaneIntersectBoundingBox(bb, planeEquation) {
+ const min = [bb[0], bb[1], bb[2]];
+ const max = [bb[3], bb[4], bb[5]];
+
+ const corners = [
+ [min[0], min[1], min[2]], // 000
+ [max[0], min[1], min[2]], // 100
+ [min[0], max[1], min[2]], // 010
+ [max[0], max[1], min[2]], // 110
+ [min[0], min[1], max[2]], // 001
+ [max[0], min[1], max[2]], // 101
+ [min[0], max[1], max[2]], // 011
+ [max[0], max[1], max[2]] // 111
+ ]
+
+ // Calculate distance from each corner to the plane
+ let hasPositive = false;
+ let hasNegative = false;
+
+ for (const corner of corners) {
+ const distance = planeEquation.A * corner[0] +
+ planeEquation.B * corner[1] +
+ planeEquation.C * corner[2] +
+ planeEquation.D;
+
+ if (distance > 0) hasPositive = true;
+ if (distance < 0) hasNegative = true;
+
+ // If we found points on both sides, the plane intersects the box
+ if (hasPositive && hasNegative) return true;
+ }
+
+ // If all points are on the same side, no intersection
+ return false;
+ }
+
+ _processModel(vertices, indices, planeEquation, triangleBuffer) {
+ const capSegments = [];
+ const vertCount = indices.length;
+
+ // Preallocate intersection result array
+ const intersection = new Float32Array(3);
+
+ for (let i = 0; i < vertCount; i += 3) {
+ // Reuse triangle buffer instead of creating new arrays
+ this._fillTriangleBuffer(vertices, indices, i, triangleBuffer);
+
+ // Early null check
+ if (!triangleBuffer[0][0] && !triangleBuffer[0][1] && !triangleBuffer[0][2]) continue;
+
+ const segment = this._sliceTriangle(planeEquation, triangleBuffer, intersection);
+ if (segment) {
+ capSegments.push(segment);
+ }
+ }
+
+ return capSegments;
+ }
+
+ _fillTriangleBuffer(vertices, indices, startIndex, triangleBuffer) {
+ for (let i = 0; i < 3; i++) {
+ const idx = indices[startIndex + i] * 3;
+ triangleBuffer[i][0] = vertices[idx];
+ triangleBuffer[i][1] = vertices[idx + 1];
+ triangleBuffer[i][2] = vertices[idx + 2];
+ }
+ }
+
+ _sliceTriangle(plane, triangle, intersectionBuffer) {
+ const intersections = [];
+ // const t0 = performance.now();
+ for (let i = 0; i < 3; i++) {
+ const p1 = triangle[i];
+ const p2 = triangle[(i + 1) % 3];
+
+ // Inline the distance calculations to avoid function calls
+ const d1 = plane.A * p1[0] + plane.B * p1[1] + plane.C * p1[2] + plane.D;
+ const d2 = plane.A * p2[0] + plane.B * p2[1] + plane.C * p2[2] + plane.D;
+
+ if (d1 * d2 > 0) continue;
+
+ const t = -d1 / (d2 - d1);
+ // Reuse intersection buffer
+ intersectionBuffer[0] = p1[0] + t * (p2[0] - p1[0]);
+ intersectionBuffer[1] = p1[1] + t * (p2[1] - p1[1]);
+ intersectionBuffer[2] = p1[2] + t * (p2[2] - p1[2]);
+
+ // Clone the buffer for storage
+ intersections.push(new Float32Array(intersectionBuffer));
+ }
+ // const t1 = performance.now();
+ // console.log('time taken in calculation: ', t1 - t0);
+
+ return intersections.length === 2 ? intersections : null;
+ }
+
+ //not used but kept for debugging
+ _buildLines(sortedSegments) {
+ for (const key in sortedSegments) {
+ for (let i = 0; i < sortedSegments[key].length; i++) {
+ const segments = sortedSegments[key][i];
+ if (segments.length <= 0) continue;
+ segments.forEach((segment, index) => {
+ new Mesh(this.scene, {
+ clippable: false,
+ geometry: new ReadableGeometry(this.scene, buildLineGeometry({
+ startPoint: segment[0],
+ endPoint: segment[1],
+ })),
+ material: new PhongMaterial(this.scene, {
+ emissive: [1, 0, 0]
+ })
+ });
+ })
+ }
+
+ }
+ }
+
+ _sortAndCombineSegments(segments) {
+
+ const orderedSegments = new Map;
+ segments.forEach((value, key) => {
+ orderedSegments.set(key, [
+ [
+ value[0] //this is also an array of two vectors
+ ]
+ ]);
+ value.splice(0, 1);
+ let index = 0;
+ while (value.length > 0) {
+ const lastPoint = orderedSegments.get(key)[index][orderedSegments.get(key)[index].length - 1][1];
+ let found = false;
+
+ for (let i = 0; i < value.length; i++) {
+ const [start, end] = value[i];
+ if (pointsEqual(lastPoint, start)) {
+ orderedSegments.get(key)[index].push(value[i]);
+ value.splice(i, 1);
+ found = true;
+ break;
+ } else if (pointsEqual(lastPoint, end)) {
+ orderedSegments.get(key)[index].push([end, start]);
+ value.splice(i, 1);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ if (pointsEqual(lastPoint, orderedSegments.get(key)[index][0][0])) {
+ if (value.length > 1) {
+ orderedSegments.get(key).push([
+ segments.get(key)[0]
+ ]);
+ value.splice(0, 1);
+ index++;
+ continue;
+ }
+
+ }
+ }
+
+ if (!found) {
+ // console.error(`Could not find a matching segment. Loop may not be closed. Key: ${key}`);
+ break;
+ }
+ }
+ })
+ return orderedSegments;
+ }
+
+ _projectSegments(segments, normal) {
+ const projectedSegments = new Map();
+
+ segments.forEach((value, key) => {
+ const arr = [];
+ for (let i = 0; i < value.length; i++) {
+ arr.push([]);
+ value[i].forEach((segment) => {
+ arr[i].push([
+ this._projectTo2D(segment[0], normal),
+ this._projectTo2D(segment[1], normal)
+ ])
+ })
+ }
+ projectedSegments.set(key, arr);
+ })
+ return projectedSegments;
+ }
+
+ _projectTo2D(point, normal) {
+ let u;
+ if (Math.abs(normal[0]) > Math.abs(normal[1]))
+ u = [-normal[2], 0, normal[0]];
+ else
+ u = [0, normal[2], -normal[1]];
+
+ u = math.normalizeVec3(u);
+ const normalTemp = math.vec3(normal);
+ const cross = math.cross3Vec3(normalTemp, u)
+ const v = math.normalizeVec3(cross);
+ const x = math.dotVec3(point, u);
+ const y = math.dotVec3(point, v);
+
+ return [x, y]
+
+ }
+
+ _createCaps(projectedSegments, plane) {
+ const caps = new Map();
+ let arr;
+ projectedSegments.forEach((value, key) => {
+ arr = [];
+ const loops = value;
+
+ // Group related loops (outer boundaries with their holes)
+ const groupedLoops = this._groupRelatedLoops(loops);
+
+ // Process each group separately
+ groupedLoops.forEach(group => {
+ // Convert the segments into a flat array of vertices and find holes
+ const { vertices, holes } = this._prepareDataForEarcut(group);
+
+ // Triangulate using earcut
+ const triangles = earcut(vertices, holes);
+
+ // // Convert triangulated 2D points back to 3D
+ const cap3D = this._convertTrianglesTo3D(triangles, vertices, plane);
+ arr.push(cap3D);
+ });
+ caps.set(key, arr);
+ })
+
+ return caps;
+ }
+
+ _groupRelatedLoops(loops) {
+ const groups = [];
+ const used = new Set();
+
+ for (let i = 0; i < loops.length; i++) {
+ if (used.has(i)) continue;
+
+ const group = [loops[i]];
+ used.add(i);
+
+ // Check remaining loops
+ for (let j = i + 1; j < loops.length; j++) {
+ if (used.has(j)) continue;
+
+ if (this._isLoopInside(loops[i], loops[j]) ||
+ this._isLoopInside(loops[j], loops[i])) {
+ group.push(loops[j]);
+ used.add(j);
+ }
+ }
+
+ groups.push(group);
+ }
+
+ return groups;
+ }
+
+ _isLoopInside(loop1, loop2) {
+ // Simple point-in-polygon test using the first point of loop1
+ const point = loop1[0][0]; // First point of first segment
+ return this._isPointInPolygon(point, loop2);
+ }
+
+ _isPointInPolygon(point, polygon) {
+ let inside = false;
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
+ const xi = polygon[i][0][0], yi = polygon[i][0][1];
+ const xj = polygon[j][0][0], yj = polygon[j][0][1];
+
+ const intersect = ((yi > point[1]) !== (yj > point[1]))
+ && (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi);
+
+ if (intersect) inside = !inside;
+ }
+
+ return inside;
+ }
+
+
+ _prepareDataForEarcut(loops) {
+ const vertices = [];
+ const holes = [];
+ let currentIndex = 0;
+
+ // First, determine which loop has the largest area - this will be our outer boundary
+ const areas = loops.map(loop => {
+ let area = 0;
+ for (let i = 0; i < loop.length; i++) {
+ const j = (i + 1) % loop.length;
+ area += loop[i][0][0] * loop[j][0][1];
+ area -= loop[j][0][0] * loop[i][0][1];
+ }
+ return Math.abs(area) / 2;
+ });
+
+ // Find index of the loop with maximum area
+ const outerLoopIndex = areas.indexOf(Math.max(...areas));
+
+ // Add the outer boundary first
+ loops[outerLoopIndex].forEach(segment => {
+ vertices.push(segment[0][0], segment[0][1]);
+ currentIndex += 2;
+ });
+
+ // Then add all other loops as holes
+ for (let i = 0; i < loops.length; i++) {
+ if (i !== outerLoopIndex) {
+ // Store the starting vertex index for this hole
+ holes.push(currentIndex / 2);
+
+ loops[i].forEach(segment => {
+ vertices.push(segment[0][0], segment[0][1]);
+ currentIndex += 2;
+ });
+ }
+ }
+
+ return { vertices, holes };
+ }
+
+ _convertTrianglesTo3D(triangles, vertices, plane) {
+ const triangles3D = [];
+
+ // Process each triangle
+ for (let i = 0; i < triangles.length; i += 3) {
+ const triangle = [];
+
+ // Convert each vertex
+ for (let j = 0; j < 3; j++) {
+ const idx = triangles[i + j] * 2;
+ const point2D = [vertices[idx], vertices[idx + 1]];
+ const point3D = this._convertTo3D(point2D, plane);
+ triangle.push(point3D);
+ }
+
+ triangles3D.push(triangle);
+ }
+
+ return triangles3D;
+ }
+
+ _convertTo3D(point2D, plane) {
+ // Reconstruct the same basis vectors used in _projectTo2D
+ let u, normal = plane.dir, planePosition = plane.pos;
+ if (Math.abs(normal[0]) > Math.abs(normal[1])) {
+ u = [-normal[2], 0, normal[0]];
+ } else {
+ u = [0, normal[2], -normal[1]];
+ }
+
+ u = math.normalizeVec3(u);
+ const normalTemp = math.vec3(normal);
+ const cross = math.cross3Vec3(normalTemp, u);
+ const v = math.normalizeVec3(cross);
+
+ // Reconstruct 3D point using the basis vectors
+ const x = point2D[0];
+ const y = point2D[1];
+ const result = [
+ u[0] * x + v[0] * y,
+ u[1] * x + v[1] * y,
+ u[2] * x + v[2] * y
+ ];
+
+ // Project the point onto the cutting plane
+ // const d = -(normal[0] * result[0] + normal[1] * result[1] + normal[2] * result[2]);
+ // const t = -d / (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]);
+
+ const t = math.dotVec3(normal, [
+ planePosition[0] - result[0],
+ planePosition[1] - result[1],
+ planePosition[2] - result[2]
+ ]);
+
+ return [
+ result[0] + normal[0] * t,
+ result[1] + normal[1] * t,
+ result[2] + normal[2] * t
+ ];
+ }
+
+ _convertCapsToGeometry(caps) {
+ const geometryData = new Map();
+ let arr;
+
+ caps.forEach((value, key) => {
+ arr = [];
+ value.forEach(capTriangles => {
+ // Create a vertex map to reuse vertices
+ const vertexMap = new Map();
+ const vertices = [];
+ const indices = [];
+ let currentIndex = 0;
+
+ capTriangles.forEach(triangle => {
+ const triangleIndices = [];
+
+ // Process each vertex of the triangle
+ triangle.forEach(vertex => {
+ // Create a key for the vertex to check for duplicates
+ const vertexKey = `${vertex[0].toFixed(6)},${vertex[1].toFixed(6)},${vertex[2].toFixed(6)}`;
+
+ if (vertexMap.has(vertexKey)) {
+ // Reuse existing vertex
+ triangleIndices.push(vertexMap.get(vertexKey));
+ } else {
+ // Add new vertex
+ vertices.push(vertex[0], vertex[1], vertex[2]);
+ vertexMap.set(vertexKey, currentIndex);
+ triangleIndices.push(currentIndex);
+ currentIndex++;
+ }
+ });
+
+ // Add triangle indices
+ indices.push(...triangleIndices);
+ });
+
+ arr.push({
+ positions: vertices,
+ indices: indices
+ });
+ });
+
+ geometryData.set(key, arr);
+ })
+
+ return geometryData;
+ }
+
+ _deletePreviousModels() {
+
+ for(const sceneModelId in this._prevIntersectionModelsMap) {
+ const objects = this._prevIntersectionModelsMap[sceneModelId];
+ objects.forEach((value, objectId) => {
+ if(this._dirtyMap[sceneModelId].get(objectId)) {
+ value.forEach((mesh) => {
+ mesh.destroy();
+ })
+ this._prevIntersectionModelsMap[sceneModelId].delete(objectId);
+ }
+ })
+ if(this._prevIntersectionModelsMap[sceneModelId].size <= 0)
+ delete this._prevIntersectionModelsMap[sceneModelId];
+
+ }
+
+ }
+
+ _getVerticesAndIndices(sceneModel) {
+ const vertices = {};
+ const indices = {};
+ const objects = sceneModel.objects;
+ for (const key in objects) {
+ const value = objects[key];
+ vertices[key] = [];
+ indices[key] = [];
+
+ if (value.meshes.length > 1) {
+ let index = 0;
+ value.meshes.forEach((mesh) => {
+ if (mesh.layer.solid) {
+ vertices[key].push([]);
+ indices[key].push([]);
+ mesh.getEachVertex((v) => {
+ vertices[key][index].push(v[0], v[1], v[2]);
+ })
+ mesh.getEachIndex((_indices) => {
+ indices[key][index].push(_indices);
+ })
+ index++;
+ }
+
+ })
+ }
+ else {
+ value.getEachVertex((_vertices) => {
+ vertices[key].push(_vertices[0], _vertices[1], _vertices[2]);
+ })
+ value.getEachIndex((_indices) => {
+ indices[key].push(_indices);
+ })
+ }
+ }
+ return { vertices, indices };
+ }
+
+ _createUVs(vertices, plane) {
+ const O = plane.pos;
+ const D = tempVec3a;
+ D.set(plane.dir);
+ math.normalizeVec3(D);
+ const P = tempVec3b;
+
+ const uvs = [ ];
+ for (let i = 0; i < vertices.length; i += 3) {
+ P[0] = vertices[i];
+ P[1] = vertices[i + 1];
+ P[2] = vertices[i + 2];
+
+ // Project P onto the plane
+ const OP = math.subVec3(P, O, tempVec3c);
+ const dist = math.dotVec3(OP, D);
+ math.subVec3(P, math.mulVec3Scalar(D, dist, tempVec3c), P);
+
+ const right = ((Math.abs(math.dotVec3(D, worldUp)) < 0.999)
+ ? math.cross3Vec3(D, worldUp, tempVec3c)
+ : worldRight);
+ const v = math.cross3Vec3(D, right, tempVec3c);
+ math.normalizeVec3(v, v);
+
+ const OP_proj = math.subVec3(P, O, P);
+ uvs.push(
+ math.dotVec3(OP_proj, math.normalizeVec3(math.cross3Vec3(v, D, tempVec3d))),
+ math.dotVec3(OP_proj, v));
+ }
+ return uvs;
+ }
+
+ destroy() {
+ this._deletePreviousModels();
+ this.scene.off(this._onModelLoaded);
+ this.scene.off(this._onModelUnloaded);
+ this.scene.off(this._onSectionPlaneCreated);
+ this.scene.off(this._onTick);
+ }
+}
+
+export { SectionCaps };
\ No newline at end of file
diff --git a/src/viewer/scene/sectionCaps/index.js b/src/viewer/scene/sectionCaps/index.js
new file mode 100644
index 0000000000..df30b200ff
--- /dev/null
+++ b/src/viewer/scene/sectionCaps/index.js
@@ -0,0 +1 @@
+export * from "./SectionCaps.js";
\ No newline at end of file