diff --git a/lib/data-structures/ChipObstacleSpatialIndex.ts b/lib/data-structures/ChipObstacleSpatialIndex.ts index c9132ac..2429dda 100644 --- a/lib/data-structures/ChipObstacleSpatialIndex.ts +++ b/lib/data-structures/ChipObstacleSpatialIndex.ts @@ -49,22 +49,101 @@ export class ChipObstacleSpatialIndex { doesOrthogonalLineIntersectChip( line: [Point, Point], + margin = 0, opts: { excludeChipIds?: string[] + eps?: number } = {}, ): boolean { + // Fast path when no margin is applied + if (margin === 0) { + const excludeChipIds = opts.excludeChipIds ?? [] + const eps = opts.eps ?? 0 + const [p1, p2] = line + const { x: x1, y: y1 } = p1 + const { x: x2, y: y2 } = p2 + + const minX = Math.min(x1, x2) + const minY = Math.min(y1, y2) + const maxX = Math.max(x1, x2) + const maxY = Math.max(y1, y2) + + const chips = this.getChipsInBounds({ + minX: minX - eps, + minY: minY - eps, + maxX: maxX + eps, + maxY: maxY + eps, + }).filter((chip) => !excludeChipIds.includes(chip.chipId)) + + const isVertical = Math.abs(x1 - x2) < eps + const isHorizontal = Math.abs(y1 - y2) < eps + + for (const chip of chips) { + const { + minX: cMinX, + minY: cMinY, + maxX: cMaxX, + maxY: cMaxY, + } = chip.bounds + + if (isVertical) { + const x = x1 + if (x <= cMinX + eps || x >= cMaxX - eps) continue + const overlap = Math.min(maxY, cMaxY) - Math.max(minY, cMinY) + if (overlap > eps) return true + } else if (isHorizontal) { + const y = y1 + if (y <= cMinY + eps || y >= cMaxY - eps) continue + const overlap = Math.min(maxX, cMaxX) - Math.max(minX, cMinX) + if (overlap > eps) return true + } + } + + return false + } + const excludeChipIds = opts.excludeChipIds ?? [] + const eps = opts.eps ?? 0 const [p1, p2] = line const { x: x1, y: y1 } = p1 const { x: x2, y: y2 } = p2 + const minX = Math.min(x1, x2) + const minY = Math.min(y1, y2) + const maxX = Math.max(x1, x2) + const maxY = Math.max(y1, y2) + + const searchMargin = eps + Math.max(0, margin) + const chips = this.getChipsInBounds({ - minX: Math.min(x1, x2), - minY: Math.min(y1, y2), - maxX: Math.max(x1, x2), - maxY: Math.max(y1, y2), + minX: minX - searchMargin, + minY: minY - searchMargin, + maxX: maxX + searchMargin, + maxY: maxY + searchMargin, }).filter((chip) => !excludeChipIds.includes(chip.chipId)) - return chips.length > 0 + const isVertical = Math.abs(x1 - x2) < eps + const isHorizontal = Math.abs(y1 - y2) < eps + + for (const chip of chips) { + const cMinX = chip.bounds.minX - margin + const cMinY = chip.bounds.minY - margin + const cMaxX = chip.bounds.maxX + margin + const cMaxY = chip.bounds.maxY + margin + + if (isVertical) { + const x = x1 + if (x <= cMinX + eps || x >= cMaxX - eps) continue + const overlap = Math.min(maxY, cMaxY) - Math.max(minY, cMinY) + if (overlap > eps) return true + } else if (isHorizontal) { + const y = y1 + if (y <= cMinY + eps || y >= cMaxY - eps) continue + const overlap = Math.min(maxX, cMaxX) - Math.max(minX, cMinX) + if (overlap > eps) return true + } + } + + return false } } diff --git a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts index 305e4e0..425eef7 100644 --- a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +++ b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts @@ -76,8 +76,35 @@ export class SchematicTraceSingleLineSolver extends BaseSolver { pin._facingDirection = getPinDirection(pin, chip) } } - const [pin1, pin2] = this.pins + + // Attempt direct straight-line connection if pins are aligned + const EPS = 1e-6 + const isDirectVertical = Math.abs(pin1.x - pin2.x) < EPS + const isDirectHorizontal = Math.abs(pin1.y - pin2.y) < EPS + if (isDirectVertical || isDirectHorizontal) { + const directPath: [Point, Point] = [ + { x: pin1.x, y: pin1.y }, + { x: pin2.x, y: pin2.y }, + ] + const excludeChipIds = Array.from(new Set(this.pins.map((p) => p.chipId))) + const intersects = + this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip( + directPath, + -EPS, + { excludeChipIds, eps: EPS }, + ) + if (!intersects) { + this.solvedTracePath = directPath + this.solved = true + this.baseElbow = directPath + this.movableSegments = [] + this.allCandidatePaths = [directPath] + this.queuedCandidatePaths = [directPath] + return + } + } + this.baseElbow = calculateElbow( { x: pin1.x, @@ -148,6 +175,8 @@ export class SchematicTraceSingleLineSolver extends BaseSolver { // Check if this candidate path is valid let pathIsValid = true + const COLLISION_EPS = 1e-6 + for (let i = 0; i < nextCandidatePath.length - 1; i++) { const start = nextCandidatePath[i] const end = nextCandidatePath[i + 1] @@ -253,10 +282,11 @@ export class SchematicTraceSingleLineSolver extends BaseSolver { if (!pathIsValid) break - const obstacleOps = { excludeChipIds } + const obstacleOps = { excludeChipIds, eps: COLLISION_EPS } const intersects = this.chipObstacleSpatialIndex.doesOrthogonalLineIntersectChip( [start, end], + 0, obstacleOps, ) if (intersects) { diff --git a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts index 4a3b558..428023a 100644 --- a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +++ b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts @@ -23,6 +23,8 @@ import { pathKey, shiftSegmentOrth } from "./pathOps" type PathKey = string +const EPS = 1e-6 + export class SchematicTraceSingleLineSolver2 extends BaseSolver { pins: MspConnectionPair["pins"] inputProblem: InputProblem @@ -61,8 +63,28 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver { this.obstacles = getObstacleRects(this.inputProblem) this.rectById = new Map(this.obstacles.map((r) => [r.chipId, r])) - // Build initial elbow path const [pin1, pin2] = this.pins + + // Attempt direct straight-line connection if pins are aligned + const isDirectVertical = Math.abs(pin1.x - pin2.x) < EPS + const isDirectHorizontal = Math.abs(pin1.y - pin2.y) < EPS + if (isDirectVertical || isDirectHorizontal) { + const directPath: Point[] = [ + { x: pin1.x, y: pin1.y }, + { x: pin2.x, y: pin2.y }, + ] + const collision = findFirstCollision(directPath, this.obstacles, { + eps: EPS, + }) + if (!collision) { + this.solvedTracePath = directPath + this.solved = true + this.baseElbow = directPath + this.aabb = aabbFromPoints(directPath[0]!, directPath[1]!) + return + } + } + // Build initial elbow path this.baseElbow = calculateElbow( { x: pin1.x, @@ -129,7 +151,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver { const { path, collisionChipIds } = state - const collision = findFirstCollision(path, this.obstacles) + const collision = findFirstCollision(path, this.obstacles, { eps: EPS }) if (!collision) { this.solvedTracePath = path diff --git a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts index 24670ff..7675285 100644 --- a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +++ b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts @@ -20,14 +20,14 @@ export const segmentIntersectsRect = ( if (vert) { const x = a.x - if (x < r.minX - eps || x > r.maxX + eps) return false + if (x <= r.minX + eps || x >= r.maxX - eps) return false const segMinY = Math.min(a.y, b.y) const segMaxY = Math.max(a.y, b.y) const overlap = Math.min(segMaxY, r.maxY) - Math.max(segMinY, r.minY) return overlap > eps } else { const y = a.y - if (y < r.minY - eps || y > r.maxY + eps) return false + if (y <= r.minY + eps || y >= r.maxY - eps) return false const segMinX = Math.min(a.x, b.x) const segMaxX = Math.max(a.x, b.x) const overlap = Math.min(segMaxX, r.maxX) - Math.max(segMinX, r.minX) @@ -40,15 +40,17 @@ export const findFirstCollision = ( rects: ChipWithBounds[], opts: { excludeRectIdsForSegment?: (segIndex: number) => Set + eps?: number } = {}, ): { segIndex: number; rect: ChipWithBounds } | null => { + const eps = opts.eps ?? EPS for (let i = 0; i < pts.length - 1; i++) { const a = pts[i]! const b = pts[i + 1]! const excluded = opts.excludeRectIdsForSegment?.(i) ?? new Set() for (const r of rects) { if (excluded.has(r.chipId)) continue - if (segmentIntersectsRect(a, b, r)) { + if (segmentIntersectsRect(a, b, r, eps)) { return { segIndex: i, rect: r } } } diff --git a/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_direct_line_scrape.test.ts b/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_direct_line_scrape.test.ts new file mode 100644 index 0000000..67eceb8 --- /dev/null +++ b/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_direct_line_scrape.test.ts @@ -0,0 +1,51 @@ +import { SchematicTraceSingleLineSolver } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver" +import { test, expect } from "bun:test" + +test("SchematicTraceSingleLineSolver traces direct line when scraping chip edge", () => { + const chipA = { + chipId: "A", + center: { x: 0, y: 0 }, + width: 1, + height: 1, + pins: [{ pinId: "A.1", x: 0.5, y: 0 }], + } + const chipB = { + chipId: "B", + center: { x: 3, y: 0 }, + width: 1, + height: 1, + pins: [{ pinId: "B.1", x: 2.5, y: 0 }], + } + const chipC = { + chipId: "C", + center: { x: 1.5, y: 0.5 }, + width: 1, + height: 1, + pins: [], + } + + const inputProblem = { + chips: [chipA, chipB, chipC], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, + } + + const solver = new SchematicTraceSingleLineSolver({ + pins: [ + { pinId: "A.1", x: 0.5, y: 0, chipId: "A" }, + { pinId: "B.1", x: 2.5, y: 0, chipId: "B" }, + ], + guidelines: [], + inputProblem: inputProblem as any, + chipMap: { A: chipA, B: chipB, C: chipC }, + }) + + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.solvedTracePath).toEqual([ + { x: 0.5, y: 0 }, + { x: 2.5, y: 0 }, + ]) +}) diff --git a/tests/solvers/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2_direct_line.test.ts b/tests/solvers/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2_direct_line.test.ts new file mode 100644 index 0000000..215da9b --- /dev/null +++ b/tests/solvers/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2_direct_line.test.ts @@ -0,0 +1,62 @@ +import { SchematicTraceSingleLineSolver2 } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2" +import { test, expect } from "bun:test" + +test("SchematicTraceSingleLineSolver2 traces direct horizontal line when possible", () => { + const chipA = { + chipId: "A", + center: { x: 0, y: 0 }, + width: 1, + height: 1, + pins: [ + { + pinId: "A.1", + x: 0.5, + y: 0, + }, + ], + } + const chipB = { + chipId: "B", + center: { x: 3, y: 0 }, + width: 1, + height: 1, + pins: [ + { + pinId: "B.1", + x: 2.5, + y: 0, + }, + ], + } + const chipC = { + chipId: "C", + center: { x: 1.5, y: 0.5 }, + width: 1, + height: 1, + pins: [], + } + + const input = { + chipMap: { + A: chipA, + B: chipB, + C: chipC, + }, + pins: [ + { pinId: "A.1", x: 0.5, y: 0, chipId: "A" }, + { pinId: "B.1", x: 2.5, y: 0, chipId: "B" }, + ], + inputProblem: { + chips: [chipA, chipB, chipC], + }, + } + + const solver = new SchematicTraceSingleLineSolver2(input as any) + solver.solve() + + expect(solver.solved).toBe(true) + expect(solver.solvedTracePath).toEqual([ + { x: 0.5, y: 0 }, + { x: 2.5, y: 0 }, + ]) +}) diff --git a/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg b/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg index 2df7563..f6e1e26 100644 --- a/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg +++ b/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg @@ -77,13 +77,13 @@ x+" data-x="4.75" data-y="-0.3000000000000007" cx="600" cy="395.8638743455498" r - + - +