@tscircuit/rectdiff 0.0.38 → 0.0.39

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/.github/workflows/bun-test.yml +1 -1
  2. package/lib/RectDiffPipeline.ts +27 -0
  3. package/lib/math/layers/getUnionZ.ts +6 -0
  4. package/lib/math/layers/getZLayerName.ts +6 -0
  5. package/lib/math/layers/getZSpanMask.ts +6 -0
  6. package/lib/math/layers/hasContiguousZSpan.ts +11 -0
  7. package/lib/math/rects/intersectRects.ts +28 -0
  8. package/lib/math/rects/mergeRects.ts +12 -0
  9. package/lib/math/rects/rectArea.ts +7 -0
  10. package/lib/math/rects/rectContainsRect.ts +18 -0
  11. package/lib/math/rects/rectsTouchOrOverlap.ts +12 -0
  12. package/lib/math/rects/subtractRects.ts +23 -0
  13. package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +108 -4
  14. package/lib/solvers/SparseMultilayerPromotionSolver/SparseMultilayerPromotionSolver.ts +134 -0
  15. package/lib/solvers/SparseMultilayerPromotionSolver/cloneNode.ts +15 -0
  16. package/lib/solvers/SparseMultilayerPromotionSolver/cloneNodeWithRect.ts +34 -0
  17. package/lib/solvers/SparseMultilayerPromotionSolver/createResidualNodes.ts +42 -0
  18. package/lib/solvers/SparseMultilayerPromotionSolver/findBestCoalesceCandidate.ts +98 -0
  19. package/lib/solvers/SparseMultilayerPromotionSolver/findBestPromotionCandidate.ts +72 -0
  20. package/lib/solvers/SparseMultilayerPromotionSolver/getUsableMultilayerVolumeShare.ts +34 -0
  21. package/lib/solvers/SparseMultilayerPromotionSolver/isFreeNode.ts +8 -0
  22. package/lib/solvers/SparseMultilayerPromotionSolver/nodeToRect.ts +13 -0
  23. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/CoalesceMultilayerTilesSolver.ts +104 -0
  24. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/PromoteSparseMultilayerCoverageSolver.ts +148 -0
  25. package/lib/solvers/SparseMultilayerPromotionSolver/solvers/TrimContainedSingleLayerCoverageSolver.ts +137 -0
  26. package/lib/solvers/SparseMultilayerPromotionSolver/types.ts +23 -0
  27. package/package.json +1 -1
  28. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +3 -3
  29. package/tests/solver/arduino-uno-inner2-ground-bottom-power/__snapshots__/arduino-uno-inner2-ground-bottom-power.snap.svg +3 -3
  30. package/tests/solver/arduino-uno-inner2-ground-inner1-power/__snapshots__/arduino-uno-inner2-ground-inner1-power.snap.svg +1 -1
  31. package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
  32. package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +3 -3
  33. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  34. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  35. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
  36. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +3 -3
  37. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +3 -3
  38. package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
  39. package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
  40. package/tests/solver/bugreport18-1b2d06/__snapshots__/bugreport18-1b2d06.snap.svg +3 -3
  41. package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
  42. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  43. package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +2 -2
  44. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  45. package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +3 -3
  46. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +2 -2
  47. package/tests/solver/interaction/__snapshots__/interaction.snap.svg +3 -3
  48. package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
  49. package/tests/solver/no-better-path/__snapshots__/no-better-path.snap.svg +1 -1
  50. package/tests/solver/offboardconnects01/__snapshots__/offboardconnects01.snap.svg +1 -1
  51. package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +3 -3
  52. package/tests/solver/repros/merge-single-layer-node/__snapshots__/merge-single-layer-node.snap.svg +3 -3
  53. package/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +2 -2
@@ -28,4 +28,4 @@ jobs:
28
28
  run: bun install
29
29
 
30
30
  - name: Run tests
31
- run: bun test --timeout 9999
31
+ run: bun test --timeout 999999
@@ -16,17 +16,20 @@ import { computeInverseRects } from "./solvers/RectDiffSeedingSolver/computeInve
16
16
  import { buildZIndexMap } from "./solvers/RectDiffSeedingSolver/layers"
17
17
  import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
18
18
  import { mergeGraphics } from "graphics-debug"
19
+ import { SparseMultilayerPromotionSolver } from "./solvers/SparseMultilayerPromotionSolver/SparseMultilayerPromotionSolver"
19
20
 
20
21
  export interface RectDiffPipelineInput {
21
22
  simpleRouteJson: SimpleRouteJson
22
23
  gridOptions?: Partial<GridFill3DOptions>
23
24
  obstacleClearance?: number
25
+ sparseMultilayerPromotionTargetShare?: number
24
26
  }
25
27
 
26
28
  export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
27
29
  rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
28
30
  gapFillSolver?: GapFillSolverPipeline
29
31
  outerLayerContainmentMergeSolver?: OuterLayerContainmentMergeSolver
32
+ sparseMultilayerPromotionSolver?: SparseMultilayerPromotionSolver
30
33
  boardVoidRects: XYRect[] | undefined
31
34
  zIndexByName?: Map<string, number>
32
35
  layerNames?: string[]
@@ -87,6 +90,25 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
87
90
  },
88
91
  ],
89
92
  ),
93
+ definePipelineStep(
94
+ "sparseMultilayerPromotionSolver",
95
+ SparseMultilayerPromotionSolver,
96
+ (rectDiffPipeline: RectDiffPipeline) => [
97
+ {
98
+ meshNodes:
99
+ rectDiffPipeline.outerLayerContainmentMergeSolver?.getOutput()
100
+ .outputNodes ??
101
+ rectDiffPipeline.gapFillSolver?.getOutput().outputNodes ??
102
+ rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput()
103
+ .meshNodes ??
104
+ [],
105
+ promotionTargetShare:
106
+ rectDiffPipeline.inputProblem
107
+ .sparseMultilayerPromotionTargetShare ?? 0.86,
108
+ simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
109
+ },
110
+ ],
111
+ ),
90
112
  ]
91
113
 
92
114
  override _setup(): void {
@@ -120,6 +142,11 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
120
142
  override getOutput(): { meshNodes: CapacityMeshNode[] } {
121
143
  const outerLayerMergeOutput =
122
144
  this.outerLayerContainmentMergeSolver?.getOutput()
145
+ const sparseMultilayerOutput =
146
+ this.sparseMultilayerPromotionSolver?.getOutput()
147
+ if (sparseMultilayerOutput) {
148
+ return { meshNodes: sparseMultilayerOutput.outputNodes }
149
+ }
123
150
  if (outerLayerMergeOutput) {
124
151
  return { meshNodes: outerLayerMergeOutput.outputNodes }
125
152
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Combine two layer lists into one ordered list.
3
+ * Duplicate layers are removed.
4
+ */
5
+ export const getUnionZ = ({ a, b }: { a: number[]; b: number[] }) =>
6
+ [...new Set([...a, ...b])].sort((x, y) => x - y)
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Build the display name for a layer span.
3
+ * This keeps layer labels consistent.
4
+ */
5
+ export const getZLayerName = ({ availableZ }: { availableZ: number[] }) =>
6
+ `z${availableZ.join(",")}`
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Turn a layer list into a numeric mask.
3
+ * This is used for fast grouping and comparisons.
4
+ */
5
+ export const getZSpanMask = ({ availableZ }: { availableZ: number[] }) =>
6
+ availableZ.reduce((mask, z) => mask | (1 << z), 0)
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Check whether the layers form one continuous range.
3
+ * Gapped layer spans return false.
4
+ */
5
+ export const hasContiguousZSpan = ({ zValues }: { zValues: number[] }) => {
6
+ for (let i = 1; i < zValues.length; i++) {
7
+ if (zValues[i]! - zValues[i - 1]! !== 1) return false
8
+ }
9
+
10
+ return true
11
+ }
@@ -0,0 +1,28 @@
1
+ import type { XYRect } from "../../rectdiff-types"
2
+ import { EPS } from "../../utils/rectdiff-geometry"
3
+
4
+ /**
5
+ * Return the shared area between two rectangles.
6
+ * Returns null when they do not overlap.
7
+ */
8
+ export const intersectRects = ({
9
+ a,
10
+ b,
11
+ }: {
12
+ a: XYRect
13
+ b: XYRect
14
+ }): XYRect | null => {
15
+ const x0 = Math.max(a.x, b.x)
16
+ const y0 = Math.max(a.y, b.y)
17
+ const x1 = Math.min(a.x + a.width, b.x + b.width)
18
+ const y1 = Math.min(a.y + a.height, b.y + b.height)
19
+
20
+ if (x1 <= x0 + EPS || y1 <= y0 + EPS) return null
21
+
22
+ return {
23
+ x: x0,
24
+ y: y0,
25
+ width: x1 - x0,
26
+ height: y1 - y0,
27
+ }
28
+ }
@@ -0,0 +1,12 @@
1
+ import type { XYRect } from "../../rectdiff-types"
2
+
3
+ /**
4
+ * Build one rectangle that covers both inputs.
5
+ * This is the outer box around the two shapes.
6
+ */
7
+ export const mergeRects = ({ a, b }: { a: XYRect; b: XYRect }): XYRect => ({
8
+ x: Math.min(a.x, b.x),
9
+ y: Math.min(a.y, b.y),
10
+ width: Math.max(a.x + a.width, b.x + b.width) - Math.min(a.x, b.x),
11
+ height: Math.max(a.y + a.height, b.y + b.height) - Math.min(a.y, b.y),
12
+ })
@@ -0,0 +1,7 @@
1
+ import type { XYRect } from "../../rectdiff-types"
2
+
3
+ /**
4
+ * Return the area of a rectangle.
5
+ * Width and height are multiplied directly.
6
+ */
7
+ export const rectArea = ({ rect }: { rect: XYRect }) => rect.width * rect.height
@@ -0,0 +1,18 @@
1
+ import type { XYRect } from "../../rectdiff-types"
2
+ import { EPS } from "../../utils/rectdiff-geometry"
3
+
4
+ /**
5
+ * Check whether one rectangle fully contains another.
6
+ * A small tolerance is used for edge cases.
7
+ */
8
+ export const rectContainsRect = ({
9
+ inner,
10
+ outer,
11
+ }: {
12
+ inner: XYRect
13
+ outer: XYRect
14
+ }) =>
15
+ inner.x + EPS >= outer.x &&
16
+ inner.y + EPS >= outer.y &&
17
+ inner.x + inner.width <= outer.x + outer.width + EPS &&
18
+ inner.y + inner.height <= outer.y + outer.height + EPS
@@ -0,0 +1,12 @@
1
+ import type { XYRect } from "../../rectdiff-types"
2
+ import { EPS } from "../../utils/rectdiff-geometry"
3
+
4
+ /**
5
+ * Check whether two rectangles touch or overlap.
6
+ * Separate rectangles return false.
7
+ */
8
+ export const rectsTouchOrOverlap = ({ a, b }: { a: XYRect; b: XYRect }) =>
9
+ a.x <= b.x + b.width + EPS &&
10
+ b.x <= a.x + a.width + EPS &&
11
+ a.y <= b.y + b.height + EPS &&
12
+ b.y <= a.y + a.height + EPS
@@ -0,0 +1,23 @@
1
+ import type { XYRect } from "../../rectdiff-types"
2
+ import { subtractRect2D } from "../../utils/rectdiff-geometry"
3
+
4
+ /**
5
+ * Remove several rectangles from one rectangle.
6
+ * The result is the list of remaining pieces.
7
+ */
8
+ export const subtractRects = ({
9
+ cutters,
10
+ target,
11
+ }: {
12
+ cutters: XYRect[]
13
+ target: XYRect
14
+ }) => {
15
+ let remaining: XYRect[] = [target]
16
+
17
+ for (const cutter of cutters) {
18
+ if (remaining.length === 0) return remaining
19
+ remaining = remaining.flatMap((piece) => subtractRect2D(piece, cutter))
20
+ }
21
+
22
+ return remaining
23
+ }
@@ -29,6 +29,22 @@ const nodeToRect = (node: CapacityMeshNode): XYRect => ({
29
29
 
30
30
  const rectArea = (rect: XYRect) => rect.width * rect.height
31
31
 
32
+ const intersectRects = (a: XYRect, b: XYRect): XYRect | null => {
33
+ const x0 = Math.max(a.x, b.x)
34
+ const y0 = Math.max(a.y, b.y)
35
+ const x1 = Math.min(a.x + a.width, b.x + b.width)
36
+ const y1 = Math.min(a.y + a.height, b.y + b.height)
37
+
38
+ if (x1 <= x0 + EPS || y1 <= y0 + EPS) return null
39
+
40
+ return {
41
+ x: x0,
42
+ y: y0,
43
+ width: x1 - x0,
44
+ height: y1 - y0,
45
+ }
46
+ }
47
+
32
48
  const cloneNode = (node: CapacityMeshNode): CapacityMeshNode => ({
33
49
  ...node,
34
50
  center: { ...node.center },
@@ -87,6 +103,7 @@ const isFullyCoveredByRects = (target: XYRect, coveringRects: XYRect[]) => {
87
103
  export class OuterLayerContainmentMergeSolver extends BaseSolver {
88
104
  private outputNodes: CapacityMeshNode[] = []
89
105
  private promotedNodeIds = new Set<string>()
106
+ private fullyPromotedNodeIds = new Set<string>()
90
107
  private residualNodeIds = new Set<string>()
91
108
 
92
109
  constructor(private input: OuterLayerContainmentMergeSolverInput) {
@@ -96,6 +113,7 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {
96
113
  override _setup() {
97
114
  this.outputNodes = this.input.meshNodes.map(cloneNode)
98
115
  this.promotedNodeIds.clear()
116
+ this.fullyPromotedNodeIds.clear()
99
117
  this.residualNodeIds.clear()
100
118
  }
101
119
 
@@ -116,6 +134,8 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {
116
134
  const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0)
117
135
  const originalNodes = this.input.meshNodes.map(cloneNode)
118
136
  const obstaclesByLayer = this.buildObstaclesByLayer(layerCount)
137
+ const shouldAllowPartialPromotion =
138
+ this.getUsableMultilayerVolumeShare(originalNodes) < 0.5
119
139
  const mutableOuterNodes = originalNodes.filter(
120
140
  (node) =>
121
141
  isFreeNode(node) &&
@@ -166,23 +186,51 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {
166
186
  continue
167
187
  }
168
188
  if (!isFullyCoveredByRects(candidateRect, oppositeSupportRects)) {
189
+ if (!shouldAllowPartialPromotion) {
190
+ continue
191
+ }
192
+
193
+ const partialPromotionRects = this.getSupportedPromotionRects({
194
+ candidateRect,
195
+ supportRects: oppositeSupportRects,
196
+ minRectSize: viaMinSize,
197
+ })
198
+
199
+ if (partialPromotionRects.length === 0) {
200
+ continue
201
+ }
202
+
203
+ for (const partialRect of partialPromotionRects) {
204
+ const promotedNode = cloneNodeWithRect(
205
+ candidate,
206
+ partialRect,
207
+ `${candidate.capacityMeshNodeId}-outer-partial-${promotedNodes.length}`,
208
+ )
209
+ promotedNode.availableZ = [topZ, bottomZ]
210
+ promotedNode.layer = `z${topZ},${bottomZ}`
211
+ promotedNodes.push(promotedNode)
212
+ promotedRects.push(partialRect)
213
+ this.promotedNodeIds.add(promotedNode.capacityMeshNodeId)
214
+ }
169
215
  continue
170
216
  }
171
217
 
172
- promotedNodes.push({
218
+ const promotedNode = {
173
219
  ...candidate,
174
220
  availableZ: [topZ, bottomZ],
175
221
  layer: `z${topZ},${bottomZ}`,
176
- })
222
+ }
223
+ promotedNodes.push(promotedNode)
177
224
  promotedRects.push(candidateRect)
178
- this.promotedNodeIds.add(candidate.capacityMeshNodeId)
225
+ this.promotedNodeIds.add(promotedNode.capacityMeshNodeId)
226
+ this.fullyPromotedNodeIds.add(candidate.capacityMeshNodeId)
179
227
  }
180
228
 
181
229
  let nextResidualId = 0
182
230
  const residualNodes: CapacityMeshNode[] = []
183
231
 
184
232
  for (const node of mutableOuterNodes) {
185
- if (this.promotedNodeIds.has(node.capacityMeshNodeId)) {
233
+ if (this.fullyPromotedNodeIds.has(node.capacityMeshNodeId)) {
186
234
  continue
187
235
  }
188
236
 
@@ -211,6 +259,62 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {
211
259
  return [...immutableNodes, ...promotedNodes, ...residualNodes]
212
260
  }
213
261
 
262
+ private getUsableMultilayerVolumeShare(nodes: CapacityMeshNode[]) {
263
+ let totalVolume = 0
264
+ let obstacleVolume = 0
265
+ let multilayerVolume = 0
266
+
267
+ for (const node of nodes) {
268
+ const volume = node.width * node.height * node.availableZ.length
269
+ totalVolume += volume
270
+ if (node._containsObstacle) {
271
+ obstacleVolume += volume
272
+ continue
273
+ }
274
+ if (node.availableZ.length > 1) {
275
+ multilayerVolume += volume
276
+ }
277
+ }
278
+
279
+ const usableVolume = totalVolume - obstacleVolume
280
+ if (usableVolume <= EPS) return 0
281
+ return multilayerVolume / usableVolume
282
+ }
283
+
284
+ private getSupportedPromotionRects(params: {
285
+ candidateRect: XYRect
286
+ supportRects: XYRect[]
287
+ minRectSize: number
288
+ }) {
289
+ const { candidateRect, supportRects, minRectSize } = params
290
+ const promotedPieces: XYRect[] = []
291
+
292
+ for (const supportRect of supportRects) {
293
+ const overlapRect = intersectRects(candidateRect, supportRect)
294
+ if (!overlapRect) continue
295
+
296
+ let remainingPieces = [overlapRect]
297
+ for (const existingPiece of promotedPieces) {
298
+ remainingPieces = remainingPieces.flatMap((piece) =>
299
+ subtractRect2D(piece, existingPiece),
300
+ )
301
+ if (remainingPieces.length === 0) break
302
+ }
303
+
304
+ for (const piece of remainingPieces) {
305
+ if (
306
+ piece.width + EPS < minRectSize ||
307
+ piece.height + EPS < minRectSize
308
+ ) {
309
+ continue
310
+ }
311
+ promotedPieces.push(piece)
312
+ }
313
+ }
314
+
315
+ return promotedPieces
316
+ }
317
+
214
318
  private buildObstaclesByLayer(layerCount: number): ObstacleWithRect[][] {
215
319
  const out = Array.from(
216
320
  { length: layerCount },
@@ -0,0 +1,134 @@
1
+ import {
2
+ BasePipelineSolver,
3
+ definePipelineStep,
4
+ type PipelineStep,
5
+ } from "@tscircuit/solver-utils"
6
+ import type { GraphicsObject } from "graphics-debug"
7
+ import type {
8
+ CapacityMeshNode,
9
+ CapacityMeshNodeId,
10
+ } from "../../types/capacity-mesh-types"
11
+ import { getZLayerName } from "../../math/layers/getZLayerName"
12
+ import { getColorForZLayer } from "../../utils/getColorForZLayer"
13
+ import { CoalesceMultilayerTilesSolver } from "./solvers/CoalesceMultilayerTilesSolver"
14
+ import { PromoteSparseMultilayerCoverageSolver } from "./solvers/PromoteSparseMultilayerCoverageSolver"
15
+ import { TrimContainedSingleLayerCoverageSolver } from "./solvers/TrimContainedSingleLayerCoverageSolver"
16
+ import type { SparseMultilayerPromotionInput } from "./types"
17
+
18
+ /**
19
+ * This pipeline makes shared multilayer regions easier to use.
20
+ * It grows shared space, removes redundant leftovers, and combines small tiles.
21
+ */
22
+ export class SparseMultilayerPromotionSolver extends BasePipelineSolver<SparseMultilayerPromotionInput> {
23
+ coalesceMultilayerTilesSolver?: CoalesceMultilayerTilesSolver
24
+ promoteSparseMultilayerCoverageSolver?: PromoteSparseMultilayerCoverageSolver
25
+ trimContainedSingleLayerCoverageSolver?: TrimContainedSingleLayerCoverageSolver
26
+
27
+ override pipelineDef: PipelineStep<any>[] = [
28
+ definePipelineStep(
29
+ "promoteSparseMultilayerCoverageSolver",
30
+ PromoteSparseMultilayerCoverageSolver,
31
+ (solver: SparseMultilayerPromotionSolver) => [
32
+ {
33
+ meshNodes: solver.inputProblem.meshNodes,
34
+ minRectSize: Math.max(
35
+ solver.inputProblem.simpleRouteJson.minViaDiameter ?? 0,
36
+ solver.inputProblem.simpleRouteJson.minTraceWidth ?? 0,
37
+ ),
38
+ promotionTargetShare: solver.inputProblem.promotionTargetShare,
39
+ },
40
+ ],
41
+ ),
42
+ definePipelineStep(
43
+ "trimContainedSingleLayerCoverageSolver",
44
+ TrimContainedSingleLayerCoverageSolver,
45
+ (solver: SparseMultilayerPromotionSolver) => [
46
+ {
47
+ meshNodes:
48
+ solver.promoteSparseMultilayerCoverageSolver?.getOutput()
49
+ .outputNodes ?? solver.inputProblem.meshNodes,
50
+ minRectSize: Math.max(
51
+ solver.inputProblem.simpleRouteJson.minViaDiameter ?? 0,
52
+ solver.inputProblem.simpleRouteJson.minTraceWidth ?? 0,
53
+ ),
54
+ promotionTargetShare: solver.inputProblem.promotionTargetShare,
55
+ },
56
+ ],
57
+ ),
58
+ definePipelineStep(
59
+ "coalesceMultilayerTilesSolver",
60
+ CoalesceMultilayerTilesSolver,
61
+ (solver: SparseMultilayerPromotionSolver) => [
62
+ {
63
+ meshNodes:
64
+ solver.trimContainedSingleLayerCoverageSolver?.getOutput()
65
+ .outputNodes ??
66
+ solver.promoteSparseMultilayerCoverageSolver?.getOutput()
67
+ .outputNodes ??
68
+ solver.inputProblem.meshNodes,
69
+ },
70
+ ],
71
+ ),
72
+ ]
73
+
74
+ override getConstructorParams() {
75
+ return [this.inputProblem]
76
+ }
77
+
78
+ override getOutput(): { outputNodes: CapacityMeshNode[] } {
79
+ return {
80
+ outputNodes:
81
+ this.coalesceMultilayerTilesSolver?.getOutput().outputNodes ??
82
+ this.trimContainedSingleLayerCoverageSolver?.getOutput().outputNodes ??
83
+ this.promoteSparseMultilayerCoverageSolver?.getOutput().outputNodes ??
84
+ this.inputProblem.meshNodes,
85
+ }
86
+ }
87
+
88
+ override finalVisualize(): GraphicsObject {
89
+ const promotedIds = new Set<CapacityMeshNodeId>([
90
+ ...(this.promoteSparseMultilayerCoverageSolver?.getOutput()
91
+ .promotedNodeIds ?? []),
92
+ ...(this.coalesceMultilayerTilesSolver?.getOutput().promotedNodeIds ??
93
+ []),
94
+ ])
95
+ const residualIds = new Set<CapacityMeshNodeId>([
96
+ ...(this.promoteSparseMultilayerCoverageSolver?.getOutput()
97
+ .residualNodeIds ?? []),
98
+ ...(this.trimContainedSingleLayerCoverageSolver?.getOutput()
99
+ .residualNodeIds ?? []),
100
+ ])
101
+
102
+ return {
103
+ title: "SparseMultilayerPromotionSolver",
104
+ coordinateSystem: "cartesian",
105
+ rects: this.getOutput().outputNodes.map((node) => {
106
+ const colors = getColorForZLayer(node.availableZ)
107
+ const isPromoted = promotedIds.has(node.capacityMeshNodeId)
108
+ const isResidual = residualIds.has(node.capacityMeshNodeId)
109
+
110
+ return {
111
+ center: node.center,
112
+ width: node.width,
113
+ height: node.height,
114
+ stroke: isPromoted
115
+ ? "rgba(168, 85, 247, 0.95)"
116
+ : isResidual
117
+ ? "rgba(14, 116, 144, 0.95)"
118
+ : colors.stroke,
119
+ fill: node._containsObstacle
120
+ ? "rgba(239, 68, 68, 0.35)"
121
+ : isPromoted
122
+ ? "rgba(192, 132, 252, 0.28)"
123
+ : isResidual
124
+ ? "rgba(34, 211, 238, 0.18)"
125
+ : colors.fill,
126
+ layer: getZLayerName({ availableZ: node.availableZ }),
127
+ }
128
+ }),
129
+ points: [],
130
+ lines: [],
131
+ texts: [],
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,15 @@
1
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
2
+
3
+ /**
4
+ * Make a safe copy of a node.
5
+ * Later steps can change the copy without touching the original.
6
+ */
7
+ export const cloneNode = ({
8
+ node,
9
+ }: {
10
+ node: CapacityMeshNode
11
+ }): CapacityMeshNode => ({
12
+ ...node,
13
+ center: { ...node.center },
14
+ availableZ: [...node.availableZ],
15
+ })
@@ -0,0 +1,34 @@
1
+ import type { XYRect } from "../../rectdiff-types"
2
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
3
+ import { getZLayerName } from "../../math/layers/getZLayerName"
4
+
5
+ /**
6
+ * Copy a node and replace its shape.
7
+ * The layer span can also be replaced when needed.
8
+ */
9
+ export const cloneNodeWithRect = ({
10
+ rect,
11
+ templateNode,
12
+ availableZ,
13
+ capacityMeshNodeId,
14
+ }: {
15
+ availableZ?: number[]
16
+ capacityMeshNodeId: string
17
+ rect: XYRect
18
+ templateNode: CapacityMeshNode
19
+ }): CapacityMeshNode => {
20
+ const nextAvailableZ = availableZ ?? templateNode.availableZ
21
+
22
+ return {
23
+ ...templateNode,
24
+ capacityMeshNodeId,
25
+ center: {
26
+ x: rect.x + rect.width / 2,
27
+ y: rect.y + rect.height / 2,
28
+ },
29
+ width: rect.width,
30
+ height: rect.height,
31
+ availableZ: [...nextAvailableZ],
32
+ layer: getZLayerName({ availableZ: nextAvailableZ }),
33
+ }
34
+ }
@@ -0,0 +1,42 @@
1
+ import { subtractRect2D } from "../../utils/rectdiff-geometry"
2
+ import type {
3
+ CapacityMeshNode,
4
+ CapacityMeshNodeId,
5
+ } from "../../types/capacity-mesh-types"
6
+ import type { XYRect } from "../../rectdiff-types"
7
+ import { cloneNodeWithRect } from "./cloneNodeWithRect"
8
+
9
+ /**
10
+ * Cut one rectangle out of a node and return the remaining pieces.
11
+ * Each remaining piece is turned back into a node.
12
+ */
13
+ export const createResidualNodes = ({
14
+ cutRect,
15
+ getNextResidualId,
16
+ idPrefix,
17
+ node,
18
+ onResidualNodeIdCreated,
19
+ }: {
20
+ cutRect: XYRect
21
+ getNextResidualId: () => number
22
+ idPrefix: string
23
+ node: CapacityMeshNode
24
+ onResidualNodeIdCreated: (nodeId: CapacityMeshNodeId) => void
25
+ }) =>
26
+ subtractRect2D(
27
+ {
28
+ x: node.center.x - node.width / 2,
29
+ y: node.center.y - node.height / 2,
30
+ width: node.width,
31
+ height: node.height,
32
+ },
33
+ cutRect,
34
+ ).map((rect) => {
35
+ const residualNode = cloneNodeWithRect({
36
+ templateNode: node,
37
+ rect,
38
+ capacityMeshNodeId: `${idPrefix}-${getNextResidualId()}`,
39
+ })
40
+ onResidualNodeIdCreated(residualNode.capacityMeshNodeId)
41
+ return residualNode
42
+ })