@tscircuit/rectdiff 0.0.34 → 0.0.35
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/lib/RectDiffPipeline.ts +23 -0
- package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +311 -0
- package/lib/types/srj-types.ts +1 -0
- package/package.json +1 -1
- package/tests/solver/arduino-uno-inner2-ground-bottom-power/__snapshots__/arduino-uno-inner2-ground-bottom-power.snap.svg +1 -1
- package/tests/solver/arduino-uno-inner2-ground-inner1-power/__snapshots__/arduino-uno-inner2-ground-inner1-power.snap.svg +3 -3
- package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
- package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
- package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
- package/tests/solver/repros/merge-single-layer-node/__snapshots__/merge-single-layer-node.snap.svg +44 -0
package/lib/RectDiffPipeline.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { GridFill3DOptions, XYRect } from "./rectdiff-types"
|
|
|
8
8
|
import type { CapacityMeshNode } from "./types/capacity-mesh-types"
|
|
9
9
|
import type { GraphicsObject } from "graphics-debug"
|
|
10
10
|
import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
|
|
11
|
+
import { OuterLayerContainmentMergeSolver } from "./solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver"
|
|
11
12
|
import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
|
|
12
13
|
import { createBaseVisualization } from "./rectdiff-visualization"
|
|
13
14
|
import { buildFinalRectDiffVisualization } from "./buildFinalRectDiffVisualization"
|
|
@@ -25,6 +26,7 @@ export interface RectDiffPipelineInput {
|
|
|
25
26
|
export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
|
|
26
27
|
rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
|
|
27
28
|
gapFillSolver?: GapFillSolverPipeline
|
|
29
|
+
outerLayerContainmentMergeSolver?: OuterLayerContainmentMergeSolver
|
|
28
30
|
boardVoidRects: XYRect[] | undefined
|
|
29
31
|
zIndexByName?: Map<string, number>
|
|
30
32
|
layerNames?: string[]
|
|
@@ -69,6 +71,22 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
69
71
|
},
|
|
70
72
|
],
|
|
71
73
|
),
|
|
74
|
+
definePipelineStep(
|
|
75
|
+
"outerLayerContainmentMergeSolver",
|
|
76
|
+
OuterLayerContainmentMergeSolver,
|
|
77
|
+
(rectDiffPipeline: RectDiffPipeline) => [
|
|
78
|
+
{
|
|
79
|
+
meshNodes:
|
|
80
|
+
rectDiffPipeline.gapFillSolver?.getOutput().outputNodes ??
|
|
81
|
+
rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput()
|
|
82
|
+
.meshNodes ??
|
|
83
|
+
[],
|
|
84
|
+
simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
|
|
85
|
+
zIndexByName: rectDiffPipeline.zIndexByName ?? new Map(),
|
|
86
|
+
obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
),
|
|
72
90
|
]
|
|
73
91
|
|
|
74
92
|
override _setup(): void {
|
|
@@ -100,6 +118,11 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
override getOutput(): { meshNodes: CapacityMeshNode[] } {
|
|
121
|
+
const outerLayerMergeOutput =
|
|
122
|
+
this.outerLayerContainmentMergeSolver?.getOutput()
|
|
123
|
+
if (outerLayerMergeOutput) {
|
|
124
|
+
return { meshNodes: outerLayerMergeOutput.outputNodes }
|
|
125
|
+
}
|
|
103
126
|
const gapFillOutput = this.gapFillSolver?.getOutput()
|
|
104
127
|
if (gapFillOutput) {
|
|
105
128
|
return { meshNodes: gapFillOutput.outputNodes }
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
2
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
3
|
+
import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
|
|
4
|
+
import type { XYRect } from "../../rectdiff-types"
|
|
5
|
+
import type { Obstacle, SimpleRouteJson } from "../../types/srj-types"
|
|
6
|
+
import { obstacleToXYRect, obstacleZs } from "../RectDiffSeedingSolver/layers"
|
|
7
|
+
import { getColorForZLayer } from "../../utils/getColorForZLayer"
|
|
8
|
+
import { subtractRect2D, overlaps, EPS } from "../../utils/rectdiff-geometry"
|
|
9
|
+
import { padRect } from "../../utils/padRect"
|
|
10
|
+
|
|
11
|
+
type OuterLayerContainmentMergeSolverInput = {
|
|
12
|
+
meshNodes: CapacityMeshNode[]
|
|
13
|
+
simpleRouteJson: SimpleRouteJson
|
|
14
|
+
zIndexByName: Map<string, number>
|
|
15
|
+
obstacleClearance?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ObstacleWithRect = {
|
|
19
|
+
obstacle: Obstacle
|
|
20
|
+
rect: XYRect
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nodeToRect = (node: CapacityMeshNode): XYRect => ({
|
|
24
|
+
x: node.center.x - node.width / 2,
|
|
25
|
+
y: node.center.y - node.height / 2,
|
|
26
|
+
width: node.width,
|
|
27
|
+
height: node.height,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const rectArea = (rect: XYRect) => rect.width * rect.height
|
|
31
|
+
|
|
32
|
+
const cloneNode = (node: CapacityMeshNode): CapacityMeshNode => ({
|
|
33
|
+
...node,
|
|
34
|
+
center: { ...node.center },
|
|
35
|
+
availableZ: [...node.availableZ],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const cloneNodeWithRect = (
|
|
39
|
+
node: CapacityMeshNode,
|
|
40
|
+
rect: XYRect,
|
|
41
|
+
capacityMeshNodeId: string,
|
|
42
|
+
): CapacityMeshNode => ({
|
|
43
|
+
...node,
|
|
44
|
+
capacityMeshNodeId,
|
|
45
|
+
center: {
|
|
46
|
+
x: rect.x + rect.width / 2,
|
|
47
|
+
y: rect.y + rect.height / 2,
|
|
48
|
+
},
|
|
49
|
+
width: rect.width,
|
|
50
|
+
height: rect.height,
|
|
51
|
+
availableZ: [...node.availableZ],
|
|
52
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const isFreeNode = (node: CapacityMeshNode) =>
|
|
56
|
+
!node._containsObstacle && !node._containsTarget
|
|
57
|
+
|
|
58
|
+
const isSingletonOuterNode = (node: CapacityMeshNode, outerZ: number) =>
|
|
59
|
+
node.availableZ.length === 1 && node.availableZ[0] === outerZ
|
|
60
|
+
|
|
61
|
+
const sameRect = (a: XYRect, b: XYRect) =>
|
|
62
|
+
Math.abs(a.x - b.x) <= EPS &&
|
|
63
|
+
Math.abs(a.y - b.y) <= EPS &&
|
|
64
|
+
Math.abs(a.width - b.width) <= EPS &&
|
|
65
|
+
Math.abs(a.height - b.height) <= EPS
|
|
66
|
+
|
|
67
|
+
const subtractRects = (target: XYRect, cutters: XYRect[]) => {
|
|
68
|
+
let remaining: XYRect[] = [target]
|
|
69
|
+
|
|
70
|
+
for (const cutter of cutters) {
|
|
71
|
+
if (remaining.length === 0) return remaining
|
|
72
|
+
|
|
73
|
+
const nextRemaining: XYRect[] = []
|
|
74
|
+
for (const piece of remaining) {
|
|
75
|
+
nextRemaining.push(...subtractRect2D(piece, cutter))
|
|
76
|
+
}
|
|
77
|
+
remaining = nextRemaining
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return remaining
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const isFullyCoveredByRects = (target: XYRect, coveringRects: XYRect[]) => {
|
|
84
|
+
return subtractRects(target, coveringRects).length === 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class OuterLayerContainmentMergeSolver extends BaseSolver {
|
|
88
|
+
private outputNodes: CapacityMeshNode[] = []
|
|
89
|
+
private promotedNodeIds = new Set<string>()
|
|
90
|
+
private residualNodeIds = new Set<string>()
|
|
91
|
+
|
|
92
|
+
constructor(private input: OuterLayerContainmentMergeSolverInput) {
|
|
93
|
+
super()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override _setup() {
|
|
97
|
+
this.outputNodes = this.input.meshNodes.map(cloneNode)
|
|
98
|
+
this.promotedNodeIds.clear()
|
|
99
|
+
this.residualNodeIds.clear()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override _step() {
|
|
103
|
+
this.outputNodes = this.processOuterLayerContainmentMerges()
|
|
104
|
+
this.solved = true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private processOuterLayerContainmentMerges(): CapacityMeshNode[] {
|
|
108
|
+
const srj = this.input.simpleRouteJson
|
|
109
|
+
const layerCount = Math.max(1, srj.layerCount || 1)
|
|
110
|
+
if (layerCount < 3) {
|
|
111
|
+
return this.input.meshNodes.map(cloneNode)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const topZ = 0
|
|
115
|
+
const bottomZ = layerCount - 1
|
|
116
|
+
const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0)
|
|
117
|
+
const originalNodes = this.input.meshNodes.map(cloneNode)
|
|
118
|
+
const obstaclesByLayer = this.buildObstaclesByLayer(layerCount)
|
|
119
|
+
const mutableOuterNodes = originalNodes.filter(
|
|
120
|
+
(node) =>
|
|
121
|
+
isFreeNode(node) &&
|
|
122
|
+
(isSingletonOuterNode(node, topZ) ||
|
|
123
|
+
isSingletonOuterNode(node, bottomZ)),
|
|
124
|
+
)
|
|
125
|
+
const immutableNodes = originalNodes.filter(
|
|
126
|
+
(node) => !mutableOuterNodes.includes(node),
|
|
127
|
+
)
|
|
128
|
+
const freeSupportRectsByOuterLayer = new Map<number, XYRect[]>()
|
|
129
|
+
freeSupportRectsByOuterLayer.set(
|
|
130
|
+
topZ,
|
|
131
|
+
originalNodes
|
|
132
|
+
.filter((node) => isFreeNode(node) && node.availableZ.includes(topZ))
|
|
133
|
+
.map(nodeToRect),
|
|
134
|
+
)
|
|
135
|
+
freeSupportRectsByOuterLayer.set(
|
|
136
|
+
bottomZ,
|
|
137
|
+
originalNodes
|
|
138
|
+
.filter((node) => isFreeNode(node) && node.availableZ.includes(bottomZ))
|
|
139
|
+
.map(nodeToRect),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const promotedNodes: CapacityMeshNode[] = []
|
|
143
|
+
const promotedRects: XYRect[] = []
|
|
144
|
+
const candidateNodes = mutableOuterNodes
|
|
145
|
+
.filter(
|
|
146
|
+
(node) =>
|
|
147
|
+
node.width + EPS >= viaMinSize && node.height + EPS >= viaMinSize,
|
|
148
|
+
)
|
|
149
|
+
.sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)))
|
|
150
|
+
|
|
151
|
+
for (const candidate of candidateNodes) {
|
|
152
|
+
const candidateZ = candidate.availableZ[0]!
|
|
153
|
+
const oppositeZ = candidateZ === topZ ? bottomZ : topZ
|
|
154
|
+
const candidateRect = nodeToRect(candidate)
|
|
155
|
+
const oppositeSupportRects =
|
|
156
|
+
freeSupportRectsByOuterLayer.get(oppositeZ) ?? []
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
!this.isTransitCompatibleAcrossIntermediateLayers({
|
|
160
|
+
rect: candidateRect,
|
|
161
|
+
fromZ: candidateZ,
|
|
162
|
+
toZ: oppositeZ,
|
|
163
|
+
obstaclesByLayer,
|
|
164
|
+
})
|
|
165
|
+
) {
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
if (!isFullyCoveredByRects(candidateRect, oppositeSupportRects)) {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
promotedNodes.push({
|
|
173
|
+
...candidate,
|
|
174
|
+
availableZ: [topZ, bottomZ],
|
|
175
|
+
layer: `z${topZ},${bottomZ}`,
|
|
176
|
+
})
|
|
177
|
+
promotedRects.push(candidateRect)
|
|
178
|
+
this.promotedNodeIds.add(candidate.capacityMeshNodeId)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let nextResidualId = 0
|
|
182
|
+
const residualNodes: CapacityMeshNode[] = []
|
|
183
|
+
|
|
184
|
+
for (const node of mutableOuterNodes) {
|
|
185
|
+
if (this.promotedNodeIds.has(node.capacityMeshNodeId)) {
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const nodeRect = nodeToRect(node)
|
|
190
|
+
const remainingPieces = subtractRects(nodeRect, promotedRects)
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
remainingPieces.length === 1 &&
|
|
194
|
+
sameRect(remainingPieces[0]!, nodeRect)
|
|
195
|
+
) {
|
|
196
|
+
residualNodes.push(node)
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const piece of remainingPieces) {
|
|
201
|
+
const residualNode = cloneNodeWithRect(
|
|
202
|
+
node,
|
|
203
|
+
piece,
|
|
204
|
+
`${node.capacityMeshNodeId}-outer-merge-${nextResidualId++}`,
|
|
205
|
+
)
|
|
206
|
+
residualNodes.push(residualNode)
|
|
207
|
+
this.residualNodeIds.add(residualNode.capacityMeshNodeId)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return [...immutableNodes, ...promotedNodes, ...residualNodes]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private buildObstaclesByLayer(layerCount: number): ObstacleWithRect[][] {
|
|
215
|
+
const out = Array.from(
|
|
216
|
+
{ length: layerCount },
|
|
217
|
+
() => [] as ObstacleWithRect[],
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
for (const obstacle of this.input.simpleRouteJson.obstacles ?? []) {
|
|
221
|
+
const baseRect = obstacleToXYRect(obstacle)
|
|
222
|
+
if (!baseRect) continue
|
|
223
|
+
const rect = padRect(baseRect, this.input.obstacleClearance ?? 0)
|
|
224
|
+
const zLayers = obstacleZs(obstacle, this.input.zIndexByName)
|
|
225
|
+
|
|
226
|
+
for (const z of zLayers) {
|
|
227
|
+
if (z < 0 || z >= layerCount) continue
|
|
228
|
+
out[z]!.push({ obstacle, rect })
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return out
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private isTransitCompatibleAcrossIntermediateLayers(params: {
|
|
236
|
+
rect: XYRect
|
|
237
|
+
fromZ: number
|
|
238
|
+
toZ: number
|
|
239
|
+
obstaclesByLayer: ObstacleWithRect[][]
|
|
240
|
+
}) {
|
|
241
|
+
const { rect, fromZ, toZ, obstaclesByLayer } = params
|
|
242
|
+
const lo = Math.min(fromZ, toZ)
|
|
243
|
+
const hi = Math.max(fromZ, toZ)
|
|
244
|
+
|
|
245
|
+
if (hi - lo < 2) return false
|
|
246
|
+
|
|
247
|
+
for (let z = lo + 1; z < hi; z++) {
|
|
248
|
+
const overlapping = (obstaclesByLayer[z] ?? []).filter((entry) =>
|
|
249
|
+
overlaps(entry.rect, rect),
|
|
250
|
+
)
|
|
251
|
+
if (overlapping.length === 0) return false
|
|
252
|
+
|
|
253
|
+
const nonCopperOverlap = overlapping.some(
|
|
254
|
+
(entry) => !entry.obstacle.isCopperPour,
|
|
255
|
+
)
|
|
256
|
+
if (nonCopperOverlap) return false
|
|
257
|
+
|
|
258
|
+
const copperRects = overlapping
|
|
259
|
+
.filter((entry) => entry.obstacle.isCopperPour)
|
|
260
|
+
.map((entry) => entry.rect)
|
|
261
|
+
|
|
262
|
+
if (!isFullyCoveredByRects(rect, copperRects)) {
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return true
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
override getOutput(): { outputNodes: CapacityMeshNode[] } {
|
|
271
|
+
return { outputNodes: this.outputNodes }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
override visualize(): GraphicsObject {
|
|
275
|
+
return {
|
|
276
|
+
title: "OuterLayerContainmentMergeSolver",
|
|
277
|
+
coordinateSystem: "cartesian",
|
|
278
|
+
rects: this.outputNodes.map((node) => {
|
|
279
|
+
const colors = getColorForZLayer(node.availableZ)
|
|
280
|
+
const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId)
|
|
281
|
+
const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId)
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
center: node.center,
|
|
285
|
+
width: node.width,
|
|
286
|
+
height: node.height,
|
|
287
|
+
stroke: isPromoted
|
|
288
|
+
? "rgba(22, 163, 74, 0.95)"
|
|
289
|
+
: isResidual
|
|
290
|
+
? "rgba(37, 99, 235, 0.95)"
|
|
291
|
+
: colors.stroke,
|
|
292
|
+
fill: node._containsObstacle
|
|
293
|
+
? "rgba(239, 68, 68, 0.35)"
|
|
294
|
+
: isPromoted
|
|
295
|
+
? "rgba(34, 197, 94, 0.28)"
|
|
296
|
+
: isResidual
|
|
297
|
+
? "rgba(59, 130, 246, 0.18)"
|
|
298
|
+
: colors.fill,
|
|
299
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
300
|
+
label: [
|
|
301
|
+
`node ${node.capacityMeshNodeId}`,
|
|
302
|
+
`z:${node.availableZ.join(",")}`,
|
|
303
|
+
].join("\n"),
|
|
304
|
+
}
|
|
305
|
+
}),
|
|
306
|
+
points: [],
|
|
307
|
+
lines: [],
|
|
308
|
+
texts: [],
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
package/lib/types/srj-types.ts
CHANGED