@tscircuit/rectdiff 0.0.13 → 0.0.15

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.
@@ -60,7 +60,6 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
60
60
  }
61
61
 
62
62
  override initialVisualize(): GraphicsObject {
63
- console.log("RectDiffPipeline - initialVisualize")
64
63
  const graphics = createBaseVisualization(
65
64
  this.inputProblem.simpleRouteJson,
66
65
  "RectDiffPipeline - Initial",
@@ -1,13 +1,21 @@
1
1
  import { BaseSolver } from "@tscircuit/solver-utils"
2
2
  import type { GraphicsObject } from "graphics-debug"
3
- import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
3
+ import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
4
4
  import { expandRectFromSeed } from "../../utils/expandRectFromSeed"
5
5
  import { finalizeRects } from "../../utils/finalizeRects"
6
6
  import { allLayerNode } from "../../utils/buildHardPlacedByLayer"
7
7
  import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
8
8
  import { rectsToMeshNodes } from "./rectsToMeshNodes"
9
9
  import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types"
10
- import type { SimpleRouteJson } from "../../types/srj-types"
10
+ import type { SimpleRouteJson } from "lib/types/srj-types"
11
+ import {
12
+ buildZIndexMap,
13
+ obstacleToXYRect,
14
+ obstacleZs,
15
+ } from "../RectDiffSeedingSolver/layers"
16
+ import RBush from "rbush"
17
+ import { rectToTree } from "../../utils/rectToTree"
18
+ import { sameTreeRect } from "../../utils/sameTreeRect"
11
19
 
12
20
  export type RectDiffExpansionSolverSnapshot = {
13
21
  srj: SimpleRouteJson
@@ -19,12 +27,10 @@ export type RectDiffExpansionSolverSnapshot = {
19
27
  // the engine only uses gridSizes here, other options are ignored
20
28
  [key: string]: any
21
29
  }
22
- obstaclesByLayer: XYRect[][]
23
30
  boardVoidRects: XYRect[]
24
31
  gridIndex: number
25
32
  candidates: Candidate3D[]
26
33
  placed: Placed3D[]
27
- placedByLayer: XYRect[][]
28
34
  expansionIndex: number
29
35
  edgeAnalysisDone: boolean
30
36
  totalSeedsThisGrid: number
@@ -33,6 +39,7 @@ export type RectDiffExpansionSolverSnapshot = {
33
39
 
34
40
  export type RectDiffExpansionSolverInput = {
35
41
  initialSnapshot: RectDiffExpansionSolverSnapshot
42
+ obstacleIndexByLayer: Array<RBush<RTreeRect>>
36
43
  }
37
44
 
38
45
  /**
@@ -52,12 +59,11 @@ export class RectDiffExpansionSolver extends BaseSolver {
52
59
  // the engine only uses gridSizes here, other options are ignored
53
60
  [key: string]: any
54
61
  }
55
- private obstaclesByLayer!: XYRect[][]
56
62
  private boardVoidRects!: XYRect[]
57
63
  private gridIndex!: number
58
64
  private candidates!: Candidate3D[]
59
65
  private placed!: Placed3D[]
60
- private placedByLayer!: XYRect[][]
66
+ private placedIndexByLayer!: Array<RBush<RTreeRect>>
61
67
  private expansionIndex!: number
62
68
  private edgeAnalysisDone!: boolean
63
69
  private totalSeedsThisGrid!: number
@@ -75,6 +81,44 @@ export class RectDiffExpansionSolver extends BaseSolver {
75
81
  this.stats = {
76
82
  gridIndex: this.gridIndex,
77
83
  }
84
+
85
+ if (this.input.obstacleIndexByLayer) {
86
+ } else {
87
+ const { zIndexByName } = buildZIndexMap(this.srj)
88
+ this.input.obstacleIndexByLayer = Array.from(
89
+ { length: this.layerCount },
90
+ () => new RBush<RTreeRect>(),
91
+ )
92
+ const insertObstacle = (rect: XYRect, z: number) => {
93
+ const tree = this.input.obstacleIndexByLayer[z]
94
+ if (tree) tree.insert(rectToTree(rect))
95
+ }
96
+ for (const voidRect of this.boardVoidRects ?? []) {
97
+ for (let z = 0; z < this.layerCount; z++) insertObstacle(voidRect, z)
98
+ }
99
+ for (const obstacle of this.srj.obstacles ?? []) {
100
+ const rect = obstacleToXYRect(obstacle as any)
101
+ if (!rect) continue
102
+ const zLayers =
103
+ obstacle.zLayers?.length && obstacle.zLayers.length > 0
104
+ ? obstacle.zLayers
105
+ : obstacleZs(obstacle as any, zIndexByName)
106
+ zLayers.forEach((z) => {
107
+ if (z >= 0 && z < this.layerCount) insertObstacle(rect, z)
108
+ })
109
+ }
110
+ }
111
+
112
+ this.placedIndexByLayer = Array.from(
113
+ { length: this.layerCount },
114
+ () => new RBush<RTreeRect>(),
115
+ )
116
+ for (const placement of this.placed ?? []) {
117
+ for (const z of placement.zLayers) {
118
+ const tree = this.placedIndexByLayer[z]
119
+ if (tree) tree.insert(rectToTree(placement.rect))
120
+ }
121
+ }
78
122
  }
79
123
 
80
124
  override _step() {
@@ -107,7 +151,8 @@ export class RectDiffExpansionSolver extends BaseSolver {
107
151
  // HARD blockers only: obstacles on p.zLayers + full-stack nodes
108
152
  const hardBlockers: XYRect[] = []
109
153
  for (const z of p.zLayers) {
110
- hardBlockers.push(...(this.obstaclesByLayer[z] ?? []))
154
+ const obstacleTree = this.input.obstacleIndexByLayer[z]
155
+ if (obstacleTree) hardBlockers.push(...obstacleTree.all())
111
156
  hardBlockers.push(...(hardPlacedByLayer[z] ?? []))
112
157
  }
113
158
 
@@ -127,9 +172,11 @@ export class RectDiffExpansionSolver extends BaseSolver {
127
172
  // Update placement + per-layer index (replace old rect object)
128
173
  this.placed[idx] = { rect: expanded, zLayers: p.zLayers }
129
174
  for (const z of p.zLayers) {
130
- const arr = this.placedByLayer[z]!
131
- const j = arr.findIndex((r) => r === oldRect)
132
- if (j >= 0) arr[j] = expanded
175
+ const tree = this.placedIndexByLayer[z]
176
+ if (tree) {
177
+ tree.remove(rectToTree(oldRect), sameTreeRect)
178
+ tree.insert(rectToTree(expanded))
179
+ }
133
180
  }
134
181
 
135
182
  // Carve overlapped soft neighbors (respect full-stack nodes)
@@ -137,8 +184,8 @@ export class RectDiffExpansionSolver extends BaseSolver {
137
184
  {
138
185
  layerCount: this.layerCount,
139
186
  placed: this.placed,
140
- placedByLayer: this.placedByLayer,
141
187
  options: this.options,
188
+ placedIndexByLayer: this.placedIndexByLayer,
142
189
  },
143
190
  idx,
144
191
  )
@@ -152,7 +199,7 @@ export class RectDiffExpansionSolver extends BaseSolver {
152
199
 
153
200
  const rects = finalizeRects({
154
201
  placed: this.placed,
155
- obstaclesByLayer: this.obstaclesByLayer,
202
+ srj: this.srj,
156
203
  boardVoidRects: this.boardVoidRects,
157
204
  })
158
205
  this._meshNodes = rectsToMeshNodes(rects)
@@ -3,12 +3,14 @@ import {
3
3
  definePipelineStep,
4
4
  type PipelineStep,
5
5
  } from "@tscircuit/solver-utils"
6
- import type { SimpleRouteJson } from "../../types/srj-types"
7
- import type { GridFill3DOptions } from "../../rectdiff-types"
8
- import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
9
- import { RectDiffSeedingSolver } from "../RectDiffSeedingSolver/RectDiffSeedingSolver"
10
- import { RectDiffExpansionSolver } from "../RectDiffExpansionSolver/RectDiffExpansionSolver"
6
+ import type { SimpleRouteJson } from "lib/types/srj-types"
7
+ import type { GridFill3DOptions, XYRect } from "lib/rectdiff-types"
8
+ import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
9
+ import { RectDiffSeedingSolver } from "lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver"
10
+ import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
11
11
  import type { GraphicsObject } from "graphics-debug"
12
+ import RBush from "rbush"
13
+ import { buildObstacleIndexes } from "./buildObstacleIndexes"
12
14
 
13
15
  export type RectDiffGridSolverPipelineInput = {
14
16
  simpleRouteJson: SimpleRouteJson
@@ -18,6 +20,17 @@ export type RectDiffGridSolverPipelineInput = {
18
20
  export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridSolverPipelineInput> {
19
21
  rectDiffSeedingSolver?: RectDiffSeedingSolver
20
22
  rectDiffExpansionSolver?: RectDiffExpansionSolver
23
+ private boardVoidRects?: XYRect[]
24
+ private obstacleIndexByLayer: Array<RBush<RTreeRect>>
25
+
26
+ constructor(inputProblem: RectDiffGridSolverPipelineInput) {
27
+ super(inputProblem)
28
+ const { obstacleIndexByLayer, boardVoidRects } = buildObstacleIndexes(
29
+ inputProblem.simpleRouteJson,
30
+ )
31
+ this.obstacleIndexByLayer = obstacleIndexByLayer
32
+ this.boardVoidRects = boardVoidRects
33
+ }
21
34
 
22
35
  override pipelineDef: PipelineStep<any>[] = [
23
36
  definePipelineStep(
@@ -27,6 +40,8 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
27
40
  {
28
41
  simpleRouteJson: pipeline.inputProblem.simpleRouteJson,
29
42
  gridOptions: pipeline.inputProblem.gridOptions,
43
+ obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
44
+ boardVoidRects: pipeline.boardVoidRects,
30
45
  },
31
46
  ],
32
47
  ),
@@ -35,7 +50,11 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
35
50
  RectDiffExpansionSolver,
36
51
  (pipeline: RectDiffGridSolverPipeline) => [
37
52
  {
38
- initialSnapshot: pipeline.rectDiffSeedingSolver!.getOutput(),
53
+ initialSnapshot: {
54
+ ...pipeline.rectDiffSeedingSolver!.getOutput(),
55
+ boardVoidRects: pipeline.boardVoidRects ?? [],
56
+ },
57
+ obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
39
58
  },
40
59
  ],
41
60
  ),
@@ -0,0 +1,70 @@
1
+ import type { SimpleRouteJson } from "lib/types/srj-types"
2
+ import RBush from "rbush"
3
+ import { computeInverseRects } from "lib/solvers/RectDiffSeedingSolver/computeInverseRects"
4
+ import {
5
+ buildZIndexMap,
6
+ obstacleToXYRect,
7
+ obstacleZs,
8
+ } from "lib/solvers/RectDiffSeedingSolver/layers"
9
+ import type { XYRect } from "lib/rectdiff-types"
10
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
11
+
12
+ export const buildObstacleIndexes = (
13
+ srj: SimpleRouteJson,
14
+ ): {
15
+ obstacleIndexByLayer: Array<RBush<RTreeRect>>
16
+ boardVoidRects: XYRect[]
17
+ } => {
18
+ const { layerNames, zIndexByName } = buildZIndexMap(srj)
19
+ const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1)
20
+ const bounds: XYRect = {
21
+ x: srj.bounds.minX,
22
+ y: srj.bounds.minY,
23
+ width: srj.bounds.maxX - srj.bounds.minX,
24
+ height: srj.bounds.maxY - srj.bounds.minY,
25
+ }
26
+ const obstacleIndexByLayer = Array.from(
27
+ { length: layerCount },
28
+ () => new RBush<RTreeRect>(),
29
+ )
30
+
31
+ const insertObstacle = (rect: XYRect, z: number) => {
32
+ const treeRect = {
33
+ ...rect,
34
+ minX: rect.x,
35
+ minY: rect.y,
36
+ maxX: rect.x + rect.width,
37
+ maxY: rect.y + rect.height,
38
+ }
39
+ obstacleIndexByLayer[z]?.insert(treeRect)
40
+ }
41
+
42
+ let boardVoidRects: XYRect[] = []
43
+ if (srj.outline && srj.outline.length > 2) {
44
+ boardVoidRects = computeInverseRects(bounds, srj.outline as any)
45
+ for (const voidRect of boardVoidRects) {
46
+ for (let z = 0; z < layerCount; z++) insertObstacle(voidRect, z)
47
+ }
48
+ }
49
+
50
+ for (const obstacle of srj.obstacles ?? []) {
51
+ const rect = obstacleToXYRect(obstacle as any)
52
+ if (!rect) continue
53
+ const zLayers = obstacleZs(obstacle as any, zIndexByName)
54
+ const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount)
55
+ if (invalidZs.length) {
56
+ throw new Error(
57
+ `RectDiff: obstacle uses z-layer indices ${invalidZs.join(",")} outside 0-${layerCount - 1}`,
58
+ )
59
+ }
60
+ if (
61
+ (!obstacle.zLayers || obstacle.zLayers.length === 0) &&
62
+ zLayers.length
63
+ ) {
64
+ obstacle.zLayers = zLayers
65
+ }
66
+ for (const z of zLayers) insertObstacle(rect, z)
67
+ }
68
+
69
+ return { obstacleIndexByLayer, boardVoidRects }
70
+ }
@@ -16,12 +16,17 @@ import { computeCandidates3D } from "./computeCandidates3D"
16
16
  import { computeEdgeCandidates3D } from "./computeEdgeCandidates3D"
17
17
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
18
18
  import { allLayerNode } from "../../utils/buildHardPlacedByLayer"
19
- import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
19
+ import { isFullyOccupiedAtPoint } from "lib/utils/isFullyOccupiedAtPoint"
20
20
  import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
21
+ import { getColorForZLayer } from "lib/utils/getColorForZLayer"
22
+ import RBush from "rbush"
23
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
21
24
 
22
25
  export type RectDiffSeedingSolverInput = {
23
26
  simpleRouteJson: SimpleRouteJson
27
+ obstacleIndexByLayer: Array<RBush<RTreeRect>>
24
28
  gridOptions?: Partial<GridFill3DOptions>
29
+ boardVoidRects?: XYRect[]
25
30
  }
26
31
 
27
32
  /**
@@ -43,12 +48,11 @@ export class RectDiffSeedingSolver extends BaseSolver {
43
48
  gridSizes: number[]
44
49
  maxMultiLayerSpan: number | undefined
45
50
  }
46
- private obstaclesByLayer!: XYRect[][]
47
- private boardVoidRects!: XYRect[]
51
+ private boardVoidRects?: XYRect[]
48
52
  private gridIndex!: number
49
53
  private candidates!: Candidate3D[]
50
54
  private placed!: Placed3D[]
51
- private placedByLayer!: XYRect[][]
55
+ private placedIndexByLayer!: Array<RBush<RTreeRect>>
52
56
  private expansionIndex!: number
53
57
  private edgeAnalysisDone!: boolean
54
58
  private totalSeedsThisGrid!: number
@@ -72,42 +76,6 @@ export class RectDiffSeedingSolver extends BaseSolver {
72
76
  height: srj.bounds.maxY - srj.bounds.minY,
73
77
  }
74
78
 
75
- const obstaclesByLayer: XYRect[][] = Array.from(
76
- { length: layerCount },
77
- () => [],
78
- )
79
-
80
- let boardVoidRects: XYRect[] = []
81
- if (srj.outline && srj.outline.length > 2) {
82
- boardVoidRects = computeInverseRects(bounds, srj.outline as any)
83
- for (const voidR of boardVoidRects) {
84
- for (let z = 0; z < layerCount; z++) {
85
- obstaclesByLayer[z]!.push(voidR)
86
- }
87
- }
88
- }
89
-
90
- for (const obstacle of srj.obstacles ?? []) {
91
- const rect = obstacleToXYRect(obstacle as any)
92
- if (!rect) continue
93
- const zLayers = obstacleZs(obstacle as any, zIndexByName)
94
- const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount)
95
- if (invalidZs.length) {
96
- throw new Error(
97
- `RectDiff: obstacle uses z-layer indices ${invalidZs.join(",")} outside 0-${
98
- layerCount - 1
99
- }`,
100
- )
101
- }
102
- if (
103
- (!obstacle.zLayers || obstacle.zLayers.length === 0) &&
104
- zLayers.length
105
- ) {
106
- obstacle.zLayers = zLayers
107
- }
108
- for (const z of zLayers) obstaclesByLayer[z]!.push(rect)
109
- }
110
-
111
79
  const trace = Math.max(0.01, srj.minTraceWidth || 0.15)
112
80
  const defaults: Required<
113
81
  Omit<GridFill3DOptions, "gridSizes" | "maxMultiLayerSpan">
@@ -137,22 +105,19 @@ export class RectDiffSeedingSolver extends BaseSolver {
137
105
  computeDefaultGridSizes(bounds),
138
106
  }
139
107
 
140
- const placedByLayer: XYRect[][] = Array.from(
141
- { length: layerCount },
142
- () => [],
143
- )
144
-
145
108
  this.srj = srj
146
109
  this.layerNames = layerNames
147
110
  this.layerCount = layerCount
148
111
  this.bounds = bounds
149
112
  this.options = options
150
- this.obstaclesByLayer = obstaclesByLayer
151
- this.boardVoidRects = boardVoidRects
113
+ this.boardVoidRects = this.input.boardVoidRects
152
114
  this.gridIndex = 0
153
115
  this.candidates = []
154
116
  this.placed = []
155
- this.placedByLayer = placedByLayer
117
+ this.placedIndexByLayer = Array.from(
118
+ { length: layerCount },
119
+ () => new RBush<RTreeRect>(),
120
+ )
156
121
  this.expansionIndex = 0
157
122
  this.edgeAnalysisDone = false
158
123
  this.totalSeedsThisGrid = 0
@@ -198,9 +163,9 @@ export class RectDiffSeedingSolver extends BaseSolver {
198
163
  bounds: this.bounds,
199
164
  gridSize: grid,
200
165
  layerCount: this.layerCount,
201
- obstaclesByLayer: this.obstaclesByLayer,
202
- placedByLayer: this.placedByLayer,
203
166
  hardPlacedByLayer,
167
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
168
+ placedIndexByLayer: this.placedIndexByLayer,
204
169
  })
205
170
  this.totalSeedsThisGrid = this.candidates.length
206
171
  this.consumedSeedsThisGrid = 0
@@ -220,8 +185,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
220
185
  bounds: this.bounds,
221
186
  minSize,
222
187
  layerCount: this.layerCount,
223
- obstaclesByLayer: this.obstaclesByLayer,
224
- placedByLayer: this.placedByLayer,
188
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
189
+ placedIndexByLayer: this.placedIndexByLayer,
225
190
  hardPlacedByLayer,
226
191
  })
227
192
  this.edgeAnalysisDone = true
@@ -247,8 +212,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
247
212
  layerCount: this.layerCount,
248
213
  minSpan: minMulti.minLayers,
249
214
  maxSpan: maxMultiLayerSpan,
250
- obstaclesByLayer: this.obstaclesByLayer,
251
- placedByLayer: hardPlacedByLayer,
215
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
216
+ additionalBlockersByLayer: hardPlacedByLayer,
252
217
  })
253
218
 
254
219
  const attempts: Array<{
@@ -276,8 +241,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
276
241
  // HARD blockers only: obstacles on those layers + full-stack nodes
277
242
  const hardBlockers: XYRect[] = []
278
243
  for (const z of attempt.layers) {
279
- if (this.obstaclesByLayer[z])
280
- hardBlockers.push(...this.obstaclesByLayer[z]!)
244
+ const obstacleLayer = this.input.obstacleIndexByLayer[z]
245
+ if (obstacleLayer) hardBlockers.push(...obstacleLayer.all())
281
246
  if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]!)
282
247
  }
283
248
 
@@ -296,15 +261,26 @@ export class RectDiffSeedingSolver extends BaseSolver {
296
261
  // Place the new node
297
262
  const placed: Placed3D = { rect, zLayers: [...attempt.layers] }
298
263
  const newIndex = this.placed.push(placed) - 1
299
- for (const z of attempt.layers) this.placedByLayer[z]!.push(rect)
264
+ for (const z of attempt.layers) {
265
+ const idx = this.placedIndexByLayer[z]
266
+ if (idx) {
267
+ idx.insert({
268
+ ...rect,
269
+ minX: rect.x,
270
+ minY: rect.y,
271
+ maxX: rect.x + rect.width,
272
+ maxY: rect.y + rect.height,
273
+ })
274
+ }
275
+ }
300
276
 
301
277
  // New: carve overlapped soft nodes
302
278
  resizeSoftOverlaps(
303
279
  {
304
280
  layerCount: this.layerCount,
305
281
  placed: this.placed,
306
- placedByLayer: this.placedByLayer,
307
282
  options: this.options,
283
+ placedIndexByLayer: this.placedIndexByLayer,
308
284
  },
309
285
  newIndex,
310
286
  )
@@ -312,14 +288,12 @@ export class RectDiffSeedingSolver extends BaseSolver {
312
288
  // New: relax candidate culling — only drop seeds that became fully occupied
313
289
  this.candidates = this.candidates.filter(
314
290
  (c) =>
315
- !isFullyOccupiedAtPoint(
316
- {
317
- layerCount: this.layerCount,
318
- obstaclesByLayer: this.obstaclesByLayer,
319
- placedByLayer: this.placedByLayer,
320
- },
321
- { x: c.x, y: c.y },
322
- ),
291
+ !isFullyOccupiedAtPoint({
292
+ layerCount: this.layerCount,
293
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
294
+ placedIndexByLayer: this.placedIndexByLayer,
295
+ point: { x: c.x, y: c.y },
296
+ }),
323
297
  )
324
298
 
325
299
  return // processed one candidate
@@ -352,12 +326,10 @@ export class RectDiffSeedingSolver extends BaseSolver {
352
326
  layerCount: this.layerCount,
353
327
  bounds: this.bounds,
354
328
  options: this.options,
355
- obstaclesByLayer: this.obstaclesByLayer,
356
329
  boardVoidRects: this.boardVoidRects,
357
330
  gridIndex: this.gridIndex,
358
331
  candidates: this.candidates,
359
332
  placed: this.placed,
360
- placedByLayer: this.placedByLayer,
361
333
  expansionIndex: this.expansionIndex,
362
334
  edgeAnalysisDone: this.edgeAnalysisDone,
363
335
  totalSeedsThisGrid: this.totalSeedsThisGrid,
@@ -365,23 +337,6 @@ export class RectDiffSeedingSolver extends BaseSolver {
365
337
  }
366
338
  }
367
339
 
368
- /** Get color based on z layer for visualization. */
369
- private getColorForZLayer(zLayers: number[]): {
370
- fill: string
371
- stroke: string
372
- } {
373
- const minZ = Math.min(...zLayers)
374
- const colors = [
375
- { fill: "#dbeafe", stroke: "#3b82f6" },
376
- { fill: "#fef3c7", stroke: "#f59e0b" },
377
- { fill: "#d1fae5", stroke: "#10b981" },
378
- { fill: "#e9d5ff", stroke: "#a855f7" },
379
- { fill: "#fed7aa", stroke: "#f97316" },
380
- { fill: "#fecaca", stroke: "#ef4444" },
381
- ] as const
382
- return colors[minZ % colors.length]!
383
- }
384
-
385
340
  /** Visualization focused on the grid seeding phase. */
386
341
  override visualize(): GraphicsObject {
387
342
  const rects: NonNullable<GraphicsObject["rects"]> = []
@@ -489,7 +444,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
489
444
  // current placements (streaming) during grid fill
490
445
  if (this.placed?.length) {
491
446
  for (const placement of this.placed) {
492
- const colors = this.getColorForZLayer(placement.zLayers)
447
+ const colors = getColorForZLayer(placement.zLayers)
493
448
  rects.push({
494
449
  center: {
495
450
  x: placement.rect.x + placement.rect.width / 2,
@@ -2,6 +2,8 @@ import type { Candidate3D, XYRect } from "../../rectdiff-types"
2
2
  import { EPS, distancePointToRectEdges } from "../../utils/rectdiff-geometry"
3
3
  import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
4
4
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
5
+ import type RBush from "rbush"
6
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
5
7
 
6
8
  /**
7
9
  * Compute candidate seed points for a given grid size.
@@ -10,16 +12,16 @@ export function computeCandidates3D(params: {
10
12
  bounds: XYRect
11
13
  gridSize: number
12
14
  layerCount: number
13
- obstaclesByLayer: XYRect[][]
14
- placedByLayer: XYRect[][]
15
+ obstacleIndexByLayer: Array<RBush<RTreeRect> | undefined>
16
+ placedIndexByLayer: Array<RBush<RTreeRect> | undefined>
15
17
  hardPlacedByLayer: XYRect[][]
16
18
  }): Candidate3D[] {
17
19
  const {
18
20
  bounds,
19
21
  gridSize,
20
22
  layerCount,
21
- obstaclesByLayer,
22
- placedByLayer,
23
+ obstacleIndexByLayer,
24
+ placedIndexByLayer,
23
25
  hardPlacedByLayer,
24
26
  } = params
25
27
  const out = new Map<string, Candidate3D>() // key by (x,y)
@@ -38,14 +40,12 @@ export function computeCandidates3D(params: {
38
40
 
39
41
  // New rule: Only drop if EVERY layer is occupied (by obstacle or node)
40
42
  if (
41
- isFullyOccupiedAtPoint(
42
- {
43
- layerCount,
44
- obstaclesByLayer,
45
- placedByLayer,
46
- },
47
- { x, y },
48
- )
43
+ isFullyOccupiedAtPoint({
44
+ layerCount,
45
+ obstacleIndexByLayer,
46
+ placedIndexByLayer,
47
+ point: { x, y },
48
+ })
49
49
  )
50
50
  continue
51
51
 
@@ -60,8 +60,8 @@ export function computeCandidates3D(params: {
60
60
  layerCount,
61
61
  minSpan: 1,
62
62
  maxSpan: undefined,
63
- obstaclesByLayer,
64
- placedByLayer: hardPlacedByLayer,
63
+ obstacleIndexByLayer,
64
+ additionalBlockersByLayer: hardPlacedByLayer,
65
65
  })
66
66
  if (s.length > bestSpan.length) {
67
67
  bestSpan = s
@@ -74,7 +74,7 @@ export function computeCandidates3D(params: {
74
74
 
75
75
  // Distance heuristic against hard blockers only (obstacles + full-stack)
76
76
  const hardAtZ = [
77
- ...(obstaclesByLayer[anchorZ] ?? []),
77
+ ...(obstacleIndexByLayer[anchorZ]?.all() ?? []),
78
78
  ...(hardPlacedByLayer[anchorZ] ?? []),
79
79
  ]
80
80
  const d = Math.min(
@@ -2,6 +2,8 @@ import type { Candidate3D, XYRect } from "../../rectdiff-types"
2
2
  import { EPS, distancePointToRectEdges } from "../../utils/rectdiff-geometry"
3
3
  import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
4
4
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
5
+ import type RBush from "rbush"
6
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
5
7
 
6
8
  /**
7
9
  * Compute exact uncovered segments along a 1D line.
@@ -79,16 +81,16 @@ export function computeEdgeCandidates3D(params: {
79
81
  bounds: XYRect
80
82
  minSize: number
81
83
  layerCount: number
82
- obstaclesByLayer: XYRect[][]
83
- placedByLayer: XYRect[][]
84
+ obstacleIndexByLayer: Array<RBush<RTreeRect> | undefined>
85
+ placedIndexByLayer: Array<RBush<RTreeRect> | undefined>
84
86
  hardPlacedByLayer: XYRect[][]
85
87
  }): Candidate3D[] {
86
88
  const {
87
89
  bounds,
88
90
  minSize,
89
91
  layerCount,
90
- obstaclesByLayer,
91
- placedByLayer,
92
+ obstacleIndexByLayer,
93
+ placedIndexByLayer,
92
94
  hardPlacedByLayer,
93
95
  } = params
94
96
 
@@ -100,14 +102,12 @@ export function computeEdgeCandidates3D(params: {
100
102
  `${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`
101
103
 
102
104
  function fullyOcc(p: { x: number; y: number }) {
103
- return isFullyOccupiedAtPoint(
104
- {
105
- layerCount,
106
- obstaclesByLayer,
107
- placedByLayer,
108
- },
109
- p,
110
- )
105
+ return isFullyOccupiedAtPoint({
106
+ layerCount,
107
+ obstacleIndexByLayer,
108
+ placedIndexByLayer,
109
+ point: p,
110
+ })
111
111
  }
112
112
 
113
113
  function pushIfFree(p: { x: number; y: number; z: number }) {
@@ -123,7 +123,7 @@ export function computeEdgeCandidates3D(params: {
123
123
 
124
124
  // Distance uses obstacles + hard nodes (soft nodes ignored for ranking)
125
125
  const hard = [
126
- ...(obstaclesByLayer[z] ?? []),
126
+ ...(obstacleIndexByLayer[z]?.all() ?? []),
127
127
  ...(hardPlacedByLayer[z] ?? []),
128
128
  ]
129
129
  const d = Math.min(
@@ -145,15 +145,15 @@ export function computeEdgeCandidates3D(params: {
145
145
  layerCount,
146
146
  minSpan: 1,
147
147
  maxSpan: undefined,
148
- obstaclesByLayer,
149
- placedByLayer: hardPlacedByLayer,
148
+ obstacleIndexByLayer,
149
+ additionalBlockersByLayer: hardPlacedByLayer,
150
150
  })
151
151
  out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true })
152
152
  }
153
153
 
154
154
  for (let z = 0; z < layerCount; z++) {
155
155
  const blockers = [
156
- ...(obstaclesByLayer[z] ?? []),
156
+ ...(obstacleIndexByLayer[z]?.all() ?? []),
157
157
  ...(hardPlacedByLayer[z] ?? []),
158
158
  ]
159
159