diff --git a/lib/dsn-pcb/circuit-json-to-dsn-json/convert-circuit-json-to-dsn-session.ts b/lib/dsn-pcb/circuit-json-to-dsn-json/convert-circuit-json-to-dsn-session.ts index cd402ea..b232069 100644 --- a/lib/dsn-pcb/circuit-json-to-dsn-json/convert-circuit-json-to-dsn-session.ts +++ b/lib/dsn-pcb/circuit-json-to-dsn-json/convert-circuit-json-to-dsn-session.ts @@ -8,17 +8,11 @@ export function convertCircuitJsonToDsnSession( dsnPcb: DsnPcb, circuitJson: AnyCircuitElement[], ): DsnSession { - // First convert to DSN PCB to reuse component/pad processing - // const dsnPcb = convertCircuitJsonToDsnJson(circuitJson) - - // console.dir(dsnPcb, { depth: null }) - const pcb_traces = su(circuitJson as any).pcb_trace.list() const source_traces = su(circuitJson as any).source_trace.list() const source_ports = su(circuitJson as any).source_port.list() const nets = su(circuitJson as any).source_net.list() - // Only applies to the traces (components are not getting affected) const transformMmToSesUnit = scale(10000) const session: DsnSession = { is_dsn_session: true, @@ -31,54 +25,54 @@ export function convertCircuitJsonToDsnSession( resolution: dsnPcb.resolution, parser: dsnPcb.parser, library_out: { - // TODO Just add vias here padstacks: [], }, network_out: { - nets: dsnPcb.network.nets - .map((net) => { - const source_net = nets.find((n) => n.name === net.name) - if (!source_net) return null - const pcb_traces_for_net = pcb_traces.filter((pcb_trace) => { - const source_trace = source_traces.find( - (st) => st.source_trace_id === pcb_trace.source_trace_id, - ) + nets: pcb_traces.map((trace) => { + const source_trace = source_traces.find( + (st) => st.source_trace_id === trace.source_trace_id, + ) + const source_net = + source_trace && + nets.find((n) => + source_trace.connected_source_net_ids.includes(n.source_net_id), + ) + const net_name = source_net?.name || trace.source_trace_id + + // TODO only supports single layer traces + const traceLayer = + "layer" in trace.route[0] && trace.route[0].layer === "bottom" + ? "bottom" + : "top" - return source_trace?.connected_source_net_ids.includes( - source_net.source_net_id, - ) - }) + const traceWidth = + "width" in trace.route[0] ? trace.route[0].width : 0.16 - return { - name: net.name, - wires: pcb_traces_for_net.flatMap((trace): Wire => { - // TODO whenever the pcb trace changes layers or changes width, - // we have to create a new wire - return { - path: { - layer: "F.Cu", - width: 0.1, // TODO get width - coordinates: trace.route - .filter( - (rp): rp is PcbTraceRoutePointWire => - rp.route_type === "wire", - ) - .map((rp) => - // Circuit JSON space to the SES space - applyToPoint(transformMmToSesUnit, { - x: rp.x, - y: rp.y, - }), - ) - .flatMap((trp) => [trp.x, trp.y]), - }, - } - }), - } - }) - .filter((net): net is { name: string; wires: Wire[] } => - Boolean(net), - ), + return { + name: net_name!, + wires: [ + { + path: { + layer: traceLayer === "bottom" ? "B.Cu" : "F.Cu", + width: traceWidth * 1000, + coordinates: trace.route + .filter( + (rp): rp is PcbTraceRoutePointWire => + rp.route_type === "wire", + ) + .map((rp) => + // Circuit JSON space to the SES space + applyToPoint(transformMmToSesUnit, { + x: rp.x, + y: rp.y, + }), + ) + .flatMap((trp) => [trp.x, trp.y]), + }, + }, + ], + } + }), }, }, } diff --git a/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces.ts b/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces.ts deleted file mode 100644 index bc085ba..0000000 --- a/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { AnyCircuitElement } from "circuit-json" -import type { DsnPcb } from "../types" - -export function processPcbTraces( - circuitElements: AnyCircuitElement[], - pcb: DsnPcb, -) { - for (const element of circuitElements) { - if (element.type === "pcb_trace") { - const pcbTrace = element - - const netName = - pcbTrace.source_trace_id || `Net-${pcb.network.nets.length + 1}` - - const wire = { - path: { - layer: - pcbTrace.route[0].route_type === "wire" - ? pcbTrace.route[0].layer === "top" - ? "F.Cu" - : "B.Cu" - : "F.Cu", // Default to F.Cu if not a wire route - width: - pcbTrace.route[0].route_type === "wire" - ? pcbTrace.route[0].width * 1000 - : 200, // Convert mm to um, or use a default value - coordinates: [] as number[], - }, - net: netName, - type: "route", - } - - for (const point of pcbTrace.route) { - wire.path.coordinates.push(point.x * 1000) // Convert mm to um - wire.path.coordinates.push(-point.y * 1000) // Negate Y to match DSN coordinate system - } - - pcb.wiring.wires.push(wire) - } - } -} diff --git a/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces/findOrCreateViaPadstack.ts b/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces/findOrCreateViaPadstack.ts new file mode 100644 index 0000000..a966ace --- /dev/null +++ b/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces/findOrCreateViaPadstack.ts @@ -0,0 +1,41 @@ +import type { DsnPcb, Padstack } from "lib/dsn-pcb/types" + +export function findOrCreateViaPadstack( + pcb: DsnPcb, + outerDiameter: number, + holeDiameter: number, +): string { + const viaName = `Via[0-1]_${outerDiameter}:${holeDiameter}_um` + + // Check if padstack already exists + const existingPadstack = pcb.library.padstacks.find((p) => p.name === viaName) + + if (existingPadstack) { + return viaName + } + + // Create new padstack for via + const viaPadstack: Padstack = { + name: viaName, + attach: "off", + shapes: [ + { + shapeType: "circle", + layer: "F.Cu", + diameter: outerDiameter, + }, + { + shapeType: "circle", + layer: "B.Cu", + diameter: outerDiameter, + }, + ], + hole: { + shape: "circle", + diameter: holeDiameter, + }, + } + + pcb.library.padstacks.push(viaPadstack) + return viaName +} diff --git a/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces/index.ts b/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces/index.ts new file mode 100644 index 0000000..15462f4 --- /dev/null +++ b/lib/dsn-pcb/circuit-json-to-dsn-json/process-pcb-traces/index.ts @@ -0,0 +1,155 @@ +import type { + AnyCircuitElement, + LayerRef, + PcbTrace, + PcbTraceRoutePoint, +} from "circuit-json" +import type { DsnPcb, DsnSession } from "../../types" +import Debug from "debug" +import { findOrCreateViaPadstack } from "./findOrCreateViaPadstack" + +const debug = Debug("dsn-converter:process-pcb-traces") + +const DEFAULT_VIA_DIAMETER = 600 // μm +const DEFAULT_VIA_HOLE = 300 // μm + +interface Wire { + path: { + layer: string + width: number + coordinates: number[] + } + net: string + type: string +} + +function createWire(opts: { + layer: LayerRef + widthMm: number + netName: string +}): Wire { + return { + path: { + layer: opts.layer === "top" ? "F.Cu" : "B.Cu", + width: opts.widthMm * 1000, + coordinates: [], + }, + net: opts.netName, + type: "route", + } +} + +export function processPcbTraces( + circuitElements: AnyCircuitElement[], + pcb: DsnPcb, +) { + for (const element of circuitElements) { + if (element.type === "pcb_trace") { + const pcbTrace = element + debug("PCB TRACE\n----------\n", pcbTrace) + const netName = + pcbTrace.source_trace_id || `Net-${pcb.network.nets.length + 1}` + + let currentLayer = "" + let currentWire: Wire | null = null + + // Process each point in the route + for (let i = 0; i < pcbTrace.route.length; i++) { + const point = pcbTrace.route[i] as PcbTraceRoutePoint + debug("POINT\n------\n", point) + + if (point.route_type === "wire") { + // If layer changed or this is the first point, start a new wire + const hasLayerChanged = currentLayer && point.layer !== currentLayer + const isFirstPoint = !currentWire + + if (isFirstPoint) { + // Start new wire on new layer + currentWire = createWire({ + layer: point.layer, + widthMm: point.width, + netName, + }) + + pcb.wiring.wires.push(currentWire) + currentLayer = point.layer + } + + if (currentWire && !hasLayerChanged) { + // Add coordinates to current wire + currentWire.path.coordinates.push(point.x * 1000) + currentWire.path.coordinates.push(point.y * 1000) + continue + } + + if (hasLayerChanged) { + const prevPoint = pcbTrace.route[i - 1] + const viaPadstackName = findOrCreateViaPadstack( + pcb, + DEFAULT_VIA_DIAMETER, + DEFAULT_VIA_HOLE, + ) + + // Add via reference to structure if not already there + if (!pcb.structure.via) { + pcb.structure.via = viaPadstackName + } + + // Create wire segment for via placement + pcb.wiring.wires.push({ + path: { + layer: currentLayer === "top" ? "F.Cu" : "B.Cu", + width: DEFAULT_VIA_DIAMETER, + coordinates: [prevPoint.x * 1000, prevPoint.y * 1000], + }, + net: netName, + type: "via", + }) + } + continue + } + + if (point.route_type === "via") { + debug("VIA\n----\n", point) + + // End current wire + if (currentWire) { + currentWire.path.coordinates.push(point.x * 1000) + currentWire.path.coordinates.push(point.y * 1000) + currentWire = null + } + + // Handle explicit via points + const viaPadstackName = findOrCreateViaPadstack( + pcb, + DEFAULT_VIA_DIAMETER, + DEFAULT_VIA_HOLE, + ) + + debug("VIA PADSTACK NAME:", viaPadstackName) + + // Add via reference to structure if not already there + if (!pcb.structure.via) { + pcb.structure.via = viaPadstackName + } + + // Create wire segment for via placement + pcb.wiring.wires.push({ + path: { + layer: point.from_layer === "top" ? "F.Cu" : "B.Cu", + width: DEFAULT_VIA_DIAMETER, + coordinates: [point.x * 1000, point.y * 1000], + }, + net: netName, + type: "via", + }) + debug("WIRING", pcb.wiring) + + currentLayer = point.to_layer + currentWire = null // Start fresh wire after via + } + } + } + } + debug("PCB WIRING AT END", pcb.wiring) +} diff --git a/lib/dsn-pcb/circuit-json-to-dsn-json/stringify-dsn-json.ts b/lib/dsn-pcb/circuit-json-to-dsn-json/stringify-dsn-json.ts index a5b42c5..7d46a6f 100644 --- a/lib/dsn-pcb/circuit-json-to-dsn-json/stringify-dsn-json.ts +++ b/lib/dsn-pcb/circuit-json-to-dsn-json/stringify-dsn-json.ts @@ -135,7 +135,11 @@ export const stringifyDsnJson = (dsnJson: DsnPcb): string => { // Wiring section result += `${indent}(wiring\n` ;(dsnJson.wiring?.wires ?? []).forEach((wire) => { - result += `${indent}${indent}(wire ${stringifyPath(wire.path, 3)}(net ${stringifyValue(wire.net)})(type ${wire.type}))\n` + if (wire.type === "via") { + result += `${indent}${indent}(via ${stringifyPath(wire.path, 3)}(net ${stringifyValue(wire.net)}))\n` + } else { + result += `${indent}${indent}(wire ${stringifyPath(wire.path, 3)}(net ${stringifyValue(wire.net)})(type ${wire.type}))\n` + } }) result += `${indent})\n` diff --git a/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-json-to-circuit-json.ts b/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-json-to-circuit-json.ts index d6b1ae8..ce7c8f2 100644 --- a/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-json-to-circuit-json.ts +++ b/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-json-to-circuit-json.ts @@ -3,7 +3,7 @@ import { fromTriangles, scale, applyToPoint } from "transformation-matrix" import type { AnyCircuitElement, PcbBoard } from "circuit-json" import type { DsnPcb, DsnSession } from "../types" import { convertPadstacksToSmtPads } from "./dsn-component-converters/convert-padstacks-to-smtpads" -import { convertWiresToPcbTraces } from "./dsn-component-converters/convert-wire-to-trace" +import { convertWiresToPcbTraces } from "./dsn-component-converters/convert-wires-to-traces" import { pairs } from "lib/utils/pairs" import { convertDsnPcbToCircuitJson } from "./convert-dsn-pcb-to-circuit-json" import { convertDsnSessionToCircuitJson } from "./convert-dsn-session-to-circuit-json" diff --git a/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-pcb-to-circuit-json.ts b/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-pcb-to-circuit-json.ts index 7e9594e..c47bd30 100644 --- a/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-pcb-to-circuit-json.ts +++ b/lib/dsn-pcb/dsn-json-to-circuit-json/convert-dsn-pcb-to-circuit-json.ts @@ -3,7 +3,7 @@ import { fromTriangles, scale, applyToPoint } from "transformation-matrix" import type { AnyCircuitElement, PcbBoard } from "circuit-json" import type { DsnPcb } from "../types" import { convertPadstacksToSmtPads } from "./dsn-component-converters/convert-padstacks-to-smtpads" -import { convertWiresToPcbTraces } from "./dsn-component-converters/convert-wire-to-trace" +import { convertWiresToPcbTraces } from "./dsn-component-converters/convert-wires-to-traces" import { pairs } from "lib/utils/pairs" import { convertNetsToSourceNetsAndTraces } from "./dsn-component-converters/convert-nets-to-source-nets-and-traces" import { convertDsnPcbComponentsToSourceComponentsAndPorts } from "./dsn-component-converters/convert-dsn-pcb-components-to-source-components-and-ports" 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 e593e94..53ef6f6 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 @@ -105,12 +105,21 @@ export function convertDsnSessionToCircuitJson( const fromLayer = connectingWires[0]?.layer || "top" const toLayer = connectingWires[1]?.layer || "bottom" - routeSegments[0].route.push({ - x: viaX, - y: viaY, - route_type: "via", - from_layer: fromLayer, - to_layer: toLayer, + // Add via point to each trace that connects to it + sessionElements.forEach((element) => { + if (element.type === "pcb_trace") { + const trace = element as PcbTrace + const lastPoint = trace.route[trace.route.length - 1] + if (lastPoint && lastPoint.x === viaX && lastPoint.y === viaY) { + trace.route.push({ + x: viaX, + y: viaY, + route_type: "via", + from_layer: fromLayer, + to_layer: toLayer, + }) + } + } }) sessionElements.push({ @@ -122,7 +131,6 @@ export function convertDsnSessionToCircuitJson( toLayer, }), }) - sessionElements.push(...routeSegments) }) } } diff --git a/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wire-to-trace.ts b/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wires-to-traces.ts similarity index 57% rename from lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wire-to-trace.ts rename to lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wires-to-traces.ts index 83b7923..c48d5fc 100644 --- a/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wire-to-trace.ts +++ b/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wires-to-traces.ts @@ -7,38 +7,56 @@ import { type Matrix, applyToPoint } from "transformation-matrix" import type { Network, Wiring } from "../../types" import { convertPolylinePathToPcbTraces } from "./convert-polyline-path-to-pcb-traces" import { convertWiringPathToPcbTraces } from "./convert-wiring-path-to-pcb-traces" +import Debug from "debug" +import { convertWiringViaToPcbVias } from "./convert-wiring-via-to-pcb-vias" + +const debug = Debug("dsn-converter:convertWiresToPcbTraces") export function convertWiresToPcbTraces( wiring: Wiring, network: Network, transformUmToMm: Matrix, ): AnyCircuitElement[] { - const traces: AnyCircuitElement[] = [] + const tracesAndVias: AnyCircuitElement[] = [] const processedNets = new Set() wiring.wires?.forEach((wire) => { + debug("WIRE\n----\n", wire) const netName = wire.net if (!netName) return - if (processedNets.has(netName) || wire.type === "shove_fixed") { + if (wire.type === "shove_fixed") { return } + if (processedNets.has(netName)) { + debug( + `Already processed wire for net "${netName}" but got another (hopefully not a duplicate wire!)`, + ) + } processedNets.add(netName) + if (wire.type === "via") { + debug("wire is actually a via!") + tracesAndVias.push( + ...convertWiringViaToPcbVias({ wire, transformUmToMm, netName }), + ) + return + } + if ("polyline_path" in wire) { - traces.push( + tracesAndVias.push( ...convertPolylinePathToPcbTraces({ wire, transformUmToMm, netName }), ) return } if ("path" in wire) { - traces.push( + tracesAndVias.push( ...convertWiringPathToPcbTraces({ wire, transformUmToMm, netName }), ) return } }) - return traces + return tracesAndVias } diff --git a/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wiring-path-to-pcb-traces.ts b/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wiring-path-to-pcb-traces.ts index 79f51b1..9817e11 100644 --- a/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wiring-path-to-pcb-traces.ts +++ b/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wiring-path-to-pcb-traces.ts @@ -5,6 +5,9 @@ import type { } from "circuit-json" import { type Matrix, applyToPoint } from "transformation-matrix" import type { Wiring } from "../../types" +import Debug from "debug" + +const debug = Debug("dsn-converter:convertWiringPathToPcbTraces") export const convertWiringPathToPcbTraces = ({ wire, diff --git a/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wiring-via-to-pcb-vias.ts b/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wiring-via-to-pcb-vias.ts new file mode 100644 index 0000000..922d942 --- /dev/null +++ b/lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-wiring-via-to-pcb-vias.ts @@ -0,0 +1,38 @@ +import type { PcbVia } from "circuit-json" +import type { Wiring } from "lib/dsn-pcb/types" +import { type Matrix, applyToPoint } from "transformation-matrix" +import Debug from "debug" + +const debug = Debug("dsn-converter:convertWiringViaToPcbVias") + +export const convertWiringViaToPcbVias = ({ + wire, + transformUmToMm, + netName, +}: { + wire: Wiring["wires"][number] + transformUmToMm: Matrix + netName: string +}): PcbVia[] => { + if (!wire.path?.coordinates || wire.path.coordinates.length < 2) { + debug("Couldn't create via") + return [] + } + + const [x, y] = wire.path.coordinates + const circuitPoint = applyToPoint(transformUmToMm, { x, y }) + + const via: PcbVia = { + type: "pcb_via", + layers: ["top", "bottom"], + pcb_via_id: `pcb_via_${netName}`, + x: Number(circuitPoint.x.toFixed(4)), + y: Number(circuitPoint.y.toFixed(4)), + // TODO look up via size + outer_diameter: 0.6, // Standard via diameter in mm + hole_diameter: 0.3, // Standard drill diameter in mm + } + debug("Created via", via) + + return [via] +} diff --git a/lib/dsn-pcb/dsn-json-to-circuit-json/merge-dsn-session-into-dsn-pcb.ts b/lib/dsn-pcb/dsn-json-to-circuit-json/merge-dsn-session-into-dsn-pcb.ts new file mode 100644 index 0000000..19a3c3c --- /dev/null +++ b/lib/dsn-pcb/dsn-json-to-circuit-json/merge-dsn-session-into-dsn-pcb.ts @@ -0,0 +1,58 @@ +import type { DsnPcb, DsnSession } from "../types" +import Debug from "debug" + +const debug = Debug("dsn-converter:mergeDsnSessionIntoDsnPcb") + +export function mergeDsnSessionIntoDsnPcb( + dsnPcb: DsnPcb, + dsnSession: DsnSession, +): DsnPcb { + // Create a deep copy of the PCB to avoid mutating the original + const mergedPcb: DsnPcb = JSON.parse(JSON.stringify(dsnPcb)) + + // Update placement if session has different component positions + if (dsnSession.placement?.components) { + mergedPcb.placement.components = dsnSession.placement.components + } + + // Add wires from session's network_out to PCB's wiring + if (dsnSession.routes?.network_out?.nets) { + // Clear existing wires since session represents final state + mergedPcb.wiring.wires = [] + + dsnSession.routes.network_out.nets.forEach((sessionNet) => { + if (sessionNet.wires) { + sessionNet.wires.forEach((wire) => { + debug("WIRE\n----\n", wire) + if (wire.path) { + mergedPcb.wiring.wires.push({ + path: { + ...wire.path, + // DsnSession represents the coordinates in ses units, which are + // 10x larger than the um units used in the DsnPcb files + coordinates: wire.path.coordinates.map((c) => c / 10), + }, + net: sessionNet.name, + type: "route", + }) + } + }) + } + }) + } + + // Add any padstacks from session's library_out to PCB's library + if (dsnSession.routes?.library_out?.padstacks) { + const existingPadstackNames = new Set( + mergedPcb.library.padstacks.map((p) => p.name), + ) + + dsnSession.routes.library_out.padstacks.forEach((padstack) => { + if (!existingPadstackNames.has(padstack.name)) { + mergedPcb.library.padstacks.push(padstack) + } + }) + } + + return mergedPcb +} diff --git a/lib/dsn-pcb/dsn-json-to-circuit-json/parse-dsn-to-dsn-json.ts b/lib/dsn-pcb/dsn-json-to-circuit-json/parse-dsn-to-dsn-json.ts index 2d5007c..b73ae5b 100644 --- a/lib/dsn-pcb/dsn-json-to-circuit-json/parse-dsn-to-dsn-json.ts +++ b/lib/dsn-pcb/dsn-json-to-circuit-json/parse-dsn-to-dsn-json.ts @@ -36,6 +36,9 @@ import type { Wire, Wiring, } from "../types" +import Debug from "debug" + +const debug = Debug("dsn-converter:parse-dsn-to-dsn-json") // **Process AST into TypeScript Interfaces** export function parseDsnToDsnJson(dsnString: string): DsnJson { @@ -921,11 +924,60 @@ export function processWiring(nodes: ASTNode[]): Wiring { ) { wiring.wires!.push(processWire(node.children!)) } + if ( + node.type === "List" && + node.children![0].type === "Atom" && + node.children![0].value === "via" + ) { + wiring.wires!.push(processVia(node.children!)) + } }) return wiring as Wiring } +function processVia(nodes: ASTNode[]): Wire { + const wire: Partial = {} + + // Find the path node which contains coordinates + const pathNode = nodes.find( + (node) => + node.type === "List" && + node.children?.[0]?.type === "Atom" && + node.children[0].value === "path", + ) + + if (pathNode?.children) { + const coords = pathNode.children + .filter((node) => node.type === "Atom" && typeof node.value === "number") + .slice(-2) // Take last two numbers as x,y coordinates + + if (coords.length === 2) { + wire.path = { + layer: "all", // vias connect all layers + width: 0, // width is defined by the padstack + coordinates: coords.map((node) => node.value as number), + } + wire.type = "via" + + // Find net name if present + const netNode = nodes.find( + (node) => + node.type === "List" && + node.children?.[0]?.type === "Atom" && + node.children[0].value === "net", + ) + if (netNode?.children?.[1]?.type === "Atom") { + wire.net = String(netNode.children[1].value) + } + + return wire as Wire + } + } + + throw new Error("Invalid via format") +} + function processWire(nodes: ASTNode[]): Wire { const wire: Partial = {} diff --git a/lib/index.ts b/lib/index.ts index eb73376..afa6613 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,5 @@ export * from "./dsn-pcb/circuit-json-to-dsn-json/convert-circuit-json-to-dsn-json.ts" +export * from "./dsn-pcb/dsn-json-to-circuit-json/merge-dsn-session-into-dsn-pcb.ts" export * from "./dsn-pcb/circuit-json-to-dsn-json/convert-circuit-json-to-dsn-string.ts" export * from "./dsn-pcb/circuit-json-to-dsn-json/convert-circuit-json-to-dsn-session.ts" export * from "./dsn-pcb/dsn-json-to-circuit-json/convert-dsn-pcb-to-circuit-json.ts" diff --git a/tests/dsn-pcb/__snapshots__/circuit-json-to-dsn-file.snap.svg b/tests/dsn-pcb/__snapshots__/circuit-json-to-dsn-file.snap.svg index 3404ce1..4ead08e 100644 --- a/tests/dsn-pcb/__snapshots__/circuit-json-to-dsn-file.snap.svg +++ b/tests/dsn-pcb/__snapshots__/circuit-json-to-dsn-file.snap.svg @@ -10,4 +10,4 @@ .pcb-silkscreen-top { stroke: #f2eda1; } .pcb-silkscreen-bottom { stroke: #f2eda1; } .pcb-silkscreen-text { fill: #f2eda1; } - \ No newline at end of file + \ No newline at end of file diff --git a/tests/dsn-pcb/convert-circuit-json-to-dsn-session.unit.test.ts b/tests/dsn-pcb/convert-circuit-json-to-dsn-session.unit.test.ts index 20b3f58..7190395 100644 --- a/tests/dsn-pcb/convert-circuit-json-to-dsn-session.unit.test.ts +++ b/tests/dsn-pcb/convert-circuit-json-to-dsn-session.unit.test.ts @@ -238,7 +238,7 @@ test("converts nets and wires", () => { expect(session.routes.network_out.nets[0].wires).toHaveLength(1) expect(session.routes.network_out.nets[0].wires[0].path).toEqual({ layer: "F.Cu", - width: 0.1, + width: 200, coordinates: [10000, 20000, 30000, 40000], }) }) diff --git a/tests/dsn-pcb/merge-dsn-session-with-conversion.test.tsx b/tests/dsn-pcb/merge-dsn-session-with-conversion.test.tsx new file mode 100644 index 0000000..547859b --- /dev/null +++ b/tests/dsn-pcb/merge-dsn-session-with-conversion.test.tsx @@ -0,0 +1,120 @@ +import { test, expect } from "bun:test" +import { Circuit } from "@tscircuit/core" +import { + convertCircuitJsonToDsnString, + convertDsnPcbToCircuitJson, + parseDsnToDsnJson, + mergeDsnSessionIntoDsnPcb, + convertDsnSessionToCircuitJson, + type DsnPcb, + type DsnSession, + convertCircuitJsonToDsnSession, + stringifyDsnJson, +} from "lib" +import looksSame from "looks-same" +import { getTestDebugUtils } from "tests/fixtures/get-test-debug-utils" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { su } from "@tscircuit/soup-util" + +test("merge-dsn-session-with-conversion", async () => { + const { writeDebugFile, getDebugFilePath, debug } = getTestDebugUtils( + import.meta.path, + ) + + // Create initial circuit with two resistors + const circuit = new Circuit() + circuit.add( + + + + + , + ) + + const baseCircuitJson = await circuit.getCircuitJson() + writeDebugFile("base.circuit.json", JSON.stringify(baseCircuitJson, null, 2)) + const dsnFile = convertCircuitJsonToDsnString(baseCircuitJson) + debug("DSN FILE\n--------\n", dsnFile) + writeDebugFile("original.dsn", dsnFile) + const originalDsnPcb = parseDsnToDsnJson(dsnFile) as DsnPcb + writeDebugFile("original.dsn.json", JSON.stringify(originalDsnPcb, null, 2)) + debug("ORIGINAL DSN PCB\n----------------\n", originalDsnPcb) + + // Create a PCB without traces by removing wiring section + const dsnPcbWithoutTraces: DsnPcb = { + ...originalDsnPcb, + wiring: { wires: [] }, + } + + // Create a session from the original PCB's wiring + const session: DsnSession = convertCircuitJsonToDsnSession( + dsnPcbWithoutTraces, + baseCircuitJson, + ) + + debug("SESSION\n-------\n", session) + + // Merge session back into PCB without traces + const mergedPcb = mergeDsnSessionIntoDsnPcb(dsnPcbWithoutTraces, session) + writeDebugFile("merged.dsn", stringifyDsnJson(mergedPcb)) + + debug("MERGED PCB\n------------\n", mergedPcb) + + // Convert both to circuit JSON for comparison + const circuitJsonFromOriginal = convertDsnPcbToCircuitJson(originalDsnPcb) + const circuitJsonFromMerged = convertDsnPcbToCircuitJson(mergedPcb) + + // Generate SVGs for visual comparison + const baseCircuitSvg = convertCircuitJsonToPcbSvg(baseCircuitJson) + const svgOriginal = convertCircuitJsonToPcbSvg(circuitJsonFromOriginal) + const svgMerged = convertCircuitJsonToPcbSvg(circuitJsonFromMerged) + + writeDebugFile("circuit.base.svg", baseCircuitSvg) + writeDebugFile("circuit.original.svg", svgOriginal) + writeDebugFile("circuit.merged.svg", svgMerged) + writeDebugFile( + "circuit.original.json", + JSON.stringify(circuitJsonFromOriginal, null, 2), + ) + writeDebugFile( + "circuit.merged.json", + JSON.stringify(circuitJsonFromMerged, null, 2), + ) + + // Verify wiring was restored + // TODO must fix dsn session conversion before this works + // expect(mergedPcb.wiring.wires).toHaveLength( + // originalDsnPcb.wiring.wires.length, + // ) + + // Compare the resulting circuit JSONs + const originalTraces = su(circuitJsonFromOriginal).pcb_trace.list() + const mergedTraces = su(circuitJsonFromMerged).pcb_trace.list() + // expect(mergedTraces).toHaveLength(originalTraces.length) + + // Compare trace coordinates + for (let i = 0; i < originalTraces.length; i++) { + const originalTrace = originalTraces[i] + const mergedTrace = mergedTraces[i] + + debug("ORIGINAL TRACE\n--------------\n", originalTrace) + debug("MERGED TRACE\n--------------\n", mergedTrace) + } + + // Compare SVGs + const looksSameResult = await looksSame( + getDebugFilePath("circuit.original.svg"), + getDebugFilePath("circuit.merged.svg"), + ) + // TODO requires fix inside convertCircuitJsonToDsnSession, currently the vias + // aren't converted properly- reference or adapt the code in processPcbTraces + // expect(looksSameResult.equal).toBe(true) // Should be identical after merge +}) diff --git a/tests/fixtures/get-test-debug-utils.ts b/tests/fixtures/get-test-debug-utils.ts index 1eaf54a..15a556e 100644 --- a/tests/fixtures/get-test-debug-utils.ts +++ b/tests/fixtures/get-test-debug-utils.ts @@ -1,13 +1,17 @@ import { mkdirSync } from "node:fs" +import Debug from "debug" /** * Usage: * - * const { writeDebugFile, getDebugFilePath } = getTestDebugUtils(import.meta.path) + * const { debug, writeDebugFile, getDebugFilePath } = getTestDebugUtils(import.meta.path) * * writeDebugFile("circuit.before.json", JSON.stringify(circuitJsonBefore)) * writeDebugFile("circuit.after.json", JSON.stringify(circuitJsonAfter)) * + * export DEBUG=dsn-converter:my-test-name + * debug("my output!") + * * const looksSameResult = await looksSame( * getDebugFilePath("circuit.before.svg"), * getDebugFilePath("circuit.after.svg"), @@ -17,6 +21,7 @@ export const getTestDebugUtils = (testPath: string) => { const testFileDir = testPath.split("/").pop()?.split(".")[0] mkdirSync(`./debug-files/${testFileDir}`, { recursive: true }) return { + debug: Debug(`dsn-converter:${testFileDir}`), writeDebugFile: (name: string, content: string) => { Bun.write(`./debug-files/${testFileDir}/${name}`, content) },