@tscircuit/rectdiff 0.0.24 → 0.0.26
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 +45 -24
- package/AGENTS.md +23 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +414 -191
- package/lib/RectDiffPipeline.ts +23 -0
- package/lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver.ts +311 -0
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +23 -23
- package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +9 -10
- package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +22 -19
- package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +1 -1
- package/lib/types/srj-types.ts +1 -0
- package/lib/utils/expandRectFromSeed.ts +8 -10
- package/lib/utils/isFullyOccupiedAtPoint.ts +2 -2
- package/lib/utils/rectdiff-geometry.ts +13 -20
- package/package.json +3 -1
- package/pages/pour.page.tsx +18 -0
- package/scripts/benchmark-slow-problem.ts +94 -0
- package/test-assets/bugreport49-634662.json +412 -0
- package/test-assets/keyboard4.json +16165 -0
- package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
- 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 +1 -1
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.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/bugreport35-191db9/__snapshots__/bugreport35-191db9.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 +44 -0
- package/tests/solver/bugreport49-634662/bugreport49-634662.test.ts +134 -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 "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
|
+
}
|
|
@@ -57,6 +57,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
57
57
|
private candidates!: Candidate3D[]
|
|
58
58
|
private placed!: Placed3D[]
|
|
59
59
|
private placedIndexByLayer!: Array<RBush<RTreeRect>>
|
|
60
|
+
private hardPlacedByLayer!: XYRect[][]
|
|
60
61
|
private expansionIndex!: number
|
|
61
62
|
private edgeAnalysisDone!: boolean
|
|
62
63
|
private totalSeedsThisGrid!: number
|
|
@@ -131,6 +132,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
131
132
|
{ length: layerCount },
|
|
132
133
|
() => new RBush<RTreeRect>(),
|
|
133
134
|
)
|
|
135
|
+
this.hardPlacedByLayer = Array.from({ length: layerCount }, () => [])
|
|
134
136
|
this.expansionIndex = 0
|
|
135
137
|
this.edgeAnalysisDone = false
|
|
136
138
|
this.totalSeedsThisGrid = 0
|
|
@@ -164,19 +166,13 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
164
166
|
} = this.options
|
|
165
167
|
const grid = gridSizes[this.gridIndex]!
|
|
166
168
|
|
|
167
|
-
// Build hard-placed map once per micro-step (cheap)
|
|
168
|
-
const hardPlacedByLayer = allLayerNode({
|
|
169
|
-
layerCount: this.layerCount,
|
|
170
|
-
placed: this.placed,
|
|
171
|
-
})
|
|
172
|
-
|
|
173
169
|
// Ensure candidates exist for this grid
|
|
174
170
|
if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
|
|
175
171
|
this.candidates = computeCandidates3D({
|
|
176
172
|
bounds: this.bounds,
|
|
177
173
|
gridSize: grid,
|
|
178
174
|
layerCount: this.layerCount,
|
|
179
|
-
hardPlacedByLayer,
|
|
175
|
+
hardPlacedByLayer: this.hardPlacedByLayer,
|
|
180
176
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
181
177
|
placedIndexByLayer: this.placedIndexByLayer,
|
|
182
178
|
})
|
|
@@ -185,9 +181,10 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
185
181
|
}
|
|
186
182
|
|
|
187
183
|
// If no candidates remain, advance grid or run edge pass or switch phase
|
|
188
|
-
if (this.candidates.length
|
|
184
|
+
if (this.consumedSeedsThisGrid >= this.candidates.length) {
|
|
189
185
|
if (this.gridIndex + 1 < gridSizes.length) {
|
|
190
186
|
this.gridIndex += 1
|
|
187
|
+
this.candidates = []
|
|
191
188
|
this.totalSeedsThisGrid = 0
|
|
192
189
|
this.consumedSeedsThisGrid = 0
|
|
193
190
|
return
|
|
@@ -200,13 +197,14 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
200
197
|
layerCount: this.layerCount,
|
|
201
198
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
202
199
|
placedIndexByLayer: this.placedIndexByLayer,
|
|
203
|
-
hardPlacedByLayer,
|
|
200
|
+
hardPlacedByLayer: this.hardPlacedByLayer,
|
|
204
201
|
})
|
|
205
202
|
this.edgeAnalysisDone = true
|
|
206
203
|
this.totalSeedsThisGrid = this.candidates.length
|
|
207
204
|
this.consumedSeedsThisGrid = 0
|
|
208
205
|
return
|
|
209
206
|
}
|
|
207
|
+
this.candidates = []
|
|
210
208
|
this.solved = true
|
|
211
209
|
this.expansionIndex = 0
|
|
212
210
|
return
|
|
@@ -214,8 +212,17 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
214
212
|
}
|
|
215
213
|
|
|
216
214
|
// Consume exactly one candidate
|
|
217
|
-
const cand = this.candidates.
|
|
218
|
-
|
|
215
|
+
const cand = this.candidates[this.consumedSeedsThisGrid++]!
|
|
216
|
+
if (
|
|
217
|
+
isFullyOccupiedAtPoint({
|
|
218
|
+
layerCount: this.layerCount,
|
|
219
|
+
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
220
|
+
placedIndexByLayer: this.placedIndexByLayer,
|
|
221
|
+
point: { x: cand.x, y: cand.y },
|
|
222
|
+
})
|
|
223
|
+
) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
219
226
|
|
|
220
227
|
// Evaluate attempts — multi-layer span first (computed ignoring soft nodes)
|
|
221
228
|
const span = longestFreeSpanAroundZ({
|
|
@@ -226,7 +233,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
226
233
|
minSpan: minMulti.minLayers,
|
|
227
234
|
maxSpan: maxMultiLayerSpan,
|
|
228
235
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
229
|
-
additionalBlockersByLayer: hardPlacedByLayer,
|
|
236
|
+
additionalBlockersByLayer: this.hardPlacedByLayer,
|
|
230
237
|
})
|
|
231
238
|
|
|
232
239
|
const attempts: Array<{
|
|
@@ -285,17 +292,10 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
285
292
|
},
|
|
286
293
|
newIndex,
|
|
287
294
|
)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
!isFullyOccupiedAtPoint({
|
|
293
|
-
layerCount: this.layerCount,
|
|
294
|
-
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
295
|
-
placedIndexByLayer: this.placedIndexByLayer,
|
|
296
|
-
point: { x: c.x, y: c.y },
|
|
297
|
-
}),
|
|
298
|
-
)
|
|
295
|
+
this.hardPlacedByLayer = allLayerNode({
|
|
296
|
+
layerCount: this.layerCount,
|
|
297
|
+
placed: this.placed,
|
|
298
|
+
})
|
|
299
299
|
|
|
300
300
|
return // processed one candidate
|
|
301
301
|
}
|
|
@@ -27,6 +27,10 @@ export function computeCandidates3D(params: {
|
|
|
27
27
|
hardPlacedByLayer,
|
|
28
28
|
} = params
|
|
29
29
|
const out = new Map<string, Candidate3D>() // key by (x,y)
|
|
30
|
+
const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) => [
|
|
31
|
+
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
32
|
+
...(hardPlacedByLayer[z] ?? []),
|
|
33
|
+
])
|
|
30
34
|
|
|
31
35
|
for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
|
|
32
36
|
for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
|
|
@@ -75,16 +79,11 @@ export function computeCandidates3D(params: {
|
|
|
75
79
|
: bestZ
|
|
76
80
|
|
|
77
81
|
// Distance heuristic against hard blockers only (obstacles + full-stack)
|
|
78
|
-
const hardAtZ = [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
distancePointToRectEdges({ x, y }, bounds),
|
|
84
|
-
...(hardAtZ.length
|
|
85
|
-
? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
86
|
-
: [Infinity]),
|
|
87
|
-
)
|
|
82
|
+
const hardAtZ = hardRectsByLayer[anchorZ] ?? []
|
|
83
|
+
let d = distancePointToRectEdges({ x, y }, bounds)
|
|
84
|
+
for (const blocker of hardAtZ) {
|
|
85
|
+
d = Math.min(d, distancePointToRectEdges({ x, y }, blocker))
|
|
86
|
+
}
|
|
88
87
|
const distance = quantize(d)
|
|
89
88
|
|
|
90
89
|
const k = `${x.toFixed(6)}|${y.toFixed(6)}`
|
|
@@ -7,6 +7,16 @@ import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
|
7
7
|
const quantize = (value: number, precision = 1e-6) =>
|
|
8
8
|
Math.round(value / precision) * precision
|
|
9
9
|
|
|
10
|
+
const toRect = (rect: XYRect | RTreeRect): XYRect =>
|
|
11
|
+
"minX" in rect
|
|
12
|
+
? {
|
|
13
|
+
x: rect.minX,
|
|
14
|
+
y: rect.minY,
|
|
15
|
+
width: rect.maxX - rect.minX,
|
|
16
|
+
height: rect.maxY - rect.minY,
|
|
17
|
+
}
|
|
18
|
+
: rect
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* Compute exact uncovered segments along a 1D line.
|
|
12
22
|
*/
|
|
@@ -109,6 +119,12 @@ export function computeEdgeCandidates3D(params: {
|
|
|
109
119
|
// Use small inset from edges for placement
|
|
110
120
|
const δ = Math.max(minSize * 0.15, EPS * 3)
|
|
111
121
|
const dedup = new Set<string>()
|
|
122
|
+
const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) =>
|
|
123
|
+
[
|
|
124
|
+
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
125
|
+
...(hardPlacedByLayer[z] ?? []),
|
|
126
|
+
].map(toRect),
|
|
127
|
+
)
|
|
112
128
|
const key = (p: { x: number; y: number; z: number }) =>
|
|
113
129
|
`${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`
|
|
114
130
|
|
|
@@ -137,21 +153,11 @@ export function computeEdgeCandidates3D(params: {
|
|
|
137
153
|
if (fullyOcc({ x, y })) return // new rule: only drop if truly impossible
|
|
138
154
|
|
|
139
155
|
// Distance uses obstacles + hard nodes (soft nodes ignored for ranking)
|
|
140
|
-
const hard = [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
y: quantize(b.y),
|
|
146
|
-
width: quantize(b.width),
|
|
147
|
-
height: quantize(b.height),
|
|
148
|
-
}))
|
|
149
|
-
const d = Math.min(
|
|
150
|
-
distancePointToRectEdges({ x, y }, bounds),
|
|
151
|
-
...(hard.length
|
|
152
|
-
? hard.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
153
|
-
: [Infinity]),
|
|
154
|
-
)
|
|
156
|
+
const hard = hardRectsByLayer[z] ?? []
|
|
157
|
+
let d = distancePointToRectEdges({ x, y }, bounds)
|
|
158
|
+
for (const blocker of hard) {
|
|
159
|
+
d = Math.min(d, distancePointToRectEdges({ x, y }, blocker))
|
|
160
|
+
}
|
|
155
161
|
const distance = quantize(d)
|
|
156
162
|
|
|
157
163
|
const k = key({ x, y, z })
|
|
@@ -180,10 +186,7 @@ export function computeEdgeCandidates3D(params: {
|
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
for (let z = 0; z < layerCount; z++) {
|
|
183
|
-
const blockers = [
|
|
184
|
-
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
185
|
-
...(hardPlacedByLayer[z] ?? []),
|
|
186
|
-
].map((b) => ({
|
|
189
|
+
const blockers = (hardRectsByLayer[z] ?? []).map((b) => ({
|
|
187
190
|
x: quantize(b.x),
|
|
188
191
|
y: quantize(b.y),
|
|
189
192
|
width: quantize(b.width),
|
|
@@ -35,7 +35,7 @@ export function longestFreeSpanAroundZ(params: {
|
|
|
35
35
|
maxY: y,
|
|
36
36
|
}
|
|
37
37
|
const obstacleIdx = obstacleIndexByLayer[layer]
|
|
38
|
-
if (obstacleIdx && obstacleIdx.
|
|
38
|
+
if (obstacleIdx && obstacleIdx.collides(query)) return false
|
|
39
39
|
|
|
40
40
|
const extras = additionalBlockersByLayer?.[layer] ?? []
|
|
41
41
|
return !extras.some((b) => containsPoint(b, { x, y }))
|
package/lib/types/srj-types.ts
CHANGED
|
@@ -175,14 +175,13 @@ const toRect = (tree: RTreeRect): XYRect => ({
|
|
|
175
175
|
})
|
|
176
176
|
|
|
177
177
|
const addBlocker = (params: {
|
|
178
|
-
rect:
|
|
179
|
-
seen: Set<
|
|
178
|
+
rect: RTreeRect
|
|
179
|
+
seen: Set<RTreeRect>
|
|
180
180
|
blockers: XYRect[]
|
|
181
181
|
}) => {
|
|
182
182
|
const { rect, seen, blockers } = params
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
seen.add(key)
|
|
183
|
+
if (seen.has(rect)) return
|
|
184
|
+
seen.add(rect)
|
|
186
185
|
blockers.push(rect)
|
|
187
186
|
}
|
|
188
187
|
|
|
@@ -225,7 +224,7 @@ export function expandRectFromSeed(params: {
|
|
|
225
224
|
const initialW = Math.max(minSide, minReq.width)
|
|
226
225
|
const initialH = Math.max(minSide, minReq.height)
|
|
227
226
|
const blockers: XYRect[] = []
|
|
228
|
-
const seen = new Set<
|
|
227
|
+
const seen = new Set<RTreeRect>()
|
|
229
228
|
const totalLayers = placedIndexByLayer.length
|
|
230
229
|
|
|
231
230
|
// Ignore the existing placement we are expanding so it doesn't self-block.
|
|
@@ -237,7 +236,7 @@ export function expandRectFromSeed(params: {
|
|
|
237
236
|
const blockersIndex = obsticalIndexByLayer[z]
|
|
238
237
|
if (blockersIndex) {
|
|
239
238
|
for (const entry of blockersIndex.search(query))
|
|
240
|
-
addBlocker({ rect:
|
|
239
|
+
addBlocker({ rect: entry, seen, blockers })
|
|
241
240
|
}
|
|
242
241
|
|
|
243
242
|
const placedLayer = placedIndexByLayer[z]
|
|
@@ -245,10 +244,9 @@ export function expandRectFromSeed(params: {
|
|
|
245
244
|
for (const entry of placedLayer.search(query)) {
|
|
246
245
|
const isFullStack = entry.zLayers.length >= totalLayers
|
|
247
246
|
if (!isFullStack) continue
|
|
248
|
-
const rect = toRect(entry)
|
|
249
247
|
if (
|
|
250
248
|
isSelfRect({
|
|
251
|
-
rect,
|
|
249
|
+
rect: entry,
|
|
252
250
|
startX,
|
|
253
251
|
startY,
|
|
254
252
|
initialW,
|
|
@@ -256,7 +254,7 @@ export function expandRectFromSeed(params: {
|
|
|
256
254
|
})
|
|
257
255
|
)
|
|
258
256
|
continue
|
|
259
|
-
addBlocker({ rect, seen, blockers })
|
|
257
|
+
addBlocker({ rect: entry, seen, blockers })
|
|
260
258
|
}
|
|
261
259
|
}
|
|
262
260
|
}
|
|
@@ -17,10 +17,10 @@ export function isFullyOccupiedAtPoint(params: OccupancyParams): boolean {
|
|
|
17
17
|
}
|
|
18
18
|
for (let z = 0; z < params.layerCount; z++) {
|
|
19
19
|
const obstacleIdx = params.obstacleIndexByLayer[z]
|
|
20
|
-
const hasObstacle = !!obstacleIdx && obstacleIdx.
|
|
20
|
+
const hasObstacle = !!obstacleIdx && obstacleIdx.collides(query)
|
|
21
21
|
|
|
22
22
|
const placedIdx = params.placedIndexByLayer[z]
|
|
23
|
-
const hasPlaced = !!placedIdx && placedIdx.
|
|
23
|
+
const hasPlaced = !!placedIdx && placedIdx.collides(query)
|
|
24
24
|
|
|
25
25
|
if (!hasObstacle && !hasPlaced) return false
|
|
26
26
|
}
|