@tscircuit/rectdiff 0.0.26 → 0.0.28
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/dist/index.d.ts +21 -0
- package/dist/index.js +488 -160
- package/lib/RectDiffPipeline.ts +23 -0
- package/lib/solvers/AdjacentLayerContainmentMergeSolver/AdjacentLayerContainmentMergeSolver.ts +456 -0
- package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +4 -1
- package/package.json +1 -1
- package/pages/bugreports/bugreport50-multi-support-layer-merge.page.tsx +19 -0
- package/tests/__snapshots__/board-outline.snap.svg +2 -2
- package/tests/outer-layer-containment-merge-solver.test.ts +73 -0
- 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/bugreport49-634662/__snapshots__/bugreport49-634662.snap.svg +1 -1
- package/tests/solver/bugreport49-634662/bugreport49-634662.test.ts +7 -11
- package/tests/solver/bugreport50-multi-support-layer-merge/__snapshots__/bugreport50-multi-support-layer-merge.snap.svg +44 -0
- package/tests/solver/bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.json +972 -0
- package/tests/solver/bugreport50-multi-support-layer-merge/bugreport50-multi-support-layer-merge.test.ts +125 -0
- 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/transitivity/__snapshots__/transitivity.snap.svg +2 -2
package/lib/RectDiffPipeline.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { SimpleRouteJson } from "./types/srj-types"
|
|
|
7
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
|
+
import { AdjacentLayerContainmentMergeSolver } from "./solvers/AdjacentLayerContainmentMergeSolver/AdjacentLayerContainmentMergeSolver"
|
|
10
11
|
import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
|
|
11
12
|
import { OuterLayerContainmentMergeSolver } from "./solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver"
|
|
12
13
|
import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
|
|
@@ -27,6 +28,7 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
27
28
|
rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
|
|
28
29
|
gapFillSolver?: GapFillSolverPipeline
|
|
29
30
|
outerLayerContainmentMergeSolver?: OuterLayerContainmentMergeSolver
|
|
31
|
+
adjacentLayerContainmentMergeSolver?: AdjacentLayerContainmentMergeSolver
|
|
30
32
|
boardVoidRects: XYRect[] | undefined
|
|
31
33
|
zIndexByName?: Map<string, number>
|
|
32
34
|
layerNames?: string[]
|
|
@@ -87,6 +89,22 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
87
89
|
},
|
|
88
90
|
],
|
|
89
91
|
),
|
|
92
|
+
definePipelineStep(
|
|
93
|
+
"adjacentLayerContainmentMergeSolver",
|
|
94
|
+
AdjacentLayerContainmentMergeSolver,
|
|
95
|
+
(rectDiffPipeline: RectDiffPipeline) => [
|
|
96
|
+
{
|
|
97
|
+
meshNodes:
|
|
98
|
+
rectDiffPipeline.outerLayerContainmentMergeSolver?.getOutput()
|
|
99
|
+
.outputNodes ??
|
|
100
|
+
rectDiffPipeline.gapFillSolver?.getOutput().outputNodes ??
|
|
101
|
+
rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput()
|
|
102
|
+
.meshNodes ??
|
|
103
|
+
[],
|
|
104
|
+
simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
),
|
|
90
108
|
]
|
|
91
109
|
|
|
92
110
|
override _setup(): void {
|
|
@@ -118,6 +136,11 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
override getOutput(): { meshNodes: CapacityMeshNode[] } {
|
|
139
|
+
const adjacentLayerMergeOutput =
|
|
140
|
+
this.adjacentLayerContainmentMergeSolver?.getOutput()
|
|
141
|
+
if (adjacentLayerMergeOutput) {
|
|
142
|
+
return { meshNodes: adjacentLayerMergeOutput.outputNodes }
|
|
143
|
+
}
|
|
121
144
|
const outerLayerMergeOutput =
|
|
122
145
|
this.outerLayerContainmentMergeSolver?.getOutput()
|
|
123
146
|
if (outerLayerMergeOutput) {
|
package/lib/solvers/AdjacentLayerContainmentMergeSolver/AdjacentLayerContainmentMergeSolver.ts
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
2
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
3
|
+
import type { XYRect } from "lib/rectdiff-types"
|
|
4
|
+
import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
|
|
5
|
+
import type { SimpleRouteJson } from "lib/types/srj-types"
|
|
6
|
+
import { getColorForZLayer } from "lib/utils/getColorForZLayer"
|
|
7
|
+
import { EPS, overlaps, subtractRect2D } from "lib/utils/rectdiff-geometry"
|
|
8
|
+
|
|
9
|
+
type AdjacentLayerContainmentMergeSolverInput = {
|
|
10
|
+
meshNodes: CapacityMeshNode[]
|
|
11
|
+
simpleRouteJson: SimpleRouteJson
|
|
12
|
+
minFragmentArea?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MIN_FRAGMENT_AREA = 0.2 ** 2
|
|
16
|
+
|
|
17
|
+
const nodeToRect = (node: CapacityMeshNode): XYRect => ({
|
|
18
|
+
x: node.center.x - node.width / 2,
|
|
19
|
+
y: node.center.y - node.height / 2,
|
|
20
|
+
width: node.width,
|
|
21
|
+
height: node.height,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const rectArea = (rect: XYRect) => rect.width * rect.height
|
|
25
|
+
|
|
26
|
+
const cloneNode = (node: CapacityMeshNode): CapacityMeshNode => ({
|
|
27
|
+
...node,
|
|
28
|
+
center: { ...node.center },
|
|
29
|
+
availableZ: [...node.availableZ],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const cloneNodeWithRect = (
|
|
33
|
+
node: CapacityMeshNode,
|
|
34
|
+
rect: XYRect,
|
|
35
|
+
capacityMeshNodeId: string,
|
|
36
|
+
): CapacityMeshNode => ({
|
|
37
|
+
...node,
|
|
38
|
+
capacityMeshNodeId,
|
|
39
|
+
center: {
|
|
40
|
+
x: rect.x + rect.width / 2,
|
|
41
|
+
y: rect.y + rect.height / 2,
|
|
42
|
+
},
|
|
43
|
+
width: rect.width,
|
|
44
|
+
height: rect.height,
|
|
45
|
+
availableZ: [...node.availableZ],
|
|
46
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const clonePromotedNodeWithRect = (
|
|
50
|
+
node: CapacityMeshNode,
|
|
51
|
+
rect: XYRect,
|
|
52
|
+
capacityMeshNodeId: string,
|
|
53
|
+
availableZ: number[],
|
|
54
|
+
): CapacityMeshNode => ({
|
|
55
|
+
...node,
|
|
56
|
+
capacityMeshNodeId,
|
|
57
|
+
center: {
|
|
58
|
+
x: rect.x + rect.width / 2,
|
|
59
|
+
y: rect.y + rect.height / 2,
|
|
60
|
+
},
|
|
61
|
+
width: rect.width,
|
|
62
|
+
height: rect.height,
|
|
63
|
+
availableZ: [...availableZ],
|
|
64
|
+
layer: `z${availableZ.join(",")}`,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const isFreeNode = (node: CapacityMeshNode) =>
|
|
68
|
+
!node._containsObstacle && !node._containsTarget
|
|
69
|
+
|
|
70
|
+
const isSingletonNodeOnLayer = (node: CapacityMeshNode, z: number) =>
|
|
71
|
+
node.availableZ.length === 1 && node.availableZ[0] === z
|
|
72
|
+
|
|
73
|
+
const sameRect = (a: XYRect, b: XYRect) =>
|
|
74
|
+
Math.abs(a.x - b.x) <= EPS &&
|
|
75
|
+
Math.abs(a.y - b.y) <= EPS &&
|
|
76
|
+
Math.abs(a.width - b.width) <= EPS &&
|
|
77
|
+
Math.abs(a.height - b.height) <= EPS
|
|
78
|
+
|
|
79
|
+
const subtractRects = (target: XYRect, cutters: XYRect[]) => {
|
|
80
|
+
let remaining: XYRect[] = [target]
|
|
81
|
+
|
|
82
|
+
for (const cutter of cutters) {
|
|
83
|
+
if (remaining.length === 0) return remaining
|
|
84
|
+
|
|
85
|
+
const nextRemaining: XYRect[] = []
|
|
86
|
+
for (const piece of remaining) {
|
|
87
|
+
nextRemaining.push(...subtractRect2D(piece, cutter))
|
|
88
|
+
}
|
|
89
|
+
remaining = nextRemaining
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return remaining
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isFullyCoveredByRects = (target: XYRect, coveringRects: XYRect[]) => {
|
|
96
|
+
return subtractRects(target, coveringRects).length === 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const sortAndDedupeCuts = (values: number[]) => {
|
|
100
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
101
|
+
const out: number[] = []
|
|
102
|
+
|
|
103
|
+
for (const value of sorted) {
|
|
104
|
+
const last = out[out.length - 1]
|
|
105
|
+
if (last == null || Math.abs(last - value) > EPS) {
|
|
106
|
+
out.push(value)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const partitionRectByRects = (target: XYRect, supportRects: XYRect[]) => {
|
|
114
|
+
const xCuts = [target.x, target.x + target.width]
|
|
115
|
+
const yCuts = [target.y, target.y + target.height]
|
|
116
|
+
const targetMaxX = target.x + target.width
|
|
117
|
+
const targetMaxY = target.y + target.height
|
|
118
|
+
|
|
119
|
+
for (const rect of supportRects) {
|
|
120
|
+
const x0 = Math.max(target.x, rect.x)
|
|
121
|
+
const x1 = Math.min(targetMaxX, rect.x + rect.width)
|
|
122
|
+
const y0 = Math.max(target.y, rect.y)
|
|
123
|
+
const y1 = Math.min(targetMaxY, rect.y + rect.height)
|
|
124
|
+
|
|
125
|
+
if (x1 <= x0 + EPS || y1 <= y0 + EPS) continue
|
|
126
|
+
|
|
127
|
+
xCuts.push(x0, x1)
|
|
128
|
+
yCuts.push(y0, y1)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const xs = sortAndDedupeCuts(xCuts)
|
|
132
|
+
const ys = sortAndDedupeCuts(yCuts)
|
|
133
|
+
const cells: XYRect[] = []
|
|
134
|
+
|
|
135
|
+
for (let xi = 0; xi < xs.length - 1; xi++) {
|
|
136
|
+
const x0 = xs[xi]!
|
|
137
|
+
const x1 = xs[xi + 1]!
|
|
138
|
+
|
|
139
|
+
if (x1 <= x0 + EPS) continue
|
|
140
|
+
|
|
141
|
+
for (let yi = 0; yi < ys.length - 1; yi++) {
|
|
142
|
+
const y0 = ys[yi]!
|
|
143
|
+
const y1 = ys[yi + 1]!
|
|
144
|
+
|
|
145
|
+
if (y1 <= y0 + EPS) continue
|
|
146
|
+
|
|
147
|
+
cells.push({
|
|
148
|
+
x: x0,
|
|
149
|
+
y: y0,
|
|
150
|
+
width: x1 - x0,
|
|
151
|
+
height: y1 - y0,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return cells
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const canMergeHorizontally = (a: XYRect, b: XYRect) =>
|
|
160
|
+
Math.abs(a.y - b.y) <= EPS &&
|
|
161
|
+
Math.abs(a.height - b.height) <= EPS &&
|
|
162
|
+
Math.abs(a.x + a.width - b.x) <= EPS
|
|
163
|
+
|
|
164
|
+
const canMergeVertically = (a: XYRect, b: XYRect) =>
|
|
165
|
+
Math.abs(a.x - b.x) <= EPS &&
|
|
166
|
+
Math.abs(a.width - b.width) <= EPS &&
|
|
167
|
+
Math.abs(a.y + a.height - b.y) <= EPS
|
|
168
|
+
|
|
169
|
+
const mergeTouchingRects = (rects: XYRect[]) => {
|
|
170
|
+
const out = rects.map((rect) => ({ ...rect }))
|
|
171
|
+
let changed = true
|
|
172
|
+
|
|
173
|
+
while (changed) {
|
|
174
|
+
changed = false
|
|
175
|
+
|
|
176
|
+
outer: for (let i = 0; i < out.length; i++) {
|
|
177
|
+
for (let j = i + 1; j < out.length; j++) {
|
|
178
|
+
const a = out[i]!
|
|
179
|
+
const b = out[j]!
|
|
180
|
+
|
|
181
|
+
if (canMergeHorizontally(a, b) || canMergeHorizontally(b, a)) {
|
|
182
|
+
const merged: XYRect = {
|
|
183
|
+
x: Math.min(a.x, b.x),
|
|
184
|
+
y: a.y,
|
|
185
|
+
width: a.width + b.width,
|
|
186
|
+
height: a.height,
|
|
187
|
+
}
|
|
188
|
+
out.splice(j, 1)
|
|
189
|
+
out.splice(i, 1, merged)
|
|
190
|
+
changed = true
|
|
191
|
+
break outer
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (canMergeVertically(a, b) || canMergeVertically(b, a)) {
|
|
195
|
+
const merged: XYRect = {
|
|
196
|
+
x: a.x,
|
|
197
|
+
y: Math.min(a.y, b.y),
|
|
198
|
+
width: a.width,
|
|
199
|
+
height: a.height + b.height,
|
|
200
|
+
}
|
|
201
|
+
out.splice(j, 1)
|
|
202
|
+
out.splice(i, 1, merged)
|
|
203
|
+
changed = true
|
|
204
|
+
break outer
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return out
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const isPromotableRect = (params: {
|
|
214
|
+
rect: XYRect
|
|
215
|
+
viaMinSize: number
|
|
216
|
+
minFragmentArea: number
|
|
217
|
+
}) => {
|
|
218
|
+
const { rect, viaMinSize, minFragmentArea } = params
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
rect.width > EPS &&
|
|
222
|
+
rect.height > EPS &&
|
|
223
|
+
rectArea(rect) + EPS >= minFragmentArea &&
|
|
224
|
+
rect.width + EPS >= viaMinSize &&
|
|
225
|
+
rect.height + EPS >= viaMinSize
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const isResidualRect = (rect: XYRect, minFragmentArea: number) =>
|
|
230
|
+
rect.width > EPS &&
|
|
231
|
+
rect.height > EPS &&
|
|
232
|
+
rectArea(rect) + EPS >= minFragmentArea
|
|
233
|
+
|
|
234
|
+
const computePromotablePieces = (params: {
|
|
235
|
+
target: XYRect
|
|
236
|
+
supportRects: XYRect[]
|
|
237
|
+
viaMinSize: number
|
|
238
|
+
minFragmentArea: number
|
|
239
|
+
}) => {
|
|
240
|
+
const { target, supportRects, viaMinSize, minFragmentArea } = params
|
|
241
|
+
const overlappingSupports = supportRects.filter((rect) =>
|
|
242
|
+
overlaps(rect, target),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if (overlappingSupports.length === 0) return []
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
isFullyCoveredByRects(target, overlappingSupports) &&
|
|
249
|
+
isPromotableRect({ rect: target, viaMinSize, minFragmentArea })
|
|
250
|
+
) {
|
|
251
|
+
return [target]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const partitioned = partitionRectByRects(target, overlappingSupports)
|
|
255
|
+
const coveredPieces = partitioned.filter((piece) =>
|
|
256
|
+
isFullyCoveredByRects(piece, overlappingSupports),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if (coveredPieces.length === 0) return []
|
|
260
|
+
|
|
261
|
+
const mergedCoveredPieces = mergeTouchingRects(coveredPieces)
|
|
262
|
+
|
|
263
|
+
return mergedCoveredPieces.filter(
|
|
264
|
+
(piece) =>
|
|
265
|
+
isFullyCoveredByRects(piece, overlappingSupports) &&
|
|
266
|
+
isPromotableRect({ rect: piece, viaMinSize, minFragmentArea }),
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export class AdjacentLayerContainmentMergeSolver extends BaseSolver {
|
|
271
|
+
private outputNodes: CapacityMeshNode[] = []
|
|
272
|
+
private promotedNodeIds = new Set<string>()
|
|
273
|
+
private residualNodeIds = new Set<string>()
|
|
274
|
+
|
|
275
|
+
constructor(private input: AdjacentLayerContainmentMergeSolverInput) {
|
|
276
|
+
super()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
override _setup() {
|
|
280
|
+
this.outputNodes = this.input.meshNodes.map(cloneNode)
|
|
281
|
+
this.promotedNodeIds.clear()
|
|
282
|
+
this.residualNodeIds.clear()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
override _step() {
|
|
286
|
+
this.outputNodes = this.processAdjacentLayerContainmentMerges()
|
|
287
|
+
this.solved = true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private processAdjacentLayerContainmentMerges(): CapacityMeshNode[] {
|
|
291
|
+
const srj = this.input.simpleRouteJson
|
|
292
|
+
const layerCount = Math.max(1, srj.layerCount || 1)
|
|
293
|
+
|
|
294
|
+
if (layerCount < 2) {
|
|
295
|
+
return this.input.meshNodes.map(cloneNode)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0)
|
|
299
|
+
const minFragmentArea = Math.max(
|
|
300
|
+
EPS,
|
|
301
|
+
this.input.minFragmentArea ?? DEFAULT_MIN_FRAGMENT_AREA,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
let workingNodes = this.input.meshNodes.map(cloneNode)
|
|
305
|
+
let nextResidualId = 0
|
|
306
|
+
let nextPromotedId = 0
|
|
307
|
+
|
|
308
|
+
for (let lowerZ = 0; lowerZ < layerCount - 1; lowerZ++) {
|
|
309
|
+
const upperZ = lowerZ + 1
|
|
310
|
+
const mutableNodes = workingNodes.filter(
|
|
311
|
+
(node) =>
|
|
312
|
+
isFreeNode(node) &&
|
|
313
|
+
(isSingletonNodeOnLayer(node, lowerZ) ||
|
|
314
|
+
isSingletonNodeOnLayer(node, upperZ)),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if (mutableNodes.length === 0) continue
|
|
318
|
+
|
|
319
|
+
const immutableNodes = workingNodes.filter(
|
|
320
|
+
(node) => !mutableNodes.includes(node),
|
|
321
|
+
)
|
|
322
|
+
const supportRectsByLayer = new Map<number, XYRect[]>()
|
|
323
|
+
|
|
324
|
+
supportRectsByLayer.set(
|
|
325
|
+
lowerZ,
|
|
326
|
+
mutableNodes
|
|
327
|
+
.filter((node) => isSingletonNodeOnLayer(node, lowerZ))
|
|
328
|
+
.map(nodeToRect),
|
|
329
|
+
)
|
|
330
|
+
supportRectsByLayer.set(
|
|
331
|
+
upperZ,
|
|
332
|
+
mutableNodes
|
|
333
|
+
.filter((node) => isSingletonNodeOnLayer(node, upperZ))
|
|
334
|
+
.map(nodeToRect),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
const promotedRects: XYRect[] = []
|
|
338
|
+
const promotedNodes: CapacityMeshNode[] = []
|
|
339
|
+
const candidateNodes = mutableNodes
|
|
340
|
+
.filter((node) =>
|
|
341
|
+
isPromotableRect({
|
|
342
|
+
rect: nodeToRect(node),
|
|
343
|
+
viaMinSize,
|
|
344
|
+
minFragmentArea,
|
|
345
|
+
}),
|
|
346
|
+
)
|
|
347
|
+
.sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)))
|
|
348
|
+
|
|
349
|
+
for (const candidate of candidateNodes) {
|
|
350
|
+
const candidateRect = nodeToRect(candidate)
|
|
351
|
+
const candidatePieces = subtractRects(
|
|
352
|
+
candidateRect,
|
|
353
|
+
promotedRects,
|
|
354
|
+
).filter((piece) => isResidualRect(piece, minFragmentArea))
|
|
355
|
+
const candidateZ = candidate.availableZ[0]!
|
|
356
|
+
const oppositeZ = candidateZ === lowerZ ? upperZ : lowerZ
|
|
357
|
+
const supportRects = supportRectsByLayer.get(oppositeZ) ?? []
|
|
358
|
+
|
|
359
|
+
for (const piece of candidatePieces) {
|
|
360
|
+
const promotablePieces = computePromotablePieces({
|
|
361
|
+
target: piece,
|
|
362
|
+
supportRects,
|
|
363
|
+
viaMinSize,
|
|
364
|
+
minFragmentArea,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
for (const promotablePiece of promotablePieces) {
|
|
368
|
+
promotedRects.push(promotablePiece)
|
|
369
|
+
|
|
370
|
+
const promotedNode = clonePromotedNodeWithRect(
|
|
371
|
+
candidate,
|
|
372
|
+
promotablePiece,
|
|
373
|
+
`${candidate.capacityMeshNodeId}-adjacent-merge-${nextPromotedId++}`,
|
|
374
|
+
[lowerZ, upperZ],
|
|
375
|
+
)
|
|
376
|
+
promotedNodes.push(promotedNode)
|
|
377
|
+
this.promotedNodeIds.add(promotedNode.capacityMeshNodeId)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const residualNodes: CapacityMeshNode[] = []
|
|
383
|
+
|
|
384
|
+
for (const node of mutableNodes) {
|
|
385
|
+
const nodeRect = nodeToRect(node)
|
|
386
|
+
const remainingPieces = subtractRects(nodeRect, promotedRects).filter(
|
|
387
|
+
(piece) => isResidualRect(piece, minFragmentArea),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if (
|
|
391
|
+
remainingPieces.length === 1 &&
|
|
392
|
+
sameRect(remainingPieces[0]!, nodeRect)
|
|
393
|
+
) {
|
|
394
|
+
residualNodes.push(node)
|
|
395
|
+
continue
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const piece of remainingPieces) {
|
|
399
|
+
const residualNode = cloneNodeWithRect(
|
|
400
|
+
node,
|
|
401
|
+
piece,
|
|
402
|
+
`${node.capacityMeshNodeId}-adjacent-residual-${nextResidualId++}`,
|
|
403
|
+
)
|
|
404
|
+
residualNodes.push(residualNode)
|
|
405
|
+
this.residualNodeIds.add(residualNode.capacityMeshNodeId)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
workingNodes = [...immutableNodes, ...promotedNodes, ...residualNodes]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return workingNodes
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
override getOutput(): { outputNodes: CapacityMeshNode[] } {
|
|
416
|
+
return { outputNodes: this.outputNodes }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
override visualize(): GraphicsObject {
|
|
420
|
+
return {
|
|
421
|
+
title: "AdjacentLayerContainmentMergeSolver",
|
|
422
|
+
coordinateSystem: "cartesian",
|
|
423
|
+
rects: this.outputNodes.map((node) => {
|
|
424
|
+
const colors = getColorForZLayer(node.availableZ)
|
|
425
|
+
const isPromoted = this.promotedNodeIds.has(node.capacityMeshNodeId)
|
|
426
|
+
const isResidual = this.residualNodeIds.has(node.capacityMeshNodeId)
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
center: node.center,
|
|
430
|
+
width: node.width,
|
|
431
|
+
height: node.height,
|
|
432
|
+
stroke: isPromoted
|
|
433
|
+
? "rgba(245, 158, 11, 0.95)"
|
|
434
|
+
: isResidual
|
|
435
|
+
? "rgba(37, 99, 235, 0.95)"
|
|
436
|
+
: colors.stroke,
|
|
437
|
+
fill: node._containsObstacle
|
|
438
|
+
? "rgba(239, 68, 68, 0.35)"
|
|
439
|
+
: isPromoted
|
|
440
|
+
? "rgba(251, 191, 36, 0.28)"
|
|
441
|
+
: isResidual
|
|
442
|
+
? "rgba(59, 130, 246, 0.18)"
|
|
443
|
+
: colors.fill,
|
|
444
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
445
|
+
label: [
|
|
446
|
+
`node ${node.capacityMeshNodeId}`,
|
|
447
|
+
`z:${node.availableZ.join(",")}`,
|
|
448
|
+
].join("\n"),
|
|
449
|
+
}
|
|
450
|
+
}),
|
|
451
|
+
points: [],
|
|
452
|
+
lines: [],
|
|
453
|
+
texts: [],
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
@@ -28,6 +28,7 @@ const nodeToRect = (node: CapacityMeshNode): XYRect => ({
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
const rectArea = (rect: XYRect) => rect.width * rect.height
|
|
31
|
+
const MIN_OUTER_LAYER_MERGE_AREA_MM2 = 1
|
|
31
32
|
|
|
32
33
|
const cloneNode = (node: CapacityMeshNode): CapacityMeshNode => ({
|
|
33
34
|
...node,
|
|
@@ -144,7 +145,9 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {
|
|
|
144
145
|
const candidateNodes = mutableOuterNodes
|
|
145
146
|
.filter(
|
|
146
147
|
(node) =>
|
|
147
|
-
node.width + EPS >= viaMinSize &&
|
|
148
|
+
node.width + EPS >= viaMinSize &&
|
|
149
|
+
node.height + EPS >= viaMinSize &&
|
|
150
|
+
rectArea(nodeToRect(node)) > MIN_OUTER_LAYER_MERGE_AREA_MM2 + EPS,
|
|
148
151
|
)
|
|
149
152
|
.sort((a, b) => rectArea(nodeToRect(b)) - rectArea(nodeToRect(a)))
|
|
150
153
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><rect data-type="rect" data-label="node" data-x="-5" data-y="1.
|
|
1
|
+
<svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><rect data-type="rect" data-label="node" data-x="-5" data-y="1.1600000000000001" x="40" y="110" width="210" height="338.79999999999995" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="5" data-y="1.1600000000000001" x="390" y="110" width="210" height="338.79999999999995" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="0" data-y="4" x="250" y="110" width="140" height="140" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-5.48" data-y="-5.16" x="40" y="471.20000000000005" width="176.39999999999998" height="58.799999999999955" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="5.48" data-y="-5.16" x="423.6" y="471.20000000000005" width="176.39999999999998" height="58.799999999999955" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-2.48" data-y="-4.84" x="216.4" y="448.79999999999995" width="33.599999999999994" height="81.20000000000005" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="2.48" data-y="-4.84" x="390" y="448.79999999999995" width="33.60000000000002" height="81.20000000000005" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-6.890000000000001" data-y="-3.999999999999999" x="40" y="448.79999999999995" width="77.69999999999996" height="22.400000000000034" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-5" data-y="-3.999999999999999" x="136.6" y="448.79999999999995" width="16.80000000000001" height="22.400000000000034" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-3.59" data-y="-3.999999999999999" x="172.3" y="448.79999999999995" width="44.099999999999994" height="22.400000000000034" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="3.59" data-y="-3.999999999999999" x="423.6" y="448.79999999999995" width="44.099999999999966" height="22.400000000000034" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="5" data-y="-3.999999999999999" x="486.6" y="448.79999999999995" width="16.799999999999955" height="22.400000000000034" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="6.889999999999999" data-y="-3.999999999999999" x="522.3" y="448.79999999999995" width="77.70000000000005" height="22.400000000000034" fill="#dbeafe" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-5.51" data-y="-4" x="117.70000000000002" y="448.79999999999995" width="18.899999999999977" height="22.40000000000009" fill="red" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-4.49" data-y="-4" x="153.4" y="448.79999999999995" width="18.899999999999977" height="22.40000000000009" fill="red" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="4.49" data-y="-4" x="467.70000000000005" y="448.79999999999995" width="18.899999999999977" height="22.40000000000009" fill="red" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="5.51" data-y="-4" x="503.4" y="448.79999999999995" width="18.899999999999977" height="22.40000000000009" fill="red" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-5.51" data-y="-3.999999999999999" x="117.70000000000002" y="448.79999999999995" width="18.899999999999977" height="22.400000000000034" fill="#fef3c7" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="-4.49" data-y="-3.999999999999999" x="153.39999999999998" y="448.79999999999995" width="18.900000000000034" height="22.400000000000034" fill="#fef3c7" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="4.49" data-y="-3.999999999999999" x="467.70000000000005" y="448.79999999999995" width="18.899999999999977" height="22.400000000000034" fill="#fef3c7" stroke="black" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="node" data-x="5.51" data-y="-3.999999999999999" x="503.4" y="448.79999999999995" width="18.899999999999977" height="22.400000000000034" fill="#fef3c7" stroke="black" stroke-width="0.02857142857142857"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="640" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
|
|
2
2
|
document.currentScript.parentElement.addEventListener('mousemove', (e) => {
|
|
3
3
|
const svg = e.currentTarget;
|
|
4
4
|
const rect = svg.getBoundingClientRect();
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
v.setAttribute('y2', '640');
|
|
21
21
|
|
|
22
22
|
// Calculate real coordinates using inverse transformation
|
|
23
|
-
const matrix = {"a":35,"c":0,"e":320,"b":0,"d":-35,"f":320
|
|
23
|
+
const matrix = {"a":35,"c":0,"e":320,"b":0,"d":-35,"f":320};
|
|
24
24
|
// Manually invert and apply the affine transform
|
|
25
25
|
// Since we only use translate and scale, we can directly compute:
|
|
26
26
|
// x' = (x - tx) / sx
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { OuterLayerContainmentMergeSolver } from "lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver"
|
|
3
|
+
import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
|
|
4
|
+
import type { SimpleRouteJson } from "lib/types/srj-types"
|
|
5
|
+
|
|
6
|
+
const makeNode = (
|
|
7
|
+
capacityMeshNodeId: string,
|
|
8
|
+
centerX: number,
|
|
9
|
+
width: number,
|
|
10
|
+
height: number,
|
|
11
|
+
availableZ: number[],
|
|
12
|
+
): CapacityMeshNode => ({
|
|
13
|
+
capacityMeshNodeId,
|
|
14
|
+
center: { x: centerX, y: 0 },
|
|
15
|
+
width,
|
|
16
|
+
height,
|
|
17
|
+
layer: `z${availableZ.join(",")}`,
|
|
18
|
+
availableZ,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("OuterLayerContainmentMergeSolver only promotes nodes larger than 1 mm^2", () => {
|
|
22
|
+
const simpleRouteJson: SimpleRouteJson = {
|
|
23
|
+
bounds: {
|
|
24
|
+
minX: -1,
|
|
25
|
+
maxX: 6,
|
|
26
|
+
minY: -1,
|
|
27
|
+
maxY: 1,
|
|
28
|
+
},
|
|
29
|
+
obstacles: [
|
|
30
|
+
{
|
|
31
|
+
type: "rect",
|
|
32
|
+
layers: ["inner1"],
|
|
33
|
+
zLayers: [1],
|
|
34
|
+
center: { x: 2.5, y: 0 },
|
|
35
|
+
width: 7,
|
|
36
|
+
height: 2,
|
|
37
|
+
connectedTo: [],
|
|
38
|
+
isCopperPour: true,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
connections: [],
|
|
42
|
+
layerCount: 3,
|
|
43
|
+
minTraceWidth: 0.1,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const solver = new OuterLayerContainmentMergeSolver({
|
|
47
|
+
meshNodes: [
|
|
48
|
+
makeNode("below-threshold", 0.4, 0.8, 1, [0]),
|
|
49
|
+
makeNode("at-threshold", 2, 1, 1, [0]),
|
|
50
|
+
makeNode("above-threshold", 4.6, 1.2, 1, [0]),
|
|
51
|
+
makeNode("below-support", 0.4, 0.8, 1, [1, 2]),
|
|
52
|
+
makeNode("at-support", 2, 1, 1, [1, 2]),
|
|
53
|
+
makeNode("above-support", 4.6, 1.2, 1, [1, 2]),
|
|
54
|
+
],
|
|
55
|
+
simpleRouteJson,
|
|
56
|
+
zIndexByName: new Map([
|
|
57
|
+
["top", 0],
|
|
58
|
+
["inner1", 1],
|
|
59
|
+
["bottom", 2],
|
|
60
|
+
]),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
solver.solve()
|
|
64
|
+
|
|
65
|
+
const outputNodes = solver.getOutput().outputNodes
|
|
66
|
+
const nodeById = new Map(
|
|
67
|
+
outputNodes.map((node) => [node.capacityMeshNodeId, node] as const),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
expect(nodeById.get("below-threshold")?.availableZ).toEqual([0])
|
|
71
|
+
expect(nodeById.get("at-threshold")?.availableZ).toEqual([0])
|
|
72
|
+
expect(nodeById.get("above-threshold")?.availableZ).toEqual([0, 2])
|
|
73
|
+
})
|