diff --git a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts index 305e4e0..5814a0b 100644 --- a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +++ b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts @@ -158,18 +158,31 @@ export class SchematicTraceSingleLineSolver extends BaseSolver { // If this segment would cross any restricted center line, reject the candidate path. const EPS = 1e-9 for (const [, rcl] of restrictedCenterLines) { + const bounds = rcl.bounds if (rcl.axes.has("x") && typeof rcl.x === "number") { - // segment strictly crosses vertical center line + // segment strictly crosses vertical center line near the chip bounds if ((start.x - rcl.x) * (end.x - rcl.x) < -EPS) { - pathIsValid = false - break + const segMinY = Math.min(start.y, end.y) + const segMaxY = Math.max(start.y, end.y) + const overlapY = + Math.min(segMaxY, bounds.maxY) - Math.max(segMinY, bounds.minY) + if (overlapY > EPS) { + pathIsValid = false + break + } } } if (rcl.axes.has("y") && typeof rcl.y === "number") { - // segment strictly crosses horizontal center line + // segment strictly crosses horizontal center line near the chip bounds if ((start.y - rcl.y) * (end.y - rcl.y) < -EPS) { - pathIsValid = false - break + const segMinX = Math.min(start.x, end.x) + const segMaxX = Math.max(start.x, end.x) + const overlapX = + Math.min(segMaxX, bounds.maxX) - Math.max(segMinX, bounds.minX) + if (overlapX > EPS) { + pathIsValid = false + break + } } } } diff --git a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines.ts b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines.ts index 50d9dda..c5cd71b 100644 --- a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines.ts +++ b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getRestrictedCenterLines.ts @@ -5,6 +5,7 @@ import type { InputProblem, PinId, } from "lib/types/InputProblem" +import { getInputChipBounds } from "lib/solvers/GuidelinesSolver/getInputChipBounds" import { getPinDirection } from "./getPinDirection" type ChipPin = InputPin & { chipId: ChipId } @@ -19,6 +20,7 @@ export type RestrictedCenterLine = { x?: number y?: number axes: Set<"x" | "y"> + bounds: ReturnType } export const getRestrictedCenterLines = (params: { @@ -107,7 +109,10 @@ export const getRestrictedCenterLines = (params: { // are present among related pins on the chip. for (const [chipId, faces] of chipFacingMap) { const axes = new Set<"x" | "y">() - const rcl: RestrictedCenterLine = { axes } + const chip = chipMap[chipId] + if (!chip) continue + const bounds = getInputChipBounds(chip) + const rcl: RestrictedCenterLine = { axes, bounds } // determine whether any side on this chip has more than one pin const counts = faces.counts diff --git a/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_long_trace.test.ts b/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_long_trace.test.ts new file mode 100644 index 0000000..ded157e --- /dev/null +++ b/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_long_trace.test.ts @@ -0,0 +1,158 @@ +import { expect, test } from "bun:test" +import { calculateElbow } from "calculate-elbow" +import { SchematicTraceSingleLineSolver } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver" +import type { InputChip, InputProblem } from "lib/types/InputProblem" + +type XY = { x: number; y: number } + +const toSvgPoint = ({ x, y }: XY): XY => ({ x, y: -y }) + +const formatNumber = (value: number): string => { + const normalized = Math.abs(value) < 1e-9 ? 0 : value + const formatted = normalized.toFixed(3) + return formatted.replace(/\.?0+$/, "") +} + +const renderTraceSnapshot = ({ + chips, + path, +}: { + chips: InputChip[] + path: XY[] +}): string => { + const worldPoints: XY[] = [...path] + + for (const chip of chips) { + const left = chip.center.x - chip.width / 2 + const right = chip.center.x + chip.width / 2 + const top = chip.center.y + chip.height / 2 + const bottom = chip.center.y - chip.height / 2 + + worldPoints.push({ x: left, y: bottom }) + worldPoints.push({ x: right, y: top }) + + for (const pin of chip.pins) { + worldPoints.push({ x: pin.x, y: pin.y }) + } + } + + const svgPoints = worldPoints.map(toSvgPoint) + const xs = svgPoints.map((p) => p.x) + const ys = svgPoints.map((p) => p.y) + const minX = Math.min(...xs) + const maxX = Math.max(...xs) + const minY = Math.min(...ys) + const maxY = Math.max(...ys) + const margin = 1 + + const viewBoxX = minX - margin + const viewBoxY = minY - margin + const viewBoxWidth = maxX - minX + margin * 2 + const viewBoxHeight = maxY - minY + margin * 2 + + const pixelScale = 40 + const svgWidth = Math.round(viewBoxWidth * pixelScale) + const svgHeight = Math.round(viewBoxHeight * pixelScale) + + const chipRectElements = chips + .map((chip) => { + const x = chip.center.x - chip.width / 2 + const y = -(chip.center.y + chip.height / 2) + return ` ` + }) + .join("\n") + + const pinCircleElements = chips + .flatMap((chip) => chip.pins) + .map((pin) => { + const { x, y } = toSvgPoint({ x: pin.x, y: pin.y }) + return ` ` + }) + .join("\n") + + const polylinePoints = path + .map((point) => { + const { x, y } = toSvgPoint(point) + return `${formatNumber(x)},${formatNumber(y)}` + }) + .join(" ") + + return [ + ``, + " ", + " ", + chipRectElements, + " ", + ` `, + " ", + pinCircleElements, + " ", + "", + ].join("\n") +} + +const buildChip = ( + chipId: string, + center: { x: number; y: number }, + width: number, + height: number, + pins: Array<{ pinId: string; x: number; y: number }>, +): InputChip => ({ + chipId, + center, + width, + height, + pins, +}) + +test("allows long traces when restricted center line is far away", async () => { + const chipA = buildChip("A", { x: 0, y: 0 }, 0.4, 0.4, [ + { pinId: "A1", x: 0, y: 0 }, + ]) + const chipB = buildChip("B", { x: 10, y: 0 }, 0.4, 0.4, [ + { pinId: "B1", x: 10, y: 0 }, + ]) + const chipC = buildChip("C", { x: 5, y: 10 }, 1, 1, [ + { pinId: "C1", x: 4.5, y: 10 }, + { pinId: "C2", x: 5.5, y: 10 }, + ]) + + const inputProblem: InputProblem = { + chips: [chipA, chipB, chipC], + directConnections: [ + { pinIds: ["A1", "C1"], netId: "N1" }, + { pinIds: ["B1", "C2"], netId: "N1" }, + ], + netConnections: [], + availableNetLabelOrientations: {}, + } + + const pins = [ + { pinId: "A1", x: 0, y: 0, chipId: "A", _facingDirection: "x+" as const }, + { pinId: "B1", x: 10, y: 0, chipId: "B", _facingDirection: "x-" as const }, + ] + + const solver = new SchematicTraceSingleLineSolver({ + pins: pins as any, + guidelines: [], + inputProblem, + chipMap: { A: chipA, B: chipB, C: chipC }, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + + const baseElbow = calculateElbow( + { x: pins[0].x, y: pins[0].y, facingDirection: pins[0]._facingDirection }, + { x: pins[1].x, y: pins[1].y, facingDirection: pins[1]._facingDirection }, + { overshoot: 0.2 }, + ) + + expect(solver.solvedTracePath).toEqual(baseElbow) + + const svg = renderTraceSnapshot({ chips: inputProblem.chips, path: solver.solvedTracePath! }) + + await expect(svg).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/solvers/SchematicTraceSingleLineSolver/__snapshots__/SchematicTraceSingleLineSolver_long_trace.snap.svg b/tests/solvers/SchematicTraceSingleLineSolver/__snapshots__/SchematicTraceSingleLineSolver_long_trace.snap.svg new file mode 100644 index 0000000..7fa6aee --- /dev/null +++ b/tests/solvers/SchematicTraceSingleLineSolver/__snapshots__/SchematicTraceSingleLineSolver_long_trace.snap.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +