diff --git a/packages/core/src/merge/mergePolygons.ts b/packages/core/src/merge/mergePolygons.ts index 1ab01685..f3240989 100644 --- a/packages/core/src/merge/mergePolygons.ts +++ b/packages/core/src/merge/mergePolygons.ts @@ -60,7 +60,8 @@ interface PolyState { interface EdgeOwners { a: Vec3; b: Vec3; - owners: number[]; + first: number; + second: number; } const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; @@ -303,6 +304,28 @@ function rotateToNonCollinearStart(vertices: Vec3[], uvs?: Vec2[]): { vertices: export function mergePolygons(input: Polygon[]): Polygon[] { const out: Polygon[] = []; const polys: PolyState[] = []; + const vertexKeyCache = new WeakMap(); + const cachedVertexKey = (vertex: Vec3): string => { + const current = vertexKeyCache.get(vertex); + if (current) return current; + const key = vertexKey(vertex); + vertexKeyCache.set(vertex, key); + return key; + }; + const cachedEdgeKey = (a: Vec3, b: Vec3): string => { + const ka = cachedVertexKey(a); + const kb = cachedVertexKey(b); + return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`; + }; + const cachedDirectedEdgeKey = (a: Vec3, b: Vec3): string => + `${cachedVertexKey(a)}>${cachedVertexKey(b)}`; + const cachedDirectedEdgeSet = (vertices: Vec3[]): Set => { + const edges = new Set(); + for (let k = 0; k < vertices.length; k++) { + edges.add(cachedDirectedEdgeKey(vertices[k], vertices[(k + 1) % vertices.length])); + } + return edges; + }; let workUnits = 0; let workBudgetExhausted = false; const consumeWork = (units: number): boolean => { @@ -319,7 +342,7 @@ export function mergePolygons(input: Polygon[]): Polygon[] { if (polygon) out.push(polygon); continue; } - const verts = polygon.vertices.map((v) => [v[0], v[1], v[2]] as Vec3); + const verts = polygon.vertices; const plane = planeOf(verts); if (!plane) { out.push(polygon); @@ -345,7 +368,7 @@ export function mergePolygons(input: Polygon[]): Polygon[] { textureTriangles, normal: plane.normal, d: plane.d, - directedEdges: directedEdgeSet(verts), + directedEdges: cachedDirectedEdgeSet(verts), alive: true, data: polygon.data, }); @@ -370,36 +393,35 @@ export function mergePolygons(input: Polygon[]): Polygon[] { if (!p.alive) continue; const n = p.vertices.length; if (!consumeWork(n)) return false; - p.directedEdges = new Set(); for (let k = 0; k < n; k++) { const a = p.vertices[k]; const b = p.vertices[(k + 1) % n]; - p.directedEdges.add(directedEdgeKey(a, b)); - const key = edgeKey(a, b); + const key = cachedEdgeKey(a, b); let edge = edgeIndex.get(key); if (!edge) { - edge = { a, b, owners: [] }; + edge = { a, b, first: i, second: -1 }; edgeIndex.set(key, edge); + } else if (edge.second < 0) { + edge.second = i; } - edge.owners.push(i); } } let mergedThisPass = false; const edgeDirection = (poly: PolyState, e0: Vec3, e1: Vec3): 1 | -1 | 0 => { - if (poly.directedEdges.has(directedEdgeKey(e0, e1))) return 1; - if (poly.directedEdges.has(directedEdgeKey(e1, e0))) return -1; + if (poly.directedEdges.has(cachedDirectedEdgeKey(e0, e1))) return 1; + if (poly.directedEdges.has(cachedDirectedEdgeKey(e1, e0))) return -1; return 0; }; for (const edge of edgeIndex.values()) { - const { owners } = edge; - // owners can have >2 if a degenerate input had three+ polys sharing - // an edge; we still try each pair below — but the simple dedupe - // skips index entries where both polys were already merged away. - if (owners.length < 2) continue; - const [ai, bi] = owners; + // Degenerate inputs can have three+ polys sharing an edge. The old + // array path only tried the first two owners, so the fixed slots keep + // that behavior without allocating for every boundary edge. + if (edge.second < 0) continue; + const ai = edge.first; + const bi = edge.second; if (ai === bi) continue; const a = polys[ai]; const b = polys[bi]; @@ -433,7 +455,7 @@ export function mergePolygons(input: Polygon[]): Polygon[] { a.vertices = merged.vertices; a.uvs = merged.uvs; - a.directedEdges = directedEdgeSet(merged.vertices); + a.directedEdges = cachedDirectedEdgeSet(merged.vertices); a.textureTriangles = hasTexture ? [...(a.textureTriangles ?? []), ...(b.textureTriangles ?? [])] : undefined; @@ -455,11 +477,11 @@ export function mergePolygons(input: Polygon[]): Polygon[] { for (const p of polys) { if (!p.alive) continue; const out_p: Polygon = { - vertices: p.vertices, + vertices: p.vertices.map((vertex) => [vertex[0], vertex[1], vertex[2]] as Vec3), color: p.color, }; if (p.texture) out_p.texture = p.texture; - if (p.uvs) out_p.uvs = p.uvs; + if (p.uvs) out_p.uvs = p.uvs.map((uv) => [uv[0], uv[1]] as Vec2); if (p.textureTriangles?.length) out_p.textureTriangles = p.textureTriangles; if (p.data) out_p.data = p.data; out.push(out_p); diff --git a/packages/core/src/merge/optimizePolygons.ts b/packages/core/src/merge/optimizePolygons.ts index 67c79ce1..16e4bfd8 100644 --- a/packages/core/src/merge/optimizePolygons.ts +++ b/packages/core/src/merge/optimizePolygons.ts @@ -143,6 +143,7 @@ interface CrackSourceContext { baseTolerance: number; polygonCount: number; indexes: Map; + candidateEdgeStats: WeakMap; } interface CrackMetrics { @@ -999,7 +1000,7 @@ function candidateCrackMetrics( stopLimits?: CrackMetricLimits, ): CrackMetricSample { const sourceEdges = source.edges; - const candidateEdges = collectEdgeStats(candidate); + const candidateEdges = candidateEdgeStatsForSource(source, candidate); const tolerance = crackToleranceForSource(source, maxBoundaryDisplacement); const internalIndex = searchTolerance > 0 ? internalSegmentIndexForSource(source, searchTolerance) @@ -1055,9 +1056,18 @@ function createCrackSourceContext(polygons: Polygon[]): CrackSourceContext { baseTolerance, polygonCount: polygons.length, indexes: new Map(), + candidateEdgeStats: new WeakMap(), }; } +function candidateEdgeStatsForSource(source: CrackSourceContext, candidate: Polygon[]): EdgeStats { + const current = source.candidateEdgeStats.get(candidate); + if (current) return current; + const stats = collectEdgeStats(candidate); + source.candidateEdgeStats.set(candidate, stats); + return stats; +} + function crackToleranceForSource(source: CrackSourceContext, maxBoundaryDisplacement = 0): number { return Math.max(source.baseTolerance, maxBoundaryDisplacement * 1.05); } @@ -1967,18 +1977,18 @@ function applyVertexPositionMovesToOrigins( origins: Map, ): Map { const moved = new Map(); + const recordVertex = (vertex: Vec3): void => { + const sourceKey = vertexKey(vertex); + const target = moves.get(sourceKey) ?? vertex; + const targetKey = vertexKey(target); + for (const origin of origins.get(sourceKey) ?? [vertex]) { + addVertexOrigin(moved, targetKey, origin); + } + }; for (const polygon of polygons) { - const vertices = [ - ...polygon.vertices, - ...(polygon.textureTriangles ?? []).flatMap((triangle) => triangle.vertices), - ]; - for (const vertex of vertices) { - const sourceKey = vertexKey(vertex); - const target = moves.get(sourceKey) ?? vertex; - const targetKey = vertexKey(target); - for (const origin of origins.get(sourceKey) ?? [vertex]) { - addVertexOrigin(moved, targetKey, origin); - } + for (const vertex of polygon.vertices) recordVertex(vertex); + for (const triangle of polygon.textureTriangles ?? []) { + for (const vertex of triangle.vertices) recordVertex(vertex); } } return moved; @@ -1989,16 +1999,16 @@ function pruneVertexOriginsToPolygons( origins: Map, ): Map { const pruned = new Map(); + const recordVertex = (vertex: Vec3): void => { + const key = vertexKey(vertex); + for (const origin of origins.get(key) ?? [vertex]) { + addVertexOrigin(pruned, key, origin); + } + }; for (const polygon of polygons) { - const vertices = [ - ...polygon.vertices, - ...(polygon.textureTriangles ?? []).flatMap((triangle) => triangle.vertices), - ]; - for (const vertex of vertices) { - const key = vertexKey(vertex); - for (const origin of origins.get(key) ?? [vertex]) { - addVertexOrigin(pruned, key, origin); - } + for (const vertex of polygon.vertices) recordVertex(vertex); + for (const triangle of polygon.textureTriangles ?? []) { + for (const vertex of triangle.vertices) recordVertex(vertex); } } return pruned; diff --git a/packages/core/src/merge/seamRepair.ts b/packages/core/src/merge/seamRepair.ts index 41514acf..cfb48418 100644 --- a/packages/core/src/merge/seamRepair.ts +++ b/packages/core/src/merge/seamRepair.ts @@ -4,6 +4,7 @@ type Vec2 = [number, number]; interface LocalBasis { origin: Vec3; + normal: Vec3; xAxis: Vec3; yAxis: Vec3; local: Vec2[]; @@ -24,6 +25,12 @@ interface EdgeRecord { key: string; a: Vec3; b: Vec3; + minX: number; + minY: number; + minZ: number; + maxX: number; + maxY: number; + maxZ: number; length: number; dir: Vec3; normal: Vec3; @@ -41,10 +48,6 @@ interface NearSeamInfo { aEnd: number; bStart: number; bEnd: number; - a0: Vec3; - a1: Vec3; - b0: Vec3; - b1: Vec3; } export type SeamOverlapCandidateKind = "true-gap" | "connected-facet" | "material-boundary"; @@ -167,6 +170,8 @@ const MIN_PARALLEL_DOT = 0.985; const MIN_FACING_DOT = 0.5; const TRUE_GAP_OVERLAP_AMOUNT_RATIO = 0.175; const DEFAULT_TRUE_GAP_OVERLAP_PX = 1.25; +const NEAR_SEAM_SWEEP_RECORD_LIMIT = 10000; +const NEAR_SEAM_SWEEP_PAIR_LIMIT = 2_000_000; const EPS = 1e-6; const TOPOLOGY_EPS = 1e-4; @@ -337,7 +342,7 @@ function localBasis(points: Vec3[]): LocalBasis | null { return [dot(d, xAxis), dot(d, yAxis)]; }); const area = signedArea(local); - return Math.abs(area) > EPS ? { origin, xAxis, yAxis, local, area } : null; + return Math.abs(area) > EPS ? { origin, normal, xAxis, yAxis, local, area } : null; } function signedArea(points: Vec2[]): number { @@ -493,7 +498,7 @@ function buildEdgeRecords( const edgeLength = length(edge); const dir = normalize(edge); const outward = edgeOutward3D(meta.basis, edgeIndex); - const normal = surfaceNormal(meta.cssPoints); + const normal = meta.basis.normal; const capacity = (meta.capacities[edgeIndex] ?? 0) * capacityScale; if (!dir || !outward || !normal || capacity <= EPS || edgeLength <= EPS) continue; records.push({ @@ -503,6 +508,12 @@ function buildEdgeRecords( key: edgeKey(a, b), a, b, + minX: Math.min(a[0], b[0]), + minY: Math.min(a[1], b[1]), + minZ: Math.min(a[2], b[2]), + maxX: Math.max(a[0], b[0]), + maxY: Math.max(a[1], b[1]), + maxZ: Math.max(a[2], b[2]), length: edgeLength, dir, normal, @@ -663,71 +674,117 @@ function buildNearSeamEdgeAmounts( candidates: SeamOverlapCandidate[] | undefined, ): void { const maxGap = options.maxGapPx; - const cellSize = Math.max(MAX_NEAR_SEAM_GAP_PX * 2, maxGap * 2); - const cells = new Map(); - for (const record of records) { - for (const key of segmentCellKeys(record, cellSize, maxGap)) { - const bucket = cells.get(key); - if (bucket) bucket.push(record); - else cells.set(key, [record]); - } + const exactSharedKeys = new Set(); + for (const [key, owners] of edgeOwners) { + if (owners.length > 1) exactSharedKeys.add(key); } - for (const record of records) { - const seen = new Set(); - for (const queryKey of segmentCellKeys(record, cellSize, maxGap)) { - const bucket = cells.get(queryKey); - if (!bucket) continue; - for (const candidate of bucket) { - if (seen.has(candidate.index)) continue; - seen.add(candidate.index); - if (candidate.index <= record.index) continue; - if (candidate.polygon === record.polygon) continue; - if (record.key === candidate.key && (edgeOwners.get(record.key)?.length ?? 0) > 1) continue; - const info = nearSeamInfo(record, candidate, maxGap); - if (!info) continue; - const kind = classifyCandidate(record, candidate); - const targetClosure = info.gap + options.overlapPx; - if (kind === "material-boundary") { - candidates?.push(seamOverlapCandidate(kind, record, candidate, info, targetClosure, 0)); - continue; - } + const measurePair = (first: EdgeRecord, second: EdgeRecord): void => { + const record = first.index < second.index ? first : second; + const candidate = first.index < second.index ? second : first; + if (candidate.polygon === record.polygon) return; + if (record.key === candidate.key && exactSharedKeys.has(record.key)) return; + if (!candidates && !compatibleRepairMaterials(record, candidate)) return; + if (!edgeBoundsCouldOverlap(record, candidate, maxGap)) return; + if (Math.abs(dot(record.dir, candidate.dir)) < MIN_PARALLEL_DOT) return; + + const info = nearSeamInfo(record, candidate, maxGap); + if (!info) return; + const kind = classifyCandidate(record, candidate); + const targetClosure = info.gap + options.overlapPx; + if (kind === "material-boundary") { + candidates?.push(seamOverlapCandidate(kind, record, candidate, info, targetClosure, 0)); + return; + } - let remainingClosure = targetClosure; - if (remainingClosure <= MIN_RESIDUAL_PATCH_GAP_PX) { - candidates?.push(seamOverlapCandidate(kind, record, candidate, info, targetClosure, 0)); - continue; - } + let remainingClosure = targetClosure; + if (remainingClosure <= MIN_RESIDUAL_PATCH_GAP_PX) { + candidates?.push(seamOverlapCandidate(kind, record, candidate, info, targetClosure, 0)); + return; + } - const maxClosureA = record.capacity * info.facingA; - const maxClosureB = candidate.capacity * info.facingB; - let closureA = Math.min(maxClosureA, remainingClosure / 2); - let closureB = Math.min(maxClosureB, remainingClosure / 2); - remainingClosure -= closureA + closureB; - if (remainingClosure > EPS) { - const extraA = Math.min(maxClosureA - closureA, remainingClosure); - closureA += extraA; - remainingClosure -= extraA; - } - if (remainingClosure > EPS) { - const extraB = Math.min(maxClosureB - closureB, remainingClosure); - closureB += extraB; - remainingClosure -= extraB; - } + const maxClosureA = record.capacity * info.facingA; + const maxClosureB = candidate.capacity * info.facingB; + let closureA = Math.min(maxClosureA, remainingClosure / 2); + let closureB = Math.min(maxClosureB, remainingClosure / 2); + remainingClosure -= closureA + closureB; + if (remainingClosure > EPS) { + const extraA = Math.min(maxClosureA - closureA, remainingClosure); + closureA += extraA; + remainingClosure -= extraA; + } + if (remainingClosure > EPS) { + const extraB = Math.min(maxClosureB - closureB, remainingClosure); + closureB += extraB; + remainingClosure -= extraB; + } - const appliedClosure = closureA + closureB; - candidates?.push(seamOverlapCandidate(kind, record, candidate, info, targetClosure, appliedClosure)); - if (closureA > EPS) addEdgeRepair(repairs, record, info.aStart, info.aEnd, closureA / info.facingA); - if (closureB > EPS) addEdgeRepair(repairs, candidate, info.bStart, info.bEnd, closureB / info.facingB); - diagnostics.nearPairs += 1; - diagnostics.maxMeasuredGapPx = Math.max(diagnostics.maxMeasuredGapPx, info.gap); - if (remainingClosure > MIN_RESIDUAL_PATCH_GAP_PX) { - diagnostics.unclosedPairs += 1; - diagnostics.maxResidualGapPx = Math.max(diagnostics.maxResidualGapPx, remainingClosure); + const appliedClosure = closureA + closureB; + candidates?.push(seamOverlapCandidate(kind, record, candidate, info, targetClosure, appliedClosure)); + if (closureA > EPS) addEdgeRepair(repairs, record, info.aStart, info.aEnd, closureA / info.facingA); + if (closureB > EPS) addEdgeRepair(repairs, candidate, info.bStart, info.bEnd, closureB / info.facingB); + diagnostics.nearPairs += 1; + diagnostics.maxMeasuredGapPx = Math.max(diagnostics.maxMeasuredGapPx, info.gap); + if (remainingClosure > MIN_RESIDUAL_PATCH_GAP_PX) { + diagnostics.unclosedPairs += 1; + diagnostics.maxResidualGapPx = Math.max(diagnostics.maxResidualGapPx, remainingClosure); + } + }; + + if (records.length <= NEAR_SEAM_SWEEP_RECORD_LIMIT) { + const sorted = [...records].sort((a, b) => a.minX - b.minX); + const sweepEnds = new Int32Array(sorted.length); + let sweepPairCount = 0; + for (let i = 0; i + 1 < sorted.length; i += 1) { + const record = sorted[i]; + const maxX = record.maxX + maxGap; + let end = i + 1; + while (end < sorted.length && sorted[end].minX <= maxX) end += 1; + sweepEnds[i] = end; + sweepPairCount += end - i - 1; + if (sweepPairCount > NEAR_SEAM_SWEEP_PAIR_LIMIT) break; + } + if (sweepPairCount <= NEAR_SEAM_SWEEP_PAIR_LIMIT) { + for (let i = 0; i + 1 < sorted.length; i += 1) { + for (let j = i + 1; j < sweepEnds[i]; j += 1) { + measurePair(sorted[i], sorted[j]); } } + return; } } + + const cellSize = Math.max(MAX_NEAR_SEAM_GAP_PX * 2, maxGap * 2); + const cells = new Map(); + for (const record of records) { + addRecordToSegmentCells(cells, record, cellSize, maxGap); + } + + const seenPairs = new Set(); + const recordCount = records.length; + for (const bucket of cells.values()) { + for (let i = 0; i + 1 < bucket.length; i += 1) { + for (let j = i + 1; j < bucket.length; j += 1) { + const first = bucket[i]; + const second = bucket[j]; + const record = first.index < second.index ? first : second; + const candidate = first.index < second.index ? second : first; + const pairKey = record.index * recordCount + candidate.index; + if (seenPairs.has(pairKey)) continue; + seenPairs.add(pairKey); + measurePair(record, candidate); + } + } + } +} + +function edgeBoundsCouldOverlap(a: EdgeRecord, b: EdgeRecord, maxGap: number): boolean { + return a.minX <= b.maxX + maxGap && + b.minX <= a.maxX + maxGap && + a.minY <= b.maxY + maxGap && + b.minY <= a.maxY + maxGap && + a.minZ <= b.maxZ + maxGap && + b.minZ <= a.maxZ + maxGap; } function cellCoords(point: Vec3, cellSize: number): [number, number, number] { @@ -738,56 +795,84 @@ function cellCoords(point: Vec3, cellSize: number): [number, number, number] { ]; } -function segmentCellKeys(record: EdgeRecord, cellSize: number, padding: number): string[] { - const minX = Math.min(record.a[0], record.b[0]) - padding; - const minY = Math.min(record.a[1], record.b[1]) - padding; - const minZ = Math.min(record.a[2], record.b[2]) - padding; - const maxX = Math.max(record.a[0], record.b[0]) + padding; - const maxY = Math.max(record.a[1], record.b[1]) + padding; - const maxZ = Math.max(record.a[2], record.b[2]) + padding; +function addRecordToSegmentCells( + cells: Map, + record: EdgeRecord, + cellSize: number, + padding: number, +): void { + const minX = record.minX - padding; + const minY = record.minY - padding; + const minZ = record.minZ - padding; + const maxX = record.maxX + padding; + const maxY = record.maxY + padding; + const maxZ = record.maxZ + padding; const [minCx, minCy, minCz] = cellCoords([minX, minY, minZ], cellSize); const [maxCx, maxCy, maxCz] = cellCoords([maxX, maxY, maxZ], cellSize); - const keys: string[] = []; for (let x = minCx; x <= maxCx; x += 1) { for (let y = minCy; y <= maxCy; y += 1) { for (let z = minCz; z <= maxCz; z += 1) { - keys.push(`${x},${y},${z}`); + const key = `${x},${y},${z}`; + const bucket = cells.get(key); + if (bucket) bucket.push(record); + else cells.set(key, [record]); } } } - return keys; } -function nearSeamInfo(a: EdgeRecord, b: EdgeRecord, maxGap: number): NearSeamInfo | null { - if (Math.abs(dot(a.dir, b.dir)) < MIN_PARALLEL_DOT) return null; +function dotFromPointToEdge(point: Vec3, edgeOrigin: Vec3, edgeDir: Vec3): number { + return ( + (point[0] - edgeOrigin[0]) * edgeDir[0] + + (point[1] - edgeOrigin[1]) * edgeDir[1] + + (point[2] - edgeOrigin[2]) * edgeDir[2] + ); +} - const bStart = dot(sub(b.a, a.a), a.dir); - const bEnd = dot(sub(b.b, a.a), a.dir); +function dotEdgePointToEdge(source: EdgeRecord, distance: number, target: EdgeRecord): number { + return ( + (source.a[0] + source.dir[0] * distance - target.a[0]) * target.dir[0] + + (source.a[1] + source.dir[1] * distance - target.a[1]) * target.dir[1] + + (source.a[2] + source.dir[2] * distance - target.a[2]) * target.dir[2] + ); +} + +function nearSeamInfo(a: EdgeRecord, b: EdgeRecord, maxGap: number): NearSeamInfo | null { + const bStart = dotFromPointToEdge(b.a, a.a, a.dir); + const bEnd = dotFromPointToEdge(b.b, a.a, a.dir); const overlapStart = Math.max(0, Math.min(bStart, bEnd)); const overlapEnd = Math.min(a.length, Math.max(bStart, bEnd)); const overlap = overlapEnd - overlapStart; const minLength = Math.min(a.length, b.length); if (overlap < Math.max(0.75, minLength * 0.12)) return null; - const a0 = add(a.a, scale(a.dir, overlapStart)); - const a1 = add(a.a, scale(a.dir, overlapEnd)); - const aPoint = add(a.a, scale(a.dir, (overlapStart + overlapEnd) / 2)); - const bT = Math.max(0, Math.min(b.length, dot(sub(aPoint, b.a), b.dir))); - const bPoint = add(b.a, scale(b.dir, bT)); - const gapVector = sub(bPoint, aPoint); - const gap = length(gapVector); + const aMidT = (overlapStart + overlapEnd) / 2; + const aMidX = a.a[0] + a.dir[0] * aMidT; + const aMidY = a.a[1] + a.dir[1] * aMidT; + const aMidZ = a.a[2] + a.dir[2] * aMidT; + const bT = Math.max(0, Math.min( + b.length, + (aMidX - b.a[0]) * b.dir[0] + + (aMidY - b.a[1]) * b.dir[1] + + (aMidZ - b.a[2]) * b.dir[2], + )); + const gapX = b.a[0] + b.dir[0] * bT - aMidX; + const gapY = b.a[1] + b.dir[1] * bT - aMidY; + const gapZ = b.a[2] + b.dir[2] * bT - aMidZ; + const gap = Math.hypot(gapX, gapY, gapZ); if (gap <= EPS) return null; if (gap > maxGap) return null; if (gap > Math.min(maxGap, Math.max(4, minLength * 0.28))) return null; - const gapDir = scale(gapVector, 1 / gap); - const facingA = dot(a.outward, gapDir); - const facingB = dot(b.outward, scale(gapDir, -1)); + const invGap = 1 / gap; + const gapDirX = gapX * invGap; + const gapDirY = gapY * invGap; + const gapDirZ = gapZ * invGap; + const facingA = a.outward[0] * gapDirX + a.outward[1] * gapDirY + a.outward[2] * gapDirZ; + const facingB = -(b.outward[0] * gapDirX + b.outward[1] * gapDirY + b.outward[2] * gapDirZ); if (facingA < MIN_FACING_DOT || facingB < MIN_FACING_DOT) return null; - const b0T = Math.max(0, Math.min(b.length, dot(sub(a0, b.a), b.dir))); - const b1T = Math.max(0, Math.min(b.length, dot(sub(a1, b.a), b.dir))); - const b0 = add(b.a, scale(b.dir, b0T)); - const b1 = add(b.a, scale(b.dir, b1T)); + const b0T = Math.max(0, Math.min(b.length, dotEdgePointToEdge(a, overlapStart, b))); + const b1T = Math.max(0, Math.min(b.length, dotEdgePointToEdge(a, overlapEnd, b))); return { gap, facingA, @@ -796,10 +881,6 @@ function nearSeamInfo(a: EdgeRecord, b: EdgeRecord, maxGap: number): NearSeamInf aEnd: overlapEnd, bStart: Math.min(b0T, b1T), bEnd: Math.max(b0T, b1T), - a0, - a1, - b0, - b1, }; }