@tscircuit/rectdiff 0.0.27 → 0.0.29
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-pver-release.yml +24 -45
- package/dist/index.d.ts +0 -46
- package/dist/index.js +220 -774
- package/lib/RectDiffPipeline.ts +0 -46
- package/lib/types/srj-types.ts +0 -1
- package/package.json +1 -2
- package/pages/repro/merge-single-layer-node.page.tsx +17 -0
- package/tests/__snapshots__/board-outline.snap.svg +2 -2
- package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
- package/tests/solver/both-points-equivalent/__snapshots__/both-points-equivalent.snap.svg +1 -1
- package/tests/solver/bugreport01-be84eb/__snapshots__/bugreport01-be84eb.snap.svg +1 -1
- package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +1 -1
- package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
- package/tests/solver/bugreport07-d3f3be/__snapshots__/bugreport07-d3f3be.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 +1 -1
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
- package/tests/solver/bugreport12-35ce1c/__snapshots__/bugreport12-35ce1c.snap.svg +1 -1
- package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +1 -1
- package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +1 -1
- package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
- package/tests/solver/bugreport20-obstacle-clipping/__snapshots__/bugreport20-obstacle-clipping.snap.svg +1 -1
- package/tests/solver/bugreport21-board-outline/__snapshots__/bugreport21-board-outline.snap.svg +2 -2
- package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
- package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +1 -1
- package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
- package/tests/solver/bugreport25-4b1d55/__snapshots__/bugreport25-4b1d55.snap.svg +1 -1
- package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
- package/tests/solver/interaction/__snapshots__/interaction.snap.svg +1 -1
- 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/repros/merge-single-layer-node/merge-single-layer-node.json +861 -0
- package/tests/solver/{bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.test.ts → repros/merge-single-layer-node/merge-single-layer-node.test.ts} +7 -42
- package/tests/solver/transitivity/__snapshots__/transitivity.snap.svg +2 -2
- package/tsconfig.json +5 -1
- package/vite.config.ts +4 -0
- package/lib/solvers/AdjacentLayerContainmentMergeSolver/AdjacentLayerContainmentMergeSolver.ts +0 -456
- package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +0 -311
- package/pages/bugreports/bugreport50-multi-support-layer-merge.page.tsx +0 -19
- package/pages/pour.page.tsx +0 -18
- package/test-assets/bugreport49-634662.json +0 -412
- package/tests/solver/bugreport49-634662/__snapshots__/bugreport49-634662.snap.svg +0 -44
- package/tests/solver/bugreport49-634662/bugreport49-634662.test.ts +0 -134
- package/tests/solver/bugreport50-multi-support-layer-merge/__snapshots__/bugreport50-multi-support-layer-merge.snap.svg +0 -44
- package/tests/solver/bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.json +0 -972
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
2
|
-
import type { GraphicsObject } from "graphics-debug"
|
|
3
|
-
import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
|
|
4
|
-
import type { XYRect } from "lib/rectdiff-types"
|
|
5
|
-
import type { Obstacle, SimpleRouteJson } from "lib/types/srj-types"
|
|
6
|
-
import { obstacleToXYRect, obstacleZs } from "../RectDiffSeedingSolver/layers"
|
|
7
|
-
import { getColorForZLayer } from "lib/utils/getColorForZLayer"
|
|
8
|
-
import { subtractRect2D, overlaps, EPS } from "lib/utils/rectdiff-geometry"
|
|
9
|
-
import { padRect } from "lib/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
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "react"
|
|
2
|
-
import { SolverDebugger3d } from "../../components/SolverDebugger3d"
|
|
3
|
-
import { RectDiffPipeline } from "../../lib/RectDiffPipeline"
|
|
4
|
-
import srjJson from "../../tests/solver/bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.json"
|
|
5
|
-
|
|
6
|
-
const simpleRouteJson =
|
|
7
|
-
srjJson.simpleRouteJson ?? srjJson.simple_route_json ?? srjJson
|
|
8
|
-
|
|
9
|
-
export default () => {
|
|
10
|
-
const solver = useMemo(
|
|
11
|
-
() =>
|
|
12
|
-
new RectDiffPipeline({
|
|
13
|
-
simpleRouteJson,
|
|
14
|
-
}),
|
|
15
|
-
[],
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
return <SolverDebugger3d solver={solver} simpleRouteJson={simpleRouteJson} />
|
|
19
|
-
}
|
package/pages/pour.page.tsx
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "react"
|
|
2
|
-
import { SolverDebugger3d } from "../components/SolverDebugger3d"
|
|
3
|
-
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
4
|
-
import bugreport49 from "../test-assets/bugreport49-634662.json"
|
|
5
|
-
|
|
6
|
-
const simpleRouteJson = bugreport49.simple_route_json ?? bugreport49
|
|
7
|
-
|
|
8
|
-
export default () => {
|
|
9
|
-
const solver = useMemo(
|
|
10
|
-
() =>
|
|
11
|
-
new RectDiffPipeline({
|
|
12
|
-
simpleRouteJson,
|
|
13
|
-
}),
|
|
14
|
-
[],
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
return <SolverDebugger3d solver={solver} simpleRouteJson={simpleRouteJson} />
|
|
18
|
-
}
|