From d324265afae2c32adc10601c6da0132ee40b6129 Mon Sep 17 00:00:00 2001 From: ijlal99 Date: Tue, 14 Jan 2025 18:23:30 +0500 Subject: [PATCH] Implement section caps through sceneModelEntity:capMaterial --- examples/slicing/SectionCaps.html | 196 +++++ ...ectionPlanesPlugin_Duplex_SectionCaps.html | 159 ++++ .../CityJSONLoaderPlugin.js | 2 +- .../lib => viewer/scene/libs}/earcut.js | 383 +++++---- src/viewer/scene/model/SceneModelEntity.js | 25 + src/viewer/scene/scene/Scene.js | 11 + src/viewer/scene/sectionCaps/SectionCaps.js | 810 ++++++++++++++++++ src/viewer/scene/sectionCaps/index.js | 1 + 8 files changed, 1397 insertions(+), 190 deletions(-) create mode 100644 examples/slicing/SectionCaps.html create mode 100644 examples/slicing/SectionPlanesPlugin_Duplex_SectionCaps.html rename src/{plugins/lib => viewer/scene/libs}/earcut.js (61%) create mode 100644 src/viewer/scene/sectionCaps/SectionCaps.js create mode 100644 src/viewer/scene/sectionCaps/index.js 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 + + + + + + + +
+ +

SceneModel

+

Non-realistic rendering, geometry reuse, triangle primitives

+

+ SceneModel is a WebGL2-based SceneModel implementation that stores model geometry as data textures on the GPU. +

+

Components Used

+ +
+ + + + + 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 + + + + + + + + +
+ +

SectionPlanesPlugin

+

Slices models open to reveal internal structures

+

In this example, we're loading an IFC2x3 BIM model from the file system, then slicing it with a section + plane.

+

Stats

+ +

Components used

+ +

Resources

+ +
+ + + + + \ 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