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
108 changes: 108 additions & 0 deletions tests/fixtures/labelPlacementDefects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
import type { InputProblem } from "lib/types/InputProblem"

const EPS = 1e-9

type Bounds = { minX: number; maxX: number; minY: number; maxY: number }

const boundsOf = (
center: { x: number; y: number },
width: number,
height: number,
): Bounds => ({
minX: center.x - width / 2,
maxX: center.x + width / 2,
minY: center.y - height / 2,
maxY: center.y + height / 2,
})

/** Interior overlap: touching edges are not an overlap. */
const overlaps = (a: Bounds, b: Bounds) =>
a.minX < b.maxX - EPS &&
a.maxX > b.minX + EPS &&
a.minY < b.maxY - EPS &&
a.maxY > b.minY + EPS

/**
* Fraction of `inner`'s area covered by `outer`.
*/
const coverageFraction = (inner: Bounds, outer: Bounds) => {
const overlapWidth = Math.max(
0,
Math.min(inner.maxX, outer.maxX) - Math.max(inner.minX, outer.minX),
)
const overlapHeight = Math.max(
0,
Math.min(inner.maxY, outer.maxY) - Math.max(inner.minY, outer.minY),
)
const area = (inner.maxX - inner.minX) * (inner.maxY - inner.minY)

return area > 0 ? (overlapWidth * overlapHeight) / area : 0
}

/**
* Mirrors `CHIP_COLLISION_MIN_OVERLAP_FRACTION` in
* `NetLabelNetLabelCollisionSolver`: a label counts as "inside" a chip only
* once the chip covers at least half of it. Smaller incidental overlaps are
* tolerated there deliberately, so counting them would report defects the
* solver is intentionally leaving alone.
*/
const INSIDE_CHIP_MIN_COVERAGE = 0.5

/**
* Net labels substantially covered by a chip body. These render behind the
* component and are illegible — see #655.
*/
export const getLabelsInsideChips = (
labels: NetLabelPlacement[],
inputProblem: InputProblem,
) => {
const offenders: Array<{ netId: string; chipId: string }> = []

for (const label of labels) {
const labelBounds = boundsOf(label.center, label.width, label.height)

for (const chip of inputProblem.chips) {
const chipBounds = boundsOf(chip.center, chip.width, chip.height)
if (
!overlaps(labelBounds, chipBounds) ||
coverageFraction(labelBounds, chipBounds) < INSIDE_CHIP_MIN_COVERAGE
) {
continue
}
offenders.push({
netId: label.netId ?? label.globalConnNetId,
chipId: chip.chipId,
})
break
}
}

return offenders
}

/** Pairs of net labels whose boxes overlap each other. */
export const getOverlappingLabelPairs = (labels: NetLabelPlacement[]) => {
const pairs: Array<{ a: string; b: string }> = []

for (let i = 0; i < labels.length; i++) {
for (let k = i + 1; k < labels.length; k++) {
const a = labels[i]!
const b = labels[k]!
if (
!overlaps(
boundsOf(a.center, a.width, a.height),
boundsOf(b.center, b.width, b.height),
)
) {
continue
}
pairs.push({
a: a.netId ?? a.globalConnNetId,
b: b.netId ?? b.globalConnNetId,
})
}
}

return pairs
}
77 changes: 77 additions & 0 deletions tests/net-label-placement-defects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect, test } from "bun:test"
import fs from "node:fs"
import path from "node:path"
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
import {
getLabelsInsideChips,
getOverlappingLabelPairs,
} from "tests/fixtures/labelPlacementDefects"

/**
* Net-label placement defects per board: labels rendered inside a chip body
* (illegible — see #655) and labels overlapping each other.
*
* These are current values, not targets. Several are open bugs. Pinning them
* means a placement change that makes a board worse fails a named assertion
* instead of disappearing into a regenerated SVG snapshot.
*
* If a change legitimately improves a board, lower the number in the same PR.
*/
const EXPECTED: Record<
string,
{ insideChips: number; overlappingPairs: number }
> = {
"bug-report-20260706T213649Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260706T220324Z": { insideChips: 0, overlappingPairs: 1 },
"bug-report-20260707T020342Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260707T092615Z": { insideChips: 1, overlappingPairs: 1 },
"bug-report-20260707T134549Z": { insideChips: 0, overlappingPairs: 1 },
"bug-report-20260707T134722Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260707T140410Z": { insideChips: 0, overlappingPairs: 1 },
"bug-report-20260707T141025Z": { insideChips: 1, overlappingPairs: 0 },
"bug-report-20260707T141421Z": { insideChips: 1, overlappingPairs: 1 },
"bug-report-20260707T230831Z": { insideChips: 4, overlappingPairs: 0 },
"bug-report-20260708T053736Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260708T055430Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260708T095725Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260716T144856Z": { insideChips: 1, overlappingPairs: 0 },
"bug-report-20260717T022934Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260717T031704Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260717T042845Z": { insideChips: 0, overlappingPairs: 0 },
"bug-report-20260721T221026Z": { insideChips: 0, overlappingPairs: 2 },
"bug-report-20260724T175257Z": { insideChips: 0, overlappingPairs: 0 },
}

const BUG_REPORTS_DIR = path.join(import.meta.dir, "bug-reports")

const measure = (board: string) => {
const inputPath = path.join(BUG_REPORTS_DIR, board, `${board}.json`)
const inputProblem = JSON.parse(fs.readFileSync(inputPath, "utf8"))

const solver = new SchematicTracePipelineSolver(inputProblem as any)
solver.solve()

// The *output* copy — `netLabelPlacements` on the solver is its input.
const labels =
solver.netLabelNetLabelCollisionSolver?.getOutput().netLabelPlacements ?? []

return {
insideChips: getLabelsInsideChips(labels, solver.inputProblem).length,
overlappingPairs: getOverlappingLabelPairs(labels).length,
}
}

test("every pinned board still exists", () => {
const onDisk = fs
.readdirSync(BUG_REPORTS_DIR)
.filter((d) => fs.existsSync(path.join(BUG_REPORTS_DIR, d, `${d}.json`)))
.sort()

expect(onDisk).toEqual(Object.keys(EXPECTED).sort())
})

for (const [board, expected] of Object.entries(EXPECTED)) {
test(`${board} label placement defects`, () => {
expect(measure(board)).toEqual(expected)
})
}
Loading