@tscircuit/rectdiff 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/index.d.ts +21 -0
  2. package/dist/index.js +488 -160
  3. package/lib/RectDiffPipeline.ts +23 -0
  4. package/lib/solvers/AdjacentLayerContainmentMergeSolver/AdjacentLayerContainmentMergeSolver.ts +456 -0
  5. package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +4 -1
  6. package/package.json +1 -1
  7. package/pages/bugreports/bugreport50-multi-support-layer-merge.page.tsx +19 -0
  8. package/tests/__snapshots__/board-outline.snap.svg +2 -2
  9. package/tests/outer-layer-containment-merge-solver.test.ts +73 -0
  10. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
  11. package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
  12. package/tests/solver/bugreport01-be84eb/__snapshots__/bugreport01-be84eb.snap.svg +1 -1
  13. package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +1 -1
  14. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  15. package/tests/solver/bugreport07-d3f3be/__snapshots__/bugreport07-d3f3be.snap.svg +1 -1
  16. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  17. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
  18. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +1 -1
  19. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
  20. package/tests/solver/bugreport12-35ce1c/__snapshots__/bugreport12-35ce1c.snap.svg +1 -1
  21. package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +1 -1
  22. package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
  23. package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
  24. package/tests/solver/bugreport20-obstacle-clipping/__snapshots__/bugreport20-obstacle-clipping.snap.svg +1 -1
  25. package/tests/solver/bugreport21-board-outline/__snapshots__/bugreport21-board-outline.snap.svg +2 -2
  26. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  27. package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +1 -1
  28. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  29. package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +1 -1
  30. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
  31. package/tests/solver/bugreport49-634662/__snapshots__/bugreport49-634662.snap.svg +1 -1
  32. package/tests/solver/bugreport49-634662/bugreport49-634662.test.ts +7 -11
  33. package/tests/solver/bugreport50-multi-support-layer-merge/__snapshots__/bugreport50-multi-support-layer-merge.snap.svg +44 -0
  34. package/tests/solver/bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.json +972 -0
  35. package/tests/solver/bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.test.ts +125 -0
  36. package/tests/solver/interaction/__snapshots__/interaction.snap.svg +1 -1
  37. package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
  38. package/tests/solver/no-better-path/__snapshots__/no-better-path.snap.svg +1 -1
  39. package/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +2 -2
package/dist/index.js CHANGED
@@ -4,6 +4,388 @@ import {
4
4
  definePipelineStep as definePipelineStep3
5
5
  } from "@tscircuit/solver-utils";
6
6
 
7
+ // lib/solvers/AdjacentLayerContainmentMergeSolver/AdjacentLayerContainmentMergeSolver.ts
8
+ import { BaseSolver } from "@tscircuit/solver-utils";
9
+
10
+ // lib/utils/getColorForZLayer.ts
11
+ var getColorForZLayer = (zLayers) => {
12
+ const minZ = Math.min(...zLayers);
13
+ const colors = [
14
+ { fill: "#dbeafe", stroke: "#3b82f6" },
15
+ { fill: "#fef3c7", stroke: "#f59e0b" },
16
+ { fill: "#d1fae5", stroke: "#10b981" },
17
+ { fill: "#e9d5ff", stroke: "#a855f7" },
18
+ { fill: "#fed7aa", stroke: "#f97316" },
19
+ { fill: "#fecaca", stroke: "#ef4444" }
20
+ ];
21
+ return colors[minZ % colors.length];
22
+ };
23
+
24
+ // lib/utils/rectdiff-geometry.ts
25
+ var EPS = 1e-9;
26
+ var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
27
+ var gt = (a, b) => a > b + EPS;
28
+ var gte = (a, b) => a > b - EPS;
29
+ var lt = (a, b) => a < b - EPS;
30
+ var lte = (a, b) => a < b + EPS;
31
+ function overlaps(a, b) {
32
+ return !(a.x + a.width <= b.x + EPS || b.x + b.width <= a.x + EPS || a.y + a.height <= b.y + EPS || b.y + b.height <= a.y + EPS);
33
+ }
34
+ function containsPoint(r, p) {
35
+ return p.x >= r.x - EPS && p.x <= r.x + r.width + EPS && p.y >= r.y - EPS && p.y <= r.y + r.height + EPS;
36
+ }
37
+ function distancePointToRectEdges(p, r) {
38
+ const minX = r.x;
39
+ const maxX = r.x + r.width;
40
+ const minY = r.y;
41
+ const maxY = r.y + r.height;
42
+ if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY) {
43
+ return Math.min(p.x - minX, maxX - p.x, p.y - minY, maxY - p.y);
44
+ }
45
+ const dx = p.x < minX ? minX - p.x : p.x > maxX ? p.x - maxX : 0;
46
+ const dy = p.y < minY ? minY - p.y : p.y > maxY ? p.y - maxY : 0;
47
+ if (dx === 0) return dy;
48
+ if (dy === 0) return dx;
49
+ return Math.hypot(dx, dy);
50
+ }
51
+ function intersect1D(r1, r2) {
52
+ const lo = Math.max(r1[0], r2[0]);
53
+ const hi = Math.min(r1[1], r2[1]);
54
+ return hi > lo + EPS ? [lo, hi] : null;
55
+ }
56
+ function subtractRect2D(A, B) {
57
+ if (!overlaps(A, B)) return [A];
58
+ const Xi = intersect1D([A.x, A.x + A.width], [B.x, B.x + B.width]);
59
+ const Yi = intersect1D([A.y, A.y + A.height], [B.y, B.y + B.height]);
60
+ if (!Xi || !Yi) return [A];
61
+ const [X0, X1] = Xi;
62
+ const [Y0, Y1] = Yi;
63
+ const out = [];
64
+ if (X0 > A.x + EPS) {
65
+ out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height });
66
+ }
67
+ if (A.x + A.width > X1 + EPS) {
68
+ out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height });
69
+ }
70
+ const midW = Math.max(0, X1 - X0);
71
+ if (midW > EPS && Y0 > A.y + EPS) {
72
+ out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y });
73
+ }
74
+ if (midW > EPS && A.y + A.height > Y1 + EPS) {
75
+ out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 });
76
+ }
77
+ return out.filter((r) => r.width > EPS && r.height > EPS);
78
+ }
79
+
80
+ // lib/solvers/AdjacentLayerContainmentMergeSolver/AdjacentLayerContainmentMergeSolver.ts
81
+ var DEFAULT_MIN_FRAGMENT_AREA = 0.2 ** 2;
82
+ var nodeToRect = (node) => ({
83
+ x: node.center.x - node.width / 2,
84
+ y: node.center.y - node.height / 2,
85
+ width: node.width,
86
+ height: node.height
87
+ });
88
+ var rectArea = (rect) => rect.width * rect.height;
89
+ var cloneNode = (node) => ({
90
+ ...node,
91
+ center: { ...node.center },
92
+ availableZ: [...node.availableZ]
93
+ });
94
+ var cloneNodeWithRect = (node, rect, capacityMeshNodeId) => ({
95
+ ...node,
96
+ capacityMeshNodeId,
97
+ center: {
98
+ x: rect.x + rect.width / 2,
99
+ y: rect.y + rect.height / 2
100
+ },
101
+ width: rect.width,
102
+ height: rect.height,
103
+ availableZ: [...node.availableZ],
104
+ layer: `z${node.availableZ.join(",")}`
105
+ });
106
+ var clonePromotedNodeWithRect = (node, rect, capacityMeshNodeId, availableZ) => ({
107
+ ...node,
108
+ capacityMeshNodeId,
109
+ center: {
110
+ x: rect.x + rect.width / 2,
111
+ y: rect.y + rect.height / 2
112
+ },
113
+ width: rect.width,
114
+ height: rect.height,
115
+ availableZ: [...availableZ],
116
+ layer: `z${availableZ.join(",")}`
117
+ });
118
+ var isFreeNode = (node) => !node._containsObstacle && !node._containsTarget;
119
+ var isSingletonNodeOnLayer = (node, z) => node.availableZ.length === 1 && node.availableZ[0] === z;
120
+ var sameRect = (a, b) => Math.abs(a.x - b.x) <= EPS && Math.abs(a.y - b.y) <= EPS && Math.abs(a.width - b.width) <= EPS && Math.abs(a.height - b.height) <= EPS;
121
+ var subtractRects = (target, cutters) => {
122
+ let remaining = [target];
123
+ for (const cutter of cutters) {
124
+ if (remaining.length === 0) return remaining;
125
+ const nextRemaining = [];
126
+ for (const piece of remaining) {
127
+ nextRemaining.push(...subtractRect2D(piece, cutter));
128
+ }
129
+ remaining = nextRemaining;
130
+ }
131
+ return remaining;
132
+ };
133
+ var isFullyCoveredByRects = (target, coveringRects) => {
134
+ return subtractRects(target, coveringRects).length === 0;
135
+ };
136
+ var sortAndDedupeCuts = (values) => {
137
+ const sorted = [...values].sort((a, b) => a - b);
138
+ const out = [];
139
+ for (const value of sorted) {
140
+ const last = out[out.length - 1];
141
+ if (last == null || Math.abs(last - value) > EPS) {
142
+ out.push(value);
143
+ }
144
+ }
145
+ return out;
146
+ };
147
+ var partitionRectByRects = (target, supportRects) => {
148
+ const xCuts = [target.x, target.x + target.width];
149
+ const yCuts = [target.y, target.y + target.height];
150
+ const targetMaxX = target.x + target.width;
151
+ const targetMaxY = target.y + target.height;
152
+ for (const rect of supportRects) {
153
+ const x0 = Math.max(target.x, rect.x);
154
+ const x1 = Math.min(targetMaxX, rect.x + rect.width);
155
+ const y0 = Math.max(target.y, rect.y);
156
+ const y1 = Math.min(targetMaxY, rect.y + rect.height);
157
+ if (x1 <= x0 + EPS || y1 <= y0 + EPS) continue;
158
+ xCuts.push(x0, x1);
159
+ yCuts.push(y0, y1);
160
+ }
161
+ const xs = sortAndDedupeCuts(xCuts);
162
+ const ys = sortAndDedupeCuts(yCuts);
163
+ const cells = [];
164
+ for (let xi = 0; xi < xs.length - 1; xi++) {
165
+ const x0 = xs[xi];
166
+ const x1 = xs[xi + 1];
167
+ if (x1 <= x0 + EPS) continue;
168
+ for (let yi = 0; yi < ys.length - 1; yi++) {
169
+ const y0 = ys[yi];
170
+ const y1 = ys[yi + 1];
171
+ if (y1 <= y0 + EPS) continue;
172
+ cells.push({
173
+ x: x0,
174
+ y: y0,
175
+ width: x1 - x0,
176
+ height: y1 - y0
177
+ });
178
+ }
179
+ }
180
+ return cells;
181
+ };
182
+ var canMergeHorizontally = (a, b) => Math.abs(a.y - b.y) <= EPS && Math.abs(a.height - b.height) <= EPS && Math.abs(a.x + a.width - b.x) <= EPS;
183
+ var canMergeVertically = (a, b) => Math.abs(a.x - b.x) <= EPS && Math.abs(a.width - b.width) <= EPS && Math.abs(a.y + a.height - b.y) <= EPS;
184
+ var mergeTouchingRects = (rects) => {
185
+ const out = rects.map((rect) => ({ ...rect }));
186
+ let changed = true;
187
+ while (changed) {
188
+ changed = false;
189
+ outer: for (let i = 0; i < out.length; i++) {
190
+ for (let j = i + 1; j < out.length; j++) {
191
+ const a = out[i];
192
+ const b = out[j];
193
+ if (canMergeHorizontally(a, b) || canMergeHorizontally(b, a)) {
194
+ const merged = {
195
+ x: Math.min(a.x, b.x),
196
+ y: a.y,
197
+ width: a.width + b.width,
198
+ height: a.height
199
+ };
200
+ out.splice(j, 1);
201
+ out.splice(i, 1, merged);
202
+ changed = true;
203
+ break outer;
204
+ }
205
+ if (canMergeVertically(a, b) || canMergeVertically(b, a)) {
206
+ const merged = {
207
+ x: a.x,
208
+ y: Math.min(a.y, b.y),
209
+ width: a.width,
210
+ height: a.height + b.height
211
+ };
212
+ out.splice(j, 1);
213
+ out.splice(i, 1, merged);
214
+ changed = true;
215
+ break outer;
216
+ }
217
+ }
218
+ }
219
+ }
220
+ return out;
221
+ };
222
+ var isPromotableRect = (params) => {
223
+ const { rect, viaMinSize, minFragmentArea } = params;
224
+ return rect.width > EPS && rect.height > EPS && rectArea(rect) + EPS >= minFragmentArea && rect.width + EPS >= viaMinSize && rect.height + EPS >= viaMinSize;
225
+ };
226
+ var isResidualRect = (rect, minFragmentArea) => rect.width > EPS && rect.height > EPS && rectArea(rect) + EPS >= minFragmentArea;
227
+ var computePromotablePieces = (params) => {
228
+ const { target, supportRects, viaMinSize, minFragmentArea } = params;
229
+ const overlappingSupports = supportRects.filter(
230
+ (rect) => overlaps(rect, target)
231
+ );
232
+ if (overlappingSupports.length === 0) return [];
233
+ if (isFullyCoveredByRects(target, overlappingSupports) && isPromotableRect({ rect: target, viaMinSize, minFragmentArea })) {
234
+ return [target];
235
+ }
236
+ const partitioned = partitionRectByRects(target, overlappingSupports);
237
+ const coveredPieces = partitioned.filter(
238
+ (piece) => isFullyCoveredByRects(piece, overlappingSupports)
239
+ );
240
+ if (coveredPieces.length === 0) return [];
241
+ const mergedCoveredPieces = mergeTouchingRects(coveredPieces);
242
+ return mergedCoveredPieces.filter(
243
+ (piece) => isFullyCoveredByRects(piece, overlappingSupports) && isPromotableRect({ rect: piece, viaMinSize, minFragmentArea })
244
+ );
245
+ };
246
+ var AdjacentLayerContainmentMergeSolver = class extends BaseSolver {
247
+ constructor(input) {
248
+ super();
249
+ this.input = input;
250
+ }
251
+ input;
252
+ outputNodes = [];
253
+ promotedNodeIds = /* @__PURE__ */ new Set();
254
+ residualNodeIds = /* @__PURE__ */ new Set();
255
+ _setup() {
256
+ this.outputNodes = this.input.meshNodes.map(cloneNode);
257
+ this.promotedNodeIds.clear();
258
+ this.residualNodeIds.clear();
259
+ }
260
+ _step() {
261
+ this.outputNodes = this.processAdjacentLayerContainmentMerges();
262
+ this.solved = true;
263
+ }
264
+ processAdjacentLayerContainmentMerges() {
265
+ const srj = this.input.simpleRouteJson;
266
+ const layerCount = Math.max(1, srj.layerCount || 1);
267
+ if (layerCount < 2) {
268
+ return this.input.meshNodes.map(cloneNode);
269
+ }
270
+ const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0);
271
+ const minFragmentArea = Math.max(
272
+ EPS,
273
+ this.input.minFragmentArea ?? DEFAULT_MIN_FRAGMENT_AREA
274
+ );
275
+ let workingNodes = this.input.meshNodes.map(cloneNode);
276
+ let nextResidualId = 0;
277
+ let nextPromotedId = 0;
278
+ for (let lowerZ = 0; lowerZ < layerCount - 1; lowerZ++) {
279
+ const upperZ = lowerZ + 1;
280
+ const mutableNodes = workingNodes.filter(
281
+ (node) => isFreeNode(node) && (isSingletonNodeOnLayer(node, lowerZ) || isSingletonNodeOnLayer(node, upperZ))
282
+ );
283
+ if (mutableNodes.length === 0) continue;
284
+ const immutableNodes = workingNodes.filter(
285
+ (node) => !mutableNodes.includes(node)
286
+ );
287
+ const supportRectsByLayer = /* @__PURE__ */ new Map();
288
+ supportRectsByLayer.set(
289
+ lowerZ,
290
+ mutableNodes.filter((node) => isSingletonNodeOnLayer(node, lowerZ)).map(nodeToRect)
291
+ );
292
+ supportRectsByLayer.set(
293
+ upperZ,
294
+ mutableNodes.filter((node) => isSingletonNodeOnLayer(node, upperZ)).map(nodeToRect)
295
+ );
296
+ const promotedRects = [];
297
+ const promotedNodes = [];
298
+ const candidateNodes = mutableNodes.filter(
299
+ (node) => isPromotableRect({
300
+ rect: nodeToRect(node),
301
+ viaMinSize,
302
+ minFragmentArea
303
+ })
304
+ ).sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)));
305
+ for (const candidate of candidateNodes) {
306
+ const candidateRect = nodeToRect(candidate);
307
+ const candidatePieces = subtractRects(
308
+ candidateRect,
309
+ promotedRects
310
+ ).filter((piece) => isResidualRect(piece, minFragmentArea));
311
+ const candidateZ = candidate.availableZ[0];
312
+ const oppositeZ = candidateZ === lowerZ ? upperZ : lowerZ;
313
+ const supportRects = supportRectsByLayer.get(oppositeZ) ?? [];
314
+ for (const piece of candidatePieces) {
315
+ const promotablePieces = computePromotablePieces({
316
+ target: piece,
317
+ supportRects,
318
+ viaMinSize,
319
+ minFragmentArea
320
+ });
321
+ for (const promotablePiece of promotablePieces) {
322
+ promotedRects.push(promotablePiece);
323
+ const promotedNode = clonePromotedNodeWithRect(
324
+ candidate,
325
+ promotablePiece,
326
+ `${candidate.capacityMeshNodeId}-adjacent-merge-${nextPromotedId++}`,
327
+ [lowerZ, upperZ]
328
+ );
329
+ promotedNodes.push(promotedNode);
330
+ this.promotedNodeIds.add(promotedNode.capacityMeshNodeId);
331
+ }
332
+ }
333
+ }
334
+ const residualNodes = [];
335
+ for (const node of mutableNodes) {
336
+ const nodeRect = nodeToRect(node);
337
+ const remainingPieces = subtractRects(nodeRect, promotedRects).filter(
338
+ (piece) => isResidualRect(piece, minFragmentArea)
339
+ );
340
+ if (remainingPieces.length === 1 && sameRect(remainingPieces[0], nodeRect)) {
341
+ residualNodes.push(node);
342
+ continue;
343
+ }
344
+ for (const piece of remainingPieces) {
345
+ const residualNode = cloneNodeWithRect(
346
+ node,
347
+ piece,
348
+ `${node.capacityMeshNodeId}-adjacent-residual-${nextResidualId++}`
349
+ );
350
+ residualNodes.push(residualNode);
351
+ this.residualNodeIds.add(residualNode.capacityMeshNodeId);
352
+ }
353
+ }
354
+ workingNodes = [...immutableNodes, ...promotedNodes, ...residualNodes];
355
+ }
356
+ return workingNodes;
357
+ }
358
+ getOutput() {
359
+ return { outputNodes: this.outputNodes };
360
+ }
361
+ visualize() {
362
+ return {
363
+ title: "AdjacentLayerContainmentMergeSolver",
364
+ coordinateSystem: "cartesian",
365
+ rects: this.outputNodes.map((node) => {
366
+ const colors = getColorForZLayer(node.availableZ);
367
+ const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId);
368
+ const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId);
369
+ return {
370
+ center: node.center,
371
+ width: node.width,
372
+ height: node.height,
373
+ stroke: isPromoted ? "rgba(245, 158, 11, 0.95)" : isResidual ? "rgba(37, 99, 235, 0.95)" : colors.stroke,
374
+ fill: node._containsObstacle ? "rgba(239, 68, 68, 0.35)" : isPromoted ? "rgba(251, 191, 36, 0.28)" : isResidual ? "rgba(59, 130, 246, 0.18)" : colors.fill,
375
+ layer: `z${node.availableZ.join(",")}`,
376
+ label: [
377
+ `node ${node.capacityMeshNodeId}`,
378
+ `z:${node.availableZ.join(",")}`
379
+ ].join("\n")
380
+ };
381
+ }),
382
+ points: [],
383
+ lines: [],
384
+ texts: []
385
+ };
386
+ }
387
+ };
388
+
7
389
  // lib/solvers/GapFillSolver/GapFillSolverPipeline.ts
8
390
  import {
9
391
  BasePipelineSolver,
@@ -11,14 +393,14 @@ import {
11
393
  } from "@tscircuit/solver-utils";
12
394
 
13
395
  // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
14
- import { BaseSolver } from "@tscircuit/solver-utils";
396
+ import { BaseSolver as BaseSolver2 } from "@tscircuit/solver-utils";
15
397
  import Flatbush from "flatbush";
16
398
 
17
399
  // lib/solvers/GapFillSolver/projectToUncoveredSegments.ts
18
- var EPS = 1e-4;
400
+ var EPS2 = 1e-4;
19
401
  function projectToUncoveredSegments(primaryEdge, overlappingEdges) {
20
- const isHorizontal = Math.abs(primaryEdge.start.y - primaryEdge.end.y) < EPS;
21
- const isVertical = Math.abs(primaryEdge.start.x - primaryEdge.end.x) < EPS;
402
+ const isHorizontal = Math.abs(primaryEdge.start.y - primaryEdge.end.y) < EPS2;
403
+ const isVertical = Math.abs(primaryEdge.start.x - primaryEdge.end.x) < EPS2;
22
404
  if (!isHorizontal && !isVertical) return [];
23
405
  const axis = isHorizontal ? "x" : "y";
24
406
  const perp = isHorizontal ? "y" : "x";
@@ -31,16 +413,16 @@ function projectToUncoveredSegments(primaryEdge, overlappingEdges) {
31
413
  const intervals = [];
32
414
  for (const e of overlappingEdges) {
33
415
  if (e === primaryEdge) continue;
34
- const eIsHorizontal = Math.abs(e.start.y - e.end.y) < EPS;
35
- const eIsVertical = Math.abs(e.start.x - e.end.x) < EPS;
416
+ const eIsHorizontal = Math.abs(e.start.y - e.end.y) < EPS2;
417
+ const eIsVertical = Math.abs(e.start.x - e.end.x) < EPS2;
36
418
  if (axis === "x" && !eIsHorizontal) continue;
37
419
  if (axis === "y" && !eIsVertical) continue;
38
- if (Math.abs(e.start[perp] - lineCoord) > EPS) continue;
420
+ if (Math.abs(e.start[perp] - lineCoord) > EPS2) continue;
39
421
  const eMin = Math.min(e.start[axis], e.end[axis]);
40
422
  const eMax = Math.max(e.start[axis], e.end[axis]);
41
423
  const s = clamp2(eMin);
42
424
  const t = clamp2(eMax);
43
- if (t - s > EPS) intervals.push({ s, e: t });
425
+ if (t - s > EPS2) intervals.push({ s, e: t });
44
426
  }
45
427
  if (intervals.length === 0) {
46
428
  return [
@@ -55,19 +437,19 @@ function projectToUncoveredSegments(primaryEdge, overlappingEdges) {
55
437
  const merged = [];
56
438
  for (const it of intervals) {
57
439
  const last = merged[merged.length - 1];
58
- if (!last || it.s > last.e + EPS) merged.push({ ...it });
440
+ if (!last || it.s > last.e + EPS2) merged.push({ ...it });
59
441
  else last.e = Math.max(last.e, it.e);
60
442
  }
61
443
  const uncovered = [];
62
444
  let cursor = pMin;
63
445
  for (const m of merged) {
64
- if (m.s > cursor + EPS) uncovered.push({ s: cursor, e: m.s });
446
+ if (m.s > cursor + EPS2) uncovered.push({ s: cursor, e: m.s });
65
447
  cursor = Math.max(cursor, m.e);
66
- if (cursor >= pMax - EPS) break;
448
+ if (cursor >= pMax - EPS2) break;
67
449
  }
68
- if (pMax > cursor + EPS) uncovered.push({ s: cursor, e: pMax });
450
+ if (pMax > cursor + EPS2) uncovered.push({ s: cursor, e: pMax });
69
451
  if (uncovered.length === 0) return [];
70
- return uncovered.filter((u) => u.e - u.s > EPS).map((u) => {
452
+ return uncovered.filter((u) => u.e - u.s > EPS2).map((u) => {
71
453
  const start = axis === "x" ? { x: u.s, y: lineCoord } : { x: lineCoord, y: u.s };
72
454
  const end = axis === "x" ? { x: u.e, y: lineCoord } : { x: lineCoord, y: u.e };
73
455
  return {
@@ -156,8 +538,8 @@ var visuallyOffsetLine = (line, options) => {
156
538
 
157
539
  // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
158
540
  import "@tscircuit/math-utils";
159
- var EPS2 = 1e-4;
160
- var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver {
541
+ var EPS3 = 1e-4;
542
+ var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver2 {
161
543
  constructor(input) {
162
544
  super();
163
545
  this.input = input;
@@ -175,7 +557,7 @@ var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver {
175
557
  ;
176
558
  [start, end] = [end, start];
177
559
  }
178
- if (Math.abs(start.x - end.x) < EPS2 && start.y > end.y) {
560
+ if (Math.abs(start.x - end.x) < EPS3 && start.y > end.y) {
179
561
  ;
180
562
  [start, end] = [end, start];
181
563
  }
@@ -221,10 +603,10 @@ var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver {
221
603
  const candidateEdge = this.unprocessedEdges.shift();
222
604
  this.lastCandidateEdge = candidateEdge;
223
605
  const nearbyEdges = this.edgeSpatialIndex.search(
224
- candidateEdge.start.x - EPS2,
225
- candidateEdge.start.y - EPS2,
226
- candidateEdge.end.x + EPS2,
227
- candidateEdge.end.y + EPS2
606
+ candidateEdge.start.x - EPS3,
607
+ candidateEdge.start.y - EPS3,
608
+ candidateEdge.end.x + EPS3,
609
+ candidateEdge.end.y + EPS3
228
610
  );
229
611
  const overlappingEdges = nearbyEdges.map((i) => this.allEdges[i]).filter((e) => e.z === candidateEdge.z);
230
612
  this.lastOverlappingEdges = overlappingEdges;
@@ -310,7 +692,7 @@ var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver {
310
692
  };
311
693
 
312
694
  // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
313
- import { BaseSolver as BaseSolver2 } from "@tscircuit/solver-utils";
695
+ import { BaseSolver as BaseSolver3 } from "@tscircuit/solver-utils";
314
696
  import RBush from "rbush";
315
697
 
316
698
  // lib/solvers/GapFillSolver/getBoundsFromCorners.ts
@@ -325,8 +707,8 @@ var getBoundsFromCorners = (corners) => {
325
707
 
326
708
  // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
327
709
  import { segmentToBoxMinDistance } from "@tscircuit/math-utils";
328
- var EPS3 = 1e-4;
329
- var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver2 {
710
+ var EPS4 = 1e-4;
711
+ var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver3 {
330
712
  constructor(input) {
331
713
  super();
332
714
  this.input = input;
@@ -392,12 +774,12 @@ var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver2 {
392
774
  let collidingNodes = null;
393
775
  let searchDistance = 1;
394
776
  const searchCorner1 = {
395
- x: segment.start.x + dx * EPS3 + normDeltaStartEnd.x * EPS3 * 10,
396
- y: segment.start.y + dy * EPS3 + normDeltaStartEnd.y * EPS3 * 10
777
+ x: segment.start.x + dx * EPS4 + normDeltaStartEnd.x * EPS4 * 10,
778
+ y: segment.start.y + dy * EPS4 + normDeltaStartEnd.y * EPS4 * 10
397
779
  };
398
780
  const searchCorner2 = {
399
- x: segment.end.x + dx * EPS3 - normDeltaStartEnd.x * EPS3 * 10,
400
- y: segment.end.y + dy * EPS3 - normDeltaStartEnd.y * EPS3 * 10
781
+ x: segment.end.x + dx * EPS4 - normDeltaStartEnd.x * EPS4 * 10,
782
+ y: segment.end.y + dy * EPS4 - normDeltaStartEnd.y * EPS4 * 10
401
783
  };
402
784
  this.lastSearchCorner1 = searchCorner1;
403
785
  this.lastSearchCorner2 = searchCorner2;
@@ -462,7 +844,7 @@ var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver2 {
462
844
  }
463
845
  };
464
846
  this.lastExpandedSegment = expandedSegment;
465
- if (nodeWidth < EPS3 || nodeHeight < EPS3) {
847
+ if (nodeWidth < EPS4 || nodeHeight < EPS4) {
466
848
  return;
467
849
  }
468
850
  this.expandedSegments.push(expandedSegment);
@@ -668,7 +1050,7 @@ var GapFillSolverPipeline = class extends BasePipelineSolver {
668
1050
  };
669
1051
 
670
1052
  // lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts
671
- import { BaseSolver as BaseSolver3 } from "@tscircuit/solver-utils";
1053
+ import { BaseSolver as BaseSolver4 } from "@tscircuit/solver-utils";
672
1054
 
673
1055
  // lib/solvers/RectDiffSeedingSolver/layers.ts
674
1056
  function layerSortKey(n) {
@@ -745,76 +1127,6 @@ function obstacleToXYRect(ob) {
745
1127
  return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h };
746
1128
  }
747
1129
 
748
- // lib/utils/getColorForZLayer.ts
749
- var getColorForZLayer = (zLayers) => {
750
- const minZ = Math.min(...zLayers);
751
- const colors = [
752
- { fill: "#dbeafe", stroke: "#3b82f6" },
753
- { fill: "#fef3c7", stroke: "#f59e0b" },
754
- { fill: "#d1fae5", stroke: "#10b981" },
755
- { fill: "#e9d5ff", stroke: "#a855f7" },
756
- { fill: "#fed7aa", stroke: "#f97316" },
757
- { fill: "#fecaca", stroke: "#ef4444" }
758
- ];
759
- return colors[minZ % colors.length];
760
- };
761
-
762
- // lib/utils/rectdiff-geometry.ts
763
- var EPS4 = 1e-9;
764
- var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
765
- var gt = (a, b) => a > b + EPS4;
766
- var gte = (a, b) => a > b - EPS4;
767
- var lt = (a, b) => a < b - EPS4;
768
- var lte = (a, b) => a < b + EPS4;
769
- function overlaps(a, b) {
770
- return !(a.x + a.width <= b.x + EPS4 || b.x + b.width <= a.x + EPS4 || a.y + a.height <= b.y + EPS4 || b.y + b.height <= a.y + EPS4);
771
- }
772
- function containsPoint(r, p) {
773
- return p.x >= r.x - EPS4 && p.x <= r.x + r.width + EPS4 && p.y >= r.y - EPS4 && p.y <= r.y + r.height + EPS4;
774
- }
775
- function distancePointToRectEdges(p, r) {
776
- const minX = r.x;
777
- const maxX = r.x + r.width;
778
- const minY = r.y;
779
- const maxY = r.y + r.height;
780
- if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY) {
781
- return Math.min(p.x - minX, maxX - p.x, p.y - minY, maxY - p.y);
782
- }
783
- const dx = p.x < minX ? minX - p.x : p.x > maxX ? p.x - maxX : 0;
784
- const dy = p.y < minY ? minY - p.y : p.y > maxY ? p.y - maxY : 0;
785
- if (dx === 0) return dy;
786
- if (dy === 0) return dx;
787
- return Math.hypot(dx, dy);
788
- }
789
- function intersect1D(r1, r2) {
790
- const lo = Math.max(r1[0], r2[0]);
791
- const hi = Math.min(r1[1], r2[1]);
792
- return hi > lo + EPS4 ? [lo, hi] : null;
793
- }
794
- function subtractRect2D(A, B) {
795
- if (!overlaps(A, B)) return [A];
796
- const Xi = intersect1D([A.x, A.x + A.width], [B.x, B.x + B.width]);
797
- const Yi = intersect1D([A.y, A.y + A.height], [B.y, B.y + B.height]);
798
- if (!Xi || !Yi) return [A];
799
- const [X0, X1] = Xi;
800
- const [Y0, Y1] = Yi;
801
- const out = [];
802
- if (X0 > A.x + EPS4) {
803
- out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height });
804
- }
805
- if (A.x + A.width > X1 + EPS4) {
806
- out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height });
807
- }
808
- const midW = Math.max(0, X1 - X0);
809
- if (midW > EPS4 && Y0 > A.y + EPS4) {
810
- out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y });
811
- }
812
- if (midW > EPS4 && A.y + A.height > Y1 + EPS4) {
813
- out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 });
814
- }
815
- return out.filter((r) => r.width > EPS4 && r.height > EPS4);
816
- }
817
-
818
1130
  // lib/utils/padRect.ts
819
1131
  var padRect = (rect, clearance) => {
820
1132
  if (!clearance || clearance <= 0) return rect;
@@ -827,19 +1139,20 @@ var padRect = (rect, clearance) => {
827
1139
  };
828
1140
 
829
1141
  // lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts
830
- var nodeToRect = (node) => ({
1142
+ var nodeToRect2 = (node) => ({
831
1143
  x: node.center.x - node.width / 2,
832
1144
  y: node.center.y - node.height / 2,
833
1145
  width: node.width,
834
1146
  height: node.height
835
1147
  });
836
- var rectArea = (rect) => rect.width * rect.height;
837
- var cloneNode = (node) => ({
1148
+ var rectArea2 = (rect) => rect.width * rect.height;
1149
+ var MIN_OUTER_LAYER_MERGE_AREA_MM2 = 1;
1150
+ var cloneNode2 = (node) => ({
838
1151
  ...node,
839
1152
  center: { ...node.center },
840
1153
  availableZ: [...node.availableZ]
841
1154
  });
842
- var cloneNodeWithRect = (node, rect, capacityMeshNodeId) => ({
1155
+ var cloneNodeWithRect2 = (node, rect, capacityMeshNodeId) => ({
843
1156
  ...node,
844
1157
  capacityMeshNodeId,
845
1158
  center: {
@@ -851,10 +1164,10 @@ var cloneNodeWithRect = (node, rect, capacityMeshNodeId) => ({
851
1164
  availableZ: [...node.availableZ],
852
1165
  layer: `z${node.availableZ.join(",")}`
853
1166
  });
854
- var isFreeNode = (node) => !node._containsObstacle && !node._containsTarget;
1167
+ var isFreeNode2 = (node) => !node._containsObstacle && !node._containsTarget;
855
1168
  var isSingletonOuterNode = (node, outerZ) => node.availableZ.length === 1 && node.availableZ[0] === outerZ;
856
- var sameRect = (a, b) => Math.abs(a.x - b.x) <= EPS4 && Math.abs(a.y - b.y) <= EPS4 && Math.abs(a.width - b.width) <= EPS4 && Math.abs(a.height - b.height) <= EPS4;
857
- var subtractRects = (target, cutters) => {
1169
+ var sameRect2 = (a, b) => Math.abs(a.x - b.x) <= EPS && Math.abs(a.y - b.y) <= EPS && Math.abs(a.width - b.width) <= EPS && Math.abs(a.height - b.height) <= EPS;
1170
+ var subtractRects2 = (target, cutters) => {
858
1171
  let remaining = [target];
859
1172
  for (const cutter of cutters) {
860
1173
  if (remaining.length === 0) return remaining;
@@ -866,10 +1179,10 @@ var subtractRects = (target, cutters) => {
866
1179
  }
867
1180
  return remaining;
868
1181
  };
869
- var isFullyCoveredByRects = (target, coveringRects) => {
870
- return subtractRects(target, coveringRects).length === 0;
1182
+ var isFullyCoveredByRects2 = (target, coveringRects) => {
1183
+ return subtractRects2(target, coveringRects).length === 0;
871
1184
  };
872
- var OuterLayerContainmentMergeSolver = class extends BaseSolver3 {
1185
+ var OuterLayerContainmentMergeSolver = class extends BaseSolver4 {
873
1186
  constructor(input) {
874
1187
  super();
875
1188
  this.input = input;
@@ -879,7 +1192,7 @@ var OuterLayerContainmentMergeSolver = class extends BaseSolver3 {
879
1192
  promotedNodeIds = /* @__PURE__ */ new Set();
880
1193
  residualNodeIds = /* @__PURE__ */ new Set();
881
1194
  _setup() {
882
- this.outputNodes = this.input.meshNodes.map(cloneNode);
1195
+ this.outputNodes = this.input.meshNodes.map(cloneNode2);
883
1196
  this.promotedNodeIds.clear();
884
1197
  this.residualNodeIds.clear();
885
1198
  }
@@ -891,15 +1204,15 @@ var OuterLayerContainmentMergeSolver = class extends BaseSolver3 {
891
1204
  const srj = this.input.simpleRouteJson;
892
1205
  const layerCount = Math.max(1, srj.layerCount || 1);
893
1206
  if (layerCount < 3) {
894
- return this.input.meshNodes.map(cloneNode);
1207
+ return this.input.meshNodes.map(cloneNode2);
895
1208
  }
896
1209
  const topZ = 0;
897
1210
  const bottomZ = layerCount - 1;
898
1211
  const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0);
899
- const originalNodes = this.input.meshNodes.map(cloneNode);
1212
+ const originalNodes = this.input.meshNodes.map(cloneNode2);
900
1213
  const obstaclesByLayer = this.buildObstaclesByLayer(layerCount);
901
1214
  const mutableOuterNodes = originalNodes.filter(
902
- (node) => isFreeNode(node) && (isSingletonOuterNode(node, topZ) || isSingletonOuterNode(node, bottomZ))
1215
+ (node) => isFreeNode2(node) && (isSingletonOuterNode(node, topZ) || isSingletonOuterNode(node, bottomZ))
903
1216
  );
904
1217
  const immutableNodes = originalNodes.filter(
905
1218
  (node) => !mutableOuterNodes.includes(node)
@@ -907,21 +1220,21 @@ var OuterLayerContainmentMergeSolver = class extends BaseSolver3 {
907
1220
  const freeSupportRectsByOuterLayer = /* @__PURE__ */ new Map();
908
1221
  freeSupportRectsByOuterLayer.set(
909
1222
  topZ,
910
- originalNodes.filter((node) => isFreeNode(node) && node.availableZ.includes(topZ)).map(nodeToRect)
1223
+ originalNodes.filter((node) => isFreeNode2(node) && node.availableZ.includes(topZ)).map(nodeToRect2)
911
1224
  );
912
1225
  freeSupportRectsByOuterLayer.set(
913
1226
  bottomZ,
914
- originalNodes.filter((node) => isFreeNode(node) && node.availableZ.includes(bottomZ)).map(nodeToRect)
1227
+ originalNodes.filter((node) => isFreeNode2(node) && node.availableZ.includes(bottomZ)).map(nodeToRect2)
915
1228
  );
916
1229
  const promotedNodes = [];
917
1230
  const promotedRects = [];
918
1231
  const candidateNodes = mutableOuterNodes.filter(
919
- (node) => node.width + EPS4 >= viaMinSize && node.height + EPS4 >= viaMinSize
920
- ).sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)));
1232
+ (node) => node.width + EPS >= viaMinSize && node.height + EPS >= viaMinSize && rectArea2(nodeToRect2(node)) > MIN_OUTER_LAYER_MERGE_AREA_MM2 + EPS
1233
+ ).sort((a, b) => rectArea2(nodeToRect2(b)) - rectArea2(nodeToRect2(a)));
921
1234
  for (const candidate of candidateNodes) {
922
1235
  const candidateZ = candidate.availableZ[0];
923
1236
  const oppositeZ = candidateZ === topZ ? bottomZ : topZ;
924
- const candidateRect = nodeToRect(candidate);
1237
+ const candidateRect = nodeToRect2(candidate);
925
1238
  const oppositeSupportRects = freeSupportRectsByOuterLayer.get(oppositeZ) ?? [];
926
1239
  if (!this.isTransitCompatibleAcrossIntermediateLayers({
927
1240
  rect: candidateRect,
@@ -931,7 +1244,7 @@ var OuterLayerContainmentMergeSolver = class extends BaseSolver3 {
931
1244
  })) {
932
1245
  continue;
933
1246
  }
934
- if (!isFullyCoveredByRects(candidateRect, oppositeSupportRects)) {
1247
+ if (!isFullyCoveredByRects2(candidateRect, oppositeSupportRects)) {
935
1248
  continue;
936
1249
  }
937
1250
  promotedNodes.push({
@@ -948,14 +1261,14 @@ var OuterLayerContainmentMergeSolver = class extends BaseSolver3 {
948
1261
  if (this.promotedNodeIds.has(node.capacityMeshNodeId)) {
949
1262
  continue;
950
1263
  }
951
- const nodeRect = nodeToRect(node);
952
- const remainingPieces = subtractRects(nodeRect, promotedRects);
953
- if (remainingPieces.length === 1 && sameRect(remainingPieces[0], nodeRect)) {
1264
+ const nodeRect = nodeToRect2(node);
1265
+ const remainingPieces = subtractRects2(nodeRect, promotedRects);
1266
+ if (remainingPieces.length === 1 && sameRect2(remainingPieces[0], nodeRect)) {
954
1267
  residualNodes.push(node);
955
1268
  continue;
956
1269
  }
957
1270
  for (const piece of remainingPieces) {
958
- const residualNode = cloneNodeWithRect(
1271
+ const residualNode = cloneNodeWithRect2(
959
1272
  node,
960
1273
  piece,
961
1274
  `${node.capacityMeshNodeId}-outer-merge-${nextResidualId++}`
@@ -998,7 +1311,7 @@ var OuterLayerContainmentMergeSolver = class extends BaseSolver3 {
998
1311
  );
999
1312
  if (nonCopperOverlap) return false;
1000
1313
  const copperRects = overlapping.filter((entry) => entry.obstacle.isCopperPour).map((entry) => entry.rect);
1001
- if (!isFullyCoveredByRects(rect, copperRects)) {
1314
+ if (!isFullyCoveredByRects2(rect, copperRects)) {
1002
1315
  return false;
1003
1316
  }
1004
1317
  }
@@ -1042,7 +1355,7 @@ import {
1042
1355
  } from "@tscircuit/solver-utils";
1043
1356
 
1044
1357
  // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
1045
- import { BaseSolver as BaseSolver4 } from "@tscircuit/solver-utils";
1358
+ import { BaseSolver as BaseSolver5 } from "@tscircuit/solver-utils";
1046
1359
 
1047
1360
  // lib/solvers/RectDiffSeedingSolver/isPointInPolygon.ts
1048
1361
  function isPointInPolygon(p, polygon) {
@@ -1105,7 +1418,7 @@ function computeInverseRects(bounds, polygon) {
1105
1418
  }
1106
1419
  const finalRects = [];
1107
1420
  rawRects.sort((a, b) => {
1108
- if (Math.abs(a.y - b.y) > EPS4) return a.y - b.y;
1421
+ if (Math.abs(a.y - b.y) > EPS) return a.y - b.y;
1109
1422
  return a.x - b.x;
1110
1423
  });
1111
1424
  let current = null;
@@ -1114,9 +1427,9 @@ function computeInverseRects(bounds, polygon) {
1114
1427
  current = r;
1115
1428
  continue;
1116
1429
  }
1117
- const sameY = Math.abs(current.y - r.y) < EPS4;
1118
- const sameHeight = Math.abs(current.height - r.height) < EPS4;
1119
- const touchesX = Math.abs(current.x + current.width - r.x) < EPS4;
1430
+ const sameY = Math.abs(current.y - r.y) < EPS;
1431
+ const sameHeight = Math.abs(current.height - r.height) < EPS;
1432
+ const touchesX = Math.abs(current.x + current.width - r.x) < EPS;
1120
1433
  if (sameY && sameHeight && touchesX) {
1121
1434
  current.width += r.width;
1122
1435
  } else {
@@ -1126,7 +1439,7 @@ function computeInverseRects(bounds, polygon) {
1126
1439
  }
1127
1440
  if (current) finalRects.push(current);
1128
1441
  finalRects.sort((a, b) => {
1129
- if (Math.abs(a.x - b.x) > EPS4) return a.x - b.x;
1442
+ if (Math.abs(a.x - b.x) > EPS) return a.x - b.x;
1130
1443
  return a.y - b.y;
1131
1444
  });
1132
1445
  const mergedVertical = [];
@@ -1136,9 +1449,9 @@ function computeInverseRects(bounds, polygon) {
1136
1449
  current = r;
1137
1450
  continue;
1138
1451
  }
1139
- const sameX = Math.abs(current.x - r.x) < EPS4;
1140
- const sameWidth = Math.abs(current.width - r.width) < EPS4;
1141
- const touchesY = Math.abs(current.y + current.height - r.y) < EPS4;
1452
+ const sameX = Math.abs(current.x - r.x) < EPS;
1453
+ const sameWidth = Math.abs(current.width - r.width) < EPS;
1454
+ const touchesY = Math.abs(current.y + current.height - r.y) < EPS;
1142
1455
  if (sameX && sameWidth && touchesY) {
1143
1456
  current.height += r.height;
1144
1457
  } else {
@@ -1204,11 +1517,11 @@ function maxExpandRight(params) {
1204
1517
  const { r, bounds, blockers, maxAspect } = params;
1205
1518
  let maxWidth = bounds.x + bounds.width - r.x;
1206
1519
  for (const b of blockers) {
1207
- const verticallyOverlaps = r.y + r.height > b.y + EPS4 && b.y + b.height > r.y + EPS4;
1520
+ const verticallyOverlaps = r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS;
1208
1521
  if (verticallyOverlaps) {
1209
1522
  if (gte(b.x, r.x + r.width)) {
1210
1523
  maxWidth = Math.min(maxWidth, b.x - r.x);
1211
- } else if (b.x + b.width > r.x + r.width - EPS4 && b.x < r.x + r.width + EPS4) {
1524
+ } else if (b.x + b.width > r.x + r.width - EPS && b.x < r.x + r.width + EPS) {
1212
1525
  return 0;
1213
1526
  }
1214
1527
  }
@@ -1225,11 +1538,11 @@ function maxExpandDown(params) {
1225
1538
  const { r, bounds, blockers, maxAspect } = params;
1226
1539
  let maxHeight = bounds.y + bounds.height - r.y;
1227
1540
  for (const b of blockers) {
1228
- const horizOverlaps = r.x + r.width > b.x + EPS4 && b.x + b.width > r.x + EPS4;
1541
+ const horizOverlaps = r.x + r.width > b.x + EPS && b.x + b.width > r.x + EPS;
1229
1542
  if (horizOverlaps) {
1230
1543
  if (gte(b.y, r.y + r.height)) {
1231
1544
  maxHeight = Math.min(maxHeight, b.y - r.y);
1232
- } else if (b.y + b.height > r.y + r.height - EPS4 && b.y < r.y + r.height + EPS4) {
1545
+ } else if (b.y + b.height > r.y + r.height - EPS && b.y < r.y + r.height + EPS) {
1233
1546
  return 0;
1234
1547
  }
1235
1548
  }
@@ -1246,11 +1559,11 @@ function maxExpandLeft(params) {
1246
1559
  const { r, bounds, blockers, maxAspect } = params;
1247
1560
  let minX = bounds.x;
1248
1561
  for (const b of blockers) {
1249
- const verticallyOverlaps = r.y + r.height > b.y + EPS4 && b.y + b.height > r.y + EPS4;
1562
+ const verticallyOverlaps = r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS;
1250
1563
  if (verticallyOverlaps) {
1251
1564
  if (lte(b.x + b.width, r.x)) {
1252
1565
  minX = Math.max(minX, b.x + b.width);
1253
- } else if (b.x < r.x + EPS4 && b.x + b.width > r.x - EPS4) {
1566
+ } else if (b.x < r.x + EPS && b.x + b.width > r.x - EPS) {
1254
1567
  return 0;
1255
1568
  }
1256
1569
  }
@@ -1267,11 +1580,11 @@ function maxExpandUp(params) {
1267
1580
  const { r, bounds, blockers, maxAspect } = params;
1268
1581
  let minY = bounds.y;
1269
1582
  for (const b of blockers) {
1270
- const horizOverlaps = r.x + r.width > b.x + EPS4 && b.x + b.width > r.x + EPS4;
1583
+ const horizOverlaps = r.x + r.width > b.x + EPS && b.x + b.width > r.x + EPS;
1271
1584
  if (horizOverlaps) {
1272
1585
  if (lte(b.y + b.height, r.y)) {
1273
1586
  minY = Math.max(minY, b.y + b.height);
1274
- } else if (b.y < r.y + EPS4 && b.y + b.height > r.y - EPS4) {
1587
+ } else if (b.y < r.y + EPS && b.y + b.height > r.y - EPS) {
1275
1588
  return 0;
1276
1589
  }
1277
1590
  }
@@ -1296,7 +1609,7 @@ var toQueryRect = (params) => {
1296
1609
  const minY = Math.max(bounds.y, rect.y);
1297
1610
  const maxX = Math.min(bounds.x + bounds.width, rect.x + rect.width);
1298
1611
  const maxY = Math.min(bounds.y + bounds.height, rect.y + rect.height);
1299
- if (maxX <= minX + EPS4 || maxY <= minY + EPS4) return null;
1612
+ if (maxX <= minX + EPS || maxY <= minY + EPS) return null;
1300
1613
  return { minX, minY, maxX, maxY };
1301
1614
  };
1302
1615
  function expandRectFromSeed(params) {
@@ -1402,7 +1715,7 @@ function expandRectFromSeed(params) {
1402
1715
  improved = true;
1403
1716
  }
1404
1717
  }
1405
- if (r.width + EPS4 >= minReq.width && r.height + EPS4 >= minReq.height) {
1718
+ if (r.width + EPS >= minReq.width && r.height + EPS >= minReq.height) {
1406
1719
  const area = r.width * r.height;
1407
1720
  if (area > bestArea) {
1408
1721
  best = quantizeRect(r);
@@ -1496,7 +1809,7 @@ function computeCandidates3D(params) {
1496
1809
  ]);
1497
1810
  for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
1498
1811
  for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
1499
- if (Math.abs(x - bounds.x) < EPS4 || Math.abs(y - bounds.y) < EPS4 || x > bounds.x + bounds.width - gridSize - EPS4 || y > bounds.y + bounds.height - gridSize - EPS4) {
1812
+ if (Math.abs(x - bounds.x) < EPS || Math.abs(y - bounds.y) < EPS || x > bounds.x + bounds.width - gridSize - EPS || y > bounds.y + bounds.height - gridSize - EPS) {
1500
1813
  continue;
1501
1814
  }
1502
1815
  if (isFullyOccupiedAtPoint({
@@ -1568,7 +1881,7 @@ function computeUncoveredSegments(params) {
1568
1881
  const s = quantize3(i.start);
1569
1882
  const e = quantize3(i.end);
1570
1883
  return { start: Math.min(s, e), end: Math.max(s, e) };
1571
- }).filter((i) => i.end > i.start + EPS4);
1884
+ }).filter((i) => i.end > i.start + EPS);
1572
1885
  if (normalizedIntervals.length === 0) {
1573
1886
  const center = (lineStartQ + lineEndQ) / 2;
1574
1887
  return [{ start: lineStartQ, end: lineEndQ, center }];
@@ -1578,7 +1891,7 @@ function computeUncoveredSegments(params) {
1578
1891
  let current = { ...sorted[0] };
1579
1892
  for (let i = 1; i < sorted.length; i++) {
1580
1893
  const interval = sorted[i];
1581
- if (interval.start <= current.end + EPS4) {
1894
+ if (interval.start <= current.end + EPS) {
1582
1895
  current.end = Math.max(current.end, interval.end);
1583
1896
  } else {
1584
1897
  merged.push(current);
@@ -1587,7 +1900,7 @@ function computeUncoveredSegments(params) {
1587
1900
  }
1588
1901
  merged.push(current);
1589
1902
  const uncovered = [];
1590
- if (merged[0].start > lineStartQ + EPS4) {
1903
+ if (merged[0].start > lineStartQ + EPS) {
1591
1904
  const start = lineStartQ;
1592
1905
  const end = merged[0].start;
1593
1906
  if (end - start >= minSegmentLength) {
@@ -1601,7 +1914,7 @@ function computeUncoveredSegments(params) {
1601
1914
  uncovered.push({ start, end, center: (start + end) / 2 });
1602
1915
  }
1603
1916
  }
1604
- if (merged[merged.length - 1].end < lineEndQ - EPS4) {
1917
+ if (merged[merged.length - 1].end < lineEndQ - EPS) {
1605
1918
  const start = merged[merged.length - 1].end;
1606
1919
  const end = lineEndQ;
1607
1920
  if (end - start >= minSegmentLength) {
@@ -1620,7 +1933,7 @@ function computeEdgeCandidates3D(params) {
1620
1933
  hardPlacedByLayer
1621
1934
  } = params;
1622
1935
  const out = [];
1623
- const \u03B4 = Math.max(minSize * 0.15, EPS4 * 3);
1936
+ const \u03B4 = Math.max(minSize * 0.15, EPS * 3);
1624
1937
  const dedup = /* @__PURE__ */ new Set();
1625
1938
  const hardRectsByLayer = Array.from(
1626
1939
  { length: layerCount },
@@ -1644,7 +1957,7 @@ function computeEdgeCandidates3D(params) {
1644
1957
  const { z } = p;
1645
1958
  const x = qx;
1646
1959
  const y = qy;
1647
- if (x < bounds.x + EPS4 || y < bounds.y + EPS4 || x > bounds.x + bounds.width - EPS4 || y > bounds.y + bounds.height - EPS4)
1960
+ if (x < bounds.x + EPS || y < bounds.y + EPS || x > bounds.x + bounds.width - EPS || y > bounds.y + bounds.height - EPS)
1648
1961
  return;
1649
1962
  if (fullyOcc({ x, y })) return;
1650
1963
  const hard = hardRectsByLayer[z] ?? [];
@@ -1781,7 +2094,7 @@ function computeEdgeCandidates3D(params) {
1781
2094
  }
1782
2095
  for (const b of blockers) {
1783
2096
  const obLeftX = b.x - \u03B4;
1784
- if (obLeftX > bounds.x + EPS4 && obLeftX < bounds.x + bounds.width - EPS4) {
2097
+ if (obLeftX > bounds.x + EPS && obLeftX < bounds.x + bounds.width - EPS) {
1785
2098
  const obLeftCovering = blockers.filter(
1786
2099
  (bl) => bl !== b && bl.x <= obLeftX && bl.x + bl.width >= obLeftX
1787
2100
  ).map((bl) => ({
@@ -1799,7 +2112,7 @@ function computeEdgeCandidates3D(params) {
1799
2112
  }
1800
2113
  }
1801
2114
  const obRightX = b.x + b.width + \u03B4;
1802
- if (obRightX > bounds.x + EPS4 && obRightX < bounds.x + bounds.width - EPS4) {
2115
+ if (obRightX > bounds.x + EPS && obRightX < bounds.x + bounds.width - EPS) {
1803
2116
  const obRightCovering = blockers.filter(
1804
2117
  (bl) => bl !== b && bl.x <= obRightX && bl.x + bl.width >= obRightX
1805
2118
  ).map((bl) => ({
@@ -1817,7 +2130,7 @@ function computeEdgeCandidates3D(params) {
1817
2130
  }
1818
2131
  }
1819
2132
  const obTopY = b.y - \u03B4;
1820
- if (obTopY > bounds.y + EPS4 && obTopY < bounds.y + bounds.height - EPS4) {
2133
+ if (obTopY > bounds.y + EPS && obTopY < bounds.y + bounds.height - EPS) {
1821
2134
  const obTopCovering = blockers.filter(
1822
2135
  (bl) => bl !== b && bl.y <= obTopY && bl.y + bl.height >= obTopY
1823
2136
  ).map((bl) => ({
@@ -1835,7 +2148,7 @@ function computeEdgeCandidates3D(params) {
1835
2148
  }
1836
2149
  }
1837
2150
  const obBottomY = b.y + b.height + \u03B4;
1838
- if (obBottomY > bounds.y + EPS4 && obBottomY < bounds.y + bounds.height - EPS4) {
2151
+ if (obBottomY > bounds.y + EPS && obBottomY < bounds.y + bounds.height - EPS) {
1839
2152
  const obBottomCovering = blockers.filter(
1840
2153
  (bl) => bl !== b && bl.y <= obBottomY && bl.y + bl.height >= obBottomY
1841
2154
  ).map((bl) => ({
@@ -1910,12 +2223,12 @@ function resizeSoftOverlaps(params, newIndex) {
1910
2223
  params.options.minMulti.height
1911
2224
  );
1912
2225
  for (const p of parts) {
1913
- if (p.width + EPS4 >= minW && p.height + EPS4 >= minH) {
2226
+ if (p.width + EPS >= minW && p.height + EPS >= minH) {
1914
2227
  toAdd.push({ rect: p, zLayers: sharedZ.slice() });
1915
2228
  }
1916
2229
  }
1917
2230
  }
1918
- const sameRect2 = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
2231
+ const sameRect3 = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
1919
2232
  removeIdx.sort((a, b) => b - a).forEach((idx) => {
1920
2233
  const rem = params.placed.splice(idx, 1)[0];
1921
2234
  if (params.placedIndexByLayer) {
@@ -1924,7 +2237,7 @@ function resizeSoftOverlaps(params, newIndex) {
1924
2237
  if (tree)
1925
2238
  tree.remove(
1926
2239
  rectToTree(rem.rect, { zLayers: rem.zLayers }),
1927
- sameRect2
2240
+ sameRect3
1928
2241
  );
1929
2242
  }
1930
2243
  }
@@ -1944,7 +2257,7 @@ function resizeSoftOverlaps(params, newIndex) {
1944
2257
 
1945
2258
  // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
1946
2259
  import RBush3 from "rbush";
1947
- var RectDiffSeedingSolver = class extends BaseSolver4 {
2260
+ var RectDiffSeedingSolver = class extends BaseSolver5 {
1948
2261
  constructor(input) {
1949
2262
  super();
1950
2263
  this.input = input;
@@ -2303,7 +2616,7 @@ z:${placement.zLayers.join(",")}`
2303
2616
  };
2304
2617
 
2305
2618
  // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
2306
- import { BaseSolver as BaseSolver5 } from "@tscircuit/solver-utils";
2619
+ import { BaseSolver as BaseSolver6 } from "@tscircuit/solver-utils";
2307
2620
 
2308
2621
  // lib/utils/finalizeRects.ts
2309
2622
  function finalizeRects(params) {
@@ -2375,7 +2688,7 @@ import RBush4 from "rbush";
2375
2688
  var sameTreeRect = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
2376
2689
 
2377
2690
  // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
2378
- var RectDiffExpansionSolver = class extends BaseSolver5 {
2691
+ var RectDiffExpansionSolver = class extends BaseSolver6 {
2379
2692
  constructor(input) {
2380
2693
  super();
2381
2694
  this.input = input;
@@ -2865,6 +3178,7 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2865
3178
  rectDiffGridSolverPipeline;
2866
3179
  gapFillSolver;
2867
3180
  outerLayerContainmentMergeSolver;
3181
+ adjacentLayerContainmentMergeSolver;
2868
3182
  boardVoidRects;
2869
3183
  zIndexByName;
2870
3184
  layerNames;
@@ -2912,6 +3226,16 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2912
3226
  obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance
2913
3227
  }
2914
3228
  ]
3229
+ ),
3230
+ definePipelineStep3(
3231
+ "adjacentLayerContainmentMergeSolver",
3232
+ AdjacentLayerContainmentMergeSolver,
3233
+ (rectDiffPipeline) => [
3234
+ {
3235
+ meshNodes: rectDiffPipeline.outerLayerContainmentMergeSolver?.getOutput().outputNodes ?? rectDiffPipeline.gapFillSolver?.getOutput().outputNodes ?? rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? [],
3236
+ simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson
3237
+ }
3238
+ ]
2915
3239
  )
2916
3240
  ];
2917
3241
  _setup() {
@@ -2937,6 +3261,10 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2937
3261
  return [this.inputProblem];
2938
3262
  }
2939
3263
  getOutput() {
3264
+ const adjacentLayerMergeOutput = this.adjacentLayerContainmentMergeSolver?.getOutput();
3265
+ if (adjacentLayerMergeOutput) {
3266
+ return { meshNodes: adjacentLayerMergeOutput.outputNodes };
3267
+ }
2940
3268
  const outerLayerMergeOutput = this.outerLayerContainmentMergeSolver?.getOutput();
2941
3269
  if (outerLayerMergeOutput) {
2942
3270
  return { meshNodes: outerLayerMergeOutput.outputNodes };