@tscircuit/rectdiff 0.0.21 → 0.0.23

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 (36) hide show
  1. package/components/SolverDebugger3d.tsx +2 -2
  2. package/dist/index.d.ts +23 -3
  3. package/dist/index.js +236 -60
  4. package/lib/RectDiffPipeline.ts +62 -22
  5. package/lib/fixtures/twoNodeExpansionFixture.ts +10 -2
  6. package/lib/rectdiff-visualization.ts +2 -1
  7. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +8 -3
  8. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +48 -9
  9. package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +14 -6
  10. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +41 -5
  11. package/lib/solvers/RectDiffSeedingSolver/computeInverseRects.ts +37 -1
  12. package/lib/solvers/RectDiffSeedingSolver/layers.ts +9 -5
  13. package/lib/utils/expandRectFromSeed.ts +11 -5
  14. package/lib/utils/finalizeRects.ts +17 -9
  15. package/lib/utils/padRect.ts +11 -0
  16. package/lib/utils/renderObstacleClearance.ts +50 -0
  17. package/package.json +1 -1
  18. package/pages/bugreport11.page.tsx +1 -0
  19. package/test-assets/bugreport-c7537683-stalling.json +1107 -0
  20. package/tests/board-outline.test.ts +1 -1
  21. package/tests/bugreport-stalling.test.ts +102 -0
  22. package/tests/fixtures/makeSimpleRouteOutlineGraphics.ts +5 -1
  23. package/tests/should-expand-node.test.ts +9 -1
  24. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +2 -2
  25. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c-clearance.snap.svg +44 -0
  26. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +2 -2
  27. package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance.test.ts +97 -0
  28. package/tests/solver/bugreport26-66b0b2/__snapshots__/bugreport26-66b0b2.snap.svg +2 -2
  29. package/tests/solver/bugreport27-dd3734/__snapshots__/bugreport27-dd3734.snap.svg +2 -2
  30. package/tests/solver/bugreport28-18a9ef/__snapshots__/bugreport28-18a9ef.snap.svg +2 -2
  31. package/tests/solver/bugreport29-7deae8/__snapshots__/bugreport29-7deae8.snap.svg +2 -2
  32. package/tests/solver/bugreport30-2174c8/__snapshots__/bugreport30-2174c8.snap.svg +2 -2
  33. package/tests/solver/bugreport33-213d45/__snapshots__/bugreport33-213d45.snap.svg +2 -2
  34. package/tests/solver/bugreport34-e9dea2/__snapshots__/bugreport34-e9dea2.snap.svg +2 -2
  35. package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +2 -2
  36. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
@@ -11,16 +11,22 @@ import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipe
11
11
  import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
12
12
  import { createBaseVisualization } from "./rectdiff-visualization"
13
13
  import { computeInverseRects } from "./solvers/RectDiffSeedingSolver/computeInverseRects"
14
+ import { buildZIndexMap } from "./solvers/RectDiffSeedingSolver/layers"
15
+ import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
16
+ import { mergeGraphics } from "graphics-debug"
14
17
 
15
18
  export interface RectDiffPipelineInput {
16
19
  simpleRouteJson: SimpleRouteJson
17
20
  gridOptions?: Partial<GridFill3DOptions>
21
+ obstacleClearance?: number
18
22
  }
19
23
 
20
24
  export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
21
25
  rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
22
26
  gapFillSolver?: GapFillSolverPipeline
23
27
  boardVoidRects: XYRect[] | undefined
28
+ zIndexByName?: Map<string, number>
29
+ layerNames?: string[]
24
30
 
25
31
  override pipelineDef: PipelineStep<any>[] = [
26
32
  definePipelineStep(
@@ -28,9 +34,21 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
28
34
  RectDiffGridSolverPipeline,
29
35
  (rectDiffPipeline: RectDiffPipeline) => [
30
36
  {
31
- simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
37
+ bounds: rectDiffPipeline.inputProblem.simpleRouteJson.bounds,
38
+ obstacles: rectDiffPipeline.inputProblem.simpleRouteJson.obstacles,
39
+ connections:
40
+ rectDiffPipeline.inputProblem.simpleRouteJson.connections,
41
+ outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline
42
+ ? { outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline }
43
+ : undefined,
44
+ layerCount: rectDiffPipeline.inputProblem.simpleRouteJson.layerCount,
32
45
  gridOptions: rectDiffPipeline.inputProblem.gridOptions,
33
46
  boardVoidRects: rectDiffPipeline.boardVoidRects,
47
+ layerNames: rectDiffPipeline.layerNames,
48
+ zIndexByName: rectDiffPipeline.zIndexByName,
49
+ minTraceWidth:
50
+ rectDiffPipeline.inputProblem.simpleRouteJson.minTraceWidth,
51
+ obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance,
34
52
  },
35
53
  ],
36
54
  ),
@@ -53,6 +71,12 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
53
71
  ]
54
72
 
55
73
  override _setup(): void {
74
+ const { zIndexByName, layerNames } = buildZIndexMap({
75
+ obstacles: this.inputProblem.simpleRouteJson.obstacles,
76
+ layerCount: this.inputProblem.simpleRouteJson.layerCount,
77
+ })
78
+ this.zIndexByName = zIndexByName
79
+ this.layerNames = layerNames
56
80
  if (this.inputProblem.simpleRouteJson.outline) {
57
81
  this.boardVoidRects = computeInverseRects(
58
82
  {
@@ -86,17 +110,23 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
86
110
  }
87
111
 
88
112
  override initialVisualize(): GraphicsObject {
89
- const graphics = createBaseVisualization(
113
+ const base = createBaseVisualization(
90
114
  this.inputProblem.simpleRouteJson,
91
115
  "RectDiffPipeline - Initial",
92
116
  )
117
+ const clearance = buildObstacleClearanceGraphics({
118
+ srj: this.inputProblem.simpleRouteJson,
119
+ clearance: this.inputProblem.obstacleClearance,
120
+ })
93
121
 
94
122
  // Show initial mesh nodes from grid pipeline if available
95
123
  const initialNodes =
96
124
  this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []
97
125
 
98
- for (const node of initialNodes) {
99
- graphics.rects!.push({
126
+ const nodeRects: GraphicsObject = {
127
+ title: "Initial Nodes",
128
+ coordinateSystem: "cartesian",
129
+ rects: initialNodes.map((node) => ({
100
130
  center: node.center,
101
131
  width: node.width,
102
132
  height: node.height,
@@ -107,17 +137,21 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
107
137
  `node ${node.capacityMeshNodeId}`,
108
138
  `z:${node.availableZ.join(",")}`,
109
139
  ].join("\n"),
110
- })
140
+ })),
111
141
  }
112
142
 
113
- return graphics
143
+ return mergeGraphics(mergeGraphics(base, clearance), nodeRects)
114
144
  }
115
145
 
116
146
  override finalVisualize(): GraphicsObject {
117
- const graphics = createBaseVisualization(
147
+ const base = createBaseVisualization(
118
148
  this.inputProblem.simpleRouteJson,
119
149
  "RectDiffPipeline - Final",
120
150
  )
151
+ const clearance = buildObstacleClearanceGraphics({
152
+ srj: this.inputProblem.simpleRouteJson,
153
+ clearance: this.inputProblem.obstacleClearance,
154
+ })
121
155
 
122
156
  const { meshNodes: outputNodes } = this.getOutput()
123
157
  const initialNodeIds = new Set(
@@ -126,22 +160,28 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
126
160
  ),
127
161
  )
128
162
 
129
- for (const node of outputNodes) {
130
- const isExpanded = !initialNodeIds.has(node.capacityMeshNodeId)
131
- graphics.rects!.push({
132
- center: node.center,
133
- width: node.width,
134
- height: node.height,
135
- stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
136
- fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
137
- layer: `z${node.availableZ.join(",")}`,
138
- label: [
139
- `${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
140
- `z:${node.availableZ.join(",")}`,
141
- ].join("\n"),
142
- })
163
+ const nodeRects: GraphicsObject = {
164
+ title: "Final Nodes",
165
+ coordinateSystem: "cartesian",
166
+ rects: outputNodes.map((node) => {
167
+ const isExpanded = !initialNodeIds.has(node.capacityMeshNodeId)
168
+ return {
169
+ center: node.center,
170
+ width: node.width,
171
+ height: node.height,
172
+ stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
173
+ fill: isExpanded
174
+ ? "rgba(0, 200, 0, 0.3)"
175
+ : "rgba(100, 100, 100, 0.1)",
176
+ layer: `z${node.availableZ.join(",")}`,
177
+ label: [
178
+ `${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
179
+ `z:${node.availableZ.join(",")}`,
180
+ ].join("\n"),
181
+ }
182
+ }),
143
183
  }
144
184
 
145
- return graphics
185
+ return mergeGraphics(mergeGraphics(base, clearance), nodeRects)
146
186
  }
147
187
  }
@@ -3,6 +3,7 @@ import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionS
3
3
  import type { SimpleRouteJson } from "../types/srj-types"
4
4
  import type { XYRect } from "../rectdiff-types"
5
5
  import type { RTreeRect } from "lib/types/capacity-mesh-types"
6
+ import { buildZIndexMap } from "../solvers/RectDiffSeedingSolver/layers"
6
7
 
7
8
  /**
8
9
  * Builds a minimal RectDiffExpansionSolver snapshot with exactly two nodes
@@ -31,9 +32,13 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => {
31
32
  )
32
33
  // Start with all-empty obstacle indexes for a "clean" scenario
33
34
 
35
+ const { zIndexByName, layerNames } = buildZIndexMap({
36
+ obstacles: srj.obstacles,
37
+ layerCount: srj.layerCount,
38
+ })
39
+
34
40
  return {
35
- srj,
36
- layerNames: ["top"],
41
+ layerNames,
37
42
  layerCount,
38
43
  bounds,
39
44
  options: { gridSizes: [1] },
@@ -55,5 +60,8 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => {
55
60
  totalSeedsThisGrid: 0,
56
61
  consumedSeedsThisGrid: 0,
57
62
  obstacleIndexByLayer,
63
+ zIndexByName,
64
+ layerNamesCanonical: layerNames,
65
+ obstacles: srj.obstacles,
58
66
  }
59
67
  }
@@ -45,6 +45,7 @@ export function createBaseVisualization(
45
45
  // Draw obstacles
46
46
  for (const obstacle of srj.obstacles ?? []) {
47
47
  if (obstacle.type === "rect" || obstacle.type === "oval") {
48
+ const layerLabel = (obstacle.zLayers ?? []).join(",") || "all"
48
49
  rects.push({
49
50
  center: { x: obstacle.center.x, y: obstacle.center.y },
50
51
  width: obstacle.width,
@@ -52,7 +53,7 @@ export function createBaseVisualization(
52
53
  fill: "#fee2e2",
53
54
  stroke: "#ef4444",
54
55
  layer: "obstacle",
55
- label: "obstacle",
56
+ label: `obstacle\nz:${layerLabel}`,
56
57
  })
57
58
  }
58
59
  }
@@ -6,13 +6,12 @@ import { finalizeRects } from "../../utils/finalizeRects"
6
6
  import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
7
7
  import { rectsToMeshNodes } from "./rectsToMeshNodes"
8
8
  import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types"
9
- import type { SimpleRouteJson } from "lib/types/srj-types"
9
+ import type { Obstacle } from "lib/types/srj-types"
10
10
  import RBush from "rbush"
11
11
  import { rectToTree } from "../../utils/rectToTree"
12
12
  import { sameTreeRect } from "../../utils/sameTreeRect"
13
13
 
14
14
  export type RectDiffExpansionSolverInput = {
15
- srj: SimpleRouteJson
16
15
  layerNames: string[]
17
16
  layerCount: number
18
17
  bounds: XYRect
@@ -29,6 +28,10 @@ export type RectDiffExpansionSolverInput = {
29
28
  totalSeedsThisGrid: number
30
29
  consumedSeedsThisGrid: number
31
30
  obstacleIndexByLayer: Array<RBush<RTreeRect>>
31
+ zIndexByName: Map<string, number>
32
+ layerNamesCanonical: string[]
33
+ obstacles: Obstacle[]
34
+ obstacleClearance?: number
32
35
  }
33
36
 
34
37
  /**
@@ -132,8 +135,10 @@ export class RectDiffExpansionSolver extends BaseSolver {
132
135
 
133
136
  const rects = finalizeRects({
134
137
  placed: this.input.placed,
135
- srj: this.input.srj,
138
+ obstacles: this.input.obstacles,
139
+ zIndexByName: this.input.zIndexByName,
136
140
  boardVoidRects: this.input.boardVoidRects,
141
+ obstacleClearance: this.input.obstacleClearance,
137
142
  })
138
143
  this._meshNodes = rectsToMeshNodes(rects)
139
144
  this.solved = true
@@ -3,7 +3,11 @@ import {
3
3
  definePipelineStep,
4
4
  type PipelineStep,
5
5
  } from "@tscircuit/solver-utils"
6
- import type { SimpleRouteJson } from "lib/types/srj-types"
6
+ import type {
7
+ Obstacle,
8
+ SimpleRouteConnection,
9
+ SimpleRouteJson,
10
+ } from "lib/types/srj-types"
7
11
  import type { GridFill3DOptions, XYRect } from "lib/rectdiff-types"
8
12
  import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
9
13
  import { RectDiffSeedingSolver } from "lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver"
@@ -11,25 +15,47 @@ import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/Rec
11
15
  import type { GraphicsObject } from "graphics-debug"
12
16
  import RBush from "rbush"
13
17
  import { buildObstacleIndexesByLayer } from "./buildObstacleIndexes"
18
+ import type { Bounds } from "@tscircuit/math-utils"
14
19
 
15
20
  export type RectDiffGridSolverPipelineInput = {
16
- simpleRouteJson: SimpleRouteJson
21
+ bounds: Bounds
22
+ obstacles: Obstacle[]
23
+ connections: SimpleRouteConnection[]
24
+ outline?: Pick<SimpleRouteJson, "outline">
25
+ layerCount: number
26
+ minTraceWidth: number
27
+ obstacleClearance?: number
17
28
  gridOptions?: Partial<GridFill3DOptions>
18
29
  boardVoidRects?: XYRect[]
30
+ layerNames?: string[]
31
+ zIndexByName?: Map<string, number>
19
32
  }
20
33
 
21
34
  export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridSolverPipelineInput> {
22
35
  rectDiffSeedingSolver?: RectDiffSeedingSolver
23
36
  rectDiffExpansionSolver?: RectDiffExpansionSolver
24
37
  private obstacleIndexByLayer: Array<RBush<RTreeRect>>
38
+ private layerNames: string[]
39
+ private zIndexByName: Map<string, number>
25
40
 
26
41
  constructor(inputProblem: RectDiffGridSolverPipelineInput) {
27
42
  super(inputProblem)
28
- const { obstacleIndexByLayer } = buildObstacleIndexesByLayer({
29
- srj: inputProblem.simpleRouteJson,
30
- boardVoidRects: inputProblem.boardVoidRects,
31
- })
43
+ const { obstacleIndexByLayer, layerNames, zIndexByName } =
44
+ buildObstacleIndexesByLayer({
45
+ srj: {
46
+ bounds: inputProblem.bounds,
47
+ obstacles: inputProblem.obstacles,
48
+ connections: inputProblem.connections,
49
+ outline: inputProblem.outline?.outline,
50
+ layerCount: inputProblem.layerCount,
51
+ minTraceWidth: inputProblem.minTraceWidth,
52
+ },
53
+ boardVoidRects: inputProblem.boardVoidRects,
54
+ obstacleClearance: inputProblem.obstacleClearance,
55
+ })
32
56
  this.obstacleIndexByLayer = obstacleIndexByLayer
57
+ this.layerNames = inputProblem.layerNames ?? layerNames
58
+ this.zIndexByName = inputProblem.zIndexByName ?? zIndexByName
33
59
  }
34
60
 
35
61
  override pipelineDef: PipelineStep<any>[] = [
@@ -38,10 +64,20 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
38
64
  RectDiffSeedingSolver,
39
65
  (pipeline: RectDiffGridSolverPipeline) => [
40
66
  {
41
- simpleRouteJson: pipeline.inputProblem.simpleRouteJson,
67
+ simpleRouteJson: {
68
+ bounds: pipeline.inputProblem.bounds,
69
+ obstacles: pipeline.inputProblem.obstacles,
70
+ connections: pipeline.inputProblem.connections,
71
+ outline: pipeline.inputProblem.outline?.outline,
72
+ layerCount: pipeline.inputProblem.layerCount,
73
+ minTraceWidth: pipeline.inputProblem.minTraceWidth,
74
+ },
42
75
  gridOptions: pipeline.inputProblem.gridOptions,
43
76
  obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
44
77
  boardVoidRects: pipeline.inputProblem.boardVoidRects,
78
+ layerNames: pipeline.layerNames,
79
+ zIndexByName: pipeline.zIndexByName,
80
+ obstacleClearance: pipeline.inputProblem.obstacleClearance,
45
81
  },
46
82
  ],
47
83
  ),
@@ -55,10 +91,9 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
55
91
  }
56
92
  return [
57
93
  {
58
- srj: pipeline.inputProblem.simpleRouteJson,
59
94
  layerNames: output.layerNames ?? [],
60
95
  boardVoidRects: pipeline.inputProblem.boardVoidRects ?? [],
61
- layerCount: pipeline.inputProblem.simpleRouteJson.layerCount,
96
+ layerCount: pipeline.inputProblem.layerCount,
62
97
  bounds: output.bounds!,
63
98
  candidates: output.candidates,
64
99
  consumedSeedsThisGrid: output.placed.length,
@@ -69,6 +104,10 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
69
104
  expansionIndex: output.expansionIndex,
70
105
  obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
71
106
  options: output.options,
107
+ zIndexByName: pipeline.zIndexByName,
108
+ layerNamesCanonical: pipeline.layerNames,
109
+ obstacles: pipeline.inputProblem.obstacles,
110
+ obstacleClearance: pipeline.inputProblem.obstacleClearance,
72
111
  },
73
112
  ]
74
113
  },
@@ -8,15 +8,22 @@ import {
8
8
  } from "lib/solvers/RectDiffSeedingSolver/layers"
9
9
  import type { XYRect } from "lib/rectdiff-types"
10
10
  import type { RTreeRect } from "lib/types/capacity-mesh-types"
11
+ import { padRect } from "lib/utils/padRect"
11
12
 
12
13
  export const buildObstacleIndexesByLayer = (params: {
13
14
  srj: SimpleRouteJson
14
15
  boardVoidRects?: XYRect[]
16
+ obstacleClearance?: number
15
17
  }): {
16
18
  obstacleIndexByLayer: Array<RBush<RTreeRect>>
19
+ layerNames: string[]
20
+ zIndexByName: Map<string, number>
17
21
  } => {
18
- const { srj, boardVoidRects } = params
19
- const { layerNames, zIndexByName } = buildZIndexMap(srj)
22
+ const { srj, boardVoidRects, obstacleClearance } = params
23
+ const { layerNames, zIndexByName } = buildZIndexMap({
24
+ obstacles: srj.obstacles,
25
+ layerCount: srj.layerCount,
26
+ })
20
27
  const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1)
21
28
  const bounds: XYRect = {
22
29
  x: srj.bounds.minX,
@@ -48,9 +55,10 @@ export const buildObstacleIndexesByLayer = (params: {
48
55
  }
49
56
 
50
57
  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)
58
+ const rectBase = obstacleToXYRect(obstacle)
59
+ if (!rectBase) continue
60
+ const rect = padRect(rectBase, obstacleClearance ?? 0)
61
+ const zLayers = obstacleZs(obstacle, zIndexByName)
54
62
  const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount)
55
63
  if (invalidZs.length) {
56
64
  throw new Error(
@@ -66,5 +74,5 @@ export const buildObstacleIndexesByLayer = (params: {
66
74
  for (const z of zLayers) insertObstacle(rect, z)
67
75
  }
68
76
 
69
- return { obstacleIndexByLayer }
77
+ return { obstacleIndexByLayer, layerNames, zIndexByName }
70
78
  }
@@ -28,6 +28,9 @@ export type RectDiffSeedingSolverInput = {
28
28
  obstacleIndexByLayer: Array<RBush<RTreeRect>>
29
29
  gridOptions?: Partial<GridFill3DOptions>
30
30
  boardVoidRects?: XYRect[]
31
+ layerNames: string[]
32
+ zIndexByName: Map<string, number>
33
+ obstacleClearance?: number
31
34
  }
32
35
 
33
36
  /**
@@ -67,7 +70,16 @@ export class RectDiffSeedingSolver extends BaseSolver {
67
70
  const srj = this.input.simpleRouteJson
68
71
  const opts = this.input.gridOptions ?? {}
69
72
 
70
- const { layerNames, zIndexByName } = buildZIndexMap(srj)
73
+ const precomputed = this.input.layerNames && this.input.zIndexByName
74
+ const { layerNames, zIndexByName } = precomputed
75
+ ? {
76
+ layerNames: this.input.layerNames!,
77
+ zIndexByName: this.input.zIndexByName!,
78
+ }
79
+ : buildZIndexMap({
80
+ obstacles: srj.obstacles,
81
+ layerCount: srj.layerCount,
82
+ })
71
83
  const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1)
72
84
 
73
85
  const bounds: XYRect = {
@@ -310,7 +322,6 @@ export class RectDiffSeedingSolver extends BaseSolver {
310
322
  */
311
323
  override getOutput() {
312
324
  return {
313
- srj: this.srj,
314
325
  layerNames: this.layerNames,
315
326
  layerCount: this.layerCount,
316
327
  bounds: this.bounds,
@@ -323,6 +334,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
323
334
  edgeAnalysisDone: this.edgeAnalysisDone,
324
335
  totalSeedsThisGrid: this.totalSeedsThisGrid,
325
336
  consumedSeedsThisGrid: this.consumedSeedsThisGrid,
337
+ obstacles: this.srj.obstacles,
338
+ obstacleClearance: this.input.obstacleClearance,
326
339
  }
327
340
  }
328
341
 
@@ -379,6 +392,31 @@ export class RectDiffSeedingSolver extends BaseSolver {
379
392
  }
380
393
  }
381
394
 
395
+ // obstacle clearance visualization (expanded)
396
+ if (this.input.obstacleClearance && this.input.obstacleClearance > 0) {
397
+ for (const obstacle of srj.obstacles ?? []) {
398
+ const pad = this.input.obstacleClearance
399
+ const expanded = {
400
+ x: obstacle.center.x - obstacle.width / 2 - pad,
401
+ y: obstacle.center.y - obstacle.height / 2 - pad,
402
+ width: obstacle.width + 2 * pad,
403
+ height: obstacle.height + 2 * pad,
404
+ }
405
+ rects.push({
406
+ center: {
407
+ x: expanded.x + expanded.width / 2,
408
+ y: expanded.y + expanded.height / 2,
409
+ },
410
+ width: expanded.width,
411
+ height: expanded.height,
412
+ fill: "rgba(234, 179, 8, 0.15)",
413
+ stroke: "rgba(202, 138, 4, 0.9)",
414
+ layer: "obstacle-clearance",
415
+ label: "clearance",
416
+ })
417
+ }
418
+ }
419
+
382
420
  // board void rects (early visualization of mask)
383
421
  if (this.boardVoidRects) {
384
422
  let outlineBBox: {
@@ -423,10 +461,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
423
461
  points.push({
424
462
  x: cand.x,
425
463
  y: cand.y,
426
- fill: "#9333ea",
427
- stroke: "#6b21a8",
428
464
  label: `z:${cand.z}`,
429
- } as any)
465
+ })
430
466
  }
431
467
  }
432
468
 
@@ -6,6 +6,31 @@ import {
6
6
  } from "../../utils/rectdiff-geometry"
7
7
  import { isPointInPolygon } from "./isPointInPolygon"
8
8
 
9
+ /**
10
+ * Simplify a polygon by reducing coordinate precision to avoid excessive grid cells.
11
+ * This rounds coordinates to a grid and removes duplicates.
12
+ */
13
+ function simplifyPolygon(
14
+ polygon: Array<{ x: number; y: number }>,
15
+ precision: number,
16
+ ): Array<{ x: number; y: number }> {
17
+ const round = (v: number) => Math.round(v / precision) * precision
18
+ const seen = new Set<string>()
19
+ const result: Array<{ x: number; y: number }> = []
20
+
21
+ for (const p of polygon) {
22
+ const rx = round(p.x)
23
+ const ry = round(p.y)
24
+ const key = `${rx},${ry}`
25
+ if (!seen.has(key)) {
26
+ seen.add(key)
27
+ result.push({ x: rx, y: ry })
28
+ }
29
+ }
30
+
31
+ return result
32
+ }
33
+
9
34
  /**
10
35
  * Decompose the empty space inside 'bounds' but outside 'polygon' into rectangles.
11
36
  * This uses a coordinate grid approach, ideal for rectilinear polygons.
@@ -16,10 +41,21 @@ export function computeInverseRects(
16
41
  ): XYRect[] {
17
42
  if (!polygon || polygon.length < 3) return []
18
43
 
44
+ // Simplify polygon if it has too many points to avoid O(n^2) performance issues
45
+ // A polygon with 350+ points (like rounded corners) creates too many grid cells
46
+ const MAX_POLYGON_POINTS = 100
47
+ const workingPolygon =
48
+ polygon.length > MAX_POLYGON_POINTS
49
+ ? simplifyPolygon(
50
+ polygon,
51
+ Math.max(bounds.width, bounds.height) / MAX_POLYGON_POINTS,
52
+ )
53
+ : polygon
54
+
19
55
  // 1. Collect unique sorted X and Y coordinates
20
56
  const xs = new Set<number>([bounds.x, bounds.x + bounds.width])
21
57
  const ys = new Set<number>([bounds.y, bounds.y + bounds.height])
22
- for (const p of polygon) {
58
+ for (const p of workingPolygon) {
23
59
  xs.add(p.x)
24
60
  ys.add(p.y)
25
61
  }
@@ -21,11 +21,15 @@ export function canonicalizeLayerOrder(names: string[]) {
21
21
  })
22
22
  }
23
23
 
24
- export function buildZIndexMap(srj: SimpleRouteJson) {
24
+ // TODO: should not take a srj
25
+ export function buildZIndexMap(params: {
26
+ obstacles?: Obstacle[]
27
+ layerCount?: number
28
+ }) {
25
29
  const names = canonicalizeLayerOrder(
26
- (srj.obstacles ?? []).flatMap((o) => o.layers ?? []),
30
+ (params.obstacles ?? []).flatMap((o) => o.layers ?? []),
27
31
  )
28
- const declaredLayerCount = Math.max(1, srj.layerCount || names.length || 1)
32
+ const declaredLayerCount = Math.max(1, params.layerCount || names.length || 1)
29
33
  const fallback = Array.from({ length: declaredLayerCount }, (_, i) =>
30
34
  i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`,
31
35
  )
@@ -78,8 +82,8 @@ export function obstacleZs(ob: Obstacle, zIndexByName: Map<string, number>) {
78
82
  }
79
83
 
80
84
  export function obstacleToXYRect(ob: Obstacle): XYRect | null {
81
- const w = ob.width as any
82
- const h = ob.height as any
85
+ const w = ob.width
86
+ const h = ob.height
83
87
  if (typeof w !== "number" || typeof h !== "number") return null
84
88
  return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h }
85
89
  }
@@ -286,14 +286,20 @@ export function expandRectFromSeed(params: {
286
286
  for (const b of blockers) if (overlaps(r, b)) continue STRATS
287
287
 
288
288
  // greedy expansions in 4 directions
289
+ // Use a minimum expansion threshold to avoid infinitesimal improvements
290
+ // that can occur with mixed-precision floating point coordinates
291
+ const MIN_EXPANSION = 1e-6
292
+ const MAX_ITERATIONS = 1000
289
293
  let improved = true
290
- while (improved) {
294
+ let iterations = 0
295
+ while (improved && iterations < MAX_ITERATIONS) {
296
+ iterations++
291
297
  improved = false
292
298
  const commonParams = { bounds, blockers, maxAspect: maxAspectRatio }
293
299
 
294
300
  collectBlockers(searchStripRight({ rect: r, bounds }))
295
301
  const eR = maxExpandRight({ ...commonParams, r })
296
- if (eR > 0) {
302
+ if (eR > MIN_EXPANSION) {
297
303
  r = { ...r, width: r.width + eR }
298
304
  collectBlockers(r)
299
305
  improved = true
@@ -301,7 +307,7 @@ export function expandRectFromSeed(params: {
301
307
 
302
308
  collectBlockers(searchStripDown({ rect: r, bounds }))
303
309
  const eD = maxExpandDown({ ...commonParams, r })
304
- if (eD > 0) {
310
+ if (eD > MIN_EXPANSION) {
305
311
  r = { ...r, height: r.height + eD }
306
312
  collectBlockers(r)
307
313
  improved = true
@@ -309,7 +315,7 @@ export function expandRectFromSeed(params: {
309
315
 
310
316
  collectBlockers(searchStripLeft({ rect: r, bounds }))
311
317
  const eL = maxExpandLeft({ ...commonParams, r })
312
- if (eL > 0) {
318
+ if (eL > MIN_EXPANSION) {
313
319
  r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }
314
320
  collectBlockers(r)
315
321
  improved = true
@@ -317,7 +323,7 @@ export function expandRectFromSeed(params: {
317
323
 
318
324
  collectBlockers(searchStripUp({ rect: r, bounds }))
319
325
  const eU = maxExpandUp({ ...commonParams, r })
320
- if (eU > 0) {
326
+ if (eU > MIN_EXPANSION) {
321
327
  r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }
322
328
  collectBlockers(r)
323
329
  improved = true
@@ -1,15 +1,16 @@
1
+ import type { Obstacle } from "lib/types/srj-types"
1
2
  import type { Placed3D, Rect3d, XYRect } from "../rectdiff-types"
2
- import type { SimpleRouteJson } from "../types/srj-types"
3
3
  import {
4
- buildZIndexMap,
5
4
  obstacleToXYRect,
6
5
  obstacleZs,
7
6
  } from "../solvers/RectDiffSeedingSolver/layers"
8
7
 
9
8
  export function finalizeRects(params: {
10
9
  placed: Placed3D[]
11
- srj: SimpleRouteJson
10
+ obstacles: Obstacle[]
12
11
  boardVoidRects: XYRect[]
12
+ zIndexByName: Map<string, number>
13
+ obstacleClearance?: number
13
14
  }): Rect3d[] {
14
15
  // Convert all placed (free space) nodes to output format
15
16
  const out: Rect3d[] = params.placed.map((p) => ({
@@ -20,23 +21,30 @@ export function finalizeRects(params: {
20
21
  zLayers: [...p.zLayers].sort((a, b) => a - b),
21
22
  }))
22
23
 
23
- const { zIndexByName } = buildZIndexMap(params.srj)
24
24
  const layersByKey = new Map<string, { rect: XYRect; layers: Set<number> }>()
25
25
 
26
- for (const obstacle of params.srj.obstacles ?? []) {
27
- const rect = obstacleToXYRect(obstacle as any)
28
- if (!rect) continue
26
+ for (const obstacle of params.obstacles ?? []) {
27
+ const baseRect = obstacleToXYRect(obstacle)
28
+ if (!baseRect) continue
29
+ const rect = params.obstacleClearance
30
+ ? {
31
+ x: baseRect.x - params.obstacleClearance,
32
+ y: baseRect.y - params.obstacleClearance,
33
+ width: baseRect.width + 2 * params.obstacleClearance,
34
+ height: baseRect.height + 2 * params.obstacleClearance,
35
+ }
36
+ : baseRect
29
37
  const zLayers =
30
38
  obstacle.zLayers?.length && obstacle.zLayers.length > 0
31
39
  ? obstacle.zLayers
32
- : obstacleZs(obstacle as any, zIndexByName)
40
+ : obstacleZs(obstacle, params.zIndexByName)
33
41
  const key = `${rect.x}:${rect.y}:${rect.width}:${rect.height}`
34
42
  let entry = layersByKey.get(key)
35
43
  if (!entry) {
36
44
  entry = { rect, layers: new Set() }
37
45
  layersByKey.set(key, entry)
38
46
  }
39
- zLayers.forEach((layer) => entry!.layers.add(layer))
47
+ zLayers.forEach((layer: number) => entry!.layers.add(layer))
40
48
  }
41
49
 
42
50
  for (const { rect, layers } of layersByKey.values()) {