UNPKG

kicad-component-converter

Version:

Convert kicad_mod or kicad_sym file into Circuit JSON or tscircuit

1,500 lines (1,487 loc) 63.9 kB
#!/usr/bin/env node // src/cli/cli.ts import { perfectCli } from "perfect-cli"; import { program } from "commander"; // src/parse-kicad-mod-to-kicad-json.ts import parseSExpression from "s-expression"; // src/kicad-zod.ts import { z } from "zod"; var point2 = z.tuple([z.coerce.number(), z.coerce.number()]); var point3 = z.tuple([z.number(), z.number(), z.number()]); var point = z.union([point2, point3]); var fp_poly_arc_segment_def = z.object({ kind: z.literal("arc"), start: point2, mid: point2, end: point2 }); var fp_poly_point_def = z.union([point2, fp_poly_arc_segment_def]); var attributes_def = z.object({ at: point, size: point2, layer: z.string(), layers: z.array(z.string()), roundrect_rratio: z.number(), uuid: z.string() }).partial(); var property_def = z.object({ key: z.string(), val: z.string(), attributes: attributes_def }); var drill_def = z.object({ oval: z.boolean().default(false), width: z.number().optional(), height: z.number().optional(), offset: point2.optional() }); var hole_def = z.object({ name: z.string(), pad_type: z.enum(["thru_hole", "smd", "np_thru_hole", "connect"]), pad_shape: z.enum([ "roundrect", "circle", "rect", "oval", "trapezoid", "custom" ]), at: point, drill: z.union([z.number(), z.array(z.any()), drill_def]).transform((a) => { if (typeof a === "number") { return { oval: false, width: a, height: a }; } if ("oval" in a) return a; if (a.length === 2) { return { oval: false, width: Number.parseFloat(a[0]), height: Number.parseFloat(a[0]), offset: point2.parse(a[1].slice(1)) }; } if (a.length === 3 || a.length === 4) { return { oval: a[0] === "oval", width: Number.parseFloat(a[1]), height: Number.parseFloat(a[2]), offset: a[3] ? point2.parse(a[3].slice(1)) : void 0 }; } return a; }).pipe(drill_def), size: z.union([ z.array(z.number()).length(2).transform(([w, h]) => ({ width: w, height: h })), z.object({ width: z.number(), height: z.number() }) ]), layers: z.array(z.string()).optional(), roundrect_rratio: z.number().optional(), uuid: z.string().optional() }); var pad_def = z.object({ name: z.string(), pad_type: z.enum(["thru_hole", "smd", "np_thru_hole", "connect"]), pad_shape: z.enum([ "roundrect", "circle", "rect", "oval", "trapezoid", "custom" ]), at: point, size: point2, drill: z.union([z.number(), z.array(z.any()), drill_def]).transform((a) => { if (typeof a === "number") { return { oval: false, width: a, height: a }; } if ("oval" in a) return a; if (a.length === 2) { return { oval: false, width: Number.parseFloat(a[0]), height: Number.parseFloat(a[0]), offset: point2.parse(a[1].slice(1)) }; } if (a.length === 3 || a.length === 4) { return { oval: a[0] === "oval", width: Number.parseFloat(a[1]), height: Number.parseFloat(a[2]), offset: a[3] ? point2.parse(a[3].slice(1)) : void 0 }; } return a; }).pipe(drill_def).optional(), layers: z.array(z.string()).optional(), roundrect_rratio: z.number().optional(), chamfer_ratio: z.number().optional(), solder_paste_margin: z.number().optional(), solder_paste_margin_ratio: z.number().optional(), clearance: z.number().optional(), zone_connection: z.union([ z.literal(0).describe("Pad is not connect to zone"), z.literal(1).describe("Pad is connected to zone using thermal relief"), z.literal(2).describe("Pad is connected to zone using solid fill") ]).optional(), thermal_width: z.number().optional(), thermal_gap: z.number().optional(), uuid: z.string().optional() }); var effects_def = z.object({ font: z.object({ size: point2, thickness: z.number().optional() }) }).partial(); var fp_text_def = z.object({ fp_text_type: z.literal("user"), text: z.string(), at: point, layer: z.string(), uuid: z.string().optional(), effects: effects_def.partial() }); var fp_arc_def = z.object({ start: point2, mid: point2, end: point2, stroke: z.object({ width: z.number(), type: z.string() }), layer: z.string(), uuid: z.string().optional() }); var fp_circle_def = z.object({ center: point2, end: point2, stroke: z.object({ width: z.number(), type: z.string() }), fill: z.string().optional(), layer: z.string(), uuid: z.string().optional() }); var fp_poly_def = z.object({ pts: z.array(fp_poly_point_def), stroke: z.object({ width: z.number(), type: z.string() }).optional(), width: z.number().optional(), layer: z.string(), uuid: z.string().optional(), fill: z.string().optional() }).transform((data) => { return { ...data, width: void 0, stroke: data.stroke ?? { width: data.width } }; }); var fp_rect_def = z.object({ start: point2, end: point2, stroke: z.object({ width: z.number(), type: z.string() }).optional(), width: z.number().optional(), fill: z.string().optional(), layer: z.string(), uuid: z.string().optional() }).transform((data) => { return { ...data, width: void 0, stroke: data.stroke ?? { width: data.width } }; }); var fp_line = z.object({ start: point2, end: point2, stroke: z.object({ width: z.number(), type: z.string() }).optional(), width: z.number().optional(), layer: z.string(), uuid: z.string().optional() }).transform((data) => { return { ...data, width: void 0, stroke: data.stroke ?? { width: data.width } }; }); var kicad_mod_json_def = z.object({ footprint_name: z.string(), version: z.string().optional(), generator: z.string().optional(), generator_version: z.string().optional(), layer: z.string(), descr: z.string().default(""), tags: z.array(z.string()).optional(), properties: z.array(property_def), fp_lines: z.array(fp_line), fp_rects: z.array(fp_rect_def).optional(), fp_texts: z.array(fp_text_def), fp_arcs: z.array(fp_arc_def), fp_circles: z.array(fp_circle_def).optional(), fp_polys: z.array(fp_poly_def).optional(), pads: z.array(pad_def), holes: z.array(hole_def).optional() }); // src/get-attr.ts var formatAttr = (val, attrKey) => { if (attrKey === "effects" && Array.isArray(val)) { const effectsObj = {}; for (const elm of val) { if (elm[0] === "font") { const fontObj = {}; for (const fontElm of elm.slice(1)) { if (fontElm.length === 2) { fontObj[fontElm[0].valueOf()] = Number.parseFloat( fontElm[1].valueOf() ); } else { fontObj[fontElm[0].valueOf()] = fontElm.slice(1).map((n) => Number.parseFloat(n.valueOf())); } } effectsObj.font = fontObj; } } return effects_def.parse(effectsObj); } if (attrKey === "pts") { return val.map((segment) => { const segmentType = segment[0]?.valueOf?.() ?? segment[0]; if (segmentType === "xy") { return segment.slice(1).map((n) => Number.parseFloat(n.valueOf())); } if (segmentType === "arc") { const arcObj = { kind: "arc" }; for (const arcAttr of segment.slice(1)) { const key = arcAttr[0].valueOf(); arcObj[key] = arcAttr.slice(1).map((n) => Number.parseFloat(n.valueOf())); } return arcObj; } return segment; }); } if (attrKey === "stroke") { const strokeObj = {}; for (const strokeElm of val) { const strokePropKey = strokeElm[0].valueOf(); strokeObj[strokePropKey] = formatAttr(strokeElm.slice(1), strokePropKey); } return strokeObj; } if (attrKey === "at" || attrKey === "size" || attrKey === "start" || attrKey === "mid" || attrKey === "end") { const nums = (Array.isArray(val) ? val : [val]).map((n) => n?.valueOf?.() ?? n).filter( (v) => typeof v === "number" || typeof v === "string" && /^[-+]?\d*\.?\d+(e[-+]?\d+)?$/i.test(v) ).map((v) => typeof v === "number" ? v : Number.parseFloat(v)); return nums; } if (attrKey === "tags") { return val.map((n) => n.valueOf()); } if (attrKey === "generator_version" || attrKey === "version") { return val[0].valueOf(); } if (val.length === 2) { return val.valueOf(); } if (attrKey === "uuid") { if (Array.isArray(val)) { return val[0].valueOf(); } return val.valueOf(); } if (/^[\d\.]+$/.test(val) && !Number.isNaN(Number.parseFloat(val))) { return Number.parseFloat(val); } if (Array.isArray(val) && val.length === 1) { return val[0].valueOf(); } if (Array.isArray(val)) { return val.map((s) => s.valueOf()); } return val; }; var getAttr = (s, key) => { for (const elm of s) { if (Array.isArray(elm) && elm[0] === key) { return formatAttr(elm.slice(1), key); } } }; // src/parse-kicad-mod-to-kicad-json.ts import Debug from "debug"; var debug = Debug("kicad-mod-converter"); var parseKicadModToKicadJson = (fileContent) => { const kicadSExpr = parseSExpression(fileContent); const footprintName = kicadSExpr[1].valueOf(); const topLevelAttributes = {}; const simpleTopLevelAttributes = Object.entries(kicad_mod_json_def.shape).filter( ([attributeKey, def]) => def._def.typeName === "ZodString" || attributeKey === "tags" ).map(([attributeKey]) => attributeKey); for (const kicadSExprRow of kicadSExpr.slice(2)) { if (!simpleTopLevelAttributes.includes(kicadSExprRow[0])) continue; const key = kicadSExprRow[0].valueOf(); const val = formatAttr(kicadSExprRow.slice(1), key); topLevelAttributes[key] = val; } const properties = kicadSExpr.slice(2).filter((row) => row[0] === "property").map((row) => { const key = row[1].valueOf(); const val = row[2].valueOf(); const attributes = attributes_def.parse( row.slice(3).reduce((acc, attrAr) => { const attrKey = attrAr[0].valueOf(); acc[attrKey] = formatAttr(attrAr.slice(1), attrKey); return acc; }, {}) ); return { key, val, attributes }; }); const padRows = kicadSExpr.slice(2).filter((row) => row[0] === "pad"); const pads = []; for (const row of padRows) { const at = getAttr(row, "at"); const size = getAttr(row, "size"); const drill = getAttr(row, "drill"); let layers = getAttr(row, "layers"); if (Array.isArray(layers)) { layers = layers.map((layer) => layer.valueOf()); } else if (typeof layers === "string") { layers = [layers]; } else if (!layers) { layers = []; } const padType = row[2].valueOf(); const isCu = layers.some( (l) => l.endsWith(".Cu") || l === "*.Cu" || l.includes(".Cu") ); if (padType === "thru_hole") { continue; } if (!isCu && padType !== "np_thru_hole") { debug(`Skipping pad without copper layer: layers=${layers.join(", ")}`); continue; } const roundrect_rratio = getAttr(row, "roundrect_rratio"); const uuid = getAttr(row, "uuid"); const padRaw = { name: row[1].valueOf(), pad_type: row[2].valueOf(), pad_shape: row[3].valueOf(), at, drill, size, layers, roundrect_rratio, uuid }; debug(`attempting to parse pad: ${JSON.stringify(padRaw, null, " ")}`); pads.push(pad_def.parse(padRaw)); } const fp_texts_rows = kicadSExpr.slice(2).filter((row) => row[0] === "fp_text"); const fp_texts = []; for (const fp_text_row of fp_texts_rows) { const text = fp_text_row[2].valueOf(); const at = getAttr(fp_text_row, "at"); const layer = getAttr(fp_text_row, "layer"); const uuid = getAttr(fp_text_row, "uuid"); const effects = getAttr(fp_text_row, "effects"); fp_texts.push({ fp_text_type: "user", text, at, layer, uuid, effects }); } const fp_lines = []; const fp_lines_rows = kicadSExpr.slice(2).filter((row) => row[0] === "fp_line"); for (const fp_line_row of fp_lines_rows) { const start = getAttr(fp_line_row, "start"); const end = getAttr(fp_line_row, "end"); const stroke = getAttr(fp_line_row, "stroke"); const layer = getAttr(fp_line_row, "layer"); const uuid = getAttr(fp_line_row, "uuid"); fp_lines.push({ start, end, stroke, layer, uuid }); } const fp_rects = []; const fp_rect_rows = kicadSExpr.slice(2).filter((row) => row[0] === "fp_rect"); for (const fp_rect_row of fp_rect_rows) { const start = getAttr(fp_rect_row, "start"); const end = getAttr(fp_rect_row, "end"); const stroke = getAttr(fp_rect_row, "stroke"); const layer = getAttr(fp_rect_row, "layer"); const fill = getAttr(fp_rect_row, "fill"); const uuid = getAttr(fp_rect_row, "uuid"); if (!start || !end || !layer) continue; fp_rects.push({ start, end, stroke, fill, layer, uuid }); } const fp_arcs = []; const fp_arcs_rows = kicadSExpr.slice(2).filter((row) => row[0] === "fp_arc"); for (const fp_arc_row of fp_arcs_rows) { const start = getAttr(fp_arc_row, "start"); const mid = getAttr(fp_arc_row, "mid"); const end = getAttr(fp_arc_row, "end"); const stroke = getAttr(fp_arc_row, "stroke"); const layer = getAttr(fp_arc_row, "layer"); const uuid = getAttr(fp_arc_row, "uuid"); if (!start || !end || !mid || !stroke || !layer) { continue; } fp_arcs.push({ start, mid, end, stroke, layer, uuid }); } const fp_circles = []; const fp_circles_rows = kicadSExpr.slice(2).filter((row) => row[0] === "fp_circle"); for (const fp_circle_row of fp_circles_rows) { const center = getAttr(fp_circle_row, "center"); const end = getAttr(fp_circle_row, "end"); const stroke = getAttr(fp_circle_row, "stroke"); const fill = getAttr(fp_circle_row, "fill"); const layer = getAttr(fp_circle_row, "layer"); const uuid = getAttr(fp_circle_row, "uuid"); if (!center || !end || !stroke || !layer) { continue; } fp_circles.push({ center, end, stroke, fill, layer, uuid }); } const fp_polys = []; const fp_polys_rows = kicadSExpr.slice(2).filter((row) => row[0] === "fp_poly"); for (const fp_poly_row of fp_polys_rows) { const pts = getAttr(fp_poly_row, "pts"); const stroke = getAttr(fp_poly_row, "stroke"); const width = getAttr(fp_poly_row, "width"); const layer = getAttr(fp_poly_row, "layer"); const uuid = getAttr(fp_poly_row, "uuid"); const fill = getAttr(fp_poly_row, "fill"); let normalizedStroke = stroke; if (!normalizedStroke && typeof width === "number") { normalizedStroke = { width, type: "solid" }; } else if (normalizedStroke && typeof normalizedStroke === "object" && typeof width === "number" && normalizedStroke.width === void 0) { normalizedStroke = { ...normalizedStroke, width }; } fp_polys.push({ pts, stroke: normalizedStroke, layer, uuid, fill }); } const holes = []; for (const row of kicadSExpr.slice(2)) { if (row[0] !== "pad") continue; if (row[2]?.valueOf?.() !== "thru_hole") continue; const name = row[1]?.valueOf?.(); const pad_type = row[2]?.valueOf?.(); const pad_shape = row[3]?.valueOf?.(); const at = getAttr(row, "at"); const drill = getAttr(row, "drill"); let size = getAttr(row, "size"); if (Array.isArray(size)) { if (size[0] === "size") size = size.slice(1); size = { width: Number(size[0]), height: Number(size[1]) }; } const uuid = getAttr(row, "uuid"); const roundrect_rratio = getAttr(row, "roundrect_rratio"); let layers = getAttr(row, "layers"); if (Array.isArray(layers)) { layers = layers.map((layer) => layer.valueOf()); } else if (typeof layers === "string") { layers = [layers]; } else if (!layers) { layers = []; } const holeRaw = { name, pad_type, pad_shape, at, drill, size, layers, roundrect_rratio, uuid }; debug(`attempting to parse holes: ${JSON.stringify(holeRaw, null, 2)}`); holes.push(hole_def.parse(holeRaw)); } return kicad_mod_json_def.parse({ footprint_name: footprintName, ...topLevelAttributes, properties, fp_lines, fp_rects, fp_texts, fp_arcs, fp_circles, pads, holes, fp_polys }); }; // src/convert-kicad-json-to-tscircuit-soup.ts import Debug2 from "debug"; // src/math/arc-utils.ts var TWO_PI = Math.PI * 2; var normalizeAngle = (angle) => { let result = angle % TWO_PI; if (result < 0) result += TWO_PI; return result; }; var directedAngleCCW = (start, target) => { const startNorm = normalizeAngle(start); let targetNorm = normalizeAngle(target); let delta = targetNorm - startNorm; if (delta < 0) delta += TWO_PI; return delta; }; function calculateCenter(start, mid, end) { const mid1 = { x: (start.x + mid.x) / 2, y: (start.y + mid.y) / 2 }; const mid2 = { x: (mid.x + end.x) / 2, y: (mid.y + end.y) / 2 }; const slope1 = -(start.x - mid.x) / (start.y - mid.y); const slope2 = -(mid.x - end.x) / (mid.y - end.y); const centerX = (mid1.y - mid2.y + slope2 * mid2.x - slope1 * mid1.x) / (slope2 - slope1); const centerY = mid1.y + slope1 * (centerX - mid1.x); return { x: centerX, y: centerY }; } function calculateRadius(center, point4) { return Math.sqrt((center.x - point4.x) ** 2 + (center.y - point4.y) ** 2); } function calculateAngle(center, point4) { return Math.atan2(point4.y - center.y, point4.x - center.x); } var getArcLength = (start, mid, end) => { const center = calculateCenter(start, mid, end); const radius = calculateRadius(center, start); const angleStart = calculateAngle(center, start); const angleMid = calculateAngle(center, mid); const angleEnd = calculateAngle(center, end); const ccwToMid = directedAngleCCW(angleStart, angleMid); const ccwToEnd = directedAngleCCW(angleStart, angleEnd); let angleDelta = ccwToEnd; if (ccwToMid > ccwToEnd) { angleDelta = ccwToEnd - TWO_PI; } return Math.abs(radius * angleDelta); }; function generateArcPath(start, mid, end, numPoints) { const center = calculateCenter(start, mid, end); const radius = calculateRadius(center, start); const angleStart = calculateAngle(center, start); const angleMid = calculateAngle(center, mid); const angleEnd = calculateAngle(center, end); const ccwToMid = directedAngleCCW(angleStart, angleMid); const ccwToEnd = directedAngleCCW(angleStart, angleEnd); let angleDelta = ccwToEnd; if (ccwToMid > ccwToEnd) { angleDelta = ccwToEnd - TWO_PI; } const path2 = []; for (let i = 0; i <= numPoints; i++) { const angle = angleStart + i / numPoints * angleDelta; const x = center.x + radius * Math.cos(angle); const y = center.y + radius * Math.sin(angle); path2.push({ x, y }); } return path2; } // src/math/make-point.ts var makePoint = (p) => { if (Array.isArray(p)) { return { x: p[0], y: p[1] }; } return p; }; // src/math/points-equal.ts var pointsEqual = (p1, p2, tolerance = 1e-4) => { return Math.abs(p1.x - p2.x) < tolerance && Math.abs(p1.y - p2.y) < tolerance; }; // src/math/find-closed-polygons.ts var findClosedPolygons = (segments) => { const polygons = []; const used = /* @__PURE__ */ new Set(); for (let i = 0; i < segments.length; i++) { if (used.has(i)) continue; const polygon = [segments[i]]; used.add(i); let currentEnd = segments[i].end; let foundNext = true; while (foundNext) { foundNext = false; if (polygon.length > 1 && pointsEqual(currentEnd, polygon[0].start)) { polygons.push(polygon); break; } for (let j = 0; j < segments.length; j++) { if (used.has(j)) continue; if (pointsEqual(currentEnd, segments[j].start)) { polygon.push(segments[j]); used.add(j); currentEnd = segments[j].end; foundNext = true; break; } else if (pointsEqual(currentEnd, segments[j].end)) { if (segments[j].type === "arc") { polygon.push({ ...segments[j], reversed: true }); } else { polygon.push({ ...segments[j], start: segments[j].end, end: segments[j].start }); } used.add(j); currentEnd = segments[j].start; foundNext = true; break; } } if (!foundNext) { for (let k = polygon.length - 1; k >= 0; k--) { const idx = segments.indexOf(polygon[k]); if (idx !== -1) used.delete(idx); } break; } } } return polygons; }; // src/math/polygon-to-points.ts var polygonToPoints = (polygon) => { const points = []; for (const segment of polygon) { if (segment.type === "line") { points.push(segment.start); } else if (segment.type === "arc" && segment.mid) { const arcLength = getArcLength(segment.start, segment.mid, segment.end); const numPoints = Math.max(3, Math.ceil(arcLength)); let arcPoints = generateArcPath( segment.start, segment.mid, segment.end, numPoints ); if (segment.reversed) { arcPoints = arcPoints.reverse(); } points.push(...arcPoints.slice(0, -1)); } } return points; }; // src/get-Silkscreen-Font-Size-From-Fp-Texts.ts function getSilkscreenFontSizeFromFpTexts(fp_texts) { if (!Array.isArray(fp_texts)) return null; const refText = fp_texts.find( (t) => t.layer?.toLowerCase() === "f.silks" && (t.text?.includes("${REFERENCE}") || t.fp_text_type?.toLowerCase() === "reference" || t.text?.match(/^R\d+|C\d+|U\d+/)) ); const fallbackText = refText || fp_texts.find( (t) => t.layer?.toLowerCase() === "f.fab" && (t.text?.includes("${REFERENCE}") || t.fp_text_type?.toLowerCase() === "reference") ); const target = refText || fallbackText; if (!target?.effects?.font?.size) return null; const [width, height] = target.effects.font.size; return height ?? width ?? 1; } // src/convert-kicad-json-to-tscircuit-soup.ts var degToRad = (deg) => deg * Math.PI / 180; var rotatePoint = (x, y, deg) => { const r = degToRad(deg); const cos = Math.cos(r); const sin = Math.sin(r); return { x: x * cos - y * sin, y: x * sin + y * cos }; }; var getAxisAlignedRectFromPoints = (points) => { const uniquePoints = [ ...new Map(points.map((p) => [`${p.x},${p.y}`, p])).values() ]; if (uniquePoints.length !== 4) return null; const xs = uniquePoints.map((p) => p.x); const ys = uniquePoints.map((p) => p.y); const uniqueXs = [...new Set(xs)]; const uniqueYs = [...new Set(ys)]; if (uniqueXs.length !== 2 || uniqueYs.length !== 2) return null; const [minX, maxX] = uniqueXs.sort((a, b) => a - b); const [minY, maxY] = uniqueYs.sort((a, b) => a - b); if (minX === void 0 || maxX === void 0 || minY === void 0 || maxY === void 0) { return null; } return { x: (minX + maxX) / 2, y: (minY + maxY) / 2, width: maxX - minX, height: maxY - minY }; }; var fpPolyHasFill = (fill) => { if (!fill) return false; const normalized = fill.toLowerCase(); return normalized !== "no" && normalized !== "none" && normalized !== "outline"; }; var getRotationDeg = (at) => { if (!at) return 0; if (Array.isArray(at) && at.length >= 3 && typeof at[2] === "number") { return at[2]; } return 0; }; var isNinetyLike = (deg) => { const n = (deg % 360 + 360) % 360; return n === 90 || n === 270; }; var normalizePortName = (name) => { if (name === void 0 || name === null) return void 0; return `${name}`; }; var getPinNumber = (name) => { const normalized = normalizePortName(name); const parsed = normalized !== void 0 ? Number(normalized) : NaN; return Number.isFinite(parsed) ? parsed : void 0; }; var debug2 = Debug2("kicad-mod-converter"); var convertKicadLayerToTscircuitLayer = (kicadLayer) => { const lowerLayer = kicadLayer.toLowerCase(); switch (lowerLayer) { case "f.cu": case "f.fab": case "f.silks": case "f.crtyd": case "edge.cuts": return "top"; case "b.cu": case "b.fab": case "b.silks": case "b.crtyd": return "bottom"; } }; var convertKicadJsonToTsCircuitSoup = async (kicadJson) => { const { fp_lines, fp_rects, fp_texts, fp_arcs, fp_circles, pads, properties, holes, fp_polys } = kicadJson; const circuitJson = []; circuitJson.push({ type: "source_component", source_component_id: "source_component_0", supplier_part_numbers: {} }); circuitJson.push({ type: "schematic_component", schematic_component_id: "schematic_component_0", source_component_id: "source_component_0", center: { x: 0, y: 0 }, rotation: 0, size: { width: 0, height: 0 } }); const portNames = /* @__PURE__ */ new Set(); const portNameToPinNumber = /* @__PURE__ */ new Map(); for (const pad of pads) { const portName = normalizePortName(pad.name); if (portName) { portNames.add(portName); const pinNumber = getPinNumber(pad.name); if (pinNumber !== void 0) { portNameToPinNumber.set(portName, pinNumber); } } } if (holes) { for (const hole of holes) { const portName = normalizePortName(hole.name); if (portName) { portNames.add(portName); const pinNumber = getPinNumber(hole.name); if (pinNumber !== void 0) { portNameToPinNumber.set(portName, pinNumber); } } } } let sourcePortId = 0; const portNameToSourcePortId = /* @__PURE__ */ new Map(); for (const portName of portNames) { const source_port_id = `source_port_${sourcePortId++}`; portNameToSourcePortId.set(portName, source_port_id); const pinNumber = portNameToPinNumber.get(portName); circuitJson.push({ type: "source_port", source_port_id, source_component_id: "source_component_0", name: portName, port_hints: [portName], pin_number: pinNumber, pin_label: pinNumber !== void 0 ? `pin${pinNumber}` : void 0 }); circuitJson.push({ type: "schematic_port", schematic_port_id: `schematic_port_${sourcePortId++}`, source_port_id, schematic_component_id: "schematic_component_0", center: { x: 0, y: 0 } }); } let minX = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; for (const pad of pads) { const x = pad.at[0]; const y = -pad.at[1]; const w = pad.size[0]; const h = pad.size[1]; minX = Math.min(minX, x - w / 2); maxX = Math.max(maxX, x + w / 2); minY = Math.min(minY, y - h / 2); maxY = Math.max(maxY, y + h / 2); } const pcb_component_id = "pcb_component_0"; circuitJson.push({ type: "pcb_component", source_component_id: "source_component_0", pcb_component_id, layer: "top", center: { x: 0, y: 0 }, rotation: 0, width: Number.isFinite(minX) ? maxX - minX : 0, height: Number.isFinite(minY) ? maxY - minY : 0 }); let pcbPortId = 0; const portNameToPcbPortId = /* @__PURE__ */ new Map(); for (const portName of portNames) { const pcb_port_id = `pcb_port_${pcbPortId++}`; const source_port_id = portNameToSourcePortId.get(portName); portNameToPcbPortId.set(portName, pcb_port_id); let x = 0; let y = 0; let layers = ["top", "bottom"]; const pad = pads.find((p) => normalizePortName(p.name) === portName); if (pad) { x = pad.at[0]; y = -pad.at[1]; layers = pad.layers ? pad.layers.map((l) => convertKicadLayerToTscircuitLayer(l)).filter(Boolean) : ["top", "bottom"]; } else if (holes) { const hole = holes.find((h) => normalizePortName(h.name) === portName); if (hole) { x = hole.at[0]; y = -hole.at[1]; layers = hole.layers ? hole.layers.map((l) => convertKicadLayerToTscircuitLayer(l)).filter(Boolean) : ["top", "bottom"]; } } circuitJson.push({ type: "pcb_port", pcb_port_id, source_port_id, pcb_component_id, x, y, layers }); } let smtpadId = 0; let platedHoleId = 0; let holeId = 0; for (const pad of pads) { const portName = normalizePortName(pad.name); const pinNumber = portName ? portNameToPinNumber.get(portName) : void 0; if (pad.pad_type === "smd") { const rotation = getRotationDeg(pad.at); const width = isNinetyLike(rotation) ? pad.size[1] : pad.size[0]; const height = isNinetyLike(rotation) ? pad.size[0] : pad.size[1]; const pcb_port_id = portName ? portNameToPcbPortId.get(portName) : void 0; const pinLabel = pinNumber !== void 0 ? `pin${pinNumber}` : void 0; circuitJson.push({ type: "pcb_smtpad", pcb_smtpad_id: `pcb_smtpad_${smtpadId++}`, shape: "rect", x: pad.at[0], y: -pad.at[1], width, height, layer: convertKicadLayerToTscircuitLayer(pad.layers?.[0] ?? "F.Cu"), pcb_component_id, port_hints: pinLabel ? [pinLabel] : portName ? [portName] : [], pcb_port_id, pin_number: pinNumber, pin_label: pinLabel }); } else if (pad.pad_type === "thru_hole") { if (pad.pad_shape === "rect") { const rotation = getRotationDeg(pad.at); const width = isNinetyLike(rotation) ? pad.size[1] : pad.size[0]; const height = isNinetyLike(rotation) ? pad.size[0] : pad.size[1]; const offX = pad.drill?.offset?.[0] ?? 0; const offY = pad.drill?.offset?.[1] ?? 0; const rotOff = rotatePoint(offX, offY, rotation); const pcb_port_id = portName ? portNameToPcbPortId.get(portName) : void 0; const pinLabel = pinNumber !== void 0 ? `pin${pinNumber}` : void 0; circuitJson.push({ type: "pcb_plated_hole", pcb_plated_hole_id: `pcb_plated_hole_${platedHoleId++}`, shape: "circular_hole_with_rect_pad", hole_shape: "circle", pad_shape: "rect", // x/y are the pad center; hole_offset_* positions the hole x: pad.at[0], y: -pad.at[1], hole_offset_x: -rotOff.x, hole_offset_y: -rotOff.y, hole_diameter: pad.drill?.width, rect_pad_width: width, rect_pad_height: height, layers: ["top", "bottom"], pcb_component_id, port_hints: pinLabel ? [pinLabel] : portName ? [portName] : [], pcb_port_id, pin_number: pinNumber, pin_label: pinLabel }); } else if (pad.pad_shape === "circle") { const pcb_port_id = portName ? portNameToPcbPortId.get(portName) : void 0; const pinLabel = pinNumber !== void 0 ? `pin${pinNumber}` : void 0; circuitJson.push({ type: "pcb_plated_hole", pcb_plated_hole_id: `pcb_plated_hole_${platedHoleId++}`, shape: "circle", x: pad.at[0], y: -pad.at[1], outer_diameter: pad.size[0], hole_diameter: pad.drill?.width, layers: ["top", "bottom"], pcb_component_id, port_hints: pinLabel ? [pinLabel] : portName ? [portName] : [], pcb_port_id, pin_number: pinNumber, pin_label: pinLabel }); } else if (pad.pad_shape === "oval") { const pcb_port_id = portName ? portNameToPcbPortId.get(portName) : void 0; const pinLabel = pinNumber !== void 0 ? `pin${pinNumber}` : void 0; circuitJson.push({ type: "pcb_plated_hole", pcb_plated_hole_id: `pcb_plated_hole_${platedHoleId++}`, shape: "pill", x: pad.at[0], y: -pad.at[1], outer_width: pad.size[0], outer_height: pad.size[1], hole_width: pad.drill?.width, hole_height: pad.drill?.height, layers: ["top", "bottom"], pcb_component_id, port_hints: pinLabel ? [pinLabel] : portName ? [portName] : [], pcb_port_id, pin_number: pinNumber, pin_label: pinLabel }); } } else if (pad.pad_type === "np_thru_hole") { if (pad.pad_shape === "circle") { circuitJson.push({ type: "pcb_hole", pcb_hole_id: `pcb_hole_${holeId++}`, x: pad.at[0], y: -pad.at[1], hole_shape: "circle", hole_diameter: pad.drill?.width ?? pad.size[0], pcb_component_id }); } } } if (holes) { for (const hole of holes) { const portName = normalizePortName(hole.name); const pinNumber = portName ? portNameToPinNumber.get(portName) : void 0; const hasCuLayer = hole.layers?.some( (l) => l.endsWith(".Cu") || l === "*.Cu" ); const rotation = getRotationDeg(hole.at); const offX = hole.drill?.offset?.[0] ?? 0; const offY = hole.drill?.offset?.[1] ?? 0; const rotOff = rotatePoint(offX, offY, rotation); const x = hole.at[0] + rotOff.x; const y = -(hole.at[1] + rotOff.y); const holeDiameter = hole.drill?.width ?? 0; const outerDiameter = hole.size?.width ?? holeDiameter; const rr = hole.roundrect_rratio ?? 0; const rectBorderRadius = rr > 0 ? Math.min( isNinetyLike(rotation) ? hole.size?.height ?? outerDiameter : hole.size?.width ?? outerDiameter, isNinetyLike(rotation) ? hole.size?.width ?? outerDiameter : hole.size?.height ?? outerDiameter ) / 2 * rr : 0; if (hasCuLayer) { if (hole.pad_shape === "rect") { const pcb_port_id = portName ? portNameToPcbPortId.get(portName) : void 0; circuitJson.push({ type: "pcb_plated_hole", pcb_plated_hole_id: `pcb_plated_hole_${platedHoleId++}`, shape: "circular_hole_with_rect_pad", hole_shape: "circle", pad_shape: "rect", // x/y are the pad center; hole_offset_* positions the hole x: hole.at[0], y: -hole.at[1], hole_offset_x: -rotOff.x, hole_offset_y: -rotOff.y, hole_diameter: holeDiameter, rect_pad_width: isNinetyLike(rotation) ? hole.size?.height ?? outerDiameter : hole.size?.width ?? outerDiameter, rect_pad_height: isNinetyLike(rotation) ? hole.size?.width ?? outerDiameter : hole.size?.height ?? outerDiameter, rect_border_radius: rectBorderRadius, port_hints: pinNumber !== void 0 ? [`pin${pinNumber}`] : portName ? [portName] : [], layers: ["top", "bottom"], pcb_component_id, pcb_port_id, pin_number: pinNumber, pin_label: pinNumber !== void 0 ? `pin${pinNumber}` : void 0 }); } else if (hole.pad_shape === "oval") { const pcb_port_id = portName ? portNameToPcbPortId.get(portName) : void 0; circuitJson.push({ type: "pcb_plated_hole", pcb_plated_hole_id: `pcb_plated_hole_${platedHoleId++}`, shape: "pill", x, y, outer_width: isNinetyLike(rotation) ? hole.size?.height ?? outerDiameter : hole.size?.width ?? outerDiameter, outer_height: isNinetyLike(rotation) ? hole.size?.width ?? outerDiameter : hole.size?.height ?? outerDiameter, hole_width: isNinetyLike(rotation) ? hole.drill?.height ?? holeDiameter : hole.drill?.width ?? holeDiameter, hole_height: isNinetyLike(rotation) ? hole.drill?.width ?? holeDiameter : hole.drill?.height ?? holeDiameter, port_hints: pinNumber !== void 0 ? [`pin${pinNumber}`] : portName ? [portName] : [], layers: ["top", "bottom"], pcb_component_id, pcb_port_id, pin_number: pinNumber, pin_label: pinNumber !== void 0 ? `pin${pinNumber}` : void 0 }); } else if (hole.pad_shape === "roundrect") { const pcb_port_id = portName ? portNameToPcbPortId.get(portName) : void 0; const offX2 = hole.drill?.offset?.[0] ?? 0; const offY2 = hole.drill?.offset?.[1] ?? 0; const rotOff2 = rotatePoint(offX2, offY2, rotation); const width = isNinetyLike(rotation) ? hole.size?.height ?? outerDiameter : hole.size?.width ?? outerDiameter; const height = isNinetyLike(rotation) ? hole.size?.width ?? outerDiameter : hole.size?.height ?? outerDiameter; circuitJson.push({ type: "pcb_plated_hole", pcb_plated_hole_id: `pcb_plated_hole_${platedHoleId++}`, shape: "circular_hole_with_rect_pad", hole_shape: "circle", pad_shape: "rect", x, y, hole_offset_x: -rotOff2.x, hole_offset_y: rotOff2.y, hole_diameter: holeDiameter, rect_pad_width: width, rect_pad_height: height, rect_border_radius: rectBorderRadius, port_hints: pinNumber !== void 0 ? [`pin${pinNumber}`] : portName ? [portName] : [], layers: ["top", "bottom"], pcb_component_id, pcb_port_id, pin_number: pinNumber, pin_label: pinNumber !== void 0 ? `pin${pinNumber}` : void 0 }); } else { const pcb_port_id = portName ? portNameToPcbPortId.get(portName) : void 0; circuitJson.push({ type: "pcb_plated_hole", pcb_plated_hole_id: `pcb_plated_hole_${platedHoleId++}`, shape: "circle", x, y, outer_diameter: outerDiameter, hole_diameter: holeDiameter, port_hints: pinNumber !== void 0 ? [`pin${pinNumber}`] : portName ? [portName] : [], layers: ["top", "bottom"], pcb_component_id, pcb_port_id, pin_number: pinNumber, pin_label: pinNumber !== void 0 ? `pin${pinNumber}` : void 0 }); } } else { circuitJson.push({ type: "pcb_hole", pcb_hole_id: `pcb_hole_${holeId++}`, x, y, hole_shape: "circle", hole_diameter: holeDiameter, pcb_component_id }); } } } const edgeCutSegments = []; const frontCourtyardSegments = []; const backCourtyardSegments = []; for (const fp_line2 of fp_lines) { const lowerLayer = fp_line2.layer.toLowerCase(); if (lowerLayer === "edge.cuts") { edgeCutSegments.push({ type: "line", start: { x: fp_line2.start[0], y: fp_line2.start[1] }, end: { x: fp_line2.end[0], y: fp_line2.end[1] }, strokeWidth: fp_line2.stroke.width }); } else if (lowerLayer === "f.crtyd") { frontCourtyardSegments.push({ type: "line", start: { x: fp_line2.start[0], y: fp_line2.start[1] }, end: { x: fp_line2.end[0], y: fp_line2.end[1] }, strokeWidth: fp_line2.stroke.width }); } else if (lowerLayer === "b.crtyd") { backCourtyardSegments.push({ type: "line", start: { x: fp_line2.start[0], y: fp_line2.start[1] }, end: { x: fp_line2.end[0], y: fp_line2.end[1] }, strokeWidth: fp_line2.stroke.width }); } } for (const fp_arc of fp_arcs) { const lowerLayer = fp_arc.layer.toLowerCase(); if (lowerLayer === "edge.cuts") { edgeCutSegments.push({ type: "arc", start: { x: fp_arc.start[0], y: fp_arc.start[1] }, mid: { x: fp_arc.mid[0], y: fp_arc.mid[1] }, end: { x: fp_arc.end[0], y: fp_arc.end[1] }, strokeWidth: fp_arc.stroke.width }); } else if (lowerLayer === "f.crtyd") { frontCourtyardSegments.push({ type: "arc", start: { x: fp_arc.start[0], y: fp_arc.start[1] }, mid: { x: fp_arc.mid[0], y: fp_arc.mid[1] }, end: { x: fp_arc.end[0], y: fp_arc.end[1] }, strokeWidth: fp_arc.stroke.width }); } else if (lowerLayer === "b.crtyd") { backCourtyardSegments.push({ type: "arc", start: { x: fp_arc.start[0], y: fp_arc.start[1] }, mid: { x: fp_arc.mid[0], y: fp_arc.mid[1] }, end: { x: fp_arc.end[0], y: fp_arc.end[1] }, strokeWidth: fp_arc.stroke.width }); } } const closedPolygons = findClosedPolygons(edgeCutSegments); let cutoutId = 0; for (const polygon of closedPolygons) { const points = polygonToPoints(polygon); if (points.length >= 3) { circuitJson.push({ type: "pcb_cutout", pcb_cutout_id: `pcb_cutout_${cutoutId++}`, shape: "polygon", points: points.map((p) => ({ x: p.x, y: -p.y })), pcb_component_id }); } } let courtyardOutlineId = 0; for (const [segments, layer] of [ [frontCourtyardSegments, "top"], [backCourtyardSegments, "bottom"] ]) { const closedCourtyardPolygons = findClosedPolygons(segments); for (const polygon of closedCourtyardPolygons) { const points = polygonToPoints(polygon); if (points.length >= 3) { circuitJson.push({ type: "pcb_courtyard_outline", pcb_courtyard_outline_id: `pcb_courtyard_outline_${courtyardOutlineId++}`, layer, pcb_component_id, outline: points.map((p) => ({ x: p.x, y: -p.y })) }); } } } if (fp_rects) { for (const fp_rect of fp_rects) { const lowerLayer = fp_rect.layer.toLowerCase(); if (lowerLayer === "f.crtyd" || lowerLayer === "b.crtyd") { const x1 = fp_rect.start[0]; const y1 = fp_rect.start[1]; const x2 = fp_rect.end[0]; const y2 = fp_rect.end[1]; circuitJson.push({ type: "pcb_courtyard_rect", pcb_courtyard_rect_id: `pcb_courtyard_rect_${courtyardOutlineId++}`, pcb_component_id, layer: convertKicadLayerToTscircuitLayer(fp_rect.layer), center: { x: (x1 + x2) / 2, y: -((y1 + y2) / 2) }, width: Math.abs(x2 - x1), height: Math.abs(y2 - y1) }); } else { debug2("Unhandled layer for fp_rect", fp_rect.layer); } } } let traceId = 0; let silkPathId = 0; let fabPathId = 0; let noteLineId = 0; for (const fp_line2 of fp_lines) { const route = [ { x: fp_line2.start[0], y: -fp_line2.start[1] }, { x: fp_line2.end[0], y: -fp_line2.end[1] } ]; const lowerLayer = fp_line2.layer.toLowerCase(); if (lowerLayer === "f.cu") { circuitJson.push({ type: "pcb_trace", pcb_trace_id: `pcb_trace_${traceId++}`, pcb_component_id, layer: convertKicadLayerToTscircuitLayer(fp_line2.layer), route, thickness: fp_line2.stroke.width }); } else if (lowerLayer === "f.silks") { circuitJson.push({ type: "pcb_silkscreen_path", pcb_silkscreen_path_id: `pcb_silkscreen_path_${silkPathId++}`, pcb_component_id, layer: "top", route, stroke_width: fp_line2.stroke.width }); } else if (lowerLayer === "edge.cuts") { debug2( "Skipping Edge.Cuts fp_line (converted to pcb_cutout)", fp_line2.layer ); } else if (lowerLayer === "f.crtyd" || lowerLayer === "b.crtyd") { debug2( "Skipping CrtYd fp_line (converted to pcb_courtyard_outline)", fp_line2.layer ); } else if (lowerLayer === "f.fab") { circuitJson.push({ type: "pcb_fabrication_note_path", fabrication_note_path_id: `fabrication_note_path_${fabPathId++}`, pcb_component_id, layer: "top", route, stroke_width: fp_line2.stroke.width, port_hints: [] }); } else if (lowerLayer.startsWith("user.")) { circuitJson.push({ type: "pcb_note_line", pcb_note_line_id: `pcb_note_line_${noteLineId++}`, pcb_component_id, x1: fp_line2.start[0], y1: -fp_line2.start[1], x2: fp_line2.end[0], y2: -fp_line2.end[1], stroke_width: fp_line2.stroke.width }); } else { debug2("Unhandled layer for fp_line", fp_line2.layer); } } if (fp_polys) { for (const fp_poly of fp_polys) { const route = []; const pushRoutePoint = (point4) => { if (!Number.isFinite(point4.x) || !Number.isFinite(point4.y)) { return; } route.push(point4); }; for (const segment of fp_poly.pts) { if (Array.isArray(segment)) { pushRoutePoint({ x: segment[0], y: -segment[1] }); continue; } if (segment && typeof segment === "object" && "kind" in segment) { if (segment.kind === "arc") { const start = makePoint(segment.start); const mid = makePoint(segment.mid); const end = makePoint(segment.end); const arcLength = getArcLength(start, mid, end); const adjustedNumPoints = Math.max(2, Math.ceil(arcLength / 0.1)); const arcPoints = generateArcPath( start, mid, end, adjustedNumPoints ).map((p) => ({ x: p.x, y: -p.y })); for (const point4 of arcPoints) { pushRoutePoint(point4); } } continue; } } const routePoints = route; const isClosed = routePoints.length > 2 && routePoints[0].x === routePoints[routePoints.length - 1].x && routePoints[0].y === routePoints[routePoints.length - 1].y; const polygonPoints = isClosed ? routePoints.slice(0, -1) : routePoints; if (routePoints.length === 0) continue; const strokeWidth = fp_poly.stroke?.width ?? 0; if (fp_poly.layer.endsWith(".Cu")) { const rect = getAxisAlignedRectFromPoints(polygonPoints); if (rect) { circuitJson.push({ type: "pcb_smtpad", pcb_smtpad_id: `pcb_smtpad_${smtpadId++}`, shape: "rect", x: rect.x, y: rect.y, width: rect.width, height: rect.height, layer: convertKicadLayerToTscircuitLayer(fp_poly.layer), pcb_component_id }); } else if (fpPolyHasFill(fp_poly.fill)) { if (polygonPoints.length >= 3) { circuitJson.push({ type: "pcb_smtpad", pcb_smtpad_id: `pcb_smtpad_${smtpadId++}`, shape: "polygon", points: polygonPoints, layer: convertKicadLayerToTscircuitLayer(fp_poly.layer), pcb_component_id }); } else if (polygonPoints.length >= 2) { circuitJson.push({ type: "pcb_trace", pcb_trace_id: `pcb_trace_${traceId++}`, pcb_component_id, layer: convertKicadLayerToTscircuitLayer(fp_poly.layer), route: polygonPoints, thickness: strokeWidth }); } } else if (polygonPoints.length >= 2) { circuitJson.push({ type: "pcb_trace", pcb_trace_id: `pcb_trace_${traceId++}`, pcb_component_id, layer: convertKicadLayerToTscircuitLayer(fp_poly.layer), route: polygonPoints, thickness: strokeWidth }); } } else if (fp_poly.layer.endsWith(".SilkS")) { circuitJson.push({ type: "pcb_silkscreen_path", pcb_silkscreen_path_id: `pcb_silkscreen_path_${silkPathId++}`, pcb_component_id, layer: convertKicadLayerToTscircuitLayer(fp_poly.layer), route: routePoints, stroke_width: strokeWidth }); } else if (fp_poly.layer.endsWith(".Fab")) { circuitJson.push({ type: "pcb_fabrication_note_path", fabrication_note_path_id: `fabrication_note_path_${fabPathId++}`, pcb_component_id, layer: convertKicadLayerToTscircuitLayer(fp_poly.layer), route: polygonPoints, stroke_width: strokeWidth, port_hints: [] }); } else if (fp_poly.layer.toLowerCase().endsWith(".crtyd")) { if (polygonPoints.length >= 3) { circuitJson.push({ type: "pcb_courtyard_polygon", pcb_courtyard_polygon_id: `pcb_courtyard_polygon_${courtyardOutlineId++}`, layer: convertKicadLayerToTscircuitLayer(fp_poly.layer), pcb_component_id, points: polygonPoints }); } } else { debug2("Unhandled layer for fp_poly", fp_poly.layer); } } } let notePathId = 0; for (const fp_arc of fp_arcs) { const lowerLayer = fp_arc.layer.toLowerCase(); if (lowerLayer === "edge.cuts") { debug2("Skipping Edge.Cuts fp_arc (converted to pcb_cutout)", fp_arc.layer); continue; } if (lowerLayer === "f.crtyd" || lowerLayer === "b.crtyd") { debug2( "Skipping CrtYd fp_arc (converted to pcb_courtyard_outline)", fp_arc.layer ); continue; } const start = makePoint(fp_arc.start); const mid = makePoint(fp_arc.mid); const end = makePoint(fp_arc.end); const arcLength = getArcLength(start, mid, end); const arcPoints = generateArcPath(start, mid, end, Math.ceil(arcLength)); if (lowerLayer.startsWith("user.")) { circuitJson.push({ type: "pcb_note_path", pcb_note_path_id: `pcb_note_path_${notePathId++}`, pcb_component_id, route: arcPoints.map((p) => ({ x: p.x, y: -p.y })), s