@tscircuit/rectdiff 0.0.37 → 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.
- package/.github/workflows/bun-test.yml +1 -1
- package/lib/RectDiffPipeline.ts +27 -0
- package/lib/math/layers/getUnionZ.ts +6 -0
- package/lib/math/layers/getZLayerName.ts +6 -0
- package/lib/math/layers/getZSpanMask.ts +6 -0
- package/lib/math/layers/hasContiguousZSpan.ts +11 -0
- package/lib/math/rects/intersectRects.ts +28 -0
- package/lib/math/rects/mergeRects.ts +12 -0
- package/lib/math/rects/rectArea.ts +7 -0
- package/lib/math/rects/rectContainsRect.ts +18 -0
- package/lib/math/rects/rectsTouchOrOverlap.ts +12 -0
- package/lib/math/rects/subtractRects.ts +23 -0
- package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +108 -4
- package/lib/solvers/RectDiffSeedingSolver/computeInverseRects.ts +1 -1
- package/lib/solvers/SparseMultilayerPromotionSolver/SparseMultilayerPromotionSolver.ts +134 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/cloneNode.ts +15 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/cloneNodeWithRect.ts +34 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/createResidualNodes.ts +42 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/findBestCoalesceCandidate.ts +98 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/findBestPromotionCandidate.ts +72 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/getUsableMultilayerVolumeShare.ts +34 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/isFreeNode.ts +8 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/nodeToRect.ts +13 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/CoalesceMultilayerTilesSolver.ts +104 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/PromoteSparseMultilayerCoverageSolver.ts +148 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/solvers/TrimContainedSingleLayerCoverageSolver.ts +137 -0
- package/lib/solvers/SparseMultilayerPromotionSolver/types.ts +23 -0
- package/package.json +1 -1
- package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +3 -3
- package/tests/solver/arduino-uno-inner2-ground-bottom-power/__snapshots__/arduino-uno-inner2-ground-bottom-power.snap.svg +3 -3
- package/tests/solver/arduino-uno-inner2-ground-inner1-power/__snapshots__/arduino-uno-inner2-ground-inner1-power.snap.svg +1 -1
- package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
- package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +3 -3
- package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
- package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
- package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
- package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +3 -3
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +3 -3
- package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
- package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
- package/tests/solver/bugreport18-1b2d06/__snapshots__/bugreport18-1b2d06.snap.svg +3 -3
- package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
- package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
- package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +2 -2
- package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
- package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +3 -3
- package/tests/solver/bugreport26-66b0b2/__snapshots__/bugreport26-66b0b2.snap.svg +2 -2
- package/tests/solver/bugreport27-dd3734/__snapshots__/bugreport27-dd3734.snap.svg +2 -2
- package/tests/solver/bugreport28-18a9ef/__snapshots__/bugreport28-18a9ef.snap.svg +2 -2
- package/tests/solver/bugreport29-7deae8/__snapshots__/bugreport29-7deae8.snap.svg +2 -2
- package/tests/solver/bugreport30-2174c8/__snapshots__/bugreport30-2174c8.snap.svg +3 -3
- package/tests/solver/bugreport33-213d45/__snapshots__/bugreport33-213d45.snap.svg +2 -2
- package/tests/solver/bugreport34-e9dea2/__snapshots__/bugreport34-e9dea2.snap.svg +2 -2
- package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +2 -2
- package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +2 -2
- package/tests/solver/interaction/__snapshots__/interaction.snap.svg +3 -3
- package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
- package/tests/solver/no-better-path/__snapshots__/no-better-path.snap.svg +1 -1
- package/tests/solver/offboardconnects01/__snapshots__/offboardconnects01.snap.svg +1 -1
- package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +3 -3
- package/tests/solver/repros/merge-single-layer-node/__snapshots__/merge-single-layer-node.snap.svg +3 -3
- package/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +2 -2
package/lib/RectDiffPipeline.ts
CHANGED
|
@@ -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,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,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
|
-
|
|
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(
|
|
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.
|
|
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 },
|
|
@@ -43,7 +43,7 @@ export function computeInverseRects(
|
|
|
43
43
|
|
|
44
44
|
// Simplify polygon if it has too many points to avoid O(n^2) performance issues
|
|
45
45
|
// A polygon with 350+ points (like rounded corners) creates too many grid cells
|
|
46
|
-
const MAX_POLYGON_POINTS =
|
|
46
|
+
const MAX_POLYGON_POINTS = 120
|
|
47
47
|
const workingPolygon =
|
|
48
48
|
polygon.length > MAX_POLYGON_POINTS
|
|
49
49
|
? simplifyPolygon(
|
|
@@ -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
|
+
})
|