Skip to content
Merged
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
193 changes: 193 additions & 0 deletions fixtures/via-graph-solver/via-graph-convex-dataset02.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
import viasByNet from "assets/ViaGraphSolver/vias-by-net.json"
import type { XYConnection } from "lib/JumperGraphSolver/jumper-graph-generator/createGraphWithConnectionsFromBaseGraph"
import { ViaGraphSolver } from "lib/ViaGraphSolver/ViaGraphSolver"
import { createConvexViaGraphFromXYConnections } from "lib/ViaGraphSolver/via-graph-generator/createConvexViaGraphFromXYConnections"
import { useMemo, useState } from "react"
import dataset from "../../datasets/jumper-graph-solver/dataset02.json"

interface DatasetSample {
config: {
numCrossings: number
seed: number
rows: number
cols: number
orientation: "vertical" | "horizontal"
}
connections: {
connectionId: string
startRegionId: string
endRegionId: string
}[]
connectionRegions: {
regionId: string
pointIds: string[]
d: {
bounds: { minX: number; maxX: number; minY: number; maxY: number }
center: { x: number; y: number }
isPad: boolean
isConnectionRegion: boolean
}
}[]
}

const typedDataset = dataset as DatasetSample[]

const extractXYConnections = (sample: DatasetSample): XYConnection[] => {
const regionMap = new Map(
sample.connectionRegions.map((r) => [r.regionId, r.d.center]),
)

return sample.connections.map((conn) => {
const start = regionMap.get(conn.startRegionId)
const end = regionMap.get(conn.endRegionId)

if (!start || !end) {
throw new Error(
`Missing region for connection ${conn.connectionId}: start=${conn.startRegionId}, end=${conn.endRegionId}`,
)
}

return {
connectionId: conn.connectionId,
start,
end,
}
})
}

export default () => {
const [selectedIndex, setSelectedIndex] = useState(0)
const [key, setKey] = useState(0)

const entry = typedDataset[selectedIndex]

const problem = useMemo(() => {
if (!entry) return null

const xyConnections = extractXYConnections(entry)
const result = createConvexViaGraphFromXYConnections(
xyConnections,
viasByNet,
)

return {
graph: result,
connections: result.connections,
tileCount: result.tileCount,
tiledViasByNet: result.tiledViasByNet,
}
}, [selectedIndex])

if (!entry || !problem) {
return (
<div style={{ padding: 20, fontFamily: "monospace" }}>
No dataset loaded. Ensure dataset02.json exists at:
<pre>datasets/jumper-graph-solver/dataset02.json</pre>
</div>
)
}

const { config } = entry
const { tileCount } = problem

// Count region types
const convexRegions = problem.graph.regions.filter((r) =>
r.regionId.startsWith("convex:"),
)
const viaRegions = problem.graph.regions.filter((r) => r.d.isViaRegion)

return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<div
style={{
padding: "10px 20px",
borderBottom: "1px solid #ccc",
background: "#f5f5f5",
fontFamily: "monospace",
fontSize: 14,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 20 }}>
<label>
Sample:{" "}
<input
type="number"
min={0}
max={typedDataset.length - 1}
value={selectedIndex}
onChange={(e) => {
const val = parseInt(e.target.value, 10)
if (!isNaN(val) && val >= 0 && val < typedDataset.length) {
setSelectedIndex(val)
setKey((k) => k + 1)
}
}}
style={{ width: 60, marginRight: 5 }}
/>
/ {typedDataset.length - 1}
</label>
<button
onClick={() => {
setSelectedIndex(Math.max(0, selectedIndex - 1))
setKey((k) => k + 1)
}}
disabled={selectedIndex === 0}
>
Prev
</button>
<button
onClick={() => {
setSelectedIndex(
Math.min(typedDataset.length - 1, selectedIndex + 1),
)
setKey((k) => k + 1)
}}
disabled={selectedIndex === typedDataset.length - 1}
>
Next
</button>
<button
onClick={() => {
setSelectedIndex(Math.floor(Math.random() * typedDataset.length))
setKey((k) => k + 1)
}}
>
Random
</button>
</div>
<div style={{ marginTop: 8 }}>
<span>
<strong>Config:</strong> {config.rows}x{config.cols}{" "}
{config.orientation}, {config.numCrossings} crossings, seed=
{config.seed}
</span>
<span style={{ marginLeft: 20 }}>
<strong>Tiles:</strong> {tileCount.cols}x{tileCount.rows}
</span>
<span style={{ marginLeft: 20 }}>
<strong>Convex regions:</strong> {convexRegions.length}
</span>
<span style={{ marginLeft: 20 }}>
<strong>Via regions:</strong> {viaRegions.length}
</span>
</div>
</div>
<div style={{ flex: 1 }}>
<GenericSolverDebugger
key={key}
createSolver={() =>
new ViaGraphSolver({
inputGraph: {
regions: problem.graph.regions,
ports: problem.graph.ports,
},
inputConnections: problem.connections,
viasByNet: problem.tiledViasByNet,
})
}
/>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import defaultViasByNet from "assets/ViaGraphSolver/vias-by-net.json"
import type { XYConnection } from "../../JumperGraphSolver/jumper-graph-generator/createGraphWithConnectionsFromBaseGraph"
import type { JumperGraph } from "../../JumperGraphSolver/jumper-types"
import type { Connection } from "../../types"
import type { ViasByNet } from "../ViaGraphSolver"
import { createViaGraphWithConnections } from "./createViaGraphWithConnections"
import { generateConvexViaTopologyRegions } from "./generateConvexViaTopologyRegions"

export type ConvexViaGraphFromXYConnectionsResult = JumperGraph & {
connections: Connection[]
tiledViasByNet: ViasByNet
tileCount: { rows: number; cols: number }
}

/**
* Calculate the bounds from XY connections with no margin.
* The bounds go edge-to-edge with the connection points.
*/
function calculateBoundsFromConnections(xyConnections: XYConnection[]): {
minX: number
maxX: number
minY: number
maxY: number
} {
if (xyConnections.length === 0) {
throw new Error("Cannot calculate bounds from empty connections array")
}

let minX = Infinity
let maxX = -Infinity
let minY = Infinity
let maxY = -Infinity

for (const conn of xyConnections) {
minX = Math.min(minX, conn.start.x, conn.end.x)
maxX = Math.max(maxX, conn.start.x, conn.end.x)
minY = Math.min(minY, conn.start.y, conn.end.y)
maxY = Math.max(maxY, conn.start.y, conn.end.y)
}

return { minX, maxX, minY, maxY }
}

/**
* Creates a complete via topology graph from XY connections using convex regions.
*
* This function uses ConvexRegionsSolver to compute convex regions around
* via region obstacles, instead of the manual T/B/L/R outer regions.
*
* It:
* 1. Calculates bounds from connection XY coordinates (no margin)
* 2. Generates per-net via region polygons on a tiled grid
* 3. Uses ConvexRegionsSolver to compute convex regions around via regions
* 4. Creates ports between adjacent convex regions and via regions
* 5. Attaches connection regions to the graph
*
* @param xyConnections - Array of connections with start/end XY coordinates
* @param viasByNet - Via positions grouped by net name (defaults to built-in vias-by-net.json)
* @param opts - Optional configuration
*/
export function createConvexViaGraphFromXYConnections(
xyConnections: XYConnection[],
viasByNet: ViasByNet = defaultViasByNet as ViasByNet,
opts?: {
tileSize?: number
portPitch?: number
clearance?: number
concavityTolerance?: number
},
): ConvexViaGraphFromXYConnectionsResult {
// Calculate bounds from connections (no margin)
const bounds = calculateBoundsFromConnections(xyConnections)

// Generate the via topology with convex regions
const { regions, ports, tiledViasByNet, tileCount } =
generateConvexViaTopologyRegions({
viasByNet,
bounds,
tileSize: opts?.tileSize,
portPitch: opts?.portPitch,
clearance: opts?.clearance,
concavityTolerance: opts?.concavityTolerance,
})

// Create base graph from regions
const baseGraph: JumperGraph = { regions, ports }

// Add connections to the graph
const graphWithConnections = createViaGraphWithConnections(
baseGraph,
xyConnections,
)

return {
...graphWithConnections,
tiledViasByNet,
tileCount,
}
}
Loading