Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,32 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {
const viaMinSize = Math.max(srj.minViaDiameter ?? 0, srj.minTraceWidth || 0)
const originalNodes = this.input.meshNodes.map(cloneNode)
const obstaclesByLayer = this.buildObstaclesByLayer(layerCount)
if (
!obstaclesByLayer
.slice(1, -1)
.some((entries) => entries.some((entry) => entry.obstacle.isCopperPour))
) {
return originalNodes
}
const mutableOuterNodes = originalNodes.filter(
(node) =>
isFreeNode(node) &&
(isSingletonOuterNode(node, topZ) ||
isSingletonOuterNode(node, bottomZ)),
)
const immutableNodes = originalNodes.filter(
(node) => !mutableOuterNodes.includes(node),
)
const freeSupportRectsByOuterLayer = new Map<number, XYRect[]>()
freeSupportRectsByOuterLayer.set(
const immutableNodes = originalNodes.filter((node) => !isFreeNode(node))
const freeSupportNodesByOuterLayer = new Map<number, CapacityMeshNode[]>()
freeSupportNodesByOuterLayer.set(
topZ,
originalNodes
.filter((node) => isFreeNode(node) && node.availableZ.includes(topZ))
.map(nodeToRect),
originalNodes.filter(
(node) => isFreeNode(node) && node.availableZ.includes(topZ),
),
)
freeSupportRectsByOuterLayer.set(
freeSupportNodesByOuterLayer.set(
bottomZ,
originalNodes
.filter((node) => isFreeNode(node) && node.availableZ.includes(bottomZ))
.map(nodeToRect),
originalNodes.filter(
(node) => isFreeNode(node) && node.availableZ.includes(bottomZ),
),
)

const promotedNodes: CapacityMeshNode[] = []
Expand All @@ -152,8 +157,15 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {
const candidateZ = candidate.availableZ[0]!
const oppositeZ = candidateZ === topZ ? bottomZ : topZ
const candidateRect = nodeToRect(candidate)
const oppositeSupportRects =
freeSupportRectsByOuterLayer.get(oppositeZ) ?? []
const oppositeSupportNodes = (
freeSupportNodesByOuterLayer.get(oppositeZ) ?? []
).filter((node) => overlaps(candidateRect, nodeToRect(node)))
const supportZs = oppositeSupportNodes[0]?.availableZ
if (!supportZs) continue

if (promotedRects.some((rect) => overlaps(candidateRect, rect))) {
continue
}

if (
!this.isTransitCompatibleAcrossIntermediateLayers({
Expand All @@ -165,28 +177,80 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {
) {
continue
}
if (!isFullyCoveredByRects(candidateRect, oppositeSupportRects)) {
if (
!isFullyCoveredByRects(
candidateRect,
oppositeSupportNodes.map(nodeToRect),
)
) {
continue
}

promotedNodes.push({
...candidate,
availableZ: [topZ, bottomZ],
layer: `z${topZ},${bottomZ}`,
})
promotedRects.push(candidateRect)
this.promotedNodeIds.add(candidate.capacityMeshNodeId)
// Keep a large region when the supporting layers are uniform. Otherwise
// split at support boundaries so no inner-layer access is lost or added.
const hasUniformSupport = oppositeSupportNodes.every(
(node) =>
node.availableZ.length === supportZs.length &&
node.availableZ.every((z) => supportZs.includes(z)),
)
const pieces = hasUniformSupport
? [{ rect: candidateRect, availableZ: supportZs }]
: oppositeSupportNodes.map((node) => {
const supportRect = nodeToRect(node)
const x = Math.max(candidateRect.x, supportRect.x)
const y = Math.max(candidateRect.y, supportRect.y)
return {
rect: {
x,
y,
width:
Math.min(
candidateRect.x + candidateRect.width,
supportRect.x + supportRect.width,
) - x,
height:
Math.min(
candidateRect.y + candidateRect.height,
supportRect.y + supportRect.height,
) - y,
},
availableZ: node.availableZ,
}
})
for (const [index, piece] of pieces.entries()) {
if (
piece.rect.width + EPS < viaMinSize ||
piece.rect.height + EPS < viaMinSize
) {
continue
}
const availableZ = [...new Set([candidateZ, ...piece.availableZ])].sort(
(a, b) => a - b,
)
const nodeId =
index === 0
? candidate.capacityMeshNodeId
: `${candidate.capacityMeshNodeId}-outer-support-${index}`
promotedNodes.push(
cloneNodeWithRect({ ...candidate, availableZ }, piece.rect, nodeId),
)
promotedRects.push(piece.rect)
this.promotedNodeIds.add(nodeId)
}
}

if (promotedNodes.length === 0) return originalNodes

let nextResidualId = 0
const residualNodes: CapacityMeshNode[] = []

for (const node of mutableOuterNodes) {
if (this.promotedNodeIds.has(node.capacityMeshNodeId)) {
for (const node of originalNodes.filter(isFreeNode)) {
const nodeRect = nodeToRect(node)
const outerZs = node.availableZ.filter((z) => z === topZ || z === bottomZ)
if (outerZs.length === 0) {
residualNodes.push(node)
continue
}

const nodeRect = nodeToRect(node)
const remainingPieces = subtractRects(nodeRect, promotedRects)

if (
Expand Down Expand Up @@ -244,27 +308,30 @@ export class OuterLayerContainmentMergeSolver extends BaseSolver {

if (hi - lo < 2) return false

// This merge reconnects outer regions separated by copper planes. Empty
// inner layers permit transit, but do not require a containment merge.
let crossesCopperPlane = false
for (let z = lo + 1; z < hi; z++) {
const overlapping = (obstaclesByLayer[z] ?? []).filter((entry) =>
overlaps(entry.rect, rect),
)
if (overlapping.length === 0) return false

if (overlapping.length === 0) continue
const nonCopperOverlap = overlapping.some(
(entry) => !entry.obstacle.isCopperPour,
)
if (nonCopperOverlap) return false

const copperRects = overlapping
.filter((entry) => entry.obstacle.isCopperPour)
.map((entry) => entry.rect)

if (!isFullyCoveredByRects(rect, copperRects)) {
if (
!isFullyCoveredByRects(
rect,
overlapping.map((entry) => entry.rect),
)
) {
return false
}
crossesCopperPlane = true
}

return true
return crossesCopperPlane
}

override getOutput(): { outputNodes: CapacityMeshNode[] } {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions tests/solver/outer-layer-transit-mesh-area.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, test } from "bun:test"
import { RectDiffPipeline } from "lib/RectDiffPipeline"
import type { SimpleRouteJson } from "lib/types/srj-types"

test("outer-layer promotion preserves free routing area and existing layer transitions", async (): Promise<void> => {
const fixturePaths = [
"tests/solver/bugreport24-05597c/bugreport24-05597c.json",
"tests/solver/bugreport22-2a75ce/bugreport22-2a75ce.json",
"test-assets/arduino-uno-inner2-ground-inner1-power.json",
"test-assets/arduino-uno-inner2-ground-bottom-power.json",
]
for (const fixturePath of fixturePaths) {
const fixture: SimpleRouteJson | { simple_route_json: SimpleRouteJson } =
await Bun.file(new URL(`../../${fixturePath}`, import.meta.url)).json()
const simpleRouteJson =
"simple_route_json" in fixture ? fixture.simple_route_json : fixture
const solver = new RectDiffPipeline({
simpleRouteJson,
maxGapFillPasses: 1,
})
solver.solve()
expect(solver.solved).toBe(true)
const originalNodes = solver.gapFillSolver!.getOutput().outputNodes
const outputNodes = solver.getOutput().meshNodes
for (let z = 0; z < simpleRouteJson.layerCount; z++) {
const [originalArea, outputArea] = [originalNodes, outputNodes].map(
(nodes) =>
nodes
.filter(
(node) =>
node.availableZ.includes(z) &&
!node._containsTarget &&
!node._containsObstacle,
)
.reduce((area, node) => area + node.width * node.height, 0),
)
expect(outputArea!).toBeCloseTo(originalArea!, 8)
for (let otherZ = z + 1; otherZ < simpleRouteJson.layerCount; otherZ++) {
const [originalViaArea, outputViaArea] = [
originalNodes,
outputNodes,
].map((nodes) =>
nodes
.filter(
(node) =>
node.availableZ.includes(z) &&
node.availableZ.includes(otherZ) &&
!node._containsTarget &&
!node._containsObstacle,
)
.reduce((area, node) => area + node.width * node.height, 0),
)
expect(outputViaArea! + 1e-8).toBeGreaterThanOrEqual(originalViaArea!)
}
}
}
})
57 changes: 57 additions & 0 deletions tests/solver/outer-layer-transit-mixed-support.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, test } from "bun:test"
import { OuterLayerContainmentMergeSolver } from "lib/solvers/OuterLayerContainmentMergeSolver/OuterLayerContainmentMergeSolver"
import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"

test("outer-layer promotion does not extend inner-layer access beyond its support", (): void => {
const meshNodes: CapacityMeshNode[] = [
{
capacityMeshNodeId: "top",
center: { x: 0, y: 0 },
width: 4,
height: 4,
layer: "z0",
availableZ: [0],
},
...[
[2, 5],
[3, 5],
].map((availableZ, index) => ({
capacityMeshNodeId: `support-${index}`,
center: { x: index === 0 ? -1 : 1, y: 0 },
width: 2,
height: 4,
layer: `z${availableZ.join(",")}`,
availableZ,
})),
]
const solver = new OuterLayerContainmentMergeSolver({
meshNodes,
simpleRouteJson: {
layerCount: 6,
bounds: { minX: -2, maxX: 2, minY: -2, maxY: 2 },
minTraceWidth: 0.08,
minViaDiameter: 0.45,
obstacles: [
{
type: "rect",
center: { x: 0, y: 0 },
width: 4,
height: 4,
layers: ["inner1"],
connectedTo: ["ground"],
isCopperPour: true,
},
],
connections: [],
},
zIndexByName: new Map([["inner1", 1]]),
})
solver.solve()
const { outputNodes } = solver.getOutput()
expect(outputNodes).toHaveLength(2)
for (const node of outputNodes) {
expect(node.width).toBe(2)
expect(node.height).toBe(4)
expect(node.availableZ).toEqual(node.center.x < 0 ? [0, 2, 5] : [0, 3, 5])
}
})
Loading
Loading