UNPKG

kicad-component-converter

Version:

Convert kicad_mod or kicad_sym file into Circuit JSON or tscircuit

1,405 lines (1,391 loc) 41.4 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); var __commonJS = (cb, mod) => function __require2() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // node_modules/ms/index.js var require_ms = __commonJS({ "node_modules/ms/index.js"(exports, module) { "use strict"; var s = 1e3; var m = s * 60; var h = m * 60; var d = h * 24; var w = d * 7; var y = d * 365.25; module.exports = function(val, options) { options = options || {}; var type = typeof val; if (type === "string" && val.length > 0) { return parse(val); } else if (type === "number" && isFinite(val)) { return options.long ? fmtLong(val) : fmtShort(val); } throw new Error( "val is not a non-empty string or a valid number. val=" + JSON.stringify(val) ); }; function parse(str) { str = String(str); if (str.length > 100) { return; } var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( str ); if (!match) { return; } var n = parseFloat(match[1]); var type = (match[2] || "ms").toLowerCase(); switch (type) { case "years": case "year": case "yrs": case "yr": case "y": return n * y; case "weeks": case "week": case "w": return n * w; case "days": case "day": case "d": return n * d; case "hours": case "hour": case "hrs": case "hr": case "h": return n * h; case "minutes": case "minute": case "mins": case "min": case "m": return n * m; case "seconds": case "second": case "secs": case "sec": case "s": return n * s; case "milliseconds": case "millisecond": case "msecs": case "msec": case "ms": return n; default: return void 0; } } function fmtShort(ms) { var msAbs = Math.abs(ms); if (msAbs >= d) { return Math.round(ms / d) + "d"; } if (msAbs >= h) { return Math.round(ms / h) + "h"; } if (msAbs >= m) { return Math.round(ms / m) + "m"; } if (msAbs >= s) { return Math.round(ms / s) + "s"; } return ms + "ms"; } function fmtLong(ms) { var msAbs = Math.abs(ms); if (msAbs >= d) { return plural(ms, msAbs, d, "day"); } if (msAbs >= h) { return plural(ms, msAbs, h, "hour"); } if (msAbs >= m) { return plural(ms, msAbs, m, "minute"); } if (msAbs >= s) { return plural(ms, msAbs, s, "second"); } return ms + " ms"; } function plural(ms, msAbs, n, name) { var isPlural = msAbs >= n * 1.5; return Math.round(ms / n) + " " + name + (isPlural ? "s" : ""); } } }); // node_modules/debug/src/common.js var require_common = __commonJS({ "node_modules/debug/src/common.js"(exports, module) { "use strict"; function setup(env) { createDebug.debug = createDebug; createDebug.default = createDebug; createDebug.coerce = coerce; createDebug.disable = disable; createDebug.enable = enable; createDebug.enabled = enabled; createDebug.humanize = require_ms(); createDebug.destroy = destroy; Object.keys(env).forEach((key) => { createDebug[key] = env[key]; }); createDebug.names = []; createDebug.skips = []; createDebug.formatters = {}; function selectColor(namespace) { let hash = 0; for (let i = 0; i < namespace.length; i++) { hash = (hash << 5) - hash + namespace.charCodeAt(i); hash |= 0; } return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; } createDebug.selectColor = selectColor; function createDebug(namespace) { let prevTime; let enableOverride = null; let namespacesCache; let enabledCache; function debug3(...args) { if (!debug3.enabled) { return; } const self = debug3; const curr = Number(/* @__PURE__ */ new Date()); const ms = curr - (prevTime || curr); self.diff = ms; self.prev = prevTime; self.curr = curr; prevTime = curr; args[0] = createDebug.coerce(args[0]); if (typeof args[0] !== "string") { args.unshift("%O"); } let index = 0; args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => { if (match === "%%") { return "%"; } index++; const formatter = createDebug.formatters[format]; if (typeof formatter === "function") { const val = args[index]; match = formatter.call(self, val); args.splice(index, 1); index--; } return match; }); createDebug.formatArgs.call(self, args); const logFn = self.log || createDebug.log; logFn.apply(self, args); } debug3.namespace = namespace; debug3.useColors = createDebug.useColors(); debug3.color = createDebug.selectColor(namespace); debug3.extend = extend; debug3.destroy = createDebug.destroy; Object.defineProperty(debug3, "enabled", { enumerable: true, configurable: false, get: () => { if (enableOverride !== null) { return enableOverride; } if (namespacesCache !== createDebug.namespaces) { namespacesCache = createDebug.namespaces; enabledCache = createDebug.enabled(namespace); } return enabledCache; }, set: (v) => { enableOverride = v; } }); if (typeof createDebug.init === "function") { createDebug.init(debug3); } return debug3; } function extend(namespace, delimiter) { const newDebug = createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace); newDebug.log = this.log; return newDebug; } function enable(namespaces) { createDebug.save(namespaces); createDebug.namespaces = namespaces; createDebug.names = []; createDebug.skips = []; const split = (typeof namespaces === "string" ? namespaces : "").trim().replace(/\s+/g, ",").split(",").filter(Boolean); for (const ns of split) { if (ns[0] === "-") { createDebug.skips.push(ns.slice(1)); } else { createDebug.names.push(ns); } } } function matchesTemplate(search, template) { let searchIndex = 0; let templateIndex = 0; let starIndex = -1; let matchIndex = 0; while (searchIndex < search.length) { if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) { if (template[templateIndex] === "*") { starIndex = templateIndex; matchIndex = searchIndex; templateIndex++; } else { searchIndex++; templateIndex++; } } else if (starIndex !== -1) { templateIndex = starIndex + 1; matchIndex++; searchIndex = matchIndex; } else { return false; } } while (templateIndex < template.length && template[templateIndex] === "*") { templateIndex++; } return templateIndex === template.length; } function disable() { const namespaces = [ ...createDebug.names, ...createDebug.skips.map((namespace) => "-" + namespace) ].join(","); createDebug.enable(""); return namespaces; } function enabled(name) { for (const skip of createDebug.skips) { if (matchesTemplate(name, skip)) { return false; } } for (const ns of createDebug.names) { if (matchesTemplate(name, ns)) { return true; } } return false; } function coerce(val) { if (val instanceof Error) { return val.stack || val.message; } return val; } function destroy() { console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."); } createDebug.enable(createDebug.load()); return createDebug; } module.exports = setup; } }); // node_modules/debug/src/browser.js var require_browser = __commonJS({ "node_modules/debug/src/browser.js"(exports, module) { "use strict"; exports.formatArgs = formatArgs; exports.save = save; exports.load = load; exports.useColors = useColors; exports.storage = localstorage(); exports.destroy = /* @__PURE__ */ (() => { let warned = false; return () => { if (!warned) { warned = true; console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."); } }; })(); exports.colors = [ "#0000CC", "#0000FF", "#0033CC", "#0033FF", "#0066CC", "#0066FF", "#0099CC", "#0099FF", "#00CC00", "#00CC33", "#00CC66", "#00CC99", "#00CCCC", "#00CCFF", "#3300CC", "#3300FF", "#3333CC", "#3333FF", "#3366CC", "#3366FF", "#3399CC", "#3399FF", "#33CC00", "#33CC33", "#33CC66", "#33CC99", "#33CCCC", "#33CCFF", "#6600CC", "#6600FF", "#6633CC", "#6633FF", "#66CC00", "#66CC33", "#9900CC", "#9900FF", "#9933CC", "#9933FF", "#99CC00", "#99CC33", "#CC0000", "#CC0033", "#CC0066", "#CC0099", "#CC00CC", "#CC00FF", "#CC3300", "#CC3333", "#CC3366", "#CC3399", "#CC33CC", "#CC33FF", "#CC6600", "#CC6633", "#CC9900", "#CC9933", "#CCCC00", "#CCCC33", "#FF0000", "#FF0033", "#FF0066", "#FF0099", "#FF00CC", "#FF00FF", "#FF3300", "#FF3333", "#FF3366", "#FF3399", "#FF33CC", "#FF33FF", "#FF6600", "#FF6633", "#FF9900", "#FF9933", "#FFCC00", "#FFCC33" ]; function useColors() { if (typeof window !== "undefined" && window.process && (window.process.type === "renderer" || window.process.__nwjs)) { return true; } if (typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { return false; } let m; return typeof document !== "undefined" && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || // Is firebug? http://stackoverflow.com/a/398120/376773 typeof window !== "undefined" && window.console && (window.console.firebug || window.console.exception && window.console.table) || // Is firefox >= v31? // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages typeof navigator !== "undefined" && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31 || // Double check webkit in userAgent just in case we are in a worker typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/); } function formatArgs(args) { args[0] = (this.useColors ? "%c" : "") + this.namespace + (this.useColors ? " %c" : " ") + args[0] + (this.useColors ? "%c " : " ") + "+" + module.exports.humanize(this.diff); if (!this.useColors) { return; } const c = "color: " + this.color; args.splice(1, 0, c, "color: inherit"); let index = 0; let lastC = 0; args[0].replace(/%[a-zA-Z%]/g, (match) => { if (match === "%%") { return; } index++; if (match === "%c") { lastC = index; } }); args.splice(lastC, 0, c); } exports.log = console.debug || console.log || (() => { }); function save(namespaces) { try { if (namespaces) { exports.storage.setItem("debug", namespaces); } else { exports.storage.removeItem("debug"); } } catch (error) { } } function load() { let r; try { r = exports.storage.getItem("debug") || exports.storage.getItem("DEBUG"); } catch (error) { } if (!r && typeof process !== "undefined" && "env" in process) { r = process.env.DEBUG; } return r; } function localstorage() { try { return localStorage; } catch (error) { } } module.exports = require_common()(exports); var { formatters } = module.exports; formatters.j = function(v) { try { return JSON.stringify(v); } catch (error) { return "[UnexpectedJSONParseError]: " + error.message; } }; } }); // node_modules/debug/src/node.js var require_node = __commonJS({ "node_modules/debug/src/node.js"(exports, module) { "use strict"; var tty = __require("tty"); var util = __require("util"); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; exports.save = save; exports.load = load; exports.useColors = useColors; exports.destroy = util.deprecate( () => { }, "Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`." ); exports.colors = [6, 2, 3, 4, 5, 1]; try { const supportsColor = __require("supports-color"); if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) { exports.colors = [ 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221 ]; } } catch (error) { } exports.inspectOpts = Object.keys(process.env).filter((key) => { return /^debug_/i.test(key); }).reduce((obj, key) => { const prop = key.substring(6).toLowerCase().replace(/_([a-z])/g, (_, k) => { return k.toUpperCase(); }); let val = process.env[key]; if (/^(yes|on|true|enabled)$/i.test(val)) { val = true; } else if (/^(no|off|false|disabled)$/i.test(val)) { val = false; } else if (val === "null") { val = null; } else { val = Number(val); } obj[prop] = val; return obj; }, {}); function useColors() { return "colors" in exports.inspectOpts ? Boolean(exports.inspectOpts.colors) : tty.isatty(process.stderr.fd); } function formatArgs(args) { const { namespace: name, useColors: useColors2 } = this; if (useColors2) { const c = this.color; const colorCode = "\x1B[3" + (c < 8 ? c : "8;5;" + c); const prefix = ` ${colorCode};1m${name} \x1B[0m`; args[0] = prefix + args[0].split("\n").join("\n" + prefix); args.push(colorCode + "m+" + module.exports.humanize(this.diff) + "\x1B[0m"); } else { args[0] = getDate() + name + " " + args[0]; } } function getDate() { if (exports.inspectOpts.hideDate) { return ""; } return (/* @__PURE__ */ new Date()).toISOString() + " "; } function log(...args) { return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + "\n"); } function save(namespaces) { if (namespaces) { process.env.DEBUG = namespaces; } else { delete process.env.DEBUG; } } function load() { return process.env.DEBUG; } function init(debug3) { debug3.inspectOpts = {}; const keys = Object.keys(exports.inspectOpts); for (let i = 0; i < keys.length; i++) { debug3.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; } } module.exports = require_common()(exports); var { formatters } = module.exports; formatters.o = function(v) { this.inspectOpts.colors = this.useColors; return util.inspect(v, this.inspectOpts).split("\n").map((str) => str.trim()).join(" "); }; formatters.O = function(v) { this.inspectOpts.colors = this.useColors; return util.inspect(v, this.inspectOpts); }; } }); // node_modules/debug/src/index.js var require_src = __commonJS({ "node_modules/debug/src/index.js"(exports, module) { "use strict"; if (typeof process === "undefined" || process.type === "renderer" || process.browser === true || process.__nwjs) { module.exports = require_browser(); } else { module.exports = require_node(); } } }); // 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 attributes_def = z.object({ at: point, size: point2, 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(), 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_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_texts: z.array(fp_text_def), fp_arcs: z.array(fp_arc_def), 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 === "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") { return val.map((n) => Number.parseFloat(n.valueOf())); } 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 var import_debug = __toESM(require_src(), 1); var debug = (0, import_debug.default)("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 = []; } if (!layers.includes("F.Cu")) { debug(`Skipping pad without F.Cu 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_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 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"); 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, 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_texts, fp_arcs, pads, holes }); }; // src/convert-kicad-json-to-tscircuit-soup.ts var import_debug2 = __toESM(require_src(), 1); // src/math/arc-utils.ts 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 angleEnd = calculateAngle(center, end); let angleDelta = angleEnd - angleStart; if (angleDelta < 0) { angleDelta += 2 * Math.PI; } return 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 angleEnd = calculateAngle(center, end); let angleDelta = angleEnd - angleStart; if (angleDelta < 0) { angleDelta += 2 * Math.PI; } const path = []; 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); path.push({ x, y }); } return path; } // src/math/make-point.ts var makePoint = (p) => { if (Array.isArray(p)) { return { x: p[0], y: p[1] }; } return p; }; // src/convert-kicad-json-to-tscircuit-soup.ts var debug2 = (0, import_debug2.default)("kicad-mod-converter"); var convertKicadLayerToTscircuitLayer = (kicadLayer) => { switch (kicadLayer) { case "F.Cu": case "F.Fab": case "F.SilkS": return "top"; case "B.Cu": case "B.Fab": case "B.SilkS": return "bottom"; } }; var convertKicadJsonToTsCircuitSoup = async (kicadJson) => { const { fp_lines, fp_texts, fp_arcs, pads, properties, holes } = kicadJson; const soup = []; soup.push({ type: "source_component", source_component_id: "generic_0", supplier_part_numbers: {} }); soup.push({ type: "schematic_component", schematic_component_id: "schematic_generic_component_0", source_component_id: "generic_0", center: { x: 0, y: 0 }, rotation: 0, size: { width: 0, height: 0 } }); let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -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_generic_component_0"; soup.push({ type: "pcb_component", source_component_id: "generic_0", pcb_component_id, layer: "top", center: { x: 0, y: 0 }, rotation: 0, width: isFinite(minX) ? maxX - minX : 0, height: isFinite(minY) ? maxY - minY : 0 }); let smtpadId = 0; let platedHoleId = 0; let holeId = 0; for (const pad of pads) { if (pad.pad_type === "smd") { soup.push({ type: "pcb_smtpad", pcb_smtpad_id: `pcb_smtpad_${smtpadId++}`, shape: "rect", x: pad.at[0], y: -pad.at[1], width: pad.size[0], height: pad.size[1], layer: convertKicadLayerToTscircuitLayer(pad.layers?.[0] ?? "F.Cu"), pcb_component_id, port_hints: [pad.name] }); } else if (pad.pad_type === "thru_hole") { if (pad.pad_shape === "circle") { soup.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: [pad.name] }); } else if (pad.pad_shape === "oval") { soup.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 }); } } else if (pad.pad_type === "np_thru_hole") { soup.push({ type: "pcb_hole", pcb_hole_id: `pcb_hole_${holeId++}`, x: pad.at[0], y: -pad.at[1], hole_diameter: pad.drill?.width, pcb_component_id }); } } if (holes) { for (const hole of holes) { const hasCuLayer = hole.layers?.some( (l) => l.endsWith(".Cu") || l === "*.Cu" ); const x = hole.at[0]; const y = -hole.at[1]; const holeDiameter = hole.drill?.width ?? 0; const outerDiameter = hole.size?.width ?? holeDiameter; if (hasCuLayer) { soup.push({ type: "pcb_plated_hole", pcb_plated_hole_id: `pcb_plated_hole_${platedHoleId++}`, shape: "circle", x, y, outer_diameter: outerDiameter, hole_diameter: holeDiameter, portHints: [hole.name], layers: ["top", "bottom"], pcb_component_id }); } else { soup.push({ type: "pcb_hole", pcb_hole_id: `pcb_hole_${holeId++}`, x, y, hole_diameter: outerDiameter, hole_shape: "circle", pcb_component_id }); } } } let traceId = 0; let silkPathId = 0; let fabPathId = 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] } ]; if (fp_line2.layer === "F.Cu") { soup.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 (fp_line2.layer === "F.SilkS") { soup.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 (fp_line2.layer === "F.Fab") { soup.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 { debug2("Unhandled layer for fp_line", fp_line2.layer); } } for (const fp_arc of fp_arcs) { 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)); soup.push({ type: "pcb_silkscreen_path", pcb_silkscreen_path_id: `pcb_silkscreen_path_${silkPathId++}`, layer: convertKicadLayerToTscircuitLayer(fp_arc.layer), pcb_component_id, route: arcPoints.map((p) => ({ x: p.x, y: -p.y })), stroke_width: fp_arc.stroke.width }); } for (const fp_text of fp_texts) { soup.push({ type: "pcb_silkscreen_text", layer: convertKicadLayerToTscircuitLayer(fp_text.layer), font: "tscircuit2024", font_size: fp_text.effects?.font?.size[0] ?? 1, pcb_component_id, anchor_position: { x: fp_text.at[0], y: -fp_text.at[1] }, anchor_alignment: "center", text: fp_text.text }); } const refProp = properties.find((prop) => prop.key === "Reference"); const valProp = properties.find((prop) => prop.key === "Value"); const propFabTexts = [refProp, valProp].filter((p) => p && Boolean(p.val)); for (const propFab of propFabTexts) { const at = propFab.attributes.at; if (!at) continue; soup.push({ type: "pcb_silkscreen_text", layer: "top", font: "tscircuit2024", font_size: 1.27, pcb_component_id, anchor_position: { x: at[0], y: -at[1] }, anchor_alignment: "center", text: propFab.val }); } return soup; }; // src/parse-kicad-mod-to-circuit-json.ts var parseKicadModToCircuitJson = async (kicadMod) => { const kicadJson = parseKicadModToKicadJson(kicadMod); const circuitJson = await convertKicadJsonToTsCircuitSoup(kicadJson); return circuitJson; }; export { convertKicadJsonToTsCircuitSoup, parseKicadModToCircuitJson, parseKicadModToKicadJson }; //# sourceMappingURL=index.js.map