@tscircuit/rectdiff 0.0.12 → 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.
Files changed (37) hide show
  1. package/dist/index.d.ts +169 -27
  2. package/dist/index.js +2012 -1672
  3. package/lib/RectDiffPipeline.ts +18 -17
  4. package/lib/{solvers/rectdiff/types.ts → rectdiff-types.ts} +0 -34
  5. package/lib/{solvers/rectdiff/visualization.ts → rectdiff-visualization.ts} +2 -1
  6. package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +9 -12
  7. package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +5 -2
  8. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +252 -0
  9. package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
  10. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +106 -0
  11. package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +70 -0
  12. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +487 -0
  13. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +109 -0
  14. package/lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts +9 -0
  15. package/lib/solvers/{rectdiff/candidates.ts → RectDiffSeedingSolver/computeEdgeCandidates3D.ts} +44 -225
  16. package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/computeInverseRects.ts +6 -2
  17. package/lib/solvers/{rectdiff → RectDiffSeedingSolver}/layers.ts +4 -3
  18. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +60 -0
  19. package/lib/types/capacity-mesh-types.ts +9 -0
  20. package/lib/utils/buildHardPlacedByLayer.ts +14 -0
  21. package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
  22. package/lib/utils/finalizeRects.ts +54 -0
  23. package/lib/utils/isFullyOccupiedAtPoint.ts +28 -0
  24. package/lib/utils/rectToTree.ts +10 -0
  25. package/lib/utils/rectdiff-geometry.ts +94 -0
  26. package/lib/utils/resizeSoftOverlaps.ts +103 -0
  27. package/lib/utils/sameTreeRect.ts +7 -0
  28. package/package.json +1 -1
  29. package/tests/board-outline.test.ts +2 -1
  30. package/tests/examples/example01.test.tsx +18 -1
  31. package/tests/obstacle-extra-layers.test.ts +1 -1
  32. package/tests/obstacle-zlayers.test.ts +1 -1
  33. package/utils/rectsEqual.ts +2 -2
  34. package/utils/rectsOverlap.ts +2 -2
  35. package/lib/solvers/RectDiffSolver.ts +0 -231
  36. package/lib/solvers/rectdiff/engine.ts +0 -481
  37. /package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/isPointInPolygon.ts +0 -0
@@ -4,12 +4,12 @@ import {
4
4
  type PipelineStep,
5
5
  } from "@tscircuit/solver-utils"
6
6
  import type { SimpleRouteJson } from "./types/srj-types"
7
- import type { GridFill3DOptions } from "./solvers/rectdiff/types"
8
- import { RectDiffSolver } from "./solvers/RectDiffSolver"
7
+ import type { GridFill3DOptions } from "./rectdiff-types"
9
8
  import type { CapacityMeshNode } from "./types/capacity-mesh-types"
10
9
  import type { GraphicsObject } from "graphics-debug"
11
- import { createBaseVisualization } from "./solvers/rectdiff/visualization"
12
10
  import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
11
+ import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
12
+ import { createBaseVisualization } from "./rectdiff-visualization"
13
13
 
14
14
  export interface RectDiffPipelineInput {
15
15
  simpleRouteJson: SimpleRouteJson
@@ -17,24 +17,19 @@ export interface RectDiffPipelineInput {
17
17
  }
18
18
 
19
19
  export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
20
- rectDiffSolver?: RectDiffSolver
20
+ rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
21
21
  gapFillSolver?: GapFillSolverPipeline
22
22
 
23
23
  override pipelineDef: PipelineStep<any>[] = [
24
24
  definePipelineStep(
25
- "rectDiffSolver",
26
- RectDiffSolver,
27
- (rectDiffPipeline) => [
25
+ "rectDiffGridSolverPipeline",
26
+ RectDiffGridSolverPipeline,
27
+ (rectDiffPipeline: RectDiffPipeline) => [
28
28
  {
29
29
  simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
30
30
  gridOptions: rectDiffPipeline.inputProblem.gridOptions,
31
31
  },
32
32
  ],
33
- {
34
- onSolved: () => {
35
- // RectDiff mesh generation completed
36
- },
37
- },
38
33
  ),
39
34
  definePipelineStep(
40
35
  "gapFillSolver",
@@ -42,7 +37,8 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
42
37
  (rectDiffPipeline: RectDiffPipeline) => [
43
38
  {
44
39
  meshNodes:
45
- rectDiffPipeline.rectDiffSolver?.getOutput().meshNodes ?? [],
40
+ rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput()
41
+ .meshNodes ?? [],
46
42
  },
47
43
  ],
48
44
  ),
@@ -57,7 +53,10 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
57
53
  if (gapFillOutput) {
58
54
  return { meshNodes: gapFillOutput.outputNodes }
59
55
  }
60
- return this.rectDiffSolver!.getOutput()
56
+ if (this.rectDiffGridSolverPipeline) {
57
+ return this.rectDiffGridSolverPipeline.getOutput()
58
+ }
59
+ return { meshNodes: [] }
61
60
  }
62
61
 
63
62
  override initialVisualize(): GraphicsObject {
@@ -67,8 +66,10 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
67
66
  "RectDiffPipeline - Initial",
68
67
  )
69
68
 
70
- // Show initial mesh nodes from rectDiffSolver if available
71
- const initialNodes = this.rectDiffSolver?.getOutput().meshNodes ?? []
69
+ // Show initial mesh nodes from grid pipeline if available
70
+ const initialNodes =
71
+ this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []
72
+
72
73
  for (const node of initialNodes) {
73
74
  graphics.rects!.push({
74
75
  center: node.center,
@@ -95,7 +96,7 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
95
96
 
96
97
  const { meshNodes: outputNodes } = this.getOutput()
97
98
  const initialNodeIds = new Set(
98
- (this.rectDiffSolver?.getOutput().meshNodes ?? []).map(
99
+ (this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []).map(
99
100
  (n) => n.capacityMeshNodeId,
100
101
  ),
101
102
  )
@@ -1,6 +1,4 @@
1
1
  // lib/solvers/rectdiff/types.ts
2
- import type { SimpleRouteJson } from "../../types/srj-types"
3
-
4
2
  export type XYRect = { x: number; y: number; width: number; height: number }
5
3
 
6
4
  export type Rect3d = {
@@ -33,35 +31,3 @@ export type Candidate3D = {
33
31
  isEdgeSeed?: boolean
34
32
  }
35
33
  export type Placed3D = { rect: XYRect; zLayers: number[] }
36
-
37
- export type Phase = "GRID" | "EXPANSION" | "GAP_FILL" | "DONE"
38
-
39
- export type RectDiffState = {
40
- // static
41
- srj: SimpleRouteJson
42
- layerNames: string[]
43
- layerCount: number
44
- bounds: XYRect
45
- options: Required<
46
- Omit<GridFill3DOptions, "gridSizes" | "maxMultiLayerSpan">
47
- > & {
48
- gridSizes: number[]
49
- maxMultiLayerSpan: number | undefined
50
- }
51
- obstaclesByLayer: XYRect[][]
52
- boardVoidRects: XYRect[] // newly added for viz
53
-
54
- // evolving
55
- phase: Phase
56
- gridIndex: number // index in gridSizes
57
- candidates: Candidate3D[]
58
- placed: Placed3D[]
59
- placedByLayer: XYRect[][]
60
- expansionIndex: number
61
- /** Whether we've already run the edge-analysis seeding pass. */
62
- edgeAnalysisDone: boolean
63
-
64
- // progress bookkeeping
65
- totalSeedsThisGrid: number
66
- consumedSeedsThisGrid: number
67
- }
@@ -1,5 +1,6 @@
1
1
  import type { GraphicsObject } from "graphics-debug"
2
- import type { SimpleRouteJson } from "../../types/srj-types"
2
+ import type { SimpleRouteJson } from "./types/srj-types"
3
+ import type { XYRect } from "./rectdiff-types"
3
4
 
4
5
  /**
5
6
  * Create basic visualization showing board bounds/outline and obstacles.
@@ -158,8 +158,7 @@ export class FindSegmentsWithAdjacentEmptySpaceSolver extends BaseSolver {
158
158
  graphics.lines.push({
159
159
  points: visuallyOffsetLine(
160
160
  [unprocessedEdge.start, unprocessedEdge.end],
161
- unprocessedEdge.facingDirection,
162
- -0.1,
161
+ { dir: unprocessedEdge.facingDirection, amt: -0.1 },
163
162
  ),
164
163
  strokeColor: "rgba(0, 0, 255, 0.5)",
165
164
  strokeDash: "5 5",
@@ -183,11 +182,10 @@ export class FindSegmentsWithAdjacentEmptySpaceSolver extends BaseSolver {
183
182
  if (this.lastOverlappingEdges) {
184
183
  for (const edge of this.lastOverlappingEdges) {
185
184
  graphics.lines.push({
186
- points: visuallyOffsetLine(
187
- [edge.start, edge.end],
188
- edge.facingDirection,
189
- 0.05,
190
- ),
185
+ points: visuallyOffsetLine([edge.start, edge.end], {
186
+ dir: edge.facingDirection,
187
+ amt: 0.05,
188
+ }),
191
189
  strokeColor: "red",
192
190
  strokeDash: "2 2",
193
191
  })
@@ -197,11 +195,10 @@ export class FindSegmentsWithAdjacentEmptySpaceSolver extends BaseSolver {
197
195
  if (this.lastUncoveredSegments) {
198
196
  for (const edge of this.lastUncoveredSegments) {
199
197
  graphics.lines.push({
200
- points: visuallyOffsetLine(
201
- [edge.start, edge.end],
202
- edge.facingDirection,
203
- -0.05,
204
- ),
198
+ points: visuallyOffsetLine([edge.start, edge.end], {
199
+ dir: edge.facingDirection,
200
+ amt: -0.05,
201
+ }),
205
202
  strokeColor: "green",
206
203
  strokeDash: "2 2",
207
204
  })
@@ -21,9 +21,12 @@ const OFFSET_DIR_MAP = {
21
21
  */
22
22
  export const visuallyOffsetLine = (
23
23
  line: Array<{ x: number; y: number }>,
24
- dir: "x-" | "x+" | "y-" | "y+",
25
- amt: number,
24
+ options: {
25
+ dir: "x-" | "x+" | "y-" | "y+"
26
+ amt: number
27
+ },
26
28
  ) => {
29
+ const { dir, amt } = options
27
30
  const offset = OFFSET_DIR_MAP[dir]
28
31
  return line.map((p) => ({
29
32
  x: p.x + offset.x * amt,
@@ -0,0 +1,252 @@
1
+ import { BaseSolver } from "@tscircuit/solver-utils"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+ import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
4
+ import { expandRectFromSeed } from "../../utils/expandRectFromSeed"
5
+ import { finalizeRects } from "../../utils/finalizeRects"
6
+ import { allLayerNode } from "../../utils/buildHardPlacedByLayer"
7
+ import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
8
+ import { rectsToMeshNodes } from "./rectsToMeshNodes"
9
+ import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-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"
19
+
20
+ export type RectDiffExpansionSolverSnapshot = {
21
+ srj: SimpleRouteJson
22
+ layerNames: string[]
23
+ layerCount: number
24
+ bounds: XYRect
25
+ options: {
26
+ gridSizes: number[]
27
+ // the engine only uses gridSizes here, other options are ignored
28
+ [key: string]: any
29
+ }
30
+ boardVoidRects: XYRect[]
31
+ gridIndex: number
32
+ candidates: Candidate3D[]
33
+ placed: Placed3D[]
34
+ expansionIndex: number
35
+ edgeAnalysisDone: boolean
36
+ totalSeedsThisGrid: number
37
+ consumedSeedsThisGrid: number
38
+ }
39
+
40
+ export type RectDiffExpansionSolverInput = {
41
+ initialSnapshot: RectDiffExpansionSolverSnapshot
42
+ obstacleIndexByLayer: Array<RBush<RTreeRect>>
43
+ }
44
+
45
+ /**
46
+ * Second phase of RectDiff: expand placed rects to their maximal extents.
47
+ *
48
+ * This solver takes the intermediate data produced by RectDiffSeedingSolver
49
+ * and runs the EXPANSION phase, then finalizes to capacity mesh nodes.
50
+ */
51
+ export class RectDiffExpansionSolver extends BaseSolver {
52
+ // Engine fields (same shape used by rectdiff/engine.ts)
53
+ private srj!: SimpleRouteJson
54
+ private layerNames!: string[]
55
+ private layerCount!: number
56
+ private bounds!: XYRect
57
+ private options!: {
58
+ gridSizes: number[]
59
+ // the engine only uses gridSizes here, other options are ignored
60
+ [key: string]: any
61
+ }
62
+ private boardVoidRects!: XYRect[]
63
+ private gridIndex!: number
64
+ private candidates!: Candidate3D[]
65
+ private placed!: Placed3D[]
66
+ private placedIndexByLayer!: Array<RBush<RTreeRect>>
67
+ private expansionIndex!: number
68
+ private edgeAnalysisDone!: boolean
69
+ private totalSeedsThisGrid!: number
70
+ private consumedSeedsThisGrid!: number
71
+
72
+ private _meshNodes: CapacityMeshNode[] = []
73
+
74
+ constructor(private input: RectDiffExpansionSolverInput) {
75
+ super()
76
+ // Copy engine snapshot fields directly onto this solver instance
77
+ Object.assign(this, this.input.initialSnapshot)
78
+ }
79
+
80
+ override _setup() {
81
+ this.stats = {
82
+ gridIndex: this.gridIndex,
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
+ }
122
+ }
123
+
124
+ override _step() {
125
+ if (this.solved) return
126
+
127
+ this._stepExpansion()
128
+
129
+ this.stats.gridIndex = this.gridIndex
130
+ this.stats.placed = this.placed.length
131
+
132
+ if (this.expansionIndex >= this.placed.length) {
133
+ this.finalizeIfNeeded()
134
+ }
135
+ }
136
+
137
+ private _stepExpansion(): void {
138
+ if (this.expansionIndex >= this.placed.length) {
139
+ return
140
+ }
141
+
142
+ const idx = this.expansionIndex
143
+ const p = this.placed[idx]!
144
+ const lastGrid = this.options.gridSizes[this.options.gridSizes.length - 1]!
145
+
146
+ const hardPlacedByLayer = allLayerNode({
147
+ layerCount: this.layerCount,
148
+ placed: this.placed,
149
+ })
150
+
151
+ // HARD blockers only: obstacles on p.zLayers + full-stack nodes
152
+ const hardBlockers: XYRect[] = []
153
+ for (const z of p.zLayers) {
154
+ const obstacleTree = this.input.obstacleIndexByLayer[z]
155
+ if (obstacleTree) hardBlockers.push(...obstacleTree.all())
156
+ hardBlockers.push(...(hardPlacedByLayer[z] ?? []))
157
+ }
158
+
159
+ const oldRect = p.rect
160
+ const expanded = expandRectFromSeed({
161
+ startX: p.rect.x + p.rect.width / 2,
162
+ startY: p.rect.y + p.rect.height / 2,
163
+ gridSize: lastGrid,
164
+ bounds: this.bounds,
165
+ blockers: hardBlockers,
166
+ initialCellRatio: 0,
167
+ maxAspectRatio: null,
168
+ minReq: { width: p.rect.width, height: p.rect.height },
169
+ })
170
+
171
+ if (expanded) {
172
+ // Update placement + per-layer index (replace old rect object)
173
+ this.placed[idx] = { rect: expanded, zLayers: p.zLayers }
174
+ for (const z of p.zLayers) {
175
+ const tree = this.placedIndexByLayer[z]
176
+ if (tree) {
177
+ tree.remove(rectToTree(oldRect), sameTreeRect)
178
+ tree.insert(rectToTree(expanded))
179
+ }
180
+ }
181
+
182
+ // Carve overlapped soft neighbors (respect full-stack nodes)
183
+ resizeSoftOverlaps(
184
+ {
185
+ layerCount: this.layerCount,
186
+ placed: this.placed,
187
+ options: this.options,
188
+ placedIndexByLayer: this.placedIndexByLayer,
189
+ },
190
+ idx,
191
+ )
192
+ }
193
+
194
+ this.expansionIndex += 1
195
+ }
196
+
197
+ private finalizeIfNeeded() {
198
+ if (this.solved) return
199
+
200
+ const rects = finalizeRects({
201
+ placed: this.placed,
202
+ srj: this.srj,
203
+ boardVoidRects: this.boardVoidRects,
204
+ })
205
+ this._meshNodes = rectsToMeshNodes(rects)
206
+ this.solved = true
207
+ }
208
+
209
+ computeProgress(): number {
210
+ if (this.solved) return 1
211
+ const grids = this.options.gridSizes.length
212
+ const base = grids / (grids + 1)
213
+ const denom = Math.max(1, this.placed.length)
214
+ const frac = denom ? this.expansionIndex / denom : 1
215
+ return Math.min(0.999, base + frac * (1 / (grids + 1)))
216
+ }
217
+
218
+ override getOutput(): { meshNodes: CapacityMeshNode[] } {
219
+ if (!this.solved && this._meshNodes.length === 0) {
220
+ this.finalizeIfNeeded()
221
+ }
222
+ return { meshNodes: this._meshNodes }
223
+ }
224
+
225
+ /** Simple visualization of expanded placements. */
226
+ override visualize(): GraphicsObject {
227
+ const rects: NonNullable<GraphicsObject["rects"]> = []
228
+
229
+ for (const placement of this.placed ?? []) {
230
+ rects.push({
231
+ center: {
232
+ x: placement.rect.x + placement.rect.width / 2,
233
+ y: placement.rect.y + placement.rect.height / 2,
234
+ },
235
+ width: placement.rect.width,
236
+ height: placement.rect.height,
237
+ stroke: "rgba(37, 99, 235, 0.9)",
238
+ fill: "rgba(191, 219, 254, 0.5)",
239
+ layer: `z${placement.zLayers.join(",")}`,
240
+ label: `expanded\nz:${placement.zLayers.join(",")}`,
241
+ })
242
+ }
243
+
244
+ return {
245
+ title: "RectDiff Expansion",
246
+ coordinateSystem: "cartesian",
247
+ rects,
248
+ points: [],
249
+ lines: [],
250
+ }
251
+ }
252
+ }
@@ -1,6 +1,5 @@
1
- // lib/solvers/rectdiff/rectsToMeshNodes.ts
2
- import type { Rect3d } from "./types"
3
1
  import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
2
+ import type { Rect3d } from "../../rectdiff-types"
4
3
 
5
4
  export function rectsToMeshNodes(rects: Rect3d[]): CapacityMeshNode[] {
6
5
  let id = 0
@@ -0,0 +1,106 @@
1
+ import {
2
+ BasePipelineSolver,
3
+ definePipelineStep,
4
+ type PipelineStep,
5
+ } from "@tscircuit/solver-utils"
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
+ import type { GraphicsObject } from "graphics-debug"
12
+ import RBush from "rbush"
13
+ import { buildObstacleIndexes } from "./buildObstacleIndexes"
14
+
15
+ export type RectDiffGridSolverPipelineInput = {
16
+ simpleRouteJson: SimpleRouteJson
17
+ gridOptions?: Partial<GridFill3DOptions>
18
+ }
19
+
20
+ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridSolverPipelineInput> {
21
+ rectDiffSeedingSolver?: RectDiffSeedingSolver
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
+ }
34
+
35
+ override pipelineDef: PipelineStep<any>[] = [
36
+ definePipelineStep(
37
+ "rectDiffSeedingSolver",
38
+ RectDiffSeedingSolver,
39
+ (pipeline: RectDiffGridSolverPipeline) => [
40
+ {
41
+ simpleRouteJson: pipeline.inputProblem.simpleRouteJson,
42
+ gridOptions: pipeline.inputProblem.gridOptions,
43
+ obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
44
+ boardVoidRects: pipeline.boardVoidRects,
45
+ },
46
+ ],
47
+ ),
48
+ definePipelineStep(
49
+ "rectDiffExpansionSolver",
50
+ RectDiffExpansionSolver,
51
+ (pipeline: RectDiffGridSolverPipeline) => [
52
+ {
53
+ initialSnapshot: {
54
+ ...pipeline.rectDiffSeedingSolver!.getOutput(),
55
+ boardVoidRects: pipeline.boardVoidRects ?? [],
56
+ },
57
+ obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
58
+ },
59
+ ],
60
+ ),
61
+ ]
62
+
63
+ override getConstructorParams() {
64
+ return [this.inputProblem]
65
+ }
66
+
67
+ override getOutput(): { meshNodes: CapacityMeshNode[] } {
68
+ if (this.rectDiffExpansionSolver) {
69
+ return this.rectDiffExpansionSolver.getOutput()
70
+ }
71
+ if (this.rectDiffSeedingSolver) {
72
+ const snapshot = this.rectDiffSeedingSolver.getOutput()
73
+ const meshNodes: CapacityMeshNode[] = snapshot.placed.map(
74
+ (placement: any, idx: number) => ({
75
+ capacityMeshNodeId: `grid-${idx}`,
76
+ center: {
77
+ x: placement.rect.x + placement.rect.width / 2,
78
+ y: placement.rect.y + placement.rect.height / 2,
79
+ },
80
+ width: placement.rect.width,
81
+ height: placement.rect.height,
82
+ availableZ: placement.zLayers,
83
+ layer: `z${placement.zLayers.join(",")}`,
84
+ }),
85
+ )
86
+ return { meshNodes }
87
+ }
88
+ return { meshNodes: [] }
89
+ }
90
+
91
+ override visualize(): GraphicsObject {
92
+ if (this.rectDiffExpansionSolver) {
93
+ return this.rectDiffExpansionSolver.visualize()
94
+ }
95
+ if (this.rectDiffSeedingSolver) {
96
+ return this.rectDiffSeedingSolver.visualize()
97
+ }
98
+ return {
99
+ title: "RectDiff Grid Pipeline",
100
+ coordinateSystem: "cartesian",
101
+ rects: [],
102
+ points: [],
103
+ lines: [],
104
+ }
105
+ }
106
+ }
@@ -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
+ }