diff --git a/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-session-to-circuit-json.ts b/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-session-to-circuit-json.ts index a179fe0..de5639e 100644 --- a/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-session-to-circuit-json.ts +++ b/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-session-to-circuit-json.ts @@ -72,6 +72,10 @@ export function convertDsnSessionToCircuitJson( const sessionElements: AnyCircuitElement[] = [] + // Track via positions globally to deduplicate across all nets. + // Maps "x,y" -> net name that first claimed the via. + const addedViaKeys = new Map() + // Process nets for vias and wires for (const net of dsnSession.routes.network_out.nets) { // Find corresponding source trace for this net @@ -120,6 +124,19 @@ export function convertDsnSessionToCircuitJson( const viaX = Number(viaPoint.x.toFixed(4)) const viaY = Number(viaPoint.y.toFixed(4)) + // Skip duplicate vias at the same coordinates + const viaKey = `${viaX},${viaY}` + const existingNet = addedViaKeys.get(viaKey) + if (existingNet) { + if (existingNet !== net.name) { + console.warn( + `[dsn-converter] Via at (${viaX}, ${viaY}) claimed by net "${existingNet}" also appears in net "${net.name}" — possible short circuit in routing`, + ) + } + return + } + addedViaKeys.set(viaKey, net.name) + // Find the wire points that connect to this via across all route segments const connectingWires = sessionElements .flatMap((segment) => diff --git a/tests/repros/repro15-duplicate-vias.test.ts b/tests/repros/repro15-duplicate-vias.test.ts new file mode 100644 index 0000000..6f9f4c6 --- /dev/null +++ b/tests/repros/repro15-duplicate-vias.test.ts @@ -0,0 +1,212 @@ +import { expect, test } from "bun:test" +import { convertDsnSessionToCircuitJson } from "lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-session-to-circuit-json" +import type { DsnPcb, DsnSession } from "lib/dsn-pcb/types" + +test("duplicate vias from freerouting session are deduplicated", () => { + // Minimal DsnPcb input with two layers and one net + const dsnPcb: DsnPcb = { + is_dsn_pcb: true, + filename: "test.dsn", + parser: { + string_quote: '"', + host_version: "test", + space_in_quoted_tokens: "on", + host_cad: "test", + }, + resolution: { unit: "um", value: 10000 }, + unit: "um", + structure: { + layers: [ + { name: "F.Cu", type: "signal", property: { index: 0 } }, + { name: "B.Cu", type: "signal", property: { index: 1 } }, + ], + boundary: { + path: { + layer: "pcb", + width: 0, + coordinates: [0, 0, 100000, 0, 100000, 100000, 0, 100000], + }, + }, + via: "Via[0-1]_600:300_um", + rule: { clearances: [{ value: 200 }], width: 250 }, + }, + placement: { components: [] }, + library: { images: [], padstacks: [] }, + network: { nets: [], classes: [] }, + wiring: { wires: [] }, + } + + // Session with duplicate vias at the same coordinates within one net + const dsnSession: DsnSession = { + is_dsn_session: true, + filename: "test.ses", + placement: { + resolution: { unit: "um", value: 10000 }, + components: [], + }, + routes: { + resolution: { unit: "um", value: 10000 }, + parser: { + string_quote: '"', + host_version: "test", + space_in_quoted_tokens: "on", + host_cad: "test", + }, + library_out: { + images: [], + padstacks: [ + { + name: "Via[0-1]_600:300_um", + shapes: [ + { shapeType: "circle", layer: "F.Cu", diameter: 600 }, + { shapeType: "circle", layer: "B.Cu", diameter: 600 }, + ], + attach: "off", + }, + ], + }, + network_out: { + nets: [ + { + name: "TestNet", + wires: [ + { + path: { + layer: "F.Cu", + width: 250, + coordinates: [10000, 20000, 30000, 20000], + }, + }, + { + path: { + layer: "B.Cu", + width: 250, + coordinates: [30000, 20000, 50000, 20000], + }, + }, + ], + // Freerouting sometimes outputs duplicate vias at the same location + vias: [ + { x: 30000, y: 20000 }, + { x: 30000, y: 20000 }, // duplicate + { x: 30000, y: 20000 }, // another duplicate + ], + }, + ], + }, + }, + } + + const result = convertDsnSessionToCircuitJson(dsnPcb, dsnSession) + + const pcbVias = result.filter((el) => el.type === "pcb_via") + + // Should only have 1 via even though 3 were in the input + expect(pcbVias.length).toBe(1) +}) + +test("duplicate vias across different nets are deduplicated", () => { + const dsnPcb: DsnPcb = { + is_dsn_pcb: true, + filename: "test.dsn", + parser: { + string_quote: '"', + host_version: "test", + space_in_quoted_tokens: "on", + host_cad: "test", + }, + resolution: { unit: "um", value: 10000 }, + unit: "um", + structure: { + layers: [ + { name: "F.Cu", type: "signal", property: { index: 0 } }, + { name: "B.Cu", type: "signal", property: { index: 1 } }, + ], + boundary: { + path: { + layer: "pcb", + width: 0, + coordinates: [0, 0, 100000, 0, 100000, 100000, 0, 100000], + }, + }, + via: "Via[0-1]_600:300_um", + rule: { clearances: [{ value: 200 }], width: 250 }, + }, + placement: { components: [] }, + library: { images: [], padstacks: [] }, + network: { nets: [], classes: [] }, + wiring: { wires: [] }, + } + + const dsnSession: DsnSession = { + is_dsn_session: true, + filename: "test.ses", + placement: { + resolution: { unit: "um", value: 10000 }, + components: [], + }, + routes: { + resolution: { unit: "um", value: 10000 }, + parser: { + string_quote: '"', + host_version: "test", + space_in_quoted_tokens: "on", + host_cad: "test", + }, + library_out: { + images: [], + padstacks: [ + { + name: "Via[0-1]_600:300_um", + shapes: [ + { shapeType: "circle", layer: "F.Cu", diameter: 600 }, + { shapeType: "circle", layer: "B.Cu", diameter: 600 }, + ], + attach: "off", + }, + ], + }, + network_out: { + nets: [ + { + name: "Net1", + wires: [ + { + path: { + layer: "F.Cu", + width: 250, + coordinates: [10000, 20000, 30000, 20000], + }, + }, + ], + vias: [ + { x: 30000, y: 20000 }, + { x: 50000, y: 40000 }, + ], + }, + { + name: "Net2", + wires: [ + { + path: { + layer: "F.Cu", + width: 250, + coordinates: [30000, 20000, 60000, 20000], + }, + }, + ], + // Same coordinates as Net1's first via + vias: [{ x: 30000, y: 20000 }], + }, + ], + }, + }, + } + + const result = convertDsnSessionToCircuitJson(dsnPcb, dsnSession) + + const pcbVias = result.filter((el) => el.type === "pcb_via") + + // Should have 2 unique vias (30000,20000) and (50000,40000), not 3 + expect(pcbVias.length).toBe(2) +})