@tscircuit/rectdiff 0.0.24 → 0.0.26

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 (31) hide show
  1. package/.github/workflows/bun-pver-release.yml +45 -24
  2. package/AGENTS.md +23 -0
  3. package/dist/index.d.ts +26 -0
  4. package/dist/index.js +414 -191
  5. package/lib/RectDiffPipeline.ts +23 -0
  6. package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +311 -0
  7. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +23 -23
  8. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +9 -10
  9. package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +22 -19
  10. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +1 -1
  11. package/lib/types/srj-types.ts +1 -0
  12. package/lib/utils/expandRectFromSeed.ts +8 -10
  13. package/lib/utils/isFullyOccupiedAtPoint.ts +2 -2
  14. package/lib/utils/rectdiff-geometry.ts +13 -20
  15. package/package.json +3 -1
  16. package/pages/pour.page.tsx +18 -0
  17. package/scripts/benchmark-slow-problem.ts +94 -0
  18. package/test-assets/bugreport49-634662.json +412 -0
  19. package/test-assets/keyboard4.json +16165 -0
  20. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
  21. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  22. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  23. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
  24. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +1 -1
  25. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
  26. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  27. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  28. package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +1 -1
  29. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
  30. package/tests/solver/bugreport49-634662/__snapshots__/bugreport49-634662.snap.svg +44 -0
  31. package/tests/solver/bugreport49-634662/bugreport49-634662.test.ts +134 -0
@@ -8,6 +8,7 @@ import type { GridFill3DOptions, XYRect } from "./rectdiff-types"
8
8
  import type { CapacityMeshNode } from "./types/capacity-mesh-types"
9
9
  import type { GraphicsObject } from "graphics-debug"
10
10
  import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
11
+ import { OuterLayerContainmentMergeSolver } from "./solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver"
11
12
  import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
12
13
  import { createBaseVisualization } from "./rectdiff-visualization"
13
14
  import { buildFinalRectDiffVisualization } from "./buildFinalRectDiffVisualization"
@@ -25,6 +26,7 @@ export interface RectDiffPipelineInput {
25
26
  export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
26
27
  rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
27
28
  gapFillSolver?: GapFillSolverPipeline
29
+ outerLayerContainmentMergeSolver?: OuterLayerContainmentMergeSolver
28
30
  boardVoidRects: XYRect[] | undefined
29
31
  zIndexByName?: Map<string, number>
30
32
  layerNames?: string[]
@@ -69,6 +71,22 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
69
71
  },
70
72
  ],
71
73
  ),
74
+ definePipelineStep(
75
+ "outerLayerContainmentMergeSolver",
76
+ OuterLayerContainmentMergeSolver,
77
+ (rectDiffPipeline: RectDiffPipeline) => [
78
+ {
79
+ meshNodes:
80
+ rectDiffPipeline.gapFillSolver?.getOutput().outputNodes ??
81
+ rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput()
82
+ .meshNodes ??
83
+ [],
84
+ simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
85
+ zIndexByName: rectDiffPipeline.zIndexByName ?? new Map(),
86
+ obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance,
87
+ },
88
+ ],
89
+ ),
72
90
  ]
73
91
 
74
92
  override _setup(): void {
@@ -100,6 +118,11 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
100
118
  }
101
119
 
102
120
  override getOutput(): { meshNodes: CapacityMeshNode[] } {
121
+ const outerLayerMergeOutput =
122
+ this.outerLayerContainmentMergeSolver?.getOutput()
123
+ if (outerLayerMergeOutput) {
124
+ return { meshNodes: outerLayerMergeOutput.outputNodes }
125
+ }
103
126
  const gapFillOutput = this.gapFillSolver?.getOutput()
104
127
  if (gapFillOutput) {
105
128
  return { meshNodes: gapFillOutput.outputNodes }
@@ -0,0 +1,311 @@
1
+ import { BaseSolver } from "@tscircuit/solver-utils"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+ import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
4
+ import type { XYRect } from "lib/rectdiff-types"
5
+ import type { Obstacle, SimpleRouteJson } from "lib/types/srj-types"
6
+ import { obstacleToXYRect, obstacleZs } from "../RectDiffSeedingSolver/layers"
7
+ import { getColorForZLayer } from "lib/utils/getColorForZLayer"
8
+ import { subtractRect2D, overlaps, EPS } from "lib/utils/rectdiff-geometry"
9
+ import { padRect } from "lib/utils/padRect"
10
+
11
+ type OuterLayerContainmentMergeSolverInput = {
12
+ meshNodes: CapacityMeshNode[]
13
+ simpleRouteJson: SimpleRouteJson
14
+ zIndexByName: Map<string, number>
15
+ obstacleClearance?: number
16
+ }
17
+
18
+ type ObstacleWithRect = {
19
+ obstacle: Obstacle
20
+ rect: XYRect
21
+ }
22
+
23
+ const nodeToRect = (node: CapacityMeshNode): XYRect => ({
24
+ x: node.center.x - node.width / 2,
25
+ y: node.center.y - node.height / 2,
26
+ width: node.width,
27
+ height: node.height,
28
+ })
29
+
30
+ const rectArea = (rect: XYRect) => rect.width * rect.height
31
+
32
+ const cloneNode = (node: CapacityMeshNode): CapacityMeshNode => ({
33
+ ...node,
34
+ center: { ...node.center },
35
+ availableZ: [...node.availableZ],
36
+ })
37
+
38
+ const cloneNodeWithRect = (
39
+ node: CapacityMeshNode,
40
+ rect: XYRect,
41
+ capacityMeshNodeId: string,
42
+ ): CapacityMeshNode => ({
43
+ ...node,
44
+ capacityMeshNodeId,
45
+ center: {
46
+ x: rect.x + rect.width / 2,
47
+ y: rect.y + rect.height / 2,
48
+ },
49
+ width: rect.width,
50
+ height: rect.height,
51
+ availableZ: [...node.availableZ],
52
+ layer: `z${node.availableZ.join(",")}`,
53
+ })
54
+
55
+ const isFreeNode = (node: CapacityMeshNode) =>
56
+ !node._containsObstacle && !node._containsTarget
57
+
58
+ const isSingletonOuterNode = (node: CapacityMeshNode, outerZ: number) =>
59
+ node.availableZ.length === 1 && node.availableZ[0] === outerZ
60
+
61
+ const sameRect = (a: XYRect, b: XYRect) =>
62
+ Math.abs(a.x - b.x) <= EPS &&
63
+ Math.abs(a.y - b.y) <= EPS &&
64
+ Math.abs(a.width - b.width) <= EPS &&
65
+ Math.abs(a.height - b.height) <= EPS
66
+
67
+ const subtractRects = (target: XYRect, cutters: XYRect[]) => {
68
+ let remaining: XYRect[] = [target]
69
+
70
+ for (const cutter of cutters) {
71
+ if (remaining.length === 0) return remaining
72
+
73
+ const nextRemaining: XYRect[] = []
74
+ for (const piece of remaining) {
75
+ nextRemaining.push(...subtractRect2D(piece, cutter))
76
+ }
77
+ remaining = nextRemaining
78
+ }
79
+
80
+ return remaining
81
+ }
82
+
83
+ const isFullyCoveredByRects = (target: XYRect, coveringRects: XYRect[]) => {
84
+ return subtractRects(target, coveringRects).length === 0
85
+ }
86
+
87
+ export class OuterLayerContainmentMergeSolver extends BaseSolver {
88
+ private outputNodes: CapacityMeshNode[] = []
89
+ private promotedNodeIds = new Set<string>()
90
+ private residualNodeIds = new Set<string>()
91
+
92
+ constructor(private input: OuterLayerContainmentMergeSolverInput) {
93
+ super()
94
+ }
95
+
96
+ override _setup() {
97
+ this.outputNodes = this.input.meshNodes.map(cloneNode)
98
+ this.promotedNodeIds.clear()
99
+ this.residualNodeIds.clear()
100
+ }
101
+
102
+ override _step() {
103
+ this.outputNodes = this.processOuterLayerContainmentMerges()
104
+ this.solved = true
105
+ }
106
+
107
+ private processOuterLayerContainmentMerges(): CapacityMeshNode[] {
108
+ const srj = this.input.simpleRouteJson
109
+ const layerCount = Math.max(1, srj.layerCount || 1)
110
+ if (layerCount < 3) {
111
+ return this.input.meshNodes.map(cloneNode)
112
+ }
113
+
114
+ const topZ = 0
115
+ const bottomZ = layerCount - 1
116
+ const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0)
117
+ const originalNodes = this.input.meshNodes.map(cloneNode)
118
+ const obstaclesByLayer = this.buildObstaclesByLayer(layerCount)
119
+ const mutableOuterNodes = originalNodes.filter(
120
+ (node) =>
121
+ isFreeNode(node) &&
122
+ (isSingletonOuterNode(node, topZ) ||
123
+ isSingletonOuterNode(node, bottomZ)),
124
+ )
125
+ const immutableNodes = originalNodes.filter(
126
+ (node) => !mutableOuterNodes.includes(node),
127
+ )
128
+ const freeSupportRectsByOuterLayer = new Map<number, XYRect[]>()
129
+ freeSupportRectsByOuterLayer.set(
130
+ topZ,
131
+ originalNodes
132
+ .filter((node) => isFreeNode(node) && node.availableZ.includes(topZ))
133
+ .map(nodeToRect),
134
+ )
135
+ freeSupportRectsByOuterLayer.set(
136
+ bottomZ,
137
+ originalNodes
138
+ .filter((node) => isFreeNode(node) && node.availableZ.includes(bottomZ))
139
+ .map(nodeToRect),
140
+ )
141
+
142
+ const promotedNodes: CapacityMeshNode[] = []
143
+ const promotedRects: XYRect[] = []
144
+ const candidateNodes = mutableOuterNodes
145
+ .filter(
146
+ (node) =>
147
+ node.width + EPS >= viaMinSize && node.height + EPS >= viaMinSize,
148
+ )
149
+ .sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)))
150
+
151
+ for (const candidate of candidateNodes) {
152
+ const candidateZ = candidate.availableZ[0]!
153
+ const oppositeZ = candidateZ === topZ ? bottomZ : topZ
154
+ const candidateRect = nodeToRect(candidate)
155
+ const oppositeSupportRects =
156
+ freeSupportRectsByOuterLayer.get(oppositeZ) ?? []
157
+
158
+ if (
159
+ !this.isTransitCompatibleAcrossIntermediateLayers({
160
+ rect: candidateRect,
161
+ fromZ: candidateZ,
162
+ toZ: oppositeZ,
163
+ obstaclesByLayer,
164
+ })
165
+ ) {
166
+ continue
167
+ }
168
+ if (!isFullyCoveredByRects(candidateRect, oppositeSupportRects)) {
169
+ continue
170
+ }
171
+
172
+ promotedNodes.push({
173
+ ...candidate,
174
+ availableZ: [topZ, bottomZ],
175
+ layer: `z${topZ},${bottomZ}`,
176
+ })
177
+ promotedRects.push(candidateRect)
178
+ this.promotedNodeIds.add(candidate.capacityMeshNodeId)
179
+ }
180
+
181
+ let nextResidualId = 0
182
+ const residualNodes: CapacityMeshNode[] = []
183
+
184
+ for (const node of mutableOuterNodes) {
185
+ if (this.promotedNodeIds.has(node.capacityMeshNodeId)) {
186
+ continue
187
+ }
188
+
189
+ const nodeRect = nodeToRect(node)
190
+ const remainingPieces = subtractRects(nodeRect, promotedRects)
191
+
192
+ if (
193
+ remainingPieces.length === 1 &&
194
+ sameRect(remainingPieces[0]!, nodeRect)
195
+ ) {
196
+ residualNodes.push(node)
197
+ continue
198
+ }
199
+
200
+ for (const piece of remainingPieces) {
201
+ const residualNode = cloneNodeWithRect(
202
+ node,
203
+ piece,
204
+ `${node.capacityMeshNodeId}-outer-merge-${nextResidualId++}`,
205
+ )
206
+ residualNodes.push(residualNode)
207
+ this.residualNodeIds.add(residualNode.capacityMeshNodeId)
208
+ }
209
+ }
210
+
211
+ return [...immutableNodes, ...promotedNodes, ...residualNodes]
212
+ }
213
+
214
+ private buildObstaclesByLayer(layerCount: number): ObstacleWithRect[][] {
215
+ const out = Array.from(
216
+ { length: layerCount },
217
+ () => [] as ObstacleWithRect[],
218
+ )
219
+
220
+ for (const obstacle of this.input.simpleRouteJson.obstacles ?? []) {
221
+ const baseRect = obstacleToXYRect(obstacle)
222
+ if (!baseRect) continue
223
+ const rect = padRect(baseRect, this.input.obstacleClearance ?? 0)
224
+ const zLayers = obstacleZs(obstacle, this.input.zIndexByName)
225
+
226
+ for (const z of zLayers) {
227
+ if (z < 0 || z >= layerCount) continue
228
+ out[z]!.push({ obstacle, rect })
229
+ }
230
+ }
231
+
232
+ return out
233
+ }
234
+
235
+ private isTransitCompatibleAcrossIntermediateLayers(params: {
236
+ rect: XYRect
237
+ fromZ: number
238
+ toZ: number
239
+ obstaclesByLayer: ObstacleWithRect[][]
240
+ }) {
241
+ const { rect, fromZ, toZ, obstaclesByLayer } = params
242
+ const lo = Math.min(fromZ, toZ)
243
+ const hi = Math.max(fromZ, toZ)
244
+
245
+ if (hi - lo < 2) return false
246
+
247
+ for (let z = lo + 1; z < hi; z++) {
248
+ const overlapping = (obstaclesByLayer[z] ?? []).filter((entry) =>
249
+ overlaps(entry.rect, rect),
250
+ )
251
+ if (overlapping.length === 0) return false
252
+
253
+ const nonCopperOverlap = overlapping.some(
254
+ (entry) => !entry.obstacle.isCopperPour,
255
+ )
256
+ if (nonCopperOverlap) return false
257
+
258
+ const copperRects = overlapping
259
+ .filter((entry) => entry.obstacle.isCopperPour)
260
+ .map((entry) => entry.rect)
261
+
262
+ if (!isFullyCoveredByRects(rect, copperRects)) {
263
+ return false
264
+ }
265
+ }
266
+
267
+ return true
268
+ }
269
+
270
+ override getOutput(): { outputNodes: CapacityMeshNode[] } {
271
+ return { outputNodes: this.outputNodes }
272
+ }
273
+
274
+ override visualize(): GraphicsObject {
275
+ return {
276
+ title: "OuterLayerContainmentMergeSolver",
277
+ coordinateSystem: "cartesian",
278
+ rects: this.outputNodes.map((node) => {
279
+ const colors = getColorForZLayer(node.availableZ)
280
+ const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId)
281
+ const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId)
282
+
283
+ return {
284
+ center: node.center,
285
+ width: node.width,
286
+ height: node.height,
287
+ stroke: isPromoted
288
+ ? "rgba(22, 163, 74, 0.95)"
289
+ : isResidual
290
+ ? "rgba(37, 99, 235, 0.95)"
291
+ : colors.stroke,
292
+ fill: node._containsObstacle
293
+ ? "rgba(239, 68, 68, 0.35)"
294
+ : isPromoted
295
+ ? "rgba(34, 197, 94, 0.28)"
296
+ : isResidual
297
+ ? "rgba(59, 130, 246, 0.18)"
298
+ : colors.fill,
299
+ layer: `z${node.availableZ.join(",")}`,
300
+ label: [
301
+ `node ${node.capacityMeshNodeId}`,
302
+ `z:${node.availableZ.join(",")}`,
303
+ ].join("\n"),
304
+ }
305
+ }),
306
+ points: [],
307
+ lines: [],
308
+ texts: [],
309
+ }
310
+ }
311
+ }
@@ -57,6 +57,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
57
57
  private candidates!: Candidate3D[]
58
58
  private placed!: Placed3D[]
59
59
  private placedIndexByLayer!: Array<RBush<RTreeRect>>
60
+ private hardPlacedByLayer!: XYRect[][]
60
61
  private expansionIndex!: number
61
62
  private edgeAnalysisDone!: boolean
62
63
  private totalSeedsThisGrid!: number
@@ -131,6 +132,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
131
132
  { length: layerCount },
132
133
  () => new RBush<RTreeRect>(),
133
134
  )
135
+ this.hardPlacedByLayer = Array.from({ length: layerCount }, () => [])
134
136
  this.expansionIndex = 0
135
137
  this.edgeAnalysisDone = false
136
138
  this.totalSeedsThisGrid = 0
@@ -164,19 +166,13 @@ export class RectDiffSeedingSolver extends BaseSolver {
164
166
  } = this.options
165
167
  const grid = gridSizes[this.gridIndex]!
166
168
 
167
- // Build hard-placed map once per micro-step (cheap)
168
- const hardPlacedByLayer = allLayerNode({
169
- layerCount: this.layerCount,
170
- placed: this.placed,
171
- })
172
-
173
169
  // Ensure candidates exist for this grid
174
170
  if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
175
171
  this.candidates = computeCandidates3D({
176
172
  bounds: this.bounds,
177
173
  gridSize: grid,
178
174
  layerCount: this.layerCount,
179
- hardPlacedByLayer,
175
+ hardPlacedByLayer: this.hardPlacedByLayer,
180
176
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
181
177
  placedIndexByLayer: this.placedIndexByLayer,
182
178
  })
@@ -185,9 +181,10 @@ export class RectDiffSeedingSolver extends BaseSolver {
185
181
  }
186
182
 
187
183
  // If no candidates remain, advance grid or run edge pass or switch phase
188
- if (this.candidates.length === 0) {
184
+ if (this.consumedSeedsThisGrid >= this.candidates.length) {
189
185
  if (this.gridIndex + 1 < gridSizes.length) {
190
186
  this.gridIndex += 1
187
+ this.candidates = []
191
188
  this.totalSeedsThisGrid = 0
192
189
  this.consumedSeedsThisGrid = 0
193
190
  return
@@ -200,13 +197,14 @@ export class RectDiffSeedingSolver extends BaseSolver {
200
197
  layerCount: this.layerCount,
201
198
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
202
199
  placedIndexByLayer: this.placedIndexByLayer,
203
- hardPlacedByLayer,
200
+ hardPlacedByLayer: this.hardPlacedByLayer,
204
201
  })
205
202
  this.edgeAnalysisDone = true
206
203
  this.totalSeedsThisGrid = this.candidates.length
207
204
  this.consumedSeedsThisGrid = 0
208
205
  return
209
206
  }
207
+ this.candidates = []
210
208
  this.solved = true
211
209
  this.expansionIndex = 0
212
210
  return
@@ -214,8 +212,17 @@ export class RectDiffSeedingSolver extends BaseSolver {
214
212
  }
215
213
 
216
214
  // Consume exactly one candidate
217
- const cand = this.candidates.shift()!
218
- this.consumedSeedsThisGrid += 1
215
+ const cand = this.candidates[this.consumedSeedsThisGrid++]!
216
+ if (
217
+ isFullyOccupiedAtPoint({
218
+ layerCount: this.layerCount,
219
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
220
+ placedIndexByLayer: this.placedIndexByLayer,
221
+ point: { x: cand.x, y: cand.y },
222
+ })
223
+ ) {
224
+ return
225
+ }
219
226
 
220
227
  // Evaluate attempts — multi-layer span first (computed ignoring soft nodes)
221
228
  const span = longestFreeSpanAroundZ({
@@ -226,7 +233,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
226
233
  minSpan: minMulti.minLayers,
227
234
  maxSpan: maxMultiLayerSpan,
228
235
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
229
- additionalBlockersByLayer: hardPlacedByLayer,
236
+ additionalBlockersByLayer: this.hardPlacedByLayer,
230
237
  })
231
238
 
232
239
  const attempts: Array<{
@@ -285,17 +292,10 @@ export class RectDiffSeedingSolver extends BaseSolver {
285
292
  },
286
293
  newIndex,
287
294
  )
288
-
289
- // New: relax candidate culling — only drop seeds that became fully occupied
290
- this.candidates = this.candidates.filter(
291
- (c) =>
292
- !isFullyOccupiedAtPoint({
293
- layerCount: this.layerCount,
294
- obstacleIndexByLayer: this.input.obstacleIndexByLayer,
295
- placedIndexByLayer: this.placedIndexByLayer,
296
- point: { x: c.x, y: c.y },
297
- }),
298
- )
295
+ this.hardPlacedByLayer = allLayerNode({
296
+ layerCount: this.layerCount,
297
+ placed: this.placed,
298
+ })
299
299
 
300
300
  return // processed one candidate
301
301
  }
@@ -27,6 +27,10 @@ export function computeCandidates3D(params: {
27
27
  hardPlacedByLayer,
28
28
  } = params
29
29
  const out = new Map<string, Candidate3D>() // key by (x,y)
30
+ const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) => [
31
+ ...(obstacleIndexByLayer[z]?.all() ?? []),
32
+ ...(hardPlacedByLayer[z] ?? []),
33
+ ])
30
34
 
31
35
  for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
32
36
  for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
@@ -75,16 +79,11 @@ export function computeCandidates3D(params: {
75
79
  : bestZ
76
80
 
77
81
  // Distance heuristic against hard blockers only (obstacles + full-stack)
78
- const hardAtZ = [
79
- ...(obstacleIndexByLayer[anchorZ]?.all() ?? []),
80
- ...(hardPlacedByLayer[anchorZ] ?? []),
81
- ]
82
- const d = Math.min(
83
- distancePointToRectEdges({ x, y }, bounds),
84
- ...(hardAtZ.length
85
- ? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b))
86
- : [Infinity]),
87
- )
82
+ const hardAtZ = hardRectsByLayer[anchorZ] ?? []
83
+ let d = distancePointToRectEdges({ x, y }, bounds)
84
+ for (const blocker of hardAtZ) {
85
+ d = Math.min(d, distancePointToRectEdges({ x, y }, blocker))
86
+ }
88
87
  const distance = quantize(d)
89
88
 
90
89
  const k = `${x.toFixed(6)}|${y.toFixed(6)}`
@@ -7,6 +7,16 @@ import type { RTreeRect } from "lib/types/capacity-mesh-types"
7
7
  const quantize = (value: number, precision = 1e-6) =>
8
8
  Math.round(value / precision) * precision
9
9
 
10
+ const toRect = (rect: XYRect | RTreeRect): XYRect =>
11
+ "minX" in rect
12
+ ? {
13
+ x: rect.minX,
14
+ y: rect.minY,
15
+ width: rect.maxX - rect.minX,
16
+ height: rect.maxY - rect.minY,
17
+ }
18
+ : rect
19
+
10
20
  /**
11
21
  * Compute exact uncovered segments along a 1D line.
12
22
  */
@@ -109,6 +119,12 @@ export function computeEdgeCandidates3D(params: {
109
119
  // Use small inset from edges for placement
110
120
  const δ = Math.max(minSize * 0.15, EPS * 3)
111
121
  const dedup = new Set<string>()
122
+ const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) =>
123
+ [
124
+ ...(obstacleIndexByLayer[z]?.all() ?? []),
125
+ ...(hardPlacedByLayer[z] ?? []),
126
+ ].map(toRect),
127
+ )
112
128
  const key = (p: { x: number; y: number; z: number }) =>
113
129
  `${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`
114
130
 
@@ -137,21 +153,11 @@ export function computeEdgeCandidates3D(params: {
137
153
  if (fullyOcc({ x, y })) return // new rule: only drop if truly impossible
138
154
 
139
155
  // Distance uses obstacles + hard nodes (soft nodes ignored for ranking)
140
- const hard = [
141
- ...(obstacleIndexByLayer[z]?.all() ?? []),
142
- ...(hardPlacedByLayer[z] ?? []),
143
- ].map((b) => ({
144
- x: quantize(b.x),
145
- y: quantize(b.y),
146
- width: quantize(b.width),
147
- height: quantize(b.height),
148
- }))
149
- const d = Math.min(
150
- distancePointToRectEdges({ x, y }, bounds),
151
- ...(hard.length
152
- ? hard.map((b) => distancePointToRectEdges({ x, y }, b))
153
- : [Infinity]),
154
- )
156
+ const hard = hardRectsByLayer[z] ?? []
157
+ let d = distancePointToRectEdges({ x, y }, bounds)
158
+ for (const blocker of hard) {
159
+ d = Math.min(d, distancePointToRectEdges({ x, y }, blocker))
160
+ }
155
161
  const distance = quantize(d)
156
162
 
157
163
  const k = key({ x, y, z })
@@ -180,10 +186,7 @@ export function computeEdgeCandidates3D(params: {
180
186
  }
181
187
 
182
188
  for (let z = 0; z < layerCount; z++) {
183
- const blockers = [
184
- ...(obstacleIndexByLayer[z]?.all() ?? []),
185
- ...(hardPlacedByLayer[z] ?? []),
186
- ].map((b) => ({
189
+ const blockers = (hardRectsByLayer[z] ?? []).map((b) => ({
187
190
  x: quantize(b.x),
188
191
  y: quantize(b.y),
189
192
  width: quantize(b.width),
@@ -35,7 +35,7 @@ export function longestFreeSpanAroundZ(params: {
35
35
  maxY: y,
36
36
  }
37
37
  const obstacleIdx = obstacleIndexByLayer[layer]
38
- if (obstacleIdx && obstacleIdx.search(query).length > 0) return false
38
+ if (obstacleIdx && obstacleIdx.collides(query)) return false
39
39
 
40
40
  const extras = additionalBlockersByLayer?.[layer] ?? []
41
41
  return !extras.some((b) => containsPoint(b, { x, y }))
@@ -19,6 +19,7 @@ export interface Obstacle {
19
19
  height: number
20
20
  connectedTo: TraceId[]
21
21
  netIsAssignable?: boolean
22
+ isCopperPour?: boolean
22
23
  offBoardConnectsTo?: TraceId[]
23
24
  }
24
25
 
@@ -175,14 +175,13 @@ const toRect = (tree: RTreeRect): XYRect => ({
175
175
  })
176
176
 
177
177
  const addBlocker = (params: {
178
- rect: XYRect
179
- seen: Set<string>
178
+ rect: RTreeRect
179
+ seen: Set<RTreeRect>
180
180
  blockers: XYRect[]
181
181
  }) => {
182
182
  const { rect, seen, blockers } = params
183
- const key = `${rect.x}|${rect.y}|${rect.width}|${rect.height}`
184
- if (seen.has(key)) return
185
- seen.add(key)
183
+ if (seen.has(rect)) return
184
+ seen.add(rect)
186
185
  blockers.push(rect)
187
186
  }
188
187
 
@@ -225,7 +224,7 @@ export function expandRectFromSeed(params: {
225
224
  const initialW = Math.max(minSide, minReq.width)
226
225
  const initialH = Math.max(minSide, minReq.height)
227
226
  const blockers: XYRect[] = []
228
- const seen = new Set<string>()
227
+ const seen = new Set<RTreeRect>()
229
228
  const totalLayers = placedIndexByLayer.length
230
229
 
231
230
  // Ignore the existing placement we are expanding so it doesn't self-block.
@@ -237,7 +236,7 @@ export function expandRectFromSeed(params: {
237
236
  const blockersIndex = obsticalIndexByLayer[z]
238
237
  if (blockersIndex) {
239
238
  for (const entry of blockersIndex.search(query))
240
- addBlocker({ rect: toRect(entry), seen, blockers })
239
+ addBlocker({ rect: entry, seen, blockers })
241
240
  }
242
241
 
243
242
  const placedLayer = placedIndexByLayer[z]
@@ -245,10 +244,9 @@ export function expandRectFromSeed(params: {
245
244
  for (const entry of placedLayer.search(query)) {
246
245
  const isFullStack = entry.zLayers.length >= totalLayers
247
246
  if (!isFullStack) continue
248
- const rect = toRect(entry)
249
247
  if (
250
248
  isSelfRect({
251
- rect,
249
+ rect: entry,
252
250
  startX,
253
251
  startY,
254
252
  initialW,
@@ -256,7 +254,7 @@ export function expandRectFromSeed(params: {
256
254
  })
257
255
  )
258
256
  continue
259
- addBlocker({ rect, seen, blockers })
257
+ addBlocker({ rect: entry, seen, blockers })
260
258
  }
261
259
  }
262
260
  }
@@ -17,10 +17,10 @@ export function isFullyOccupiedAtPoint(params: OccupancyParams): boolean {
17
17
  }
18
18
  for (let z = 0; z < params.layerCount; z++) {
19
19
  const obstacleIdx = params.obstacleIndexByLayer[z]
20
- const hasObstacle = !!obstacleIdx && obstacleIdx.search(query).length > 0
20
+ const hasObstacle = !!obstacleIdx && obstacleIdx.collides(query)
21
21
 
22
22
  const placedIdx = params.placedIndexByLayer[z]
23
- const hasPlaced = !!placedIdx && placedIdx.search(query).length > 0
23
+ const hasPlaced = !!placedIdx && placedIdx.collides(query)
24
24
 
25
25
  if (!hasObstacle && !hasPlaced) return false
26
26
  }