kicad-component-converter
Version:
Convert kicad_mod or kicad_sym file into Circuit JSON or tscircuit
1,498 lines (1,487 loc) • 54 kB
JavaScript
// 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 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/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 })),
stroke_width: fp_arc.stroke.width
});
continue;
}
const tscircuitLayer = convertKicadLayerToTscircuitLa