diff --git a/tests/features/topology/__snapshots__/srj24-sample4-topology-repro.snap.svg b/tests/features/topology/__snapshots__/srj24-sample4-topology-repro.snap.svg
new file mode 100644
index 000000000..497443e52
--- /dev/null
+++ b/tests/features/topology/__snapshots__/srj24-sample4-topology-repro.snap.svg
@@ -0,0 +1,44 @@
+
\ No newline at end of file
diff --git a/tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts b/tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts
new file mode 100644
index 000000000..f3d229aeb
--- /dev/null
+++ b/tests/features/topology/fixtures/srj24-sample4-topology.fixture.ts
@@ -0,0 +1,285 @@
+import type { GraphicsObject } from "graphics-debug"
+import type {
+ CapacityMeshEdge,
+ CapacityMeshNode,
+ Obstacle,
+ SimpleRouteJson,
+} from "lib/types"
+
+export interface Srj24Sample4TopologyFixture {
+ inputSrj: SimpleRouteJson
+ capacityNodes: CapacityMeshNode[]
+ bgaCoreObstacleIds: ReadonlySet
+}
+
+type VisualizedCapacityMeshNode = CapacityMeshNode & {
+ _allowsViaInPad?: boolean
+}
+
+const BGA_COMPONENT_ID = "mixed-pad-bga"
+const BGA_TOP_PORT_ID = "bga-top-port"
+const BOTTOM_PORT_ID = "bottom-port"
+const GRID_COORDINATES = [-1.3, -0.65, 0, 0.65, 1.3]
+
+function createPad({
+ obstacleId,
+ componentId = BGA_COMPONENT_ID,
+ x,
+ y,
+ width,
+ height,
+ layer = "top",
+ connectedTo = [],
+}: {
+ obstacleId: string
+ componentId?: string
+ x: number
+ y: number
+ width: number
+ height: number
+ layer?: string
+ connectedTo?: string[]
+}): Obstacle {
+ return {
+ obstacleId,
+ componentId,
+ type: "rect",
+ layers: [layer],
+ center: { x, y },
+ width,
+ height,
+ connectedTo,
+ }
+}
+
+function createBgaCore(): Obstacle[] {
+ return GRID_COORDINATES.flatMap((y, row) =>
+ GRID_COORDINATES.map((x, column) =>
+ createPad({
+ obstacleId: `core-${row}-${column}`,
+ x,
+ y,
+ width: 0.254,
+ height: 0.254,
+ connectedTo: x === 0 && y === 0 ? [BGA_TOP_PORT_ID] : [],
+ }),
+ ),
+ )
+}
+
+function createSpecializedPerimeterPads(): Obstacle[] {
+ const horizontalPads = [-1, 1].flatMap((direction) =>
+ GRID_COORDINATES.map((x, index) =>
+ createPad({
+ obstacleId: `horizontal-${direction}-${index}`,
+ x,
+ y: direction * 1.95,
+ width: 0.3302,
+ height: 0.1578,
+ }),
+ ),
+ )
+ const verticalPads = [-1, 1].flatMap((direction) =>
+ GRID_COORDINATES.map((y, index) =>
+ createPad({
+ obstacleId: `vertical-${direction}-${index}`,
+ x: direction * 1.95,
+ y,
+ width: 0.1578,
+ height: 0.3302,
+ }),
+ ),
+ )
+
+ return [...horizontalPads, ...verticalPads]
+}
+
+function createCapacityNode(
+ capacityMeshNodeId: string,
+ availableZ: number[],
+ overrides: Partial,
+): CapacityMeshNode {
+ return {
+ capacityMeshNodeId,
+ center: { x: 0, y: 0 },
+ width: 0.254,
+ height: 0.254,
+ layer: `z${availableZ.join(",")}`,
+ availableZ,
+ ...overrides,
+ }
+}
+
+export function createSrj24Sample4TopologyFixture(): Srj24Sample4TopologyFixture {
+ const bgaCore = createBgaCore()
+ const bottomPad = createPad({
+ obstacleId: "stacked-bottom-pad",
+ componentId: "bottom-component",
+ x: 0.17,
+ y: 0,
+ width: 0.59,
+ height: 0.64,
+ layer: "bottom",
+ connectedTo: [BOTTOM_PORT_ID],
+ })
+
+ return {
+ inputSrj: {
+ layerCount: 6,
+ minTraceWidth: 0.1,
+ minViaPadDiameter: 0.3,
+ minViaHoleDiameter: 0.15,
+ obstacles: [...bgaCore, ...createSpecializedPerimeterPads(), bottomPad],
+ connections: [
+ {
+ name: "stacked-terminal-net",
+ pointsToConnect: [
+ { pointId: BGA_TOP_PORT_ID, x: 0, y: 0, layer: "top" },
+ { pointId: BOTTOM_PORT_ID, x: 0.17, y: 0, layer: "bottom" },
+ ],
+ },
+ ],
+ bounds: { minX: -4, maxX: 4, minY: -4, maxY: 4 },
+ },
+ capacityNodes: [
+ createCapacityNode("target-top", [0], {
+ _containsTarget: true,
+ _containsObstacle: true,
+ _targetConnectionName: "stacked-terminal-net",
+ }),
+ createCapacityNode("target-bottom", [5], {
+ center: { x: 0.17, y: 0 },
+ width: 0.59,
+ height: 0.64,
+ _containsTarget: true,
+ _containsObstacle: true,
+ _targetConnectionName: "stacked-terminal-net",
+ }),
+ createCapacityNode("free-top", [0], {
+ center: { x: 0.452, y: 0 },
+ width: 0.65,
+ }),
+ createCapacityNode("free-bottom", [5], {
+ center: { x: 0.665, y: 0 },
+ width: 0.4,
+ }),
+ ],
+ bgaCoreObstacleIds: new Set(
+ bgaCore.map((obstacle) => obstacle.obstacleId!),
+ ),
+ }
+}
+
+export function visualizeMixedComponent({
+ inputSrj,
+ selectedObstacleIds = new Set(),
+}: {
+ inputSrj: SimpleRouteJson
+ selectedObstacleIds?: ReadonlySet
+}): GraphicsObject {
+ return {
+ rects: inputSrj.obstacles.map((obstacle) => {
+ const selected =
+ obstacle.obstacleId !== undefined &&
+ selectedObstacleIds.has(obstacle.obstacleId)
+
+ return {
+ center: obstacle.center,
+ width: obstacle.width,
+ height: obstacle.height,
+ fill: selected
+ ? "#86efac"
+ : obstacle.layers.includes("bottom")
+ ? "#fca5a5"
+ : "#cbd5e1",
+ stroke: selected ? "#15803d" : "#64748b",
+ label: obstacle.obstacleId,
+ }
+ }),
+ }
+}
+
+export function visualizeLayerAccess({
+ nodes,
+ edges,
+}: {
+ nodes: VisualizedCapacityMeshNode[]
+ edges: CapacityMeshEdge[]
+}): GraphicsObject {
+ const orderedNodeIds = [
+ "target-top",
+ "free-top",
+ "target-bottom",
+ "free-bottom",
+ ]
+ const xByNodeId = new Map(
+ orderedNodeIds.map((nodeId, index) => [nodeId, index * 1.4]),
+ )
+ const nodeById = new Map(nodes.map((node) => [node.capacityMeshNodeId, node]))
+ const rects: NonNullable = []
+ const lines: NonNullable = []
+ const texts: NonNullable = []
+
+ for (let z = 0; z < 6; z++) {
+ texts.push({
+ x: -0.6,
+ y: 2.5 - z,
+ text: `z${z}`,
+ anchorSide: "center_right",
+ fontSize: 0.18,
+ })
+ }
+
+ for (const node of nodes) {
+ const x = xByNodeId.get(node.capacityMeshNodeId)!
+ texts.push({
+ x,
+ y: 3.05,
+ text: node.capacityMeshNodeId,
+ anchorSide: "bottom_center",
+ fontSize: 0.13,
+ })
+ for (const z of node.availableZ) {
+ rects.push({
+ center: { x, y: 2.5 - z },
+ width: 0.8,
+ height: 0.46,
+ fill: node._containsTarget ? "#fca5a5" : "#bae6fd",
+ stroke: node._allowsViaInPad ? "#15803d" : "#475569",
+ })
+ }
+ if (node._allowsViaInPad && node.availableZ.length > 1) {
+ lines.push({
+ points: [
+ { x, y: -2.23 },
+ { x, y: 2.23 },
+ ],
+ strokeColor: "#16a34a",
+ strokeWidth: 0.1,
+ })
+ }
+ }
+
+ for (const edge of edges) {
+ const [firstNodeId, secondNodeId] = edge.nodeIds
+ const firstNode = nodeById.get(firstNodeId)
+ const secondNode = nodeById.get(secondNodeId)
+ if (!firstNode || !secondNode) continue
+
+ const sharedLayers = firstNode.availableZ.filter((z) =>
+ secondNode.availableZ.includes(z),
+ )
+ for (const z of sharedLayers) {
+ lines.push({
+ points: [
+ { x: xByNodeId.get(firstNodeId)!, y: 2.5 - z },
+ { x: xByNodeId.get(secondNodeId)!, y: 2.5 - z },
+ ],
+ strokeColor: "#2563eb",
+ strokeWidth: 0.08,
+ })
+ }
+ }
+
+ return { rects, lines, texts }
+}
diff --git a/tests/features/topology/srj24-sample4-topology-repro.test.ts b/tests/features/topology/srj24-sample4-topology-repro.test.ts
new file mode 100644
index 000000000..e96d9724f
--- /dev/null
+++ b/tests/features/topology/srj24-sample4-topology-repro.test.ts
@@ -0,0 +1,77 @@
+import { expect, test } from "bun:test"
+import { CapacityMeshEdgeSolver } from "lib/solvers/CapacityMeshSolver/CapacityMeshEdgeSolver"
+import { ComponentDetectionSolver } from "lib/solvers/ComponentDetectionSolver/ComponentDetectionSolver"
+import { getGraphicsSvgFrames } from "tests/fixtures/solver-svg-frames"
+import {
+ createSrj24Sample4TopologyFixture,
+ visualizeLayerAccess,
+ visualizeMixedComponent,
+} from "./fixtures/srj24-sample4-topology.fixture"
+
+test("reproduces the srj24 sample 4 topology gap", async (): Promise => {
+ const fixture = createSrj24Sample4TopologyFixture()
+ const componentDetectionSolver = new ComponentDetectionSolver({
+ inputSrj: fixture.inputSrj,
+ })
+ componentDetectionSolver.solve()
+ const detectedComponents = componentDetectionSolver.getOutput()
+
+ const edgeSolver = new CapacityMeshEdgeSolver(fixture.capacityNodes)
+ edgeSolver.solve()
+ const targetTop = fixture.capacityNodes.find(
+ (node) => node.capacityMeshNodeId === "target-top",
+ )!
+ const targetBottom = fixture.capacityNodes.find(
+ (node) => node.capacityMeshNodeId === "target-bottom",
+ )!
+
+ expect(
+ detectedComponents.some(
+ (component) =>
+ component.componentId === "mixed-pad-bga" &&
+ component.componentKind === "bga",
+ ),
+ ).toBe(false)
+ expect(edgeSolver.hasEdgeBetween(targetTop, targetBottom)).toBe(false)
+
+ const svg = getGraphicsSvgFrames({
+ frames: [
+ {
+ name: "Before: mixed pad package",
+ step: 0,
+ iteration: 0,
+ graphics: visualizeMixedComponent({ inputSrj: fixture.inputSrj }),
+ },
+ {
+ name: "Hidden regular BGA core",
+ step: 1,
+ iteration: 1,
+ graphics: visualizeMixedComponent({
+ inputSrj: fixture.inputSrj,
+ selectedObstacleIds: fixture.bgaCoreObstacleIds,
+ }),
+ },
+ {
+ name: "Before: isolated terminal layers",
+ step: 2,
+ iteration: 2,
+ graphics: visualizeLayerAccess({
+ nodes: fixture.capacityNodes,
+ edges: [],
+ }),
+ },
+ {
+ name: "Before: no cross-layer edge",
+ step: 3,
+ iteration: 3,
+ graphics: visualizeLayerAccess({
+ nodes: fixture.capacityNodes,
+ edges: edgeSolver.edges,
+ }),
+ },
+ ],
+ columns: 2,
+ })
+
+ await expect(svg).toMatchSvgSnapshot(import.meta.path, { scale: 2 })
+})