trimesh-boolean
Advanced tools
+5
-3
| { | ||
| "name": "trimesh-boolean", | ||
| "version": "0.5.3", | ||
| "version": "0.5.4", | ||
| "description": "Triangle mesh boolean operations — supports open surfaces, terrain intersection, and mesh repair", | ||
@@ -53,3 +53,5 @@ "type": "module", | ||
| "@kninnug/constrainautor": "^4.0.1", | ||
| "delaunator": "^5.0.1" | ||
| "delaunator": "^5.0.1", | ||
| "robust-predicates": "^3.0.3", | ||
| "tiny-exact-math": "^0.0.1" | ||
| }, | ||
@@ -66,3 +68,3 @@ "peerDependencies": { | ||
| "@rollup/plugin-node-resolve": "^16.0.0", | ||
| "@rollup/plugin-terser": "^0.4.4", | ||
| "@rollup/plugin-terser": "^1.0.0", | ||
| "jszip": "^3.10.1", | ||
@@ -69,0 +71,0 @@ "rollup": "^4.0.0", |
+95
-103
@@ -7,2 +7,5 @@ /** | ||
| * that share a pool vertex are connected by definition. | ||
| * | ||
| * At junction vertices (degree 3+), picks the smoothest continuation | ||
| * by comparing outgoing directions against the incoming direction. | ||
| */ | ||
@@ -16,2 +19,6 @@ | ||
| * | ||
| * At junction vertices where multiple unused segments meet, the algorithm | ||
| * picks the segment whose direction is most aligned with the incoming | ||
| * direction (largest dot product), preventing U-turns and backtracks. | ||
| * | ||
| * @param {Array<{ p0: PoolVertex, p1: PoolVertex, idxA: number, idxB: number }>} segments | ||
@@ -39,2 +46,59 @@ * @returns {Array<Array<PoolVertex>>} Array of polylines (each an array of pool vertices) | ||
| var used = new Uint8Array(segments.length); | ||
| /** | ||
| * Compute unit direction from prev to curr. | ||
| * Returns null if the points are coincident. | ||
| */ | ||
| function direction(prev, curr) { | ||
| var dx = curr.x - prev.x; | ||
| var dy = curr.y - prev.y; | ||
| var dz = curr.z - prev.z; | ||
| var len = Math.sqrt(dx * dx + dy * dy + dz * dz); | ||
| if (len < 1e-15) return null; | ||
| return { x: dx / len, y: dy / len, z: dz / len }; | ||
| } | ||
| /** | ||
| * Pick the best unused neighbor at `vert`. When `inDir` is provided | ||
| * (junction resolution), pick the neighbor whose outgoing direction | ||
| * has the largest dot product with inDir (smoothest continuation). | ||
| * When inDir is null (first step), pick any unused neighbor. | ||
| */ | ||
| function pickBest(vert, inDir) { | ||
| var neighbors = adj[vert.id]; | ||
| if (!neighbors) return null; | ||
| var best = null; | ||
| var bestDot = -Infinity; | ||
| for (var ni = 0; ni < neighbors.length; ni++) { | ||
| var nb = neighbors[ni]; | ||
| if (used[nb.segIdx]) continue; | ||
| if (!inDir) { | ||
| // No incoming direction — return first available | ||
| return nb; | ||
| } | ||
| // Compute outgoing direction and score by alignment | ||
| var dx = nb.otherEnd.x - vert.x; | ||
| var dy = nb.otherEnd.y - vert.y; | ||
| var dz = nb.otherEnd.z - vert.z; | ||
| var len = Math.sqrt(dx * dx + dy * dy + dz * dz); | ||
| if (len < 1e-15) { | ||
| // Degenerate segment — lowest priority | ||
| if (!best) { best = nb; bestDot = -Infinity; } | ||
| continue; | ||
| } | ||
| var dot = (dx * inDir.x + dy * inDir.y + dz * inDir.z) / len; | ||
| if (dot > bestDot) { | ||
| bestDot = dot; | ||
| best = nb; | ||
| } | ||
| } | ||
| return best; | ||
| } | ||
| var polylines = []; | ||
@@ -50,47 +114,41 @@ | ||
| // Extend tail | ||
| var extending = true; | ||
| while (extending) { | ||
| extending = false; | ||
| // Extend tail with angle-based junction resolution | ||
| while (true) { | ||
| var tailVert = tailChain[tailChain.length - 1]; | ||
| var neighbors = adj[tailVert.id]; | ||
| if (!neighbors) break; | ||
| var tailPrev = tailChain[tailChain.length - 2]; | ||
| var inDir = direction(tailPrev, tailVert); | ||
| var nb = pickBest(tailVert, inDir); | ||
| if (!nb) break; | ||
| for (var ni = 0; ni < neighbors.length; ni++) { | ||
| var nb = neighbors[ni]; | ||
| if (used[nb.segIdx]) continue; | ||
| // Check if this would close the loop | ||
| if (nb.otherEnd === tailChain[0]) { | ||
| // Close the loop — don't add the vertex again, | ||
| // the caller checks first === last by reference | ||
| used[nb.segIdx] = 1; | ||
| tailChain.push(nb.otherEnd); | ||
| return buildResult(headChain, tailChain, polylines, used, segments, adj); | ||
| } | ||
| // Check if this would close the loop | ||
| var chainStart = headChain.length > 0 ? headChain[headChain.length - 1] : tailChain[0]; | ||
| if (nb.otherEnd === chainStart) { | ||
| used[nb.segIdx] = 1; | ||
| tailChain.push(nb.otherEnd); | ||
| extending = true; | ||
| break; // Only follow one neighbor per step | ||
| break; // Loop closed | ||
| } | ||
| used[nb.segIdx] = 1; | ||
| tailChain.push(nb.otherEnd); | ||
| } | ||
| // Extend head | ||
| extending = true; | ||
| while (extending) { | ||
| extending = false; | ||
| var headVert = headChain.length > 0 ? headChain[headChain.length - 1] : tailChain[0]; | ||
| var headNeighbors = adj[headVert.id]; | ||
| if (!headNeighbors) break; | ||
| // Extend head with angle-based junction resolution | ||
| while (true) { | ||
| var headVert, headPrev; | ||
| if (headChain.length >= 2) { | ||
| headVert = headChain[headChain.length - 1]; | ||
| headPrev = headChain[headChain.length - 2]; | ||
| } else if (headChain.length === 1) { | ||
| headVert = headChain[0]; | ||
| headPrev = tailChain[0]; | ||
| } else { | ||
| headVert = tailChain[0]; | ||
| headPrev = tailChain[1]; | ||
| } | ||
| var hDir = direction(headPrev, headVert); | ||
| var hNb = pickBest(headVert, hDir); | ||
| if (!hNb) break; | ||
| for (var hi = 0; hi < headNeighbors.length; hi++) { | ||
| var hb = headNeighbors[hi]; | ||
| if (used[hb.segIdx]) continue; | ||
| used[hb.segIdx] = 1; | ||
| headChain.push(hb.otherEnd); | ||
| extending = true; | ||
| break; | ||
| } | ||
| used[hNb.segIdx] = 1; | ||
| headChain.push(hNb.otherEnd); | ||
| } | ||
@@ -112,67 +170,1 @@ | ||
| } | ||
| /** | ||
| * Helper to finish building result when a loop closes during tail extension. | ||
| * Continues chaining remaining unused segments. | ||
| */ | ||
| function buildResult(headChain, tailChain, polylines, used, segments, adj) { | ||
| // Combine current chain | ||
| var chain; | ||
| if (headChain.length > 0) { | ||
| headChain.reverse(); | ||
| chain = headChain.concat(tailChain); | ||
| } else { | ||
| chain = tailChain; | ||
| } | ||
| polylines.push(chain); | ||
| // Continue with remaining unused segments | ||
| for (var s = 0; s < segments.length; s++) { | ||
| if (used[s]) continue; | ||
| used[s] = 1; | ||
| var tChain = [segments[s].p0, segments[s].p1]; | ||
| var hChain = []; | ||
| // Extend tail | ||
| var extending = true; | ||
| while (extending) { | ||
| extending = false; | ||
| var tailVert = tChain[tChain.length - 1]; | ||
| var neighbors = adj[tailVert.id]; | ||
| if (!neighbors) break; | ||
| for (var ni = 0; ni < neighbors.length; ni++) { | ||
| if (used[neighbors[ni].segIdx]) continue; | ||
| used[neighbors[ni].segIdx] = 1; | ||
| tChain.push(neighbors[ni].otherEnd); | ||
| extending = true; | ||
| break; | ||
| } | ||
| } | ||
| // Extend head | ||
| extending = true; | ||
| while (extending) { | ||
| extending = false; | ||
| var headVert = hChain.length > 0 ? hChain[hChain.length - 1] : tChain[0]; | ||
| var headNeighbors = adj[headVert.id]; | ||
| if (!headNeighbors) break; | ||
| for (var hi = 0; hi < headNeighbors.length; hi++) { | ||
| if (used[headNeighbors[hi].segIdx]) continue; | ||
| used[headNeighbors[hi].segIdx] = 1; | ||
| hChain.push(headNeighbors[hi].otherEnd); | ||
| extending = true; | ||
| break; | ||
| } | ||
| } | ||
| if (hChain.length > 0) { | ||
| hChain.reverse(); | ||
| polylines.push(hChain.concat(tChain)); | ||
| } else { | ||
| polylines.push(tChain); | ||
| } | ||
| } | ||
| return polylines; | ||
| } |
+102
-4
@@ -55,2 +55,84 @@ /** | ||
| // ── Walk-direction classification (open meshes, interior components) ── | ||
| /** | ||
| * Build a map of walk edge directions from meshEdgePolys segments. | ||
| * Returns { edgeKey → { dx, dy, dz, mx, my, mz } } | ||
| */ | ||
| function buildWalkEdgeDirMap(meshEp) { | ||
| if (!meshEp || !meshEp.segments) return {}; | ||
| var dirMap = {}; | ||
| var prevVert = null; | ||
| for (var si = 0; si < meshEp.segments.length; si++) { | ||
| var seg = meshEp.segments[si]; | ||
| for (var vi = 0; vi < seg.verts.length; vi++) { | ||
| var v = seg.verts[vi]; | ||
| if (prevVert) { | ||
| var k0 = vKey(prevVert), k1 = vKey(v); | ||
| if (k0 !== k1) { | ||
| var ek = edgeKey(k0, k1); | ||
| dirMap[ek] = { | ||
| dx: v.x - prevVert.x, dy: v.y - prevVert.y, dz: v.z - prevVert.z, | ||
| mx: (prevVert.x + v.x) * 0.5, my: (prevVert.y + v.y) * 0.5, mz: (prevVert.z + v.z) * 0.5 | ||
| }; | ||
| } | ||
| } | ||
| prevVert = v; | ||
| } | ||
| } | ||
| return dirMap; | ||
| } | ||
| /** | ||
| * Classify an interior component (no boundary vertices) by checking which | ||
| * side of the barrier edges its triangles fall on, using the walk direction. | ||
| * | ||
| * Cross product of (walk direction) × (edge midpoint → centroid) dotted | ||
| * with the triangle surface normal: positive = LEFT = inside. | ||
| * | ||
| * @returns {boolean} true if inside | ||
| */ | ||
| function classifyByWalkDirection(comp, megaSoup, barrierEdges, edgeToTris, walkDirMap) { | ||
| var leftVotes = 0, rightVotes = 0; | ||
| for (var ti = 0; ti < comp.triIndices.length; ti++) { | ||
| var triIdx = comp.triIndices[ti]; | ||
| var tri = megaSoup[triIdx]; | ||
| var ks = [vKey(tri.v0), vKey(tri.v1), vKey(tri.v2)]; | ||
| var vs = [tri.v0, tri.v1, tri.v2]; | ||
| for (var e = 0; e < 3; e++) { | ||
| var ne = (e + 1) % 3; | ||
| var ek = edgeKey(ks[e], ks[ne]); | ||
| if (!barrierEdges[ek]) continue; | ||
| var dir = walkDirMap[ek]; | ||
| if (!dir) continue; | ||
| // Triangle centroid | ||
| var cx = (vs[0].x + vs[1].x + vs[2].x) / 3 - dir.mx; | ||
| var cy = (vs[0].y + vs[1].y + vs[2].y) / 3 - dir.my; | ||
| var cz = (vs[0].z + vs[1].z + vs[2].z) / 3 - dir.mz; | ||
| // Triangle normal | ||
| var e1x = vs[1].x - vs[0].x, e1y = vs[1].y - vs[0].y, e1z = vs[1].z - vs[0].z; | ||
| var e2x = vs[2].x - vs[0].x, e2y = vs[2].y - vs[0].y, e2z = vs[2].z - vs[0].z; | ||
| var nx = e1y * e2z - e1z * e2y; | ||
| var ny = e1z * e2x - e1x * e2z; | ||
| var nz = e1x * e2y - e1y * e2x; | ||
| // Cross: walkDir × toCentroid, dot with normal | ||
| var cross_dot = (dir.dy * cz - dir.dz * cy) * nx + | ||
| (dir.dz * cx - dir.dx * cz) * ny + | ||
| (dir.dx * cy - dir.dy * cx) * nz; | ||
| if (cross_dot > 0) leftVotes++; | ||
| else if (cross_dot < 0) rightVotes++; | ||
| } | ||
| } | ||
| // LEFT = inside (positive cross product) | ||
| return leftVotes > rightVotes; | ||
| } | ||
| // ── Barrier-normal classification (closed meshes) ── | ||
@@ -334,2 +416,9 @@ | ||
| // ── Build walk edge direction maps for open meshes ── | ||
| var walkDirA = null, walkDirB = null; | ||
| if (meshEdgePolys) { | ||
| if (isOpenA && meshEdgePolys.A) walkDirA = buildWalkEdgeDirMap(meshEdgePolys.A); | ||
| if (isOpenB && meshEdgePolys.B) walkDirB = buildWalkEdgeDirMap(meshEdgePolys.B); | ||
| } | ||
| // ── Classify each component + extract boundary walks ── | ||
@@ -358,3 +447,12 @@ var aInside = [], aOutside = []; | ||
| if (isOpen) { | ||
| isInside = classifyByBoundaryTopology(comp, megaSoup, bverts); | ||
| // Open mesh: boundary touch = outside | ||
| // Interior components: barrier-normal (other mesh's normal at barrier) | ||
| var touchesBoundary = !classifyByBoundaryTopology(comp, megaSoup, bverts); | ||
| if (touchesBoundary) { | ||
| isInside = false; | ||
| } else { | ||
| // Interior component — use barrier-normal (works for both meshes | ||
| // because it checks the OTHER mesh's normal direction) | ||
| isInside = classifyByBarrierNormal(comp, megaSoup, barrierEdges, edgeToTris); | ||
| } | ||
| } else { | ||
@@ -386,6 +484,6 @@ isInside = classifyByBarrierNormal(comp, megaSoup, barrierEdges, edgeToTris); | ||
| console.log("[BMS] Classification (hybrid): A: " + aInside.length + " inside, " + | ||
| aOutside.length + " outside" + (isOpenA ? " (topo)" : " (dot)") + | ||
| console.log("[BMS] Classification (walk): A: " + aInside.length + " inside, " + | ||
| aOutside.length + " outside" + (isOpenA ? " (walk)" : " (dot)") + | ||
| ". B: " + bInside.length + " inside, " + | ||
| bOutside.length + " outside" + (isOpenB ? " (topo)" : " (dot)") + | ||
| bOutside.length + " outside" + (isOpenB ? " (walk)" : " (dot)") + | ||
| ". Components: " + components.length + "."); | ||
@@ -392,0 +490,0 @@ |
+276
-131
@@ -20,8 +20,19 @@ /** | ||
| /** | ||
| * Extract ordered boundary loop(s) from a triangle soup. | ||
| * Returns the largest loop as an ordered array of {key, vertex}. | ||
| * chainedOpenEdge — Walk the complete open boundary of a mesh as a CLOSED polygon. | ||
| * | ||
| * Uses a half-edge structure to correctly navigate bowtie (non-manifold) | ||
| * vertices. At each boundary vertex, the next boundary half-edge is found | ||
| * by walking the triangle fan around the vertex until the next boundary | ||
| * edge is reached. | ||
| * | ||
| * Returns the largest loop as an ordered array of {key, vertex}, | ||
| * with the first vertex repeated at the end to close the polygon. | ||
| * Returns [] if the mesh has no open edges. | ||
| */ | ||
| function extractBoundaryLoop(tris) { | ||
| var edgeCount = {}; | ||
| var edgeVerts = {}; | ||
| function chainedOpenEdge(tris) { | ||
| // Step 1: Build half-edge structure | ||
| // Each directed half-edge is keyed as "fromKey|toKey" | ||
| var vertMap = {}; // vertKey → vertex object | ||
| var halfEdges = {}; // "from|to" → true (exists) | ||
| var heNextInTri = {}; // "from|to" → "to|next" (next half-edge in same triangle) | ||
@@ -32,44 +43,93 @@ for (var i = 0; i < tris.length; i++) { | ||
| var ks = [vKey(vs[0]), vKey(vs[1]), vKey(vs[2])]; | ||
| for (var e = 0; e < 3; e++) { | ||
| vertMap[ks[e]] = vs[e]; | ||
| var ne = (e + 1) % 3; | ||
| var ek = edgeKey(ks[e], ks[ne]); | ||
| if (!edgeCount[ek]) { edgeCount[ek] = 0; edgeVerts[ek] = { ka: ks[e], kb: ks[ne], a: vs[e], b: vs[ne] }; } | ||
| edgeCount[ek]++; | ||
| var nne = (e + 2) % 3; | ||
| var heKey = ks[e] + "|" + ks[ne]; | ||
| halfEdges[heKey] = true; | ||
| heNextInTri[heKey] = ks[ne] + "|" + ks[nne]; | ||
| } | ||
| } | ||
| // Build boundary adjacency (edges with count === 1) | ||
| var bdryAdj = {}; | ||
| var bdryVertMap = {}; | ||
| for (var ek2 in edgeCount) { | ||
| if (edgeCount[ek2] !== 1) continue; | ||
| var ev = edgeVerts[ek2]; | ||
| if (!bdryAdj[ev.ka]) bdryAdj[ev.ka] = []; | ||
| bdryAdj[ev.ka].push(ev.kb); | ||
| if (!bdryAdj[ev.kb]) bdryAdj[ev.kb] = []; | ||
| bdryAdj[ev.kb].push(ev.ka); | ||
| bdryVertMap[ev.ka] = ev.a; | ||
| bdryVertMap[ev.kb] = ev.b; | ||
| // Step 2: Find boundary half-edges (no twin exists) | ||
| var boundary = {}; // "from|to" → true | ||
| var bdryFromVert = {}; // vertKey → ["from|to", ...] (boundary half-edges starting from this vert) | ||
| for (var hk in halfEdges) { | ||
| var sep = hk.indexOf("|"); | ||
| var fromK = hk.substring(0, sep); | ||
| var toK = hk.substring(sep + 1); | ||
| var twin = toK + "|" + fromK; | ||
| if (!halfEdges[twin]) { | ||
| boundary[hk] = true; | ||
| if (!bdryFromVert[fromK]) bdryFromVert[fromK] = []; | ||
| bdryFromVert[fromK].push(hk); | ||
| } | ||
| } | ||
| // Walk the largest boundary loop | ||
| var visited = {}; | ||
| if (Object.keys(boundary).length === 0) return []; | ||
| // Step 3: Build "next boundary" map | ||
| // For boundary half-edge A→V, find the next boundary half-edge V→B | ||
| // by walking the triangle fan around V: | ||
| // A→V is in some triangle → next in that triangle is V→C | ||
| // if V→C is boundary → done (next = V→C) | ||
| // if V→C is interior → find twin C→V → next in twin's triangle is V→D | ||
| // repeat until we find a boundary half-edge | ||
| var nextBoundary = {}; // "A|V" → "V|B" | ||
| for (var bhk in boundary) { | ||
| var sep2 = bhk.indexOf("|"); | ||
| var toV = bhk.substring(sep2 + 1); | ||
| // Walk the fan around V starting from the next half-edge in A→V's triangle | ||
| var cursor = heNextInTri[bhk]; // V→C in the same triangle as A→V | ||
| var safety = 1000; | ||
| while (safety-- > 0) { | ||
| if (boundary[cursor]) { | ||
| nextBoundary[bhk] = cursor; | ||
| break; | ||
| } | ||
| // cursor is interior V→D — find twin D→V, then get next in that triangle | ||
| var csep = cursor.indexOf("|"); | ||
| var cFrom = cursor.substring(0, csep); | ||
| var cTo = cursor.substring(csep + 1); | ||
| var cTwin = cTo + "|" + cFrom; | ||
| if (!halfEdges[cTwin]) break; // broken mesh | ||
| cursor = heNextInTri[cTwin]; // next in twin's triangle = V→E | ||
| } | ||
| } | ||
| // Step 4: Walk boundary loops using nextBoundary | ||
| var used = {}; | ||
| var bestLoop = null; | ||
| for (var startKey in bdryAdj) { | ||
| if (visited[startKey]) continue; | ||
| for (var startHE in boundary) { | ||
| if (used[startHE]) continue; | ||
| var loop = []; | ||
| var current = startKey; | ||
| while (current && !visited[current]) { | ||
| visited[current] = true; | ||
| loop.push({ key: current, vertex: bdryVertMap[current] }); | ||
| var neighbors = bdryAdj[current]; | ||
| var next = null; | ||
| if (neighbors) { | ||
| for (var ni = 0; ni < neighbors.length; ni++) { | ||
| if (!visited[neighbors[ni]]) { next = neighbors[ni]; break; } | ||
| var cur = startHE; | ||
| var safety2 = Object.keys(boundary).length + 2; | ||
| while (safety2-- > 0) { | ||
| if (used[cur]) { | ||
| // If we closed back to start, add closing vertex | ||
| if (cur === startHE && loop.length > 0) { | ||
| var csep2 = cur.indexOf("|"); | ||
| var closingKey = cur.substring(0, csep2); | ||
| loop.push({ key: closingKey, vertex: vertMap[closingKey] }); | ||
| } | ||
| break; | ||
| } | ||
| current = next; | ||
| used[cur] = true; | ||
| var csep3 = cur.indexOf("|"); | ||
| var curFrom = cur.substring(0, csep3); | ||
| loop.push({ key: curFrom, vertex: vertMap[curFrom] }); | ||
| cur = nextBoundary[cur]; | ||
| if (!cur) break; | ||
| } | ||
| if (!bestLoop || loop.length > bestLoop.length) bestLoop = loop; | ||
@@ -209,19 +269,2 @@ } | ||
| /** | ||
| * Walk along boundary loop from index startIdx to endIdx (inclusive). | ||
| * Walks in the forward direction (wrapping around). | ||
| */ | ||
| function walkBoundarySegment(loop, startIdx, endIdx) { | ||
| var result = []; | ||
| var n = loop.length; | ||
| var i = startIdx; | ||
| while (true) { | ||
| result.push(loop[i].vertex); | ||
| if (i === endIdx) break; | ||
| i = (i + 1) % n; | ||
| if (result.length > n + 1) break; // safety | ||
| } | ||
| return result; | ||
| } | ||
| // ── Main entry point ── | ||
@@ -246,2 +289,4 @@ | ||
| */ | ||
| export { chainedOpenEdge }; | ||
| export function bmsClosePolylines(polylines, trisA, trisB, megaSoup, segments) { | ||
@@ -315,7 +360,8 @@ if (polylines.length === 0) { | ||
| // Step 1: Extract boundary loop from ORIGINAL mesh (not mega soup) | ||
| var boundaryLoop = extractBoundaryLoop(meshTris); | ||
| // Step 1: Trace open edges — complete closed boundary polygon | ||
| var boundaryLoop = chainedOpenEdge(meshTris); | ||
| var hasBoundary = boundaryLoop.length > 0; | ||
| if (!hasBoundary) { | ||
| // Closed mesh — just trace intersection(s) | ||
| var segs = []; | ||
@@ -325,11 +371,11 @@ for (var ci = 0; ci < chains.length; ci++) { | ||
| } | ||
| return { segments: segs, closed: false }; | ||
| var isClosed = chains.length > 0 && chains[0].length > 2 && | ||
| chains[0][0] === chains[0][chains[0].length - 1]; | ||
| return { segments: segs, closed: isClosed }; | ||
| } | ||
| // Step 2: Build mesh vertex adjacency from GRAPH TRIS (mega soup with pool vertices) | ||
| // This ensures pool vertex keys are in the adjacency graph | ||
| // Pass barrier edges so graph-walks go AROUND intersections, not through them | ||
| // Step 2: Build mesh vertex adjacency (barriers excluded — walks can't cross intersection) | ||
| var meshGraph = buildMeshVertexAdj(graphTris, barrierEdgeSet); | ||
| // Build boundary vertex set and loop index map | ||
| // Build boundary vertex set and index map | ||
| var bdryVertSet = {}; | ||
@@ -341,41 +387,92 @@ var bdryKeyToIdx = {}; | ||
| } | ||
| var bdryLen = boundaryLoop.length - 1; // edges (loop is closed, first=last) | ||
| // Step 3: For each chain, find its nearest boundary attachment point | ||
| // Find the chain vertex closest to any boundary vertex, then graph-walk | ||
| // from that chain vertex to the boundary to get the attachment. | ||
| var chainAttachments = []; | ||
| // Step 3: For EACH chain, find graph walks to boundary. | ||
| // | ||
| // Closed chains (loops): ONE graph walk — enter and exit at the same vertex. | ||
| // Open chains: TWO graph walks — enter at one endpoint, exit at the other. | ||
| // gwIn: boundary → chain entry point | ||
| // gwOut: chain exit point → boundary | ||
| // | ||
| // connections[] stores: { chainIdx, gwIn, gwOut, bdryIdxIn, bdryIdxOut } | ||
| var connections = []; | ||
| var MAX_BFS = 50000; | ||
| for (var ci2 = 0; ci2 < chains.length; ci2++) { | ||
| var chain = chains[ci2]; | ||
| var chainIsClosed = chain.length > 2 && chain[0] === chain[chain.length - 1]; | ||
| // Find the chain vertex nearest to the boundary | ||
| // Use graph-walk from first chain vertex to boundary | ||
| var chainVertKeys = {}; | ||
| for (var cvi = 0; cvi < chain.length; cvi++) { | ||
| chainVertKeys[vKey(chain[cvi])] = cvi; | ||
| } | ||
| if (chainIsClosed) { | ||
| // Closed loop: find shortest walk from ANY loop vertex to boundary | ||
| var bestGW = null; | ||
| var bestLen = MAX_BFS; | ||
| var bestVertIdx = -1; | ||
| for (var cvi = 0; cvi < chain.length; cvi++) { | ||
| var cvKey = vKey(chain[cvi]); | ||
| var gw = graphWalkToSet(cvKey, bdryVertSet, meshGraph.adj, meshGraph.vertMap, bestLen); | ||
| if (gw && gw.path.length < bestLen) { | ||
| bestLen = gw.path.length; | ||
| bestGW = gw; | ||
| bestVertIdx = cvi; | ||
| } | ||
| } | ||
| if (bestGW) { | ||
| var bIdx = bdryKeyToIdx[bestGW.targetKey]; | ||
| connections.push({ | ||
| chainIdx: ci2, | ||
| entryVertIdx: bestVertIdx, | ||
| gwIn: bestGW.path.slice().reverse(), // boundary → intersection | ||
| gwOut: bestGW.path.slice(), // intersection → boundary (same path) | ||
| bdryIdxIn: bIdx !== undefined ? bIdx : 0, | ||
| bdryIdxOut: bIdx !== undefined ? bIdx : 0 | ||
| }); | ||
| } | ||
| } else { | ||
| // Open chain: find walks from BOTH endpoints | ||
| var startKey = vKey(chain[0]); | ||
| var endKey = vKey(chain[chain.length - 1]); | ||
| var gwStart = graphWalkToSet(startKey, bdryVertSet, meshGraph.adj, meshGraph.vertMap, MAX_BFS); | ||
| var gwEnd = graphWalkToSet(endKey, bdryVertSet, meshGraph.adj, meshGraph.vertMap, MAX_BFS); | ||
| // Try graph-walk from each end of the chain to the boundary | ||
| var firstKey = vKey(chain[0]); | ||
| var gw = graphWalkToSet(firstKey, bdryVertSet, meshGraph.adj, meshGraph.vertMap, 10000); | ||
| if (!gw) { | ||
| // Chain can't reach boundary — skip | ||
| continue; | ||
| if (gwStart && gwEnd) { | ||
| // Enter at start, exit at end | ||
| var bIdxIn = bdryKeyToIdx[gwStart.targetKey]; | ||
| var bIdxOut = bdryKeyToIdx[gwEnd.targetKey]; | ||
| connections.push({ | ||
| chainIdx: ci2, | ||
| entryVertIdx: 0, | ||
| gwIn: gwStart.path.slice().reverse(), // boundary → chain[0] | ||
| gwOut: gwEnd.path.slice(), // chain[last] → boundary | ||
| bdryIdxIn: bIdxIn !== undefined ? bIdxIn : 0, | ||
| bdryIdxOut: bIdxOut !== undefined ? bIdxOut : 0 | ||
| }); | ||
| } else if (gwStart) { | ||
| // Only start reached boundary — use same walk in/out | ||
| var bIdx2 = bdryKeyToIdx[gwStart.targetKey]; | ||
| connections.push({ | ||
| chainIdx: ci2, | ||
| entryVertIdx: 0, | ||
| gwIn: gwStart.path.slice().reverse(), | ||
| gwOut: gwStart.path.slice(), | ||
| bdryIdxIn: bIdx2 !== undefined ? bIdx2 : 0, | ||
| bdryIdxOut: bIdx2 !== undefined ? bIdx2 : 0 | ||
| }); | ||
| } else if (gwEnd) { | ||
| // Only end reached boundary — reverse chain direction | ||
| var bIdx3 = bdryKeyToIdx[gwEnd.targetKey]; | ||
| connections.push({ | ||
| chainIdx: ci2, | ||
| entryVertIdx: chain.length - 1, | ||
| gwIn: gwEnd.path.slice().reverse(), | ||
| gwOut: gwEnd.path.slice(), | ||
| bdryIdxIn: bIdx3 !== undefined ? bIdx3 : 0, | ||
| bdryIdxOut: bIdx3 !== undefined ? bIdx3 : 0 | ||
| }); | ||
| } | ||
| // If neither endpoint reached boundary, skip this chain | ||
| } | ||
| var bdryIdx = bdryKeyToIdx[gw.targetKey]; | ||
| chainAttachments.push({ | ||
| chainIdx: ci2, | ||
| chain: chain, | ||
| chainEntryKey: firstKey, | ||
| chainEntryIdx: 0, | ||
| graphWalkToBoundary: gw.path, | ||
| boundaryKey: gw.targetKey, | ||
| boundaryLoopIdx: bdryIdx !== undefined ? bdryIdx : 0 | ||
| }); | ||
| } | ||
| if (chainAttachments.length === 0) { | ||
| // No chains could reach boundary | ||
| if (connections.length === 0) { | ||
| // No chain could reach the boundary — fallback | ||
| var fallbackSegs = []; | ||
@@ -388,61 +485,109 @@ for (var fi = 0; fi < chains.length; fi++) { | ||
| // Step 4: Sort chain attachments by their position along the boundary loop | ||
| chainAttachments.sort(function (a, b) { return a.boundaryLoopIdx - b.boundaryLoopIdx; }); | ||
| // Step 4: Sort connections by boundary entry index (position around the loop). | ||
| // This ensures we visit intersections in order as we walk the boundary. | ||
| connections.sort(function (a, b) { return a.bdryIdxIn - b.bdryIdxIn; }); | ||
| // Step 5: Build the mesh edge poly — bmsMontyWalk | ||
| // Start from intersection, walk OUT to boundary, turn left, walk boundary, | ||
| // graph-walk back IN to next chain. The polygon encapsulates the INSIDE region. | ||
| // Step 5: Build ONE continuous closed polygon. | ||
| // | ||
| // For each chain: | ||
| // 1. Follow intersection chain (yellow) | ||
| // 2. Graph-walk OUT from chain end to boundary (magenta) | ||
| // 3. Edge-walk along boundary to next chain's attachment (magenta) | ||
| // 4. Graph-walk IN from boundary to next chain start (magenta) | ||
| // 5. Repeat | ||
| // Algorithm (unidirectional, visits each intersection in boundary order): | ||
| // START at first connection's entry boundary point | ||
| // for each connection i: | ||
| // walk boundary → to connection[i].bdryIdxIn | ||
| // graph walk IN → from boundary to chain entry point | ||
| // traverse chain (entry → exit) | ||
| // graph walk OUT → from chain exit to boundary | ||
| // walk remaining boundary → back to start | ||
| // = CLOSED | ||
| var resultSegments = []; | ||
| var n = chainAttachments.length; | ||
| var startBdryIdx = connections[0].bdryIdxIn; | ||
| // Track current position on boundary (index into boundaryLoop) | ||
| var curBdryIdx = startBdryIdx; | ||
| for (var ai = 0; ai < n; ai++) { | ||
| var att = chainAttachments[ai]; | ||
| var nextAtt = chainAttachments[(ai + 1) % n]; | ||
| for (var ci3 = 0; ci3 < connections.length; ci3++) { | ||
| var conn = connections[ci3]; | ||
| // 5a) Follow the intersection chain (yellow) | ||
| resultSegments.push({ verts: att.chain.slice(), type: "intersection" }); | ||
| // 5b) Graph-walk OUT from chain end to boundary (magenta) | ||
| var chainEndKey = vKey(att.chain[att.chain.length - 1]); | ||
| var isClosed = (chainEndKey === att.chainEntryKey) || (att.chain[att.chain.length - 1] === att.chain[0]); | ||
| if (isClosed) { | ||
| // Closed chain — walk out the same path we'd walk in | ||
| if (att.graphWalkToBoundary.length >= 2) { | ||
| resultSegments.push({ verts: att.graphWalkToBoundary.slice(), type: "walk" }); | ||
| // 5a) Boundary segment: from current position to this connection's entry | ||
| if (ci3 > 0) { | ||
| var fromIdx = curBdryIdx; | ||
| var toIdx = conn.bdryIdxIn; | ||
| if (fromIdx !== toIdx) { | ||
| var bdrySegVerts = []; | ||
| if (toIdx > fromIdx) { | ||
| for (var bsi = fromIdx; bsi <= toIdx; bsi++) { | ||
| bdrySegVerts.push(boundaryLoop[bsi].vertex); | ||
| } | ||
| } else { | ||
| // Wrap around boundary | ||
| for (var bsi2 = fromIdx; bsi2 < bdryLen; bsi2++) { | ||
| bdrySegVerts.push(boundaryLoop[bsi2].vertex); | ||
| } | ||
| for (var bsi3 = 0; bsi3 <= toIdx; bsi3++) { | ||
| bdrySegVerts.push(boundaryLoop[bsi3].vertex); | ||
| } | ||
| } | ||
| if (bdrySegVerts.length >= 2) { | ||
| resultSegments.push({ verts: bdrySegVerts, type: "walk" }); | ||
| } | ||
| } | ||
| } else { | ||
| // Open chain — graph-walk from chain END to boundary | ||
| var gwOut = graphWalkToSet(chainEndKey, bdryVertSet, meshGraph.adj, meshGraph.vertMap, 10000); | ||
| if (gwOut && gwOut.path.length >= 2) { | ||
| resultSegments.push({ verts: gwOut.path, type: "walk" }); | ||
| } | ||
| } | ||
| // 5c) Edge-walk along boundary to next chain's boundary attachment (magenta) | ||
| var fromBdryIdx = att.boundaryLoopIdx; | ||
| var toBdryIdx = nextAtt.boundaryLoopIdx; | ||
| // 5b) Graph walk IN: boundary → chain entry vertex | ||
| if (conn.gwIn.length >= 2) { | ||
| resultSegments.push({ verts: conn.gwIn, type: "walk" }); | ||
| } | ||
| if (fromBdryIdx !== toBdryIdx) { | ||
| var bdryWalk = walkBoundarySegment(boundaryLoop, fromBdryIdx, toBdryIdx); | ||
| if (bdryWalk.length >= 2) { | ||
| resultSegments.push({ verts: bdryWalk, type: "walk" }); | ||
| // 5c) Orient chain so entry vertex is at [0]. | ||
| var chain = chains[conn.chainIdx].slice(); | ||
| var chainIsClosed = chain.length > 2 && chain[0] === chain[chain.length - 1]; | ||
| if (conn.entryVertIdx > 0) { | ||
| if (chainIsClosed) { | ||
| // Rotate closed loop: walk vertex at [0] and repeated at [end] | ||
| var rotChain = []; | ||
| for (var rci = conn.entryVertIdx; rci < chain.length - 1; rci++) { | ||
| rotChain.push(chain[rci]); | ||
| } | ||
| for (var rci2 = 0; rci2 <= conn.entryVertIdx; rci2++) { | ||
| rotChain.push(chain[rci2]); | ||
| } | ||
| chain = rotChain; | ||
| } else if (conn.entryVertIdx === chain.length - 1) { | ||
| // Open chain entered at last vertex — reverse to enter at [0] | ||
| chain.reverse(); | ||
| } | ||
| } | ||
| resultSegments.push({ verts: chain, type: "intersection" }); | ||
| // 5d) Graph-walk IN from boundary to next chain entry point (magenta) | ||
| var gwIn = nextAtt.graphWalkToBoundary.slice().reverse(); | ||
| if (gwIn.length >= 2) { | ||
| resultSegments.push({ verts: gwIn, type: "walk" }); | ||
| // 5d) Graph walk OUT: chain exit vertex → boundary | ||
| if (conn.gwOut.length >= 2) { | ||
| resultSegments.push({ verts: conn.gwOut, type: "walk" }); | ||
| } | ||
| // Update current boundary position to exit point | ||
| curBdryIdx = conn.bdryIdxOut; | ||
| } | ||
| // 5e) Final boundary segment: from last exit back to first entry (closing). | ||
| var closingVerts = []; | ||
| if (curBdryIdx !== startBdryIdx) { | ||
| // Walk forward from last exit to first entry (wrapping around) | ||
| for (var cbi = curBdryIdx; cbi < bdryLen; cbi++) { | ||
| closingVerts.push(boundaryLoop[cbi].vertex); | ||
| } | ||
| for (var cbi2 = 0; cbi2 <= startBdryIdx; cbi2++) { | ||
| closingVerts.push(boundaryLoop[cbi2].vertex); | ||
| } | ||
| } else { | ||
| // Same position — full boundary loop | ||
| for (var cbi3 = startBdryIdx; cbi3 < bdryLen; cbi3++) { | ||
| closingVerts.push(boundaryLoop[cbi3].vertex); | ||
| } | ||
| for (var cbi4 = 0; cbi4 <= startBdryIdx; cbi4++) { | ||
| closingVerts.push(boundaryLoop[cbi4].vertex); | ||
| } | ||
| } | ||
| if (closingVerts.length >= 2) { | ||
| resultSegments.push({ verts: closingVerts, type: "walk" }); | ||
| } | ||
| return { segments: resultSegments, closed: true }; | ||
| } |
+49
-6
@@ -13,2 +13,3 @@ /** | ||
| import { bmsChain } from "./bmsChain.js"; | ||
| import { determinant } from "tiny-exact-math"; | ||
@@ -79,3 +80,2 @@ /** | ||
| var BARY_TOL = -1e-4; | ||
| var validSteiner = []; // pool vertex objects | ||
@@ -95,2 +95,35 @@ | ||
| // Exact point-in-triangle test using rational arithmetic. | ||
| // Projects ORIGINAL 3D coordinates to the best-fit 2D plane (XY, XZ, or YZ) | ||
| // to avoid float errors from the toLocal projection. | ||
| var anx = Math.abs(lnx), any = Math.abs(lny), anz = Math.abs(lnz); | ||
| var projA, projB; // which axes to use for 2D projection | ||
| if (anx >= any && anx >= anz) { | ||
| // Normal dominated by X → project to YZ plane | ||
| projA = function(v) { return v.y; }; | ||
| projB = function(v) { return v.z; }; | ||
| } else if (any >= anz) { | ||
| // Normal dominated by Y → project to XZ plane | ||
| projA = function(v) { return v.x; }; | ||
| projB = function(v) { return v.z; }; | ||
| } else { | ||
| // Normal dominated by Z → project to XY plane | ||
| projA = function(v) { return v.x; }; | ||
| projB = function(v) { return v.y; }; | ||
| } | ||
| var tv0 = { x: projA(tri.v0), y: projB(tri.v0) }; | ||
| var tv1 = { x: projA(tri.v1), y: projB(tri.v1) }; | ||
| var tv2 = { x: projA(tri.v2), y: projB(tri.v2) }; | ||
| function exactPointInTri3D(p) { | ||
| var pp = { x: projA(p), y: projB(p) }; | ||
| var d0 = determinant(tv0, tv1, pp); | ||
| var d1 = determinant(tv1, tv2, pp); | ||
| var d2 = determinant(tv2, tv0, pp); | ||
| var hasNeg = (d0 < 0) || (d1 < 0) || (d2 < 0); | ||
| var hasPos = (d0 > 0) || (d1 > 0) || (d2 > 0); | ||
| return !(hasNeg && hasPos); | ||
| } | ||
| var BARY_TOL = -1e-4; // float fallback tolerance | ||
| for (var s = 0; s < segments.length; s++) { | ||
@@ -114,7 +147,17 @@ var seg = segments[s]; | ||
| // Validate: must be inside the triangle (barycentric check) | ||
| var lp = toLocal(p); | ||
| var bc = baryCoords(lp[0], lp[1]); | ||
| if (bc[0] < BARY_TOL || bc[1] < BARY_TOL || bc[2] < BARY_TOL) { | ||
| continue; | ||
| // Validate: must be inside the triangle. | ||
| // Primary: exact rational test on original 3D coords (projected to best plane) | ||
| // Fallback: float barycentric with tolerance (handles near-edge Steiner points | ||
| // whose computed position drifted slightly outside due to float intersection math) | ||
| if (!exactPointInTri3D(p)) { | ||
| var lp = toLocal(p); | ||
| var bc = baryCoords(lp[0], lp[1]); | ||
| if (bc[0] < BARY_TOL || bc[1] < BARY_TOL || bc[2] < BARY_TOL) { | ||
| // Last chance: accept if the point is very close to an edge | ||
| // (within 1% of triangle size) — intersection drift | ||
| var minBC = Math.min(bc[0], bc[1], bc[2]); | ||
| if (minBC < -0.01) { | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
@@ -121,0 +164,0 @@ |
+1
-1
@@ -67,3 +67,3 @@ /** | ||
| export { bmsChain } from "./bms/bmsChain.js"; | ||
| export { bmsClosePolylines } from "./bms/bmsClose.js"; | ||
| export { bmsClosePolylines, chainedOpenEdge } from "./bms/bmsClose.js"; | ||
| export { bmsClassify } from "./bms/bmsClassify.js"; | ||
@@ -70,0 +70,0 @@ export { bmsBooleanOp } from "./bms/bmsBooleanOp.js"; |
@@ -17,2 +17,3 @@ /** | ||
| import { orient3d } from "robust-predicates"; | ||
| import { triNormal } from "../normals/triNormal.js"; | ||
@@ -34,11 +35,9 @@ import { cross } from "../util/math.js"; | ||
| export function triTriIntersection(triA, triB) { | ||
| // Plane of triangle B | ||
| var nB = triNormal(triB); | ||
| var dB = -(nB.x * triB.v0.x + nB.y * triB.v0.y + nB.z * triB.v0.z); | ||
| // Robust orientation: signed distances of triA vertices to plane(triB) | ||
| // orient3d returns a value proportional to 6× signed tetrahedron volume; | ||
| // its sign is guaranteed correct even for near-degenerate configurations. | ||
| var dA0 = orient3d(triB.v0.x, triB.v0.y, triB.v0.z, triB.v1.x, triB.v1.y, triB.v1.z, triB.v2.x, triB.v2.y, triB.v2.z, triA.v0.x, triA.v0.y, triA.v0.z); | ||
| var dA1 = orient3d(triB.v0.x, triB.v0.y, triB.v0.z, triB.v1.x, triB.v1.y, triB.v1.z, triB.v2.x, triB.v2.y, triB.v2.z, triA.v1.x, triA.v1.y, triA.v1.z); | ||
| var dA2 = orient3d(triB.v0.x, triB.v0.y, triB.v0.z, triB.v1.x, triB.v1.y, triB.v1.z, triB.v2.x, triB.v2.y, triB.v2.z, triA.v2.x, triA.v2.y, triA.v2.z); | ||
| // Signed distances of triA vertices to plane B | ||
| var dA0 = nB.x * triA.v0.x + nB.y * triA.v0.y + nB.z * triA.v0.z + dB; | ||
| var dA1 = nB.x * triA.v1.x + nB.y * triA.v1.y + nB.z * triA.v1.z + dB; | ||
| var dA2 = nB.x * triA.v2.x + nB.y * triA.v2.y + nB.z * triA.v2.z + dB; | ||
| // All on same side -> no intersection | ||
@@ -48,11 +47,7 @@ if (dA0 > 0 && dA1 > 0 && dA2 > 0) return null; | ||
| // Plane of triangle A | ||
| var nA = triNormal(triA); | ||
| var dA = -(nA.x * triA.v0.x + nA.y * triA.v0.y + nA.z * triA.v0.z); | ||
| // Robust orientation: signed distances of triB vertices to plane(triA) | ||
| var dB0 = orient3d(triA.v0.x, triA.v0.y, triA.v0.z, triA.v1.x, triA.v1.y, triA.v1.z, triA.v2.x, triA.v2.y, triA.v2.z, triB.v0.x, triB.v0.y, triB.v0.z); | ||
| var dB1 = orient3d(triA.v0.x, triA.v0.y, triA.v0.z, triA.v1.x, triA.v1.y, triA.v1.z, triA.v2.x, triA.v2.y, triA.v2.z, triB.v1.x, triB.v1.y, triB.v1.z); | ||
| var dB2 = orient3d(triA.v0.x, triA.v0.y, triA.v0.z, triA.v1.x, triA.v1.y, triA.v1.z, triA.v2.x, triA.v2.y, triA.v2.z, triB.v2.x, triB.v2.y, triB.v2.z); | ||
| // Signed distances of triB vertices to plane A | ||
| var dB0 = nA.x * triB.v0.x + nA.y * triB.v0.y + nA.z * triB.v0.z + dA; | ||
| var dB1 = nA.x * triB.v1.x + nA.y * triB.v1.y + nA.z * triB.v1.z + dA; | ||
| var dB2 = nA.x * triB.v2.x + nA.y * triB.v2.y + nA.z * triB.v2.z + dA; | ||
| // All on same side -> no intersection | ||
@@ -62,2 +57,6 @@ if (dB0 > 0 && dB1 > 0 && dB2 > 0) return null; | ||
| // Float normals for geometric computation (line direction, projections) | ||
| var nA = triNormal(triA); | ||
| var nB = triNormal(triB); | ||
| // Near-parallel planes | ||
@@ -75,4 +74,8 @@ var dotN = nA.x * nB.x + nA.y * nB.y + nA.z * nB.z; | ||
| // Plane constants for line-point computation | ||
| var planeDA = -(nA.x * triA.v0.x + nA.y * triA.v0.y + nA.z * triA.v0.z); | ||
| var planeDB = -(nB.x * triB.v0.x + nB.y * triB.v0.y + nB.z * triB.v0.z); | ||
| // A point on the intersection line (needed for relative projection) | ||
| var linePoint = findLinePoint(nA, dA, nB, dB, lineDir); | ||
| var linePoint = findLinePoint(nA, planeDA, nB, planeDB, lineDir); | ||
| if (!linePoint) return null; | ||
@@ -105,2 +108,25 @@ | ||
| // Reproject onto both triangle planes to eliminate floating-point drift. | ||
| // Solves for the minimal correction in the span of both normals so the | ||
| // point lies exactly on both planes: P' = P - alpha*nA - beta*nB | ||
| var denom = 1 - dotN * dotN; | ||
| if (Math.abs(denom) > 1e-15) { | ||
| var invD = 1 / denom; | ||
| var rA0 = nA.x * p0.x + nA.y * p0.y + nA.z * p0.z + planeDA; | ||
| var rB0 = nB.x * p0.x + nB.y * p0.y + nB.z * p0.z + planeDB; | ||
| var a0 = (rA0 - dotN * rB0) * invD; | ||
| var b0 = (rB0 - dotN * rA0) * invD; | ||
| p0.x -= a0 * nA.x + b0 * nB.x; | ||
| p0.y -= a0 * nA.y + b0 * nB.y; | ||
| p0.z -= a0 * nA.z + b0 * nB.z; | ||
| var rA1 = nA.x * p1.x + nA.y * p1.y + nA.z * p1.z + planeDA; | ||
| var rB1 = nB.x * p1.x + nB.y * p1.y + nB.z * p1.z + planeDB; | ||
| var a1 = (rA1 - dotN * rB1) * invD; | ||
| var b1 = (rB1 - dotN * rA1) * invD; | ||
| p1.x -= a1 * nA.x + b1 * nB.x; | ||
| p1.y -= a1 * nA.y + b1 * nB.y; | ||
| p1.z -= a1 * nA.z + b1 * nB.z; | ||
| } | ||
| // Skip degenerate segments | ||
@@ -129,22 +155,21 @@ var dx = p0.x - p1.x, dy = p0.y - p1.y, dz = p0.z - p1.z; | ||
| export function triTriIntersectionDetailed(triA, triB) { | ||
| var nB = triNormal(triB); | ||
| var dB = -(nB.x * triB.v0.x + nB.y * triB.v0.y + nB.z * triB.v0.z); | ||
| // Robust orientation: signed distances of triA vertices to plane(triB) | ||
| var dA0 = orient3d(triB.v0.x, triB.v0.y, triB.v0.z, triB.v1.x, triB.v1.y, triB.v1.z, triB.v2.x, triB.v2.y, triB.v2.z, triA.v0.x, triA.v0.y, triA.v0.z); | ||
| var dA1 = orient3d(triB.v0.x, triB.v0.y, triB.v0.z, triB.v1.x, triB.v1.y, triB.v1.z, triB.v2.x, triB.v2.y, triB.v2.z, triA.v1.x, triA.v1.y, triA.v1.z); | ||
| var dA2 = orient3d(triB.v0.x, triB.v0.y, triB.v0.z, triB.v1.x, triB.v1.y, triB.v1.z, triB.v2.x, triB.v2.y, triB.v2.z, triA.v2.x, triA.v2.y, triA.v2.z); | ||
| var dA0 = nB.x * triA.v0.x + nB.y * triA.v0.y + nB.z * triA.v0.z + dB; | ||
| var dA1 = nB.x * triA.v1.x + nB.y * triA.v1.y + nB.z * triA.v1.z + dB; | ||
| var dA2 = nB.x * triA.v2.x + nB.y * triA.v2.y + nB.z * triA.v2.z + dB; | ||
| if (dA0 > 0 && dA1 > 0 && dA2 > 0) return null; | ||
| if (dA0 < 0 && dA1 < 0 && dA2 < 0) return null; | ||
| var nA = triNormal(triA); | ||
| var dA = -(nA.x * triA.v0.x + nA.y * triA.v0.y + nA.z * triA.v0.z); | ||
| // Robust orientation: signed distances of triB vertices to plane(triA) | ||
| var dB0 = orient3d(triA.v0.x, triA.v0.y, triA.v0.z, triA.v1.x, triA.v1.y, triA.v1.z, triA.v2.x, triA.v2.y, triA.v2.z, triB.v0.x, triB.v0.y, triB.v0.z); | ||
| var dB1 = orient3d(triA.v0.x, triA.v0.y, triA.v0.z, triA.v1.x, triA.v1.y, triA.v1.z, triA.v2.x, triA.v2.y, triA.v2.z, triB.v1.x, triB.v1.y, triB.v1.z); | ||
| var dB2 = orient3d(triA.v0.x, triA.v0.y, triA.v0.z, triA.v1.x, triA.v1.y, triA.v1.z, triA.v2.x, triA.v2.y, triA.v2.z, triB.v2.x, triB.v2.y, triB.v2.z); | ||
| var dB0 = nA.x * triB.v0.x + nA.y * triB.v0.y + nA.z * triB.v0.z + dA; | ||
| var dB1 = nA.x * triB.v1.x + nA.y * triB.v1.y + nA.z * triB.v1.z + dA; | ||
| var dB2 = nA.x * triB.v2.x + nA.y * triB.v2.y + nA.z * triB.v2.z + dA; | ||
| if (dB0 > 0 && dB1 > 0 && dB2 > 0) return null; | ||
| if (dB0 < 0 && dB1 < 0 && dB2 < 0) return null; | ||
| var nA = triNormal(triA); | ||
| var nB = triNormal(triB); | ||
| var dotN = nA.x * nB.x + nA.y * nB.y + nA.z * nB.z; | ||
@@ -158,3 +183,6 @@ if (Math.abs(dotN) > 0.9999) return null; | ||
| var linePoint = findLinePoint(nA, dA, nB, dB, lineDir); | ||
| var planeDA = -(nA.x * triA.v0.x + nA.y * triA.v0.y + nA.z * triA.v0.z); | ||
| var planeDB = -(nB.x * triB.v0.x + nB.y * triB.v0.y + nB.z * triB.v0.z); | ||
| var linePoint = findLinePoint(nA, planeDA, nB, planeDB, lineDir); | ||
| if (!linePoint) return null; | ||
@@ -221,3 +249,3 @@ | ||
| params.push(param); | ||
| } else if (Math.abs(di) < 1e-10) { | ||
| } else if (di === 0) { | ||
| // Vertex on the plane -- relative projection | ||
@@ -224,0 +252,0 @@ var param2 = (verts[i].x - linePoint.x) * lineDir.x + (verts[i].y - linePoint.y) * lineDir.y + (verts[i].z - linePoint.z) * lineDir.z; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
5472288
13.88%38743
8.05%5
66.67%+ Added
+ Added
+ Added