@tscircuit/rectdiff 0.0.13 → 0.0.14

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.
@@ -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,16 @@ 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 RBush from "rbush"
22
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
21
23
 
22
24
  export type RectDiffSeedingSolverInput = {
23
25
  simpleRouteJson: SimpleRouteJson
26
+ obstacleIndexByLayer: Array<RBush<RTreeRect>>
24
27
  gridOptions?: Partial<GridFill3DOptions>
28
+ boardVoidRects?: XYRect[]
25
29
  }
26
30
 
27
31
  /**
@@ -43,12 +47,11 @@ export class RectDiffSeedingSolver extends BaseSolver {
43
47
  gridSizes: number[]
44
48
  maxMultiLayerSpan: number | undefined
45
49
  }
46
- private obstaclesByLayer!: XYRect[][]
47
- private boardVoidRects!: XYRect[]
50
+ private boardVoidRects?: XYRect[]
48
51
  private gridIndex!: number
49
52
  private candidates!: Candidate3D[]
50
53
  private placed!: Placed3D[]
51
- private placedByLayer!: XYRect[][]
54
+ private placedIndexByLayer!: Array<RBush<RTreeRect>>
52
55
  private expansionIndex!: number
53
56
  private edgeAnalysisDone!: boolean
54
57
  private totalSeedsThisGrid!: number
@@ -72,42 +75,6 @@ export class RectDiffSeedingSolver extends BaseSolver {
72
75
  height: srj.bounds.maxY - srj.bounds.minY,
73
76
  }
74
77
 
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
78
  const trace = Math.max(0.01, srj.minTraceWidth || 0.15)
112
79
  const defaults: Required<
113
80
  Omit<GridFill3DOptions, "gridSizes" | "maxMultiLayerSpan">
@@ -137,22 +104,19 @@ export class RectDiffSeedingSolver extends BaseSolver {
137
104
  computeDefaultGridSizes(bounds),
138
105
  }
139
106
 
140
- const placedByLayer: XYRect[][] = Array.from(
141
- { length: layerCount },
142
- () => [],
143
- )
144
-
145
107
  this.srj = srj
146
108
  this.layerNames = layerNames
147
109
  this.layerCount = layerCount
148
110
  this.bounds = bounds
149
111
  this.options = options
150
- this.obstaclesByLayer = obstaclesByLayer
151
- this.boardVoidRects = boardVoidRects
112
+ this.boardVoidRects = this.input.boardVoidRects
152
113
  this.gridIndex = 0
153
114
  this.candidates = []
154
115
  this.placed = []
155
- this.placedByLayer = placedByLayer
116
+ this.placedIndexByLayer = Array.from(
117
+ { length: layerCount },
118
+ () => new RBush<RTreeRect>(),
119
+ )
156
120
  this.expansionIndex = 0
157
121
  this.edgeAnalysisDone = false
158
122
  this.totalSeedsThisGrid = 0
@@ -198,9 +162,9 @@ export class RectDiffSeedingSolver extends BaseSolver {
198
162
  bounds: this.bounds,
199
163
  gridSize: grid,
200
164
  layerCount: this.layerCount,
201
- obstaclesByLayer: this.obstaclesByLayer,
202
- placedByLayer: this.placedByLayer,
203
165
  hardPlacedByLayer,
166
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
167
+ placedIndexByLayer: this.placedIndexByLayer,
204
168
  })
205
169
  this.totalSeedsThisGrid = this.candidates.length
206
170
  this.consumedSeedsThisGrid = 0
@@ -220,8 +184,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
220
184
  bounds: this.bounds,
221
185
  minSize,
222
186
  layerCount: this.layerCount,
223
- obstaclesByLayer: this.obstaclesByLayer,
224
- placedByLayer: this.placedByLayer,
187
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
188
+ placedIndexByLayer: this.placedIndexByLayer,
225
189
  hardPlacedByLayer,
226
190
  })
227
191
  this.edgeAnalysisDone = true
@@ -247,8 +211,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
247
211
  layerCount: this.layerCount,
248
212
  minSpan: minMulti.minLayers,
249
213
  maxSpan: maxMultiLayerSpan,
250
- obstaclesByLayer: this.obstaclesByLayer,
251
- placedByLayer: hardPlacedByLayer,
214
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
215
+ additionalBlockersByLayer: hardPlacedByLayer,
252
216
  })
253
217
 
254
218
  const attempts: Array<{
@@ -276,8 +240,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
276
240
  // HARD blockers only: obstacles on those layers + full-stack nodes
277
241
  const hardBlockers: XYRect[] = []
278
242
  for (const z of attempt.layers) {
279
- if (this.obstaclesByLayer[z])
280
- hardBlockers.push(...this.obstaclesByLayer[z]!)
243
+ const obstacleLayer = this.input.obstacleIndexByLayer[z]
244
+ if (obstacleLayer) hardBlockers.push(...obstacleLayer.all())
281
245
  if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]!)
282
246
  }
283
247
 
@@ -296,15 +260,26 @@ export class RectDiffSeedingSolver extends BaseSolver {
296
260
  // Place the new node
297
261
  const placed: Placed3D = { rect, zLayers: [...attempt.layers] }
298
262
  const newIndex = this.placed.push(placed) - 1
299
- for (const z of attempt.layers) this.placedByLayer[z]!.push(rect)
263
+ for (const z of attempt.layers) {
264
+ const idx = this.placedIndexByLayer[z]
265
+ if (idx) {
266
+ idx.insert({
267
+ ...rect,
268
+ minX: rect.x,
269
+ minY: rect.y,
270
+ maxX: rect.x + rect.width,
271
+ maxY: rect.y + rect.height,
272
+ })
273
+ }
274
+ }
300
275
 
301
276
  // New: carve overlapped soft nodes
302
277
  resizeSoftOverlaps(
303
278
  {
304
279
  layerCount: this.layerCount,
305
280
  placed: this.placed,
306
- placedByLayer: this.placedByLayer,
307
281
  options: this.options,
282
+ placedIndexByLayer: this.placedIndexByLayer,
308
283
  },
309
284
  newIndex,
310
285
  )
@@ -312,14 +287,12 @@ export class RectDiffSeedingSolver extends BaseSolver {
312
287
  // New: relax candidate culling — only drop seeds that became fully occupied
313
288
  this.candidates = this.candidates.filter(
314
289
  (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
- ),
290
+ !isFullyOccupiedAtPoint({
291
+ layerCount: this.layerCount,
292
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
293
+ placedIndexByLayer: this.placedIndexByLayer,
294
+ point: { x: c.x, y: c.y },
295
+ }),
323
296
  )
324
297
 
325
298
  return // processed one candidate
@@ -352,12 +325,10 @@ export class RectDiffSeedingSolver extends BaseSolver {
352
325
  layerCount: this.layerCount,
353
326
  bounds: this.bounds,
354
327
  options: this.options,
355
- obstaclesByLayer: this.obstaclesByLayer,
356
328
  boardVoidRects: this.boardVoidRects,
357
329
  gridIndex: this.gridIndex,
358
330
  candidates: this.candidates,
359
331
  placed: this.placed,
360
- placedByLayer: this.placedByLayer,
361
332
  expansionIndex: this.expansionIndex,
362
333
  edgeAnalysisDone: this.edgeAnalysisDone,
363
334
  totalSeedsThisGrid: this.totalSeedsThisGrid,
@@ -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
 
@@ -1,5 +1,7 @@
1
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
1
2
  import type { XYRect } from "../../rectdiff-types"
2
3
  import { clamp, containsPoint } from "../../utils/rectdiff-geometry"
4
+ import type RBush from "rbush"
3
5
 
4
6
  /**
5
7
  * Find the longest contiguous free span around z (optionally capped).
@@ -11,8 +13,8 @@ export function longestFreeSpanAroundZ(params: {
11
13
  layerCount: number
12
14
  minSpan: number
13
15
  maxSpan: number | undefined
14
- obstaclesByLayer: XYRect[][]
15
- placedByLayer: XYRect[][]
16
+ obstacleIndexByLayer: Array<RBush<RTreeRect> | undefined>
17
+ additionalBlockersByLayer?: XYRect[][]
16
18
  }): number[] {
17
19
  const {
18
20
  x,
@@ -21,16 +23,22 @@ export function longestFreeSpanAroundZ(params: {
21
23
  layerCount,
22
24
  minSpan,
23
25
  maxSpan,
24
- obstaclesByLayer,
25
- placedByLayer,
26
+ obstacleIndexByLayer,
27
+ additionalBlockersByLayer,
26
28
  } = params
27
29
 
28
30
  const isFreeAt = (layer: number) => {
29
- const blockers = [
30
- ...(obstaclesByLayer[layer] ?? []),
31
- ...(placedByLayer[layer] ?? []),
32
- ]
33
- return !blockers.some((b) => containsPoint(b, { x, y }))
31
+ const query = {
32
+ minX: x,
33
+ minY: y,
34
+ maxX: x,
35
+ maxY: y,
36
+ }
37
+ const obstacleIdx = obstacleIndexByLayer[layer]
38
+ if (obstacleIdx && obstacleIdx.search(query).length > 0) return false
39
+
40
+ const extras = additionalBlockersByLayer?.[layer] ?? []
41
+ return !extras.some((b) => containsPoint(b, { x, y }))
34
42
  }
35
43
  let lo = z
36
44
  let hi = z