@tscircuit/rectdiff 0.0.22 → 0.0.24

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 (53) hide show
  1. package/components/SolverDebugger3d.tsx +2 -2
  2. package/dist/index.d.ts +23 -3
  3. package/dist/index.js +291 -80
  4. package/lib/RectDiffPipeline.ts +42 -35
  5. package/lib/buildFinalRectDiffVisualization.ts +46 -0
  6. package/lib/fixtures/twoNodeExpansionFixture.ts +10 -2
  7. package/lib/rectdiff-visualization.ts +2 -1
  8. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +8 -3
  9. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +48 -9
  10. package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +14 -6
  11. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +16 -5
  12. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +12 -2
  13. package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +53 -13
  14. package/lib/solvers/RectDiffSeedingSolver/layers.ts +9 -5
  15. package/lib/utils/buildOutlineGraphics.ts +39 -0
  16. package/lib/utils/expandRectFromSeed.ts +11 -1
  17. package/lib/utils/finalizeRects.ts +17 -9
  18. package/lib/utils/padRect.ts +11 -0
  19. package/lib/utils/renderObstacleClearance.ts +50 -0
  20. package/package.json +1 -1
  21. package/pages/bugreport11.page.tsx +1 -0
  22. package/tests/board-outline.test.ts +1 -1
  23. package/tests/fixtures/makeSimpleRouteOutlineGraphics.ts +5 -1
  24. package/tests/should-expand-node.test.ts +9 -1
  25. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
  26. package/tests/solver/bugreport01-be84eb/__snapshots__/bugreport01-be84eb.snap.svg +2 -2
  27. package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +2 -2
  28. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  29. package/tests/solver/bugreport07-d3f3be/__snapshots__/bugreport07-d3f3be.snap.svg +1 -1
  30. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  31. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +2 -2
  32. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +2 -2
  33. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
  34. package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance-equivalence.test.ts +52 -0
  35. package/tests/solver/bugreport12-35ce1c/__snapshots__/bugreport12-35ce1c.snap.svg +1 -1
  36. package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
  37. package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +2 -2
  38. package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
  39. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  40. package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +1 -1
  41. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  42. package/tests/solver/bugreport26-66b0b2/__snapshots__/bugreport26-66b0b2.snap.svg +2 -2
  43. package/tests/solver/bugreport27-dd3734/__snapshots__/bugreport27-dd3734.snap.svg +2 -2
  44. package/tests/solver/bugreport28-18a9ef/__snapshots__/bugreport28-18a9ef.snap.svg +2 -2
  45. package/tests/solver/bugreport29-7deae8/__snapshots__/bugreport29-7deae8.snap.svg +2 -2
  46. package/tests/solver/bugreport30-2174c8/__snapshots__/bugreport30-2174c8.snap.svg +1 -1
  47. package/tests/solver/bugreport33-213d45/__snapshots__/bugreport33-213d45.snap.svg +2 -2
  48. package/tests/solver/bugreport34-e9dea2/__snapshots__/bugreport34-e9dea2.snap.svg +2 -2
  49. package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +2 -2
  50. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
  51. package/tests/solver/interaction/__snapshots__/interaction.snap.svg +1 -1
  52. package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
  53. package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +1 -1
@@ -10,17 +10,24 @@ import type { GraphicsObject } from "graphics-debug"
10
10
  import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
11
11
  import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
12
12
  import { createBaseVisualization } from "./rectdiff-visualization"
13
+ import { buildFinalRectDiffVisualization } from "./buildFinalRectDiffVisualization"
13
14
  import { computeInverseRects } from "./solvers/RectDiffSeedingSolver/computeInverseRects"
15
+ import { buildZIndexMap } from "./solvers/RectDiffSeedingSolver/layers"
16
+ import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
17
+ import { mergeGraphics } from "graphics-debug"
14
18
 
15
19
  export interface RectDiffPipelineInput {
16
20
  simpleRouteJson: SimpleRouteJson
17
21
  gridOptions?: Partial<GridFill3DOptions>
22
+ obstacleClearance?: number
18
23
  }
19
24
 
20
25
  export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
21
26
  rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
22
27
  gapFillSolver?: GapFillSolverPipeline
23
28
  boardVoidRects: XYRect[] | undefined
29
+ zIndexByName?: Map<string, number>
30
+ layerNames?: string[]
24
31
 
25
32
  override pipelineDef: PipelineStep<any>[] = [
26
33
  definePipelineStep(
@@ -28,9 +35,21 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
28
35
  RectDiffGridSolverPipeline,
29
36
  (rectDiffPipeline: RectDiffPipeline) => [
30
37
  {
31
- simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
38
+ bounds: rectDiffPipeline.inputProblem.simpleRouteJson.bounds,
39
+ obstacles: rectDiffPipeline.inputProblem.simpleRouteJson.obstacles,
40
+ connections:
41
+ rectDiffPipeline.inputProblem.simpleRouteJson.connections,
42
+ outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline
43
+ ? { outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline }
44
+ : undefined,
45
+ layerCount: rectDiffPipeline.inputProblem.simpleRouteJson.layerCount,
32
46
  gridOptions: rectDiffPipeline.inputProblem.gridOptions,
33
47
  boardVoidRects: rectDiffPipeline.boardVoidRects,
48
+ layerNames: rectDiffPipeline.layerNames,
49
+ zIndexByName: rectDiffPipeline.zIndexByName,
50
+ minTraceWidth:
51
+ rectDiffPipeline.inputProblem.simpleRouteJson.minTraceWidth,
52
+ obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance,
34
53
  },
35
54
  ],
36
55
  ),
@@ -53,6 +72,12 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
53
72
  ]
54
73
 
55
74
  override _setup(): void {
75
+ const { zIndexByName, layerNames } = buildZIndexMap({
76
+ obstacles: this.inputProblem.simpleRouteJson.obstacles,
77
+ layerCount: this.inputProblem.simpleRouteJson.layerCount,
78
+ })
79
+ this.zIndexByName = zIndexByName
80
+ this.layerNames = layerNames
56
81
  if (this.inputProblem.simpleRouteJson.outline) {
57
82
  this.boardVoidRects = computeInverseRects(
58
83
  {
@@ -86,17 +111,23 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
86
111
  }
87
112
 
88
113
  override initialVisualize(): GraphicsObject {
89
- const graphics = createBaseVisualization(
114
+ const base = createBaseVisualization(
90
115
  this.inputProblem.simpleRouteJson,
91
116
  "RectDiffPipeline - Initial",
92
117
  )
118
+ const clearance = buildObstacleClearanceGraphics({
119
+ srj: this.inputProblem.simpleRouteJson,
120
+ clearance: this.inputProblem.obstacleClearance,
121
+ })
93
122
 
94
123
  // Show initial mesh nodes from grid pipeline if available
95
124
  const initialNodes =
96
125
  this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []
97
126
 
98
- for (const node of initialNodes) {
99
- graphics.rects!.push({
127
+ const nodeRects: GraphicsObject = {
128
+ title: "Initial Nodes",
129
+ coordinateSystem: "cartesian",
130
+ rects: initialNodes.map((node) => ({
100
131
  center: node.center,
101
132
  width: node.width,
102
133
  height: node.height,
@@ -107,41 +138,17 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
107
138
  `node ${node.capacityMeshNodeId}`,
108
139
  `z:${node.availableZ.join(",")}`,
109
140
  ].join("\n"),
110
- })
141
+ })),
111
142
  }
112
143
 
113
- return graphics
144
+ return mergeGraphics(mergeGraphics(base, clearance), nodeRects)
114
145
  }
115
146
 
116
147
  override finalVisualize(): GraphicsObject {
117
- const graphics = createBaseVisualization(
118
- this.inputProblem.simpleRouteJson,
119
- "RectDiffPipeline - Final",
120
- )
121
-
122
- const { meshNodes: outputNodes } = this.getOutput()
123
- const initialNodeIds = new Set(
124
- (this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []).map(
125
- (n) => n.capacityMeshNodeId,
126
- ),
127
- )
128
-
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
- })
143
- }
144
-
145
- return graphics
148
+ return buildFinalRectDiffVisualization({
149
+ srj: this.inputProblem.simpleRouteJson,
150
+ meshNodes: this.getOutput().meshNodes,
151
+ obstacleClearance: this.inputProblem.obstacleClearance,
152
+ })
146
153
  }
147
154
  }
@@ -0,0 +1,46 @@
1
+ import { mergeGraphics, type GraphicsObject } from "graphics-debug"
2
+ import type { CapacityMeshNode } from "./types/capacity-mesh-types"
3
+ import type { SimpleRouteJson } from "./types/srj-types"
4
+ import { getColorForZLayer } from "./utils/getColorForZLayer"
5
+ import { buildOutlineGraphics } from "./utils/buildOutlineGraphics"
6
+ import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
7
+
8
+ type BuildFinalVisualizationParams = {
9
+ srj: SimpleRouteJson
10
+ meshNodes: CapacityMeshNode[]
11
+ obstacleClearance?: number
12
+ }
13
+
14
+ export const buildFinalRectDiffVisualization = ({
15
+ srj,
16
+ meshNodes,
17
+ obstacleClearance,
18
+ }: BuildFinalVisualizationParams): GraphicsObject => {
19
+ const outline = buildOutlineGraphics({ srj })
20
+ const clearance = buildObstacleClearanceGraphics({
21
+ srj,
22
+ clearance: obstacleClearance,
23
+ })
24
+ const rects = meshNodes.map((node) => ({
25
+ center: node.center,
26
+ width: node.width,
27
+ height: node.height,
28
+ stroke: getColorForZLayer(node.availableZ).stroke,
29
+ fill: node._containsObstacle
30
+ ? "#fca5a5"
31
+ : getColorForZLayer(node.availableZ).fill,
32
+ layer: `z${node.availableZ.join(",")}`,
33
+ label: `node ${node.capacityMeshNodeId}\nz:${node.availableZ.join(",")}`,
34
+ }))
35
+
36
+ const nodesGraphic: GraphicsObject = {
37
+ title: "RectDiffPipeline - Final",
38
+ coordinateSystem: "cartesian",
39
+ rects,
40
+ lines: [],
41
+ points: [],
42
+ texts: [],
43
+ }
44
+
45
+ return mergeGraphics(mergeGraphics(nodesGraphic, outline), clearance)
46
+ }
@@ -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
 
@@ -423,10 +436,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
423
436
  points.push({
424
437
  x: cand.x,
425
438
  y: cand.y,
426
- fill: "#9333ea",
427
- stroke: "#6b21a8",
428
439
  label: `z:${cand.z}`,
429
- } as any)
440
+ })
430
441
  }
431
442
  }
432
443
 
@@ -4,6 +4,8 @@ import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
4
4
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
5
5
  import type RBush from "rbush"
6
6
  import type { RTreeRect } from "lib/types/capacity-mesh-types"
7
+ const quantize = (value: number, precision = 1e-6) =>
8
+ Math.round(value / precision) * precision
7
9
 
8
10
  /**
9
11
  * Compute candidate seed points for a given grid size.
@@ -83,13 +85,14 @@ export function computeCandidates3D(params: {
83
85
  ? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b))
84
86
  : [Infinity]),
85
87
  )
88
+ const distance = quantize(d)
86
89
 
87
90
  const k = `${x.toFixed(6)}|${y.toFixed(6)}`
88
91
  const cand: Candidate3D = {
89
92
  x,
90
93
  y,
91
94
  z: anchorZ,
92
- distance: d,
95
+ distance,
93
96
  zSpanLen: bestSpan.length,
94
97
  }
95
98
  const prev = out.get(k)
@@ -104,6 +107,13 @@ export function computeCandidates3D(params: {
104
107
  }
105
108
 
106
109
  const arr = Array.from(out.values())
107
- arr.sort((a, b) => b.zSpanLen! - a.zSpanLen! || b.distance - a.distance)
110
+ arr.sort(
111
+ (a, b) =>
112
+ b.zSpanLen! - a.zSpanLen! ||
113
+ b.distance - a.distance ||
114
+ a.z - b.z ||
115
+ a.x - b.x ||
116
+ a.y - b.y,
117
+ )
108
118
  return arr
109
119
  }
@@ -4,6 +4,8 @@ import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
4
4
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
5
5
  import type RBush from "rbush"
6
6
  import type { RTreeRect } from "lib/types/capacity-mesh-types"
7
+ const quantize = (value: number, precision = 1e-6) =>
8
+ Math.round(value / precision) * precision
7
9
 
8
10
  /**
9
11
  * Compute exact uncovered segments along a 1D line.
@@ -15,14 +17,23 @@ function computeUncoveredSegments(params: {
15
17
  minSegmentLength: number
16
18
  }): Array<{ start: number; end: number; center: number }> {
17
19
  const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params
20
+ const lineStartQ = quantize(lineStart)
21
+ const lineEndQ = quantize(lineEnd)
22
+ const normalizedIntervals = coveringIntervals
23
+ .map((i) => {
24
+ const s = quantize(i.start)
25
+ const e = quantize(i.end)
26
+ return { start: Math.min(s, e), end: Math.max(s, e) }
27
+ })
28
+ .filter((i) => i.end > i.start + EPS)
18
29
 
19
- if (coveringIntervals.length === 0) {
20
- const center = (lineStart + lineEnd) / 2
21
- return [{ start: lineStart, end: lineEnd, center }]
30
+ if (normalizedIntervals.length === 0) {
31
+ const center = (lineStartQ + lineEndQ) / 2
32
+ return [{ start: lineStartQ, end: lineEndQ, center }]
22
33
  }
23
34
 
24
35
  // Sort intervals by start position
25
- const sorted = [...coveringIntervals].sort((a, b) => a.start - b.start)
36
+ const sorted = [...normalizedIntervals].sort((a, b) => a.start - b.start)
26
37
 
27
38
  // Merge overlapping intervals
28
39
  const merged: Array<{ start: number; end: number }> = []
@@ -45,8 +56,8 @@ function computeUncoveredSegments(params: {
45
56
  const uncovered: Array<{ start: number; end: number; center: number }> = []
46
57
 
47
58
  // Check gap before first interval
48
- if (merged[0]!.start > lineStart + EPS) {
49
- const start = lineStart
59
+ if (merged[0]!.start > lineStartQ + EPS) {
60
+ const start = lineStartQ
50
61
  const end = merged[0]!.start
51
62
  if (end - start >= minSegmentLength) {
52
63
  uncovered.push({ start, end, center: (start + end) / 2 })
@@ -63,9 +74,9 @@ function computeUncoveredSegments(params: {
63
74
  }
64
75
 
65
76
  // Check gap after last interval
66
- if (merged[merged.length - 1]!.end < lineEnd - EPS) {
77
+ if (merged[merged.length - 1]!.end < lineEndQ - EPS) {
67
78
  const start = merged[merged.length - 1]!.end
68
- const end = lineEnd
79
+ const end = lineEndQ
69
80
  if (end - start >= minSegmentLength) {
70
81
  uncovered.push({ start, end, center: (start + end) / 2 })
71
82
  }
@@ -111,7 +122,11 @@ export function computeEdgeCandidates3D(params: {
111
122
  }
112
123
 
113
124
  function pushIfFree(p: { x: number; y: number; z: number }) {
114
- const { x, y, z } = p
125
+ const qx = quantize(p.x)
126
+ const qy = quantize(p.y)
127
+ const { z } = p
128
+ const x = qx
129
+ const y = qy
115
130
  if (
116
131
  x < bounds.x + EPS ||
117
132
  y < bounds.y + EPS ||
@@ -125,13 +140,19 @@ export function computeEdgeCandidates3D(params: {
125
140
  const hard = [
126
141
  ...(obstacleIndexByLayer[z]?.all() ?? []),
127
142
  ...(hardPlacedByLayer[z] ?? []),
128
- ]
143
+ ].map((b) => ({
144
+ x: quantize(b.x),
145
+ y: quantize(b.y),
146
+ width: quantize(b.width),
147
+ height: quantize(b.height),
148
+ }))
129
149
  const d = Math.min(
130
150
  distancePointToRectEdges({ x, y }, bounds),
131
151
  ...(hard.length
132
152
  ? hard.map((b) => distancePointToRectEdges({ x, y }, b))
133
153
  : [Infinity]),
134
154
  )
155
+ const distance = quantize(d)
135
156
 
136
157
  const k = key({ x, y, z })
137
158
  if (dedup.has(k)) return
@@ -148,14 +169,26 @@ export function computeEdgeCandidates3D(params: {
148
169
  obstacleIndexByLayer,
149
170
  additionalBlockersByLayer: hardPlacedByLayer,
150
171
  })
151
- out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true })
172
+ out.push({
173
+ x,
174
+ y,
175
+ z,
176
+ distance,
177
+ zSpanLen: span.length,
178
+ isEdgeSeed: true,
179
+ })
152
180
  }
153
181
 
154
182
  for (let z = 0; z < layerCount; z++) {
155
183
  const blockers = [
156
184
  ...(obstacleIndexByLayer[z]?.all() ?? []),
157
185
  ...(hardPlacedByLayer[z] ?? []),
158
- ]
186
+ ].map((b) => ({
187
+ x: quantize(b.x),
188
+ y: quantize(b.y),
189
+ width: quantize(b.width),
190
+ height: quantize(b.height),
191
+ }))
159
192
 
160
193
  // 1) Board edges — find exact uncovered segments along each edge
161
194
 
@@ -372,6 +405,13 @@ export function computeEdgeCandidates3D(params: {
372
405
  }
373
406
 
374
407
  // Strong multi-layer preference then distance.
375
- out.sort((a, b) => b.zSpanLen! - a.zSpanLen! || b.distance - a.distance)
408
+ out.sort(
409
+ (a, b) =>
410
+ b.zSpanLen! - a.zSpanLen! ||
411
+ b.distance - a.distance ||
412
+ a.z - b.z ||
413
+ a.x - b.x ||
414
+ a.y - b.y,
415
+ )
376
416
  return out
377
417
  }