diff --git a/fixtures/via-graph-solver/via-graph-solver01.fixture.tsx b/fixtures/via-graph-solver/via-graph-solver01.fixture.tsx index e7dfbf7..1ea69bd 100644 --- a/fixtures/via-graph-solver/via-graph-solver01.fixture.tsx +++ b/fixtures/via-graph-solver/via-graph-solver01.fixture.tsx @@ -1,8 +1,8 @@ import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" import { ViaGraphSolver } from "lib/ViaGraphSolver/ViaGraphSolver" import { createViaGraphWithConnections } from "lib/ViaGraphSolver/via-graph-generator/createViaGraphWithConnections" import { generateViaTopologyRegions } from "lib/ViaGraphSolver/via-graph-generator/generateViaTopologyRegions" -import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" const baseGraph = generateViaTopologyRegions(viaTile, { graphSize: 5, diff --git a/fixtures/via-graph-solver/via-graph-solver02.fixture.tsx b/fixtures/via-graph-solver/via-graph-solver02.fixture.tsx index 608f479..aedc07b 100644 --- a/fixtures/via-graph-solver/via-graph-solver02.fixture.tsx +++ b/fixtures/via-graph-solver/via-graph-solver02.fixture.tsx @@ -1,8 +1,8 @@ import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" import { ViaGraphSolver } from "lib/ViaGraphSolver/ViaGraphSolver" import { createViaGraphWithConnections } from "lib/ViaGraphSolver/via-graph-generator/createViaGraphWithConnections" import { generateViaTopologyRegions } from "lib/ViaGraphSolver/via-graph-generator/generateViaTopologyRegions" -import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" const baseGraph = generateViaTopologyRegions(viaTile, { graphSize: 5, diff --git a/fixtures/via-graph-solver/via-graph-solver03.fixture.tsx b/fixtures/via-graph-solver/via-graph-solver03.fixture.tsx index d3343fa..56fea5e 100644 --- a/fixtures/via-graph-solver/via-graph-solver03.fixture.tsx +++ b/fixtures/via-graph-solver/via-graph-solver03.fixture.tsx @@ -1,8 +1,8 @@ import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" import { ViaGraphSolver } from "lib/ViaGraphSolver/ViaGraphSolver" import { createViaGraphWithConnections } from "lib/ViaGraphSolver/via-graph-generator/createViaGraphWithConnections" import { generateViaTopologyRegions } from "lib/ViaGraphSolver/via-graph-generator/generateViaTopologyRegions" -import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" const baseGraph = generateViaTopologyRegions(viaTile, { graphSize: 5, diff --git a/fixtures/via-graph-solver/via-graph-solver04.fixture.tsx b/fixtures/via-graph-solver/via-graph-solver04.fixture.tsx index c899b88..434cbc8 100644 --- a/fixtures/via-graph-solver/via-graph-solver04.fixture.tsx +++ b/fixtures/via-graph-solver/via-graph-solver04.fixture.tsx @@ -1,8 +1,8 @@ import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" import { ViaGraphSolver } from "lib/ViaGraphSolver/ViaGraphSolver" import { createViaGraphWithConnections } from "lib/ViaGraphSolver/via-graph-generator/createViaGraphWithConnections" import { generateViaTopologyRegions } from "lib/ViaGraphSolver/via-graph-generator/generateViaTopologyRegions" -import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" const baseGraph = generateViaTopologyRegions(viaTile, { graphSize: 5, diff --git a/fixtures/via-graph-solver/via-graph-solver05.fixture.tsx b/fixtures/via-graph-solver/via-graph-solver05.fixture.tsx index c5a71b4..94831ae 100644 --- a/fixtures/via-graph-solver/via-graph-solver05.fixture.tsx +++ b/fixtures/via-graph-solver/via-graph-solver05.fixture.tsx @@ -1,4 +1,5 @@ import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" import { createConnectionPort } from "lib/JumperGraphSolver/jumper-graph-generator/createConnectionPort" import { createConnectionRegion } from "lib/JumperGraphSolver/jumper-graph-generator/createConnectionRegion" import type { @@ -11,7 +12,6 @@ import type { ViaByNet, ViaTile } from "lib/ViaGraphSolver/ViaGraphSolver" import { ViaGraphSolver } from "lib/ViaGraphSolver/ViaGraphSolver" import { findBoundaryRegionForPolygons } from "lib/ViaGraphSolver/via-graph-generator/findBoundaryRegionForPolygons" import { generateViaTopologyRegions } from "lib/ViaGraphSolver/via-graph-generator/generateViaTopologyRegions" -import viaTile from "assets/ViaGraphSolver/via-tile-4-regions.json" // ── Configuration ────────────────────────────────────────────────────── const TILE_SIZE = 5 @@ -462,11 +462,11 @@ for (const xyConn of connections) { allRegions.push(endRegion) // Find the nearest boundary region for each endpoint - const startBoundary = findBoundaryRegionForPolygons( - start.x, - start.y, - allRegions, - ) + const startBoundary = findBoundaryRegionForPolygons({ + x: start.x, + y: start.y, + regions: allRegions, + }) if (startBoundary) { const startPort = createConnectionPort( `conn:${connectionId}:start-port`, @@ -477,7 +477,11 @@ for (const xyConn of connections) { allPorts.push(startPort) } - const endBoundary = findBoundaryRegionForPolygons(end.x, end.y, allRegions) + const endBoundary = findBoundaryRegionForPolygons({ + x: end.x, + y: end.y, + regions: allRegions, + }) if (endBoundary) { const endPort = createConnectionPort( `conn:${connectionId}:end-port`, diff --git a/lib/ViaGraphSolver/ViaGraphSolver.ts b/lib/ViaGraphSolver/ViaGraphSolver.ts index af2aeef..f4e5bf3 100644 --- a/lib/ViaGraphSolver/ViaGraphSolver.ts +++ b/lib/ViaGraphSolver/ViaGraphSolver.ts @@ -184,6 +184,25 @@ export class ViaGraphSolver extends HyperGraphSolver { return crossings * this.crossingPenalty + crossings * this.crossingPenaltySq } + override isRipRequiredForPortUsage( + region: JRegion, + _port1: JPort, + _port2: JPort, + ): boolean { + // Via regions are exclusive - if the region has any assignment from a + // different connection, using it requires ripping. This ensures the solver + // properly considers via region exclusivity during pathfinding. + if (region.d.isViaRegion) { + const assignments = region.assignments ?? [] + return assignments.some( + (a) => + a.connection.mutuallyConnectedNetworkId !== + this.currentConnection!.mutuallyConnectedNetworkId, + ) + } + return false + } + override getRipsRequiredForPortUsage( region: JRegion, port1: JPort, diff --git a/lib/ViaGraphSolver/polygonPerimeterUtils.ts b/lib/ViaGraphSolver/polygonPerimeterUtils.ts index 7c3db7f..9f49b46 100644 --- a/lib/ViaGraphSolver/polygonPerimeterUtils.ts +++ b/lib/ViaGraphSolver/polygonPerimeterUtils.ts @@ -12,6 +12,7 @@ type Point = { x: number; y: number } /** * Check if two 2D line segments intersect (excluding shared endpoints). * Uses the cross product method for robust intersection detection. + * Also detects T-intersections where one segment's endpoint lies on the other segment. */ function lineSegmentsIntersect( p1: Point, @@ -42,11 +43,55 @@ function lineSegmentsIntersect( const d3 = cross(p1, p2, p3) const d4 = cross(p1, p2, p4) - // Check if segments straddle each other + // Check if segments straddle each other (proper crossing) if (d1 * d2 < 0 && d3 * d4 < 0) { return true } + // Check for T-intersections: one segment's endpoint lies ON the other segment + // This happens when one cross product is ~0 and the point is within segment bounds + const pointOnSegment = ( + point: Point, + segStart: Point, + segEnd: Point, + crossProduct: number, + ) => { + if (Math.abs(crossProduct) > eps) return false + // Point is on the line, check if it's within segment bounds + const minX = Math.min(segStart.x, segEnd.x) - eps + const maxX = Math.max(segStart.x, segEnd.x) + eps + const minY = Math.min(segStart.y, segEnd.y) - eps + const maxY = Math.max(segStart.y, segEnd.y) + eps + return ( + point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY + ) + } + + // Check if p1 or p2 lies on segment p3-p4 (but not at endpoints, already checked) + if (pointOnSegment(p1, p3, p4, d1)) { + // p1 is on segment p3-p4, check it's not at the endpoints + if (!pointsCoincident(p1, p3) && !pointsCoincident(p1, p4)) { + return true + } + } + if (pointOnSegment(p2, p3, p4, d2)) { + if (!pointsCoincident(p2, p3) && !pointsCoincident(p2, p4)) { + return true + } + } + + // Check if p3 or p4 lies on segment p1-p2 (but not at endpoints) + if (pointOnSegment(p3, p1, p2, d3)) { + if (!pointsCoincident(p3, p1) && !pointsCoincident(p3, p2)) { + return true + } + } + if (pointOnSegment(p4, p1, p2, d4)) { + if (!pointsCoincident(p4, p1) && !pointsCoincident(p4, p2)) { + return true + } + } + return false } diff --git a/lib/ViaGraphSolver/via-graph-generator/createConvexViaGraphFromXYConnections.ts b/lib/ViaGraphSolver/via-graph-generator/createConvexViaGraphFromXYConnections.ts index dc6d139..462fbb5 100644 --- a/lib/ViaGraphSolver/via-graph-generator/createConvexViaGraphFromXYConnections.ts +++ b/lib/ViaGraphSolver/via-graph-generator/createConvexViaGraphFromXYConnections.ts @@ -95,6 +95,8 @@ export function createConvexViaGraphFromXYConnections( const baseGraph: JumperGraph = { regions, ports } // Add connections to the graph + // Note: findBoundaryRegionForPolygons auto-detects convex topology by checking + // for filler regions and only connects to them (avoiding tiny isolated convex regions) const graphWithConnections = createViaGraphWithConnections( baseGraph, xyConnections, diff --git a/lib/ViaGraphSolver/via-graph-generator/createViaGraphWithConnections.ts b/lib/ViaGraphSolver/via-graph-generator/createViaGraphWithConnections.ts index 745f3e4..7325339 100644 --- a/lib/ViaGraphSolver/via-graph-generator/createViaGraphWithConnections.ts +++ b/lib/ViaGraphSolver/via-graph-generator/createViaGraphWithConnections.ts @@ -46,11 +46,11 @@ export const createViaGraphWithConnections = ( ) regions.push(endRegion) - const startBoundary = findBoundaryRegionForPolygons( - start.x, - start.y, - baseGraph.regions, - ) + const startBoundary = findBoundaryRegionForPolygons({ + x: start.x, + y: start.y, + regions: baseGraph.regions, + }) if (startBoundary) { const startPort = createConnectionPort( `conn:${connectionId}:start-port`, @@ -61,11 +61,11 @@ export const createViaGraphWithConnections = ( ports.push(startPort) } - const endBoundary = findBoundaryRegionForPolygons( - end.x, - end.y, - baseGraph.regions, - ) + const endBoundary = findBoundaryRegionForPolygons({ + x: end.x, + y: end.y, + regions: baseGraph.regions, + }) if (endBoundary) { const endPort = createConnectionPort( `conn:${connectionId}:end-port`, diff --git a/lib/ViaGraphSolver/via-graph-generator/findBoundaryRegionForPolygons.ts b/lib/ViaGraphSolver/via-graph-generator/findBoundaryRegionForPolygons.ts index 0a198e4..a4b7edb 100644 --- a/lib/ViaGraphSolver/via-graph-generator/findBoundaryRegionForPolygons.ts +++ b/lib/ViaGraphSolver/via-graph-generator/findBoundaryRegionForPolygons.ts @@ -49,12 +49,20 @@ function closestPointOnPolygonEdge( * * Only considers non-pad, non-throughJumper, non-connectionRegion regions * that have polygon data. + * + * For convex topologies (detected by presence of filler regions), only + * connects to filler regions to avoid isolated tiny convex regions inside + * the tile grid. */ -export const findBoundaryRegionForPolygons = ( - x: number, - y: number, - regions: JRegion[], -): BoundaryRegionResult | null => { +export const findBoundaryRegionForPolygons = (params: { + x: number + y: number + regions: JRegion[] +}): BoundaryRegionResult | null => { + const { x, y, regions } = params + // Check if this is a convex topology by looking for filler regions + const hasFillerRegions = regions.some((r) => r.regionId.startsWith("filler:")) + let closestRegion: JRegion | null = null let closestDistance = Infinity let closestPortPosition = { x, y } @@ -67,6 +75,10 @@ export const findBoundaryRegionForPolygons = ( ) continue + // For convex topologies, only connect to filler regions to avoid + // isolated tiny convex regions inside the tile grid. + if (hasFillerRegions && !region.regionId.startsWith("filler:")) continue + const polygon = region.d.polygon if (!polygon || polygon.length < 3) continue diff --git a/lib/ViaGraphSolver/via-graph-generator/generateConvexViaTopologyRegions.ts b/lib/ViaGraphSolver/via-graph-generator/generateConvexViaTopologyRegions.ts index a0a2d81..852d411 100644 --- a/lib/ViaGraphSolver/via-graph-generator/generateConvexViaTopologyRegions.ts +++ b/lib/ViaGraphSolver/via-graph-generator/generateConvexViaTopologyRegions.ts @@ -250,6 +250,76 @@ function rectPolygonFromBounds(b: Bounds): Point[] { ] } +/** + * Find the port positions on each side (top, bottom, left, right) of a via region polygon. + * Each side gets 1 port at the midpoint of the segment that defines that side's extreme. + * + * @param polygon - The via region polygon + * @returns Object with port positions for each side (may be null if no segment exists on that side) + */ +function findViaRegionSidePorts(polygon: Point[]): { + top: Point | null + bottom: Point | null + left: Point | null + right: Point | null +} { + if (polygon.length < 3) { + return { top: null, bottom: null, left: null, right: null } + } + + const bounds = boundsFromPolygon(polygon) + const tolerance = 0.001 + + let topPort: Point | null = null + let bottomPort: Point | null = null + let leftPort: Point | null = null + let rightPort: Point | null = null + + // Find segments on each side of the bounding box + for (let i = 0; i < polygon.length; i++) { + const p1 = polygon[i] + const p2 = polygon[(i + 1) % polygon.length] + + // Check if this segment is on the top edge (maxY) + if ( + Math.abs(p1.y - bounds.maxY) < tolerance && + Math.abs(p2.y - bounds.maxY) < tolerance + ) { + // Segment is on top edge, port at midpoint + topPort = { x: (p1.x + p2.x) / 2, y: bounds.maxY } + } + + // Check if this segment is on the bottom edge (minY) + if ( + Math.abs(p1.y - bounds.minY) < tolerance && + Math.abs(p2.y - bounds.minY) < tolerance + ) { + // Segment is on bottom edge, port at midpoint + bottomPort = { x: (p1.x + p2.x) / 2, y: bounds.minY } + } + + // Check if this segment is on the left edge (minX) + if ( + Math.abs(p1.x - bounds.minX) < tolerance && + Math.abs(p2.x - bounds.minX) < tolerance + ) { + // Segment is on left edge, port at midpoint + leftPort = { x: bounds.minX, y: (p1.y + p2.y) / 2 } + } + + // Check if this segment is on the right edge (maxX) + if ( + Math.abs(p1.x - bounds.maxX) < tolerance && + Math.abs(p2.x - bounds.maxX) < tolerance + ) { + // Segment is on right edge, port at midpoint + rightPort = { x: bounds.maxX, y: (p1.y + p2.y) / 2 } + } + } + + return { top: topPort, bottom: bottomPort, left: leftPort, right: rightPort } +} + /** * Extend via region polygon to tile boundary when extremely close (< threshold). * Only extends polygon edges that are within threshold of the tile boundary. @@ -937,34 +1007,184 @@ export function generateConvexViaTopologyRegions(opts: { // Step 6: Create ports between tile edge regions and filler regions // Check both convex regions and via regions (via regions may touch tile edge when extended) + // Ports are placed at the CENTER of each filler strip to prevent diagonal routes + // Track port positions per filler region to ensure minimum spacing of portPitch + // + // Instead of using findSharedEdges (which can fail with short tile edges), + // we directly check for bounds adjacency based on the filler region's edge. + const fillerPortPositions = new Map>() for (const fillerRegion of fillerRegions) { + fillerPortPositions.set(fillerRegion.regionId, []) + const fillerBounds = fillerRegion.d.bounds + const stripWidth = fillerBounds.maxX - fillerBounds.minX + const stripHeight = fillerBounds.maxY - fillerBounds.minY + const isHorizontalStrip = stripWidth > stripHeight + + // Calculate the number of ports and their positions along the filler strip + // For horizontal strips (top/bottom): ports at evenly-spaced X positions + // For vertical strips (left/right): ports at evenly-spaced Y positions + const stripSize = isHorizontalStrip ? stripWidth : stripHeight + const numPorts = Math.max(1, Math.floor(stripSize / portPitch)) + const actualPitch = stripSize / numPorts + + // Determine which edge of the filler region is adjacent to the tile grid + // based on the filler region's position relative to the grid + const adjacencyTolerance = clearance * 2 + const isTopFiller = fillerRegion.regionId.startsWith("filler:top:") + const isBottomFiller = fillerRegion.regionId.startsWith("filler:bottom:") + const isLeftFiller = fillerRegion.regionId.startsWith("filler:left:") + const isRightFiller = fillerRegion.regionId.startsWith("filler:right:") + // Find which tile regions (convex or via) are adjacent to this filler region const tileRegions = [...convexRegions, ...viaRegions] for (const tileRegion of tileRegions) { - const sharedEdges = findSharedEdges( - tileRegion.d.polygon!, - fillerRegion.d.polygon!, - clearance * 2, - ) + const tileBounds = tileRegion.d.bounds + const eps = 0.001 + + // Check if the tile region is adjacent to this filler region + // based on bounds overlap and edge proximity + let isAdjacent = false + let edgeX: number | null = null + let edgeY: number | null = null + + if (isHorizontalStrip) { + // For horizontal strips (top/bottom), check X overlap and Y adjacency + const overlapMinX = Math.max(fillerBounds.minX, tileBounds.minX) + const overlapMaxX = Math.min(fillerBounds.maxX, tileBounds.maxX) + const hasXOverlap = overlapMaxX > overlapMinX + eps + + if (hasXOverlap) { + if (isTopFiller) { + // Top filler: tile should be adjacent to filler's bottom edge (minY) + isAdjacent = + Math.abs(tileBounds.maxY - fillerBounds.minY) < adjacencyTolerance + edgeY = fillerBounds.minY + } else if (isBottomFiller) { + // Bottom filler: tile should be adjacent to filler's top edge (maxY) + isAdjacent = + Math.abs(tileBounds.minY - fillerBounds.maxY) < adjacencyTolerance + edgeY = fillerBounds.maxY + } + } + } else { + // For vertical strips (left/right), check Y overlap and X adjacency + const overlapMinY = Math.max(fillerBounds.minY, tileBounds.minY) + const overlapMaxY = Math.min(fillerBounds.maxY, tileBounds.maxY) + const hasYOverlap = overlapMaxY > overlapMinY + eps + + if (hasYOverlap) { + if (isLeftFiller) { + // Left filler: tile should be adjacent to filler's right edge (maxX) + isAdjacent = + Math.abs(tileBounds.minX - fillerBounds.maxX) < adjacencyTolerance + edgeX = fillerBounds.maxX + } else if (isRightFiller) { + // Right filler: tile should be adjacent to filler's left edge (minX) + isAdjacent = + Math.abs(tileBounds.maxX - fillerBounds.minX) < adjacencyTolerance + edgeX = fillerBounds.minX + } + } + } - for (const edge of sharedEdges) { - const portPositions = createPortsAlongEdge(edge, portPitch) + if (!isAdjacent) continue + + // Create ports based on the overlap region between filler and tile + // Calculate the overlap region first + let overlapMin: number + let overlapMax: number + if (isHorizontalStrip) { + overlapMin = Math.max(fillerBounds.minX, tileBounds.minX) + overlapMax = Math.min(fillerBounds.maxX, tileBounds.maxX) + } else { + overlapMin = Math.max(fillerBounds.minY, tileBounds.minY) + overlapMax = Math.min(fillerBounds.maxY, tileBounds.maxY) + } - for (const pos of portPositions) { - createPort( - `filler:${tileRegion.regionId}-${fillerRegion.regionId}:${portIdCounter++}`, - tileRegion, - fillerRegion, - pos, + const overlapSize = overlapMax - overlapMin + if (overlapSize < eps) continue + + // Calculate number of ports based on overlap size, ensuring at least 1 port + const overlapNumPorts = Math.max(1, Math.floor(overlapSize / portPitch)) + const overlapActualPitch = overlapSize / overlapNumPorts + + for (let i = 0; i < overlapNumPorts; i++) { + let pos: { x: number; y: number } + + if (isHorizontalStrip) { + // Port X is centered within each segment of the overlap region + const x = overlapMin + (i + 0.5) * overlapActualPitch + pos = { x, y: edgeY! } + } else { + // Port Y is centered within each segment of the overlap region + const y = overlapMin + (i + 0.5) * overlapActualPitch + pos = { x: edgeX!, y } + } + + // Verify the port position is actually within the tile region's polygon + // (bounding box overlap doesn't guarantee polygon overlap for non-rectangular polygons) + if (tileRegion.d.polygon) { + // Test a point inside the tile region's polygon + // The test point needs to be far enough from the edge to be inside the polygon, + // accounting for the gap between the filler and tile region + // Use the gap distance + a small margin to ensure we're inside the tile polygon + let testPoint: Point + if (isTopFiller) { + // Gap is fillerBounds.minY - tileBounds.maxY, test point should be inside tile + const gap = fillerBounds.minY - tileBounds.maxY + const testOffset = gap + 0.01 + testPoint = { x: pos.x, y: pos.y - testOffset } + } else if (isBottomFiller) { + const gap = tileBounds.minY - fillerBounds.maxY + const testOffset = gap + 0.01 + testPoint = { x: pos.x, y: pos.y + testOffset } + } else if (isLeftFiller) { + const gap = tileBounds.minX - fillerBounds.maxX + const testOffset = gap + 0.01 + testPoint = { x: pos.x + testOffset, y: pos.y } + } else { + // isRightFiller + const gap = fillerBounds.minX - tileBounds.maxX + const testOffset = gap + 0.01 + testPoint = { x: pos.x - testOffset, y: pos.y } + } + + if (!pointInPolygon(testPoint, tileRegion.d.polygon)) { + continue // Skip this port position as it's outside the tile region's polygon + } + } + + // Check if this position is too close to an existing port in this filler region + const existingPositions = fillerPortPositions.get( + fillerRegion.regionId, + )! + const tooClose = existingPositions.some((existing) => { + const dist = Math.sqrt( + (pos.x - existing.x) ** 2 + (pos.y - existing.y) ** 2, ) + return dist < portPitch + }) + + if (tooClose) { + continue } + + // Track this position + existingPositions.push(pos) + + createPort( + `filler:${tileRegion.regionId}-${fillerRegion.regionId}:${portIdCounter++}`, + tileRegion, + fillerRegion, + pos, + ) } } } // Step 7: Create ports between adjacent filler regions - // Only create ports if the shared edge is at least portPitch (trace width) long - // This prevents creating ports at corners where regions are too thin + // Always create at least one port at the midpoint for connectivity, + // even if the edge is shorter than portPitch for (let i = 0; i < fillerRegions.length; i++) { for (let j = i + 1; j < fillerRegions.length; j++) { const region1 = fillerRegions[i] @@ -982,12 +1202,25 @@ export function generateConvexViaTopologyRegions(opts: { (edge.to.x - edge.from.x) ** 2 + (edge.to.y - edge.from.y) ** 2, ) - // Skip if edge is shorter than trace width (portPitch) - if (edgeLength < portPitch) { + // Skip edges that are essentially zero-length + if (edgeLength < 0.01) { continue } - const portPositions = createPortsAlongEdge(edge, portPitch) + // For short edges, just create one port at the midpoint + // For longer edges, distribute ports along the edge + let portPositions: Array<{ x: number; y: number }> + if (edgeLength < portPitch) { + // Single port at midpoint for short edges + portPositions = [ + { + x: (edge.from.x + edge.to.x) / 2, + y: (edge.from.y + edge.to.y) / 2, + }, + ] + } else { + portPositions = createPortsAlongEdge(edge, portPitch) + } for (const pos of portPositions) { createPort( @@ -1001,26 +1234,101 @@ export function generateConvexViaTopologyRegions(opts: { } } - // Step 8: Create ports between via regions and convex regions within each tile - // (Via ↔ Filler ports are already created in Step 6) + // Step 8: Create ports between via regions and adjacent regions (convex or filler) + // Each via region gets exactly 1 port on each side (top, bottom, left, right) + // at the midpoint of the segment that defines that side's extreme + // Search all non-via regions (convex + filler) for adjacency + const nonViaRegions = allRegions.filter((r) => !r.d.isViaRegion) + const createdViaSidePorts = new Set() // Track "viaRegionId:side" to avoid duplicates + for (const viaRegion of viaRegions) { - for (const convexRegion of convexRegions) { - const sharedEdges = findSharedEdges( - viaRegion.d.polygon!, - convexRegion.d.polygon!, - clearance * 2, - ) + const sidePorts = findViaRegionSidePorts(viaRegion.d.polygon!) + const viaBounds = viaRegion.d.bounds + + // For each side, find adjacent regions and create a single port + const sides = ["top", "bottom", "left", "right"] as const + for (const side of sides) { + const portPos = sidePorts[side] + if (!portPos) continue + + // Skip if we already created a port for this via region side + const sideKey = `${viaRegion.regionId}:${side}` + if (createdViaSidePorts.has(sideKey)) continue + + // Find adjacent region by checking bounds adjacency and containment of test point + // We look for a region that: + // 1. Has bounds adjacent to the via region's side + // 2. Contains a test point just outside the via region on that side + const adjacencyTolerance = 0.2 // Allow small gap between regions (clearance) + const testOffset = adjacencyTolerance // Test point must be beyond any clearance gap + + let testPoint: Point + switch (side) { + case "top": + testPoint = { x: portPos.x, y: viaBounds.maxY + testOffset } + break + case "bottom": + testPoint = { x: portPos.x, y: viaBounds.minY - testOffset } + break + case "left": + testPoint = { x: viaBounds.minX - testOffset, y: portPos.y } + break + case "right": + testPoint = { x: viaBounds.maxX + testOffset, y: portPos.y } + break + } - for (const edge of sharedEdges) { - const portPositions = createPortsAlongEdge(edge, portPitch) + for (const adjacentRegion of nonViaRegions) { + const adjBounds = adjacentRegion.d.bounds + + // Check if bounds are potentially adjacent on this side + let boundsAdjacent = false + switch (side) { + case "top": + // Adjacent region should be above (its minY near our maxY) + boundsAdjacent = + Math.abs(adjBounds.minY - viaBounds.maxY) < adjacencyTolerance && + adjBounds.minX < portPos.x && + adjBounds.maxX > portPos.x + break + case "bottom": + // Adjacent region should be below (its maxY near our minY) + boundsAdjacent = + Math.abs(adjBounds.maxY - viaBounds.minY) < adjacencyTolerance && + adjBounds.minX < portPos.x && + adjBounds.maxX > portPos.x + break + case "left": + // Adjacent region should be to the left (its maxX near our minX) + boundsAdjacent = + Math.abs(adjBounds.maxX - viaBounds.minX) < adjacencyTolerance && + adjBounds.minY < portPos.y && + adjBounds.maxY > portPos.y + break + case "right": + // Adjacent region should be to the right (its minX near our maxX) + boundsAdjacent = + Math.abs(adjBounds.minX - viaBounds.maxX) < adjacencyTolerance && + adjBounds.minY < portPos.y && + adjBounds.maxY > portPos.y + break + } - for (const pos of portPositions) { + if (!boundsAdjacent) continue + + // Verify the test point is inside the adjacent region's polygon + if ( + adjacentRegion.d.polygon && + pointInPolygon(testPoint, adjacentRegion.d.polygon) + ) { createPort( - `via-convex:${viaRegion.regionId}-${convexRegion.regionId}:${portIdCounter++}`, + `via-side:${viaRegion.regionId}:${side}:${portIdCounter++}`, viaRegion, - convexRegion, - pos, + adjacentRegion, + portPos, ) + createdViaSidePorts.add(sideKey) + break // Only one port per side per via region } } } diff --git a/tests/via-graph-solver/__snapshots__/via-graph-convex-dataset02-3-regions.snap.svg b/tests/via-graph-solver/__snapshots__/via-graph-convex-dataset02-3-regions.snap.svg index 0042fb0..839aa52 100644 --- a/tests/via-graph-solver/__snapshots__/via-graph-convex-dataset02-3-regions.snap.svg +++ b/tests/via-graph-solver/__snapshots__/via-graph-convex-dataset02-3-regions.snap.svg @@ -1,34 +1,34 @@ -