Skip to content

Route automatic breakout exits toward external endpoints - #3381

Closed
seveibar wants to merge 1 commit into
mainfrom
fix/breakout-exit-routing-quality
Closed

Route automatic breakout exits toward external endpoints#3381
seveibar wants to merge 1 commit into
mainfrom
fix/breakout-exit-routing-quality

Conversation

@seveibar

Copy link
Copy Markdown
Contributor

Summary

  • choose a nearer breakout edge for safe standalone outlier connections while preserving the shared edge for buses, differential pairs, and dense fanouts
  • center each split edge group toward its external endpoints
  • lock the QFP16 Pipeline9 regression fixture to zero vias and update its PCB/autorouting snapshots

Why

The routing-quality regression surfaced in #3378 because every connection in the single breakout region was forced through the aggregate right edge. The RESET/R1 connection has its external endpoint on the left, so that start/end choice created a long cross-region escape and unnecessary layer changes. The right-side header exits were also centered on the internal pads instead of the external header endpoints.

This keeps the original winding solve as the baseline feasibility check and only splits ungrouped singleton outliers when at least two routes remain on the original edge. Custom solvers and multi-region breakouts are unchanged.

The affected Pipeline9 fixture goes from 3 vias to 0.

Testing

  • bun run build
  • bun test tests/breakout (19 pass, 2 skip, 0 fail)
  • bun test (1,431 pass, 40 skip, 0 fail)
  • biome check on changed TypeScript files
  • git diff --check

Related to #3378.

@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tscircuit-core-benchmarks Ready Ready Preview Aug 23, 2026 12:03am

Request Review

Comment on lines +321 to +338
const isInsideRegionBounds =
pcbPort.x >= region.bounds.minX &&
pcbPort.x <= region.bounds.maxX &&
pcbPort.y >= region.bounds.minY &&
pcbPort.y <= region.bounds.maxY
if (isInsideRegion) continue
externalPcbPorts.set(pcbPort.pcb_port_id, {
if (!isInsideRegionBounds) {
aggregateExternalTargetsByPcbPortId.set(pcbPort.pcb_port_id, {
x: pcbPort.x,
y: pcbPort.y,
})
}
const targets =
externalTargetsByConnectionId.get(sourceTrace.source_trace_id) ?? []
targets.push({
x: pcbPort.x,
y: pcbPort.y,
})
externalTargetsByConnectionId.set(sourceTrace.source_trace_id, targets)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical bug: externalTargetsByConnectionId is being populated with ALL targets (both internal and external) but should only contain external targets. After the isInsideRegionBounds check determines a target is NOT inside bounds (line 326), it correctly adds to aggregateExternalTargetsByPcbPortId. However, lines 332-338 unconditionally add ALL pcb ports to externalTargetsByConnectionId regardless of whether they're inside or outside the region bounds.

Fix:

if (!isInsideRegionBounds) {
  aggregateExternalTargetsByPcbPortId.set(pcbPort.pcb_port_id, {
    x: pcbPort.x,
    y: pcbPort.y,
  })
  const targets =
    externalTargetsByConnectionId.get(sourceTrace.source_trace_id) ?? []
  targets.push({
    x: pcbPort.x,
    y: pcbPort.y,
  })
  externalTargetsByConnectionId.set(sourceTrace.source_trace_id, targets)
}

This will cause incorrect routing decisions since internal targets will be included when computing preferred edges and center points for external routing.

Suggested change
const isInsideRegionBounds =
pcbPort.x >= region.bounds.minX &&
pcbPort.x <= region.bounds.maxX &&
pcbPort.y >= region.bounds.minY &&
pcbPort.y <= region.bounds.maxY
if (isInsideRegion) continue
externalPcbPorts.set(pcbPort.pcb_port_id, {
if (!isInsideRegionBounds) {
aggregateExternalTargetsByPcbPortId.set(pcbPort.pcb_port_id, {
x: pcbPort.x,
y: pcbPort.y,
})
}
const targets =
externalTargetsByConnectionId.get(sourceTrace.source_trace_id) ?? []
targets.push({
x: pcbPort.x,
y: pcbPort.y,
})
externalTargetsByConnectionId.set(sourceTrace.source_trace_id, targets)
const isInsideRegionBounds =
pcbPort.x >= region.bounds.minX &&
pcbPort.x <= region.bounds.maxX &&
pcbPort.y >= region.bounds.minY &&
pcbPort.y <= region.bounds.maxY
if (!isInsideRegionBounds) {
aggregateExternalTargetsByPcbPortId.set(pcbPort.pcb_port_id, {
x: pcbPort.x,
y: pcbPort.y,
})
const targets =
externalTargetsByConnectionId.get(sourceTrace.source_trace_id) ?? []
targets.push({
x: pcbPort.x,
y: pcbPort.y,
})
externalTargetsByConnectionId.set(sourceTrace.source_trace_id, targets)
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@seveibar

Copy link
Copy Markdown
Contributor Author

Superseded by the properly split implementation: winding solver behavior in tscircuit/winding-breakout-point-solver#6, the public endpoint contract in tscircuit/props#809, and Core integration/snapshots in #3382. Closing this Core-owned solver workaround in favor of the winding-owned fix.

@seveibar seveibar closed this Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant