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 @@ -16,7 +16,12 @@ export const segmentIntersectsRect = <TRect extends RectBounds>(
): boolean => {
const vert = isVertical(a, b, eps)
const horz = isHorizontal(a, b, eps)
if (!vert && !horz) return false
if (!vert && !horz) {
// Near-axis-aligned segments (jittered pin coordinates) previously fell
// through as collision-free here. Slanted segments must still be tested
// against the rect instead of being declared collision-free.
return segmentIntersectsRectSlanted(a, b, r, eps)
}

if (vert) {
const x = a.x
Expand All @@ -35,6 +40,46 @@ export const segmentIntersectsRect = <TRect extends RectBounds>(
}
}

/**
* General segment/AABB intersection via slab (Liang-Barsky) clipping.
* Only used for non-axis-aligned segments; axis-aligned fast paths above
* keep the historical overlap semantics (strictly inside counts, touching
* an edge does not).
*/
const segmentIntersectsRectSlanted = <TRect extends RectBounds>(
a: Point,
b: Point,
r: TRect,
eps = EPS,
): boolean => {
const dx = b.x - a.x
const dy = b.y - a.y
let tMin = 0
let tMax = 1
const clip = (p: number, q: number): boolean => {
if (Math.abs(p) <= eps) return q > eps
const t = q / p
if (p < 0) {
if (t > tMax) return false
if (t > tMin) tMin = t
} else {
if (t < tMin) return false
if (t < tMax) tMax = t
}
return true
}

if (!clip(-dx, a.x - r.minX)) return false
if (!clip(dx, r.maxX - a.x)) return false
if (!clip(-dy, a.y - r.minY)) return false
if (!clip(dy, r.maxY - a.y)) return false

// A degenerate intersection interval (tMin ~= tMax) means the segment
// merely touches an edge or corner — matching the axis-aligned paths'
// "touching does not count" semantics, require a real overlap.
return tMax - tMin > eps
}

export const segmentOverlapsRectBoundary = <TRect extends RectBounds>(
a: Point,
b: Point,
Expand Down
31 changes: 22 additions & 9 deletions lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,15 @@ export class TraceOverlapShiftSolver extends BaseSolver {

declare activeSubSolver: TraceOverlapIssueSolver | null

correctedTraceMap: Record<MspConnectionPairId, SolvedTracePath> = {}
// Net pairs whose corrections oscillated A -> B -> A. They are suspended
// for the rest of the pass so independent pairs can still be corrected.
suspendedNetPairKeys: Set<string> = new Set()
/**
* A traceNetIsland is a set of traces that are connected via the globalConnMap
*/
traceNetIslands: Record<ConnNetId, Array<SolvedTracePath>> = {}

correctedTraceMap: Record<MspConnectionPairId, SolvedTracePath> = {}
// Keep only the current and previous layouts to detect a two-state cycle
// without accumulating routing history for the whole solve.
recentTraceStates: TraceState[] = []
Expand Down Expand Up @@ -105,7 +108,9 @@ export class TraceOverlapShiftSolver extends BaseSolver {
overlappingTraceSegments: Array<OverlappingTraceSegmentLocator>
interactionKind: TraceInteractionKind
} | null {
// Detect the next set of overlapping segments between two different net islands.
// Detect the next set of overlapping segments between two different net
// islands. Net pairs whose corrections oscillated earlier this pass are
// suspended: they are skipped so independent pairs keep being corrected.
const EPS = 2e-3

const netIds = Object.keys(this.traceNetIslands)
Expand All @@ -114,6 +119,9 @@ export class TraceOverlapShiftSolver extends BaseSolver {
for (let j = i + 1; j < netIds.length; j++) {
const netA = netIds[i]!
const netB = netIds[j]!
if (this.suspendedNetPairKeys.has([netA, netB].sort().join("~"))) {
continue
}
const pathsA = this.traceNetIslands[netA] || []
const pathsB = this.traceNetIslands[netB] || []

Expand Down Expand Up @@ -374,16 +382,21 @@ export class TraceOverlapShiftSolver extends BaseSolver {
...this.activeSubSolver.correctedTraceMap,
}
// Returning to the older retained layout means corrections are bouncing
// A -> B -> A. Keep B and finish this pass instead of retrying forever.
// A -> B -> A. Keep B and suspend only this net pair — other net pairs
// are independent and may still need correction this pass.
if (this.returnsToPreviousTraceState(nextTraceState)) {
const suspendedPairKey = this.activeSubSolver.overlappingTraceSegments
.map((locator) => locator.connNetId)
.sort()
.join("~")
this.suspendedNetPairKeys.add(suspendedPairKey)
this.activeSubSolver = null
this.solved = true
return
} else {
this.correctedTraceMap = nextTraceState
this.rememberTraceState(nextTraceState)
this.activeSubSolver = null
this.traceNetIslands = this.computeTraceNetIslands()
}
this.correctedTraceMap = nextTraceState
this.rememberTraceState(nextTraceState)
this.activeSubSolver = null
this.traceNetIslands = this.computeTraceNetIslands()
}

if (this.activeSubSolver) {
Expand Down
82 changes: 41 additions & 41 deletions tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
206 changes: 206 additions & 0 deletions tests/repros/assets/issue-1102-oscillating-pair-abort.input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
{
"chips": [
{
"chipId": "schematic_component_23",
"center": {
"x": 11.084375,
"y": -5.325
},
"width": 2.3000000000000007,
"height": 3.2000000000000006,
"pins": [
{
"pinId": "schematic_port_75",
"x": 9.934375,
"y": -4.325
},
{
"pinId": "schematic_port_79",
"x": 9.934375,
"y": -5.125
},
{
"pinId": "schematic_port_80",
"x": 9.934375,
"y": -5.325
},
{
"pinId": "schematic_port_96",
"x": 12.234375,
"y": -5.025
},
{
"pinId": "schematic_port_100",
"x": 12.234375,
"y": -4.2250000000000005
},
{
"pinId": "schematic_port_101",
"x": 12.234375,
"y": -4.025
}
],
"sectionId": "USB to UART"
},
{
"chipId": "schematic_component_26",
"center": {
"x": 7.629375,
"y": -3.9250000000000007
},
"width": 0.7800000000000002,
"height": 0.7600000000000011,
"pins": [
{
"pinId": "schematic_port_106",
"x": 7.564375,
"y": -4.3050000000000015,
"_facingDirection": "y-"
}
],
"sectionId": "USB to UART"
},
{
"chipId": "schematic_component_27",
"center": {
"x": 9.221875,
"y": -5.55
},
"width": 1.205,
"height": 0.5999999999999996,
"pins": [
{
"pinId": "schematic_port_108",
"x": 8.944374999999999,
"y": -5.25,
"_facingDirection": "y+"
},
{
"pinId": "schematic_port_109",
"x": 8.944374999999999,
"y": -5.85,
"_facingDirection": "y-"
}
],
"sectionId": "USB to UART"
},
{
"chipId": "schematic_component_28",
"center": {
"x": 9.221875,
"y": -4.23
},
"width": 1.205,
"height": 0.6000000000000014,
"pins": [
{
"pinId": "schematic_port_111",
"x": 8.944374999999999,
"y": -3.9299999999999997
}
],
"sectionId": "USB to UART"
},
{
"chipId": "schematic_component_32",
"center": {
"x": 15.769374999999998,
"y": -4.724999999999999
},
"width": 2,
"height": 0.8000000000000007,
"pins": [
{
"pinId": "schematic_port_118",
"x": 14.769374999999998,
"y": -4.524999999999999
},
{
"pinId": "schematic_port_119",
"x": 14.769374999999998,
"y": -4.724999999999999
},
{
"pinId": "schematic_port_121",
"x": 16.769374999999997,
"y": -4.924999999999999
},
{
"pinId": "schematic_port_122",
"x": 16.769374999999997,
"y": -4.724999999999999
}
],
"sectionId": "USB to UART"
}
],
"directConnections": [
{
"netId": ".R5 > .pin1 to .U6 > .VBUS",
"pinIds": [
"schematic_port_108",
"schematic_port_80"
]
}
],
"netConnections": [
{
"netId": "GND",
"isGround": true,
"netLabelWidth": 0.42,
"netLabelHeight": 0.48,
"pinIds": [
"schematic_port_75",
"schematic_port_101",
"schematic_port_106",
"schematic_port_109"
]
},
{
"netId": "VBUS",
"netLabelWidth": 0.42,
"netLabelHeight": 0.6,
"pinIds": [
"schematic_port_79",
"schematic_port_111"
]
},
{
"netId": "RTS",
"netLabelWidth": 0.48,
"pinIds": [
"schematic_port_96",
"schematic_port_118",
"schematic_port_122"
]
},
{
"netId": "DTR",
"netLabelWidth": 0.48,
"pinIds": [
"schematic_port_100",
"schematic_port_119",
"schematic_port_121"
]
}
],
"textBoxes": [],
"availableNetLabelOrientations": {
"GND": [
"y-"
],
"VBUS": [
"y+"
],
"RTS": [
"x-",
"x+"
],
"DTR": [
"x-",
"x+"
]
},
"maxMspPairDistance": 8,
"_hideRatsNet": false
}
4 changes: 3 additions & 1 deletion tests/repros/board-1273-trace-overlap-cycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ test("board 1273 solves within the trace overlap iteration limit", async () => {

expect(solver.solved).toBe(true)
expect(solver.failed).toBe(false)
expect(solver.traceOverlapShiftSolver?.iterations).toBe(27)
// Issue #1102 fix: oscillating pairs are suspended instead of aborting the
// pass, so more independent pairs get corrected before the pass ends.
expect(solver.traceOverlapShiftSolver?.iterations).toBe(37)
expect(solver.postLabelTraceOverlapShiftSolver?.iterations).toBe(1)
await expect(solver).toMatchSolverSnapshot(import.meta.path)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect, test } from "bun:test"
import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"

// https://github.com/tscircuit/schematic-trace-solver/issues/1099
// Real pin coordinates are not always exactly on the routing grid: a segment
// between x = 5.3999378 and x = 5.4 has dx = 6.22e-5 — far below the schematic
// grid but five orders of magnitude above the 1e-9 axis-alignment EPS. Such
// segments used to bypass every obstacle check.

const rect = {
minX: 0,
maxX: 2,
minY: 0,
maxY: 2,
}

test("nearly vertical segment crossing a chip body is a collision", () => {
// dx = 6.22e-5 (from issue), dy = 4 — passes straight through the rect
expect(
segmentIntersectsRect({ x: 0.5 + 6.22e-5, y: -1 }, { x: 0.5, y: 3 }, rect),
).toBe(true)
})

test("nearly horizontal segment crossing a chip body is a collision", () => {
expect(
segmentIntersectsRect({ x: -1, y: 1 + 6.22e-5 }, { x: 3, y: 1 }, rect),
).toBe(true)
})

test("nearly vertical segment outside the rect stays collision-free", () => {
expect(
segmentIntersectsRect({ x: 2.5, y: -1 }, { x: 2.5 + 6.22e-5, y: 3 }, rect),
).toBe(false)
})

test("slanted segment through the rect interior is a collision", () => {
expect(segmentIntersectsRect({ x: -1, y: -1 }, { x: 3, y: 3 }, rect)).toBe(
true,
)
})

test("slanted segment missing the rect is collision-free", () => {
expect(segmentIntersectsRect({ x: -1, y: 2.5 }, { x: 3, y: 6 }, rect)).toBe(
false,
)
})

test("slanted segment touching only a corner is collision-free", () => {
// Glances off the top-right corner without entering the interior
expect(
segmentIntersectsRect({ x: 2.2, y: 1.8 }, { x: 1.8, y: 2.2 }, rect),
).toBe(false)
})
Loading