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
JavaScript
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