Skip to content

Commit acd2273

Browse files
perf: speed up seam repair and lossy merging (#26)
* perf: speed up seam overlap repair * perf: reduce lossy merge allocation overhead * perf: reduce merge polygon edge allocations --------- Co-authored-by: agustin-littlehat <minotopo@gmail.com>
1 parent 5a972db commit acd2273

3 files changed

Lines changed: 247 additions & 134 deletions

File tree

packages/core/src/merge/mergePolygons.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ interface PolyState {
6060
interface EdgeOwners {
6161
a: Vec3;
6262
b: Vec3;
63-
owners: number[];
63+
first: number;
64+
second: number;
6465
}
6566

6667
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:
303304
export function mergePolygons(input: Polygon[]): Polygon[] {
304305
const out: Polygon[] = [];
305306
const polys: PolyState[] = [];
307+
const vertexKeyCache = new WeakMap<Vec3, string>();
308+
const cachedVertexKey = (vertex: Vec3): string => {
309+
const current = vertexKeyCache.get(vertex);
310+
if (current) return current;
311+
const key = vertexKey(vertex);
312+
vertexKeyCache.set(vertex, key);
313+
return key;
314+
};
315+
const cachedEdgeKey = (a: Vec3, b: Vec3): string => {
316+
const ka = cachedVertexKey(a);
317+
const kb = cachedVertexKey(b);
318+
return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
319+
};
320+
const cachedDirectedEdgeKey = (a: Vec3, b: Vec3): string =>
321+
`${cachedVertexKey(a)}>${cachedVertexKey(b)}`;
322+
const cachedDirectedEdgeSet = (vertices: Vec3[]): Set<string> => {
323+
const edges = new Set<string>();
324+
for (let k = 0; k < vertices.length; k++) {
325+
edges.add(cachedDirectedEdgeKey(vertices[k], vertices[(k + 1) % vertices.length]));
326+
}
327+
return edges;
328+
};
306329
let workUnits = 0;
307330
let workBudgetExhausted = false;
308331
const consumeWork = (units: number): boolean => {
@@ -319,7 +342,7 @@ export function mergePolygons(input: Polygon[]): Polygon[] {
319342
if (polygon) out.push(polygon);
320343
continue;
321344
}
322-
const verts = polygon.vertices.map((v) => [v[0], v[1], v[2]] as Vec3);
345+
const verts = polygon.vertices;
323346
const plane = planeOf(verts);
324347
if (!plane) {
325348
out.push(polygon);
@@ -345,7 +368,7 @@ export function mergePolygons(input: Polygon[]): Polygon[] {
345368
textureTriangles,
346369
normal: plane.normal,
347370
d: plane.d,
348-
directedEdges: directedEdgeSet(verts),
371+
directedEdges: cachedDirectedEdgeSet(verts),
349372
alive: true,
350373
data: polygon.data,
351374
});
@@ -370,36 +393,35 @@ export function mergePolygons(input: Polygon[]): Polygon[] {
370393
if (!p.alive) continue;
371394
const n = p.vertices.length;
372395
if (!consumeWork(n)) return false;
373-
p.directedEdges = new Set();
374396
for (let k = 0; k < n; k++) {
375397
const a = p.vertices[k];
376398
const b = p.vertices[(k + 1) % n];
377-
p.directedEdges.add(directedEdgeKey(a, b));
378-
const key = edgeKey(a, b);
399+
const key = cachedEdgeKey(a, b);
379400
let edge = edgeIndex.get(key);
380401
if (!edge) {
381-
edge = { a, b, owners: [] };
402+
edge = { a, b, first: i, second: -1 };
382403
edgeIndex.set(key, edge);
404+
} else if (edge.second < 0) {
405+
edge.second = i;
383406
}
384-
edge.owners.push(i);
385407
}
386408
}
387409

388410
let mergedThisPass = false;
389411

390412
const edgeDirection = (poly: PolyState, e0: Vec3, e1: Vec3): 1 | -1 | 0 => {
391-
if (poly.directedEdges.has(directedEdgeKey(e0, e1))) return 1;
392-
if (poly.directedEdges.has(directedEdgeKey(e1, e0))) return -1;
413+
if (poly.directedEdges.has(cachedDirectedEdgeKey(e0, e1))) return 1;
414+
if (poly.directedEdges.has(cachedDirectedEdgeKey(e1, e0))) return -1;
393415
return 0;
394416
};
395417

396418
for (const edge of edgeIndex.values()) {
397-
const { owners } = edge;
398-
// owners can have >2 if a degenerate input had three+ polys sharing
399-
// an edge; we still try each pair below — but the simple dedupe
400-
// skips index entries where both polys were already merged away.
401-
if (owners.length < 2) continue;
402-
const [ai, bi] = owners;
419+
// Degenerate inputs can have three+ polys sharing an edge. The old
420+
// array path only tried the first two owners, so the fixed slots keep
421+
// that behavior without allocating for every boundary edge.
422+
if (edge.second < 0) continue;
423+
const ai = edge.first;
424+
const bi = edge.second;
403425
if (ai === bi) continue;
404426
const a = polys[ai];
405427
const b = polys[bi];
@@ -433,7 +455,7 @@ export function mergePolygons(input: Polygon[]): Polygon[] {
433455

434456
a.vertices = merged.vertices;
435457
a.uvs = merged.uvs;
436-
a.directedEdges = directedEdgeSet(merged.vertices);
458+
a.directedEdges = cachedDirectedEdgeSet(merged.vertices);
437459
a.textureTriangles = hasTexture
438460
? [...(a.textureTriangles ?? []), ...(b.textureTriangles ?? [])]
439461
: undefined;
@@ -455,11 +477,11 @@ export function mergePolygons(input: Polygon[]): Polygon[] {
455477
for (const p of polys) {
456478
if (!p.alive) continue;
457479
const out_p: Polygon = {
458-
vertices: p.vertices,
480+
vertices: p.vertices.map((vertex) => [vertex[0], vertex[1], vertex[2]] as Vec3),
459481
color: p.color,
460482
};
461483
if (p.texture) out_p.texture = p.texture;
462-
if (p.uvs) out_p.uvs = p.uvs;
484+
if (p.uvs) out_p.uvs = p.uvs.map((uv) => [uv[0], uv[1]] as Vec2);
463485
if (p.textureTriangles?.length) out_p.textureTriangles = p.textureTriangles;
464486
if (p.data) out_p.data = p.data;
465487
out.push(out_p);

packages/core/src/merge/optimizePolygons.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ interface CrackSourceContext {
143143
baseTolerance: number;
144144
polygonCount: number;
145145
indexes: Map<string, SegmentIndex>;
146+
candidateEdgeStats: WeakMap<Polygon[], EdgeStats>;
146147
}
147148

148149
interface CrackMetrics {
@@ -999,7 +1000,7 @@ function candidateCrackMetrics(
9991000
stopLimits?: CrackMetricLimits,
10001001
): CrackMetricSample {
10011002
const sourceEdges = source.edges;
1002-
const candidateEdges = collectEdgeStats(candidate);
1003+
const candidateEdges = candidateEdgeStatsForSource(source, candidate);
10031004
const tolerance = crackToleranceForSource(source, maxBoundaryDisplacement);
10041005
const internalIndex = searchTolerance > 0
10051006
? internalSegmentIndexForSource(source, searchTolerance)
@@ -1055,9 +1056,18 @@ function createCrackSourceContext(polygons: Polygon[]): CrackSourceContext {
10551056
baseTolerance,
10561057
polygonCount: polygons.length,
10571058
indexes: new Map(),
1059+
candidateEdgeStats: new WeakMap(),
10581060
};
10591061
}
10601062

1063+
function candidateEdgeStatsForSource(source: CrackSourceContext, candidate: Polygon[]): EdgeStats {
1064+
const current = source.candidateEdgeStats.get(candidate);
1065+
if (current) return current;
1066+
const stats = collectEdgeStats(candidate);
1067+
source.candidateEdgeStats.set(candidate, stats);
1068+
return stats;
1069+
}
1070+
10611071
function crackToleranceForSource(source: CrackSourceContext, maxBoundaryDisplacement = 0): number {
10621072
return Math.max(source.baseTolerance, maxBoundaryDisplacement * 1.05);
10631073
}
@@ -1967,18 +1977,18 @@ function applyVertexPositionMovesToOrigins(
19671977
origins: Map<string, Vec3[]>,
19681978
): Map<string, Vec3[]> {
19691979
const moved = new Map<string, Vec3[]>();
1980+
const recordVertex = (vertex: Vec3): void => {
1981+
const sourceKey = vertexKey(vertex);
1982+
const target = moves.get(sourceKey) ?? vertex;
1983+
const targetKey = vertexKey(target);
1984+
for (const origin of origins.get(sourceKey) ?? [vertex]) {
1985+
addVertexOrigin(moved, targetKey, origin);
1986+
}
1987+
};
19701988
for (const polygon of polygons) {
1971-
const vertices = [
1972-
...polygon.vertices,
1973-
...(polygon.textureTriangles ?? []).flatMap((triangle) => triangle.vertices),
1974-
];
1975-
for (const vertex of vertices) {
1976-
const sourceKey = vertexKey(vertex);
1977-
const target = moves.get(sourceKey) ?? vertex;
1978-
const targetKey = vertexKey(target);
1979-
for (const origin of origins.get(sourceKey) ?? [vertex]) {
1980-
addVertexOrigin(moved, targetKey, origin);
1981-
}
1989+
for (const vertex of polygon.vertices) recordVertex(vertex);
1990+
for (const triangle of polygon.textureTriangles ?? []) {
1991+
for (const vertex of triangle.vertices) recordVertex(vertex);
19821992
}
19831993
}
19841994
return moved;
@@ -1989,16 +1999,16 @@ function pruneVertexOriginsToPolygons(
19891999
origins: Map<string, Vec3[]>,
19902000
): Map<string, Vec3[]> {
19912001
const pruned = new Map<string, Vec3[]>();
2002+
const recordVertex = (vertex: Vec3): void => {
2003+
const key = vertexKey(vertex);
2004+
for (const origin of origins.get(key) ?? [vertex]) {
2005+
addVertexOrigin(pruned, key, origin);
2006+
}
2007+
};
19922008
for (const polygon of polygons) {
1993-
const vertices = [
1994-
...polygon.vertices,
1995-
...(polygon.textureTriangles ?? []).flatMap((triangle) => triangle.vertices),
1996-
];
1997-
for (const vertex of vertices) {
1998-
const key = vertexKey(vertex);
1999-
for (const origin of origins.get(key) ?? [vertex]) {
2000-
addVertexOrigin(pruned, key, origin);
2001-
}
2009+
for (const vertex of polygon.vertices) recordVertex(vertex);
2010+
for (const triangle of polygon.textureTriangles ?? []) {
2011+
for (const vertex of triangle.vertices) recordVertex(vertex);
20022012
}
20032013
}
20042014
return pruned;

0 commit comments

Comments
 (0)