kipm
Version:
KiCad component package manager
727 lines (641 loc) • 24.9 kB
JavaScript
// export_kicad_footprint.js
// Global imports
const { acos, cos, sin, sqrt, PI } = Math;
// Import required EasyEDA and KiCad models and constants
const { ee_footprint, convertToMm } = require("../easyeda/parametersEasyeda");
const {
KiFootprintInfo,
Ki3dModel,
Ki3dModelBase,
KiFootprint,
KiFootprintPad,
KiFootprintTrack,
KiFootprintHole,
KiFootprintVia,
KiFootprintCircle,
KiFootprintRectangle,
KiFootprintArc,
KiFootprintText,
KI_MODULE_INFO,
KI_FP_TYPE,
KI_REFERENCE,
KI_PACKAGE_VALUE,
KI_FAB_REF,
KI_LINE,
KI_PAD,
KI_HOLE,
KI_VIA,
KI_CIRCLE,
KI_ARC,
KI_TEXT,
KI_MODEL_3D,
KI_END_FILE, KI_RECT,
} = require("./parametersKicadFootprint");
const { KI_PAD_SHAPE, KI_PAD_LAYER, KI_PAD_LAYER_THT, KI_LAYERS } = require("./parametersKicadFootprint");
function pyStr(num) {
let s = num.toString();
// If the number is finite and doesn't already include a decimal point, append ".0"
if (isFinite(num) && !s.includes('.')) {
s += '.0';
}
return s;
}
// ---------------------------------------
// A simple templating function to handle placeholders like {key} and {key:.2f}
function formatTemplate(template, data) {
return template.replace(/{(\w+)(:[^}]+)?}/g, (match, key, formatSpec) => {
let value = data[key];
if (value === undefined) return match;
if (formatSpec) {
// Convert value to a number if possible.
const num = Number(value);
if (!isNaN(num)) {
value = num;
// Extract the desired number of decimals from the format specifier.
const m = formatSpec.match(/\.([0-9]+)f/);
if (m) {
const decimals = parseInt(m[1], 10);
// Preserve negative zero if needed.
const isNegZero = (1 / value === -Infinity);
value = value.toFixed(decimals);
if (isNegZero && !value.startsWith("-")) {
value = "-" + value;
}
}
}
}
return value;
});
}
function toRadians(n) {
return (n / 180.0) * PI;
}
function toDegrees(n) {
return (n / PI) * 180.0;
}
// Elliptical arc implementation (based on SVG specification notes)
function computeArc(
start_x,
start_y,
radius_x,
radius_y,
angle,
large_arc_flag,
sweep_flag,
end_x,
end_y
) {
// Compute the half distance between the current and final point
const dx2 = (start_x - end_x) / 2.0;
const dy2 = (start_y - end_y) / 2.0;
// Convert angle from degrees to radians
angle = toRadians(angle % 360.0);
const cos_angle = cos(angle);
const sin_angle = sin(angle);
// Step 1: Compute (x1, y1)
const x1 = cos_angle * dx2 + sin_angle * dy2;
const y1 = -sin_angle * dx2 + cos_angle * dy2;
// Ensure radii are large enough
radius_x = Math.abs(radius_x);
radius_y = Math.abs(radius_y);
let Pradius_x = radius_x * radius_x;
let Pradius_y = radius_y * radius_y;
const Px1 = x1 * x1;
const Py1 = y1 * y1;
// Check that radii are large enough
let radiiCheck =
Pradius_x !== 0 && Pradius_y !== 0 ? Px1 / Pradius_x + Py1 / Pradius_y : 0;
if (radiiCheck > 1) {
const factor = sqrt(radiiCheck);
radius_x = factor * radius_x;
radius_y = factor * radius_y;
Pradius_x = radius_x * radius_x;
Pradius_y = radius_y * radius_y;
}
// Step 2: Compute (cx1, cy1)
const sign = large_arc_flag === sweep_flag ? -1 : 1;
let sq = 0;
if (Pradius_x * Py1 + Pradius_y * Px1 > 0) {
sq =
(Pradius_x * Pradius_y - Pradius_x * Py1 - Pradius_y * Px1) /
(Pradius_x * Py1 + Pradius_y * Px1);
}
sq = Math.max(sq, 0);
const coef = sign * sqrt(sq);
const cx1 = coef * ((radius_x * y1) / radius_y);
const cy1 = radius_x !== 0 ? coef * -((radius_y * x1) / radius_x) : 0;
// Step 3: Compute (cx, cy) from (cx1, cy1)
const sx2 = (start_x + end_x) / 2.0;
const sy2 = (start_y + end_y) / 2.0;
const cx = sx2 + (cos_angle * cx1 - sin_angle * cy1);
const cy = sy2 + (sin_angle * cx1 + cos_angle * cy1);
// Step 4: Compute the angle extent (dangle)
const ux = radius_x !== 0 ? (x1 - cx1) / radius_x : 0;
const uy = radius_y !== 0 ? (y1 - cy1) / radius_y : 0;
const vx = radius_x !== 0 ? (-x1 - cx1) / radius_x : 0;
const vy = radius_y !== 0 ? (-y1 - cy1) / radius_y : 0;
const n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
const p = ux * vx + uy * vy;
const sign2 = ux * vy - uy * vx < 0 ? -1 : 1;
let angle_extent;
if (n !== 0) {
const ratio = p / n;
if (Math.abs(ratio) < 1) {
angle_extent = toDegrees(sign2 * acos(ratio));
} else {
angle_extent = 360 + 359; // Fallback value
}
} else {
angle_extent = 360 + 359;
}
if (!sweep_flag && angle_extent > 0) {
angle_extent -= 360;
} else if (sweep_flag && angle_extent < 0) {
angle_extent += 360;
}
const angleExtent_sign = angle_extent < 0 ? 1 : -1;
angle_extent = (Math.abs(angle_extent) % 360) * angleExtent_sign;
return [cx, cy, angle_extent];
}
// ---------------------------------------
function fpToKi(dim) {
if (dim !== "" && dim !== null && !isNaN(parseFloat(dim))) {
return parseFloat((parseFloat(dim) * 10 * 0.0254).toFixed(2));
}
return dim;
}
// ---------------------------------------
function drillToKi(hole_radius, hole_length, pad_height, pad_width) {
if (
hole_radius > 0 &&
hole_length !== "" &&
hole_length !== null &&
hole_length !== 0
) {
const max_distance_hole = Math.max(hole_radius * 2, hole_length);
const pos_0 = pad_height - max_distance_hole;
const pos_90 = pad_width - max_distance_hole;
const max_distance = Math.max(pos_0, pos_90);
if (max_distance === pos_0) {
return `(drill oval ${hole_radius * 2} ${hole_length})`;
} else {
return `(drill oval ${hole_length} ${hole_radius * 2})`;
}
}
if (hole_radius > 0) {
return `(drill ${2 * hole_radius})`;
}
return "";
}
// ---------------------------------------
function angleToKi(rotation) {
if (!isNaN(rotation)) {
return rotation > 180 ? -(360 - rotation) : rotation;
}
return "";
}
// ---------------------------------------
function rotate(x, y, degrees) {
const radians = (degrees / 180) * 2 * PI;
const new_x = x * cos(radians) - y * sin(radians);
const new_y = x * sin(radians) + y * cos(radians);
return [new_x, new_y];
}
// ---------------------------------------
// ExporterFootprintKicad class
class ExporterFootprintKicad {
constructor(footprint, model_3d, translation) {
this.model_3d = model_3d;
this.input = footprint;
this.translation = translation;
if (!(this.input instanceof ee_footprint)) {
console.error("Unsupported conversion");
} else {
this.generateKicadFootprint();
}
}
generateKicadFootprint() {
// Convert dimensions from EasyEDA to KiCad (assumes convert_to_mm method exists)
this.input.bbox.convert_to_mm();
const fields = [
this.input.pads,
this.input.tracks,
this.input.holes,
this.input.vias,
this.input.circles,
this.input.rectangles,
this.input.texts,
];
fields.forEach(fieldArray => {
fieldArray.forEach(field => {
if (typeof field.convert_to_mm === "function") {
field.convert_to_mm();
}
});
});
// Ensure input.info exists. If not, set defaults.
const infoData = this.input.info || { name: "unknown", fp_type: "tht" };
const ki_info = new KiFootprintInfo({
name: infoData.name,
fp_type: infoData.fp_type,
});
let ki_3d_model_info = null;
if (this.model_3d !== null && this.model_3d !== undefined) {
this.model_3d.convert_to_mm();
// @todo console.log(' Need to fix offset here...');
// Old easyeda2kicad z calculation:
// z: this.input.info && this.input.info.fp_type === "smd" ? -parseFloat(this.model_3d.translation.z.toFixed(2)) : 0,
if (!this.translation.fixedZHere) {
this.translation.fixedZHere = true;
this.translation.z += parseFloat(this.model_3d.translation.z.toFixed(2));
}
ki_3d_model_info = new Ki3dModel({
name: this.model_3d.name,
translation: this.translation,
rotation: new Ki3dModelBase({
x: (360 - this.model_3d.rotation.x) % 360,
y: (360 - this.model_3d.rotation.y) % 360,
z: (360 - this.model_3d.rotation.z) % 360,
}),
raw_wrl: null,
});
}
this.output = new KiFootprint({
info: ki_info,
model_3d: ki_3d_model_info,
});
// For pads
this.input.pads.forEach(ee_pad => {
const ki_pad = new KiFootprintPad({
type: ee_pad.hole_radius > 0 ? "thru_hole" : "smd",
shape: KI_PAD_SHAPE.hasOwnProperty(ee_pad.shape)
? KI_PAD_SHAPE[ee_pad.shape]
: "custom",
pos_x: ee_pad.center_x - this.input.bbox.x,
pos_y: ee_pad.center_y - this.input.bbox.y,
width: Math.max(ee_pad.width, 0.01),
height: Math.max(ee_pad.height, 0.01),
layers:
(ee_pad.hole_radius <= 0 ? KI_PAD_LAYER : KI_PAD_LAYER_THT)[
ee_pad.layer_id
] || "",
number: ee_pad.number,
drill: 0.0,
orientation: angleToKi(ee_pad.rotation),
polygon: "",
});
ki_pad.drill = drillToKi(
ee_pad.hole_radius,
ee_pad.hole_length,
ki_pad.height,
ki_pad.width
);
if (ki_pad.number.includes("(") && ki_pad.number.includes(")")) {
const match = ki_pad.number.match(/\(([^)]+)\)/);
if (match) {
ki_pad.number = match[1];
}
}
// For custom polygon
if (ki_pad.shape === "custom") {
const point_list = ee_pad.points.split(" ").map(fpToKi);
if (point_list.length <= 0) {
console.warn(
`PAD ${ee_pad.id} is a polygon, but has no points defined`
);
} else {
// Set the pad width and height to the smallest value allowed by KiCad.
ki_pad.width = 0.005;
ki_pad.height = 0.005;
// The points of the polygon always seem to correspond to coordinates when orientation=0.
ki_pad.orientation = 0;
// Generate polygon with coordinates relative to the base pad's position.
let path = "";
for (let i = 0; i < point_list.length; i += 2) {
const x_val = parseFloat(
(point_list[i] - this.input.bbox.x - ki_pad.pos_x).toFixed(2)
);
const y_val = parseFloat(
(point_list[i + 1] - this.input.bbox.y - ki_pad.pos_y).toFixed(2)
);
path += `(xy ${x_val} ${y_val})`;
}
ki_pad.polygon = `\n\t\t(primitives \n\t\t\t(gr_poly \n\t\t\t\t(pts ${path}\n\t\t\t\t) \n\t\t\t\t(width 0.1) \n\t\t\t)\n\t\t)\n\t`;
}
}
this.output.pads.push(ki_pad);
});
// For tracks
this.input.tracks.forEach(ee_track => {
const ki_track = new KiFootprintTrack({
layers: KI_PAD_LAYER.hasOwnProperty(ee_track.layer_id)
? KI_PAD_LAYER[ee_track.layer_id]
: "F.Fab",
stroke_width: Math.max(ee_track.stroke_width, 0.01),
});
const point_list = ee_track.points.split(" ").map(fpToKi);
for (let i = 0; i < point_list.length - 3; i += 2) {
ki_track.points_start_x.push(
parseFloat((point_list[i] - this.input.bbox.x).toFixed(2))
);
ki_track.points_start_y.push(
parseFloat((point_list[i + 1] - this.input.bbox.y).toFixed(2))
);
ki_track.points_end_x.push(
parseFloat((point_list[i + 2] - this.input.bbox.x).toFixed(2))
);
ki_track.points_end_y.push(
parseFloat((point_list[i + 3] - this.input.bbox.y).toFixed(2))
);
}
this.output.tracks.push(ki_track);
});
// For holes
this.input.holes.forEach(ee_hole => {
const ki_hole = new KiFootprintHole({
pos_x: ee_hole.center_x - this.input.bbox.x,
pos_y: ee_hole.center_y - this.input.bbox.y,
size: ee_hole.radius * 2,
});
this.output.holes.push(ki_hole);
});
// For vias
this.input.vias.forEach(ee_via => {
const ki_via = new KiFootprintVia({
pos_x: ee_via.center_x - this.input.bbox.x,
pos_y: ee_via.center_y - this.input.bbox.y,
size: ee_via.radius * 2,
diameter: ee_via.diameter,
});
this.output.vias.push(ki_via);
});
// For circles
this.input.circles.forEach(ee_circle => {
const ki_circle = new KiFootprintCircle({
cx: ee_circle.cx - this.input.bbox.x,
cy: ee_circle.cy - this.input.bbox.y,
end_x: 0.0,
end_y: 0.0,
layers: KI_LAYERS.hasOwnProperty(ee_circle.layer_id)
? KI_LAYERS[ee_circle.layer_id]
: "F.Fab",
stroke_width: Math.max(ee_circle.stroke_width, 0.01),
});
ki_circle.end_x = ki_circle.cx + ee_circle.radius;
ki_circle.end_y = ki_circle.cy;
this.output.circles.push(ki_circle);
});
// For rectangles
this.input.rectangles.forEach(ee_rectangle => {
const ki_rectangle = new KiFootprintRectangle({
layers: KI_PAD_LAYER.hasOwnProperty(ee_rectangle.layer_id)
? KI_PAD_LAYER[ee_rectangle.layer_id]
: "F.Fab",
stroke_width: Math.max(ee_rectangle.stroke_width, 0.01),
});
const start_x = ee_rectangle.x - this.input.bbox.x;
const start_y = ee_rectangle.y - this.input.bbox.y;
const width = ee_rectangle.width;
const height = ee_rectangle.height;
ki_rectangle.points_start_x = [
start_x,
start_x + width,
start_x + width,
start_x,
];
ki_rectangle.points_start_y = [
start_y,
start_y,
start_y + height,
start_y + height,
];
ki_rectangle.points_end_x = [
start_x + width,
start_x + width,
start_x,
start_x,
];
ki_rectangle.points_end_y = [
start_y,
start_y + height,
start_y + height,
start_y,
];
this.output.rectangles.push(ki_rectangle);
});
// For arcs
this.input.arcs.forEach(ee_arc => {
let arc_path = ee_arc.path
.replace(/,/g, " ")
.replace("M ", "M")
.replace("A ", "A");
// Extract the start coordinates from the arc string.
const mIndex = arc_path.indexOf("A");
const startCoords = arc_path.substring(1, mIndex).trim().split(" ");
let start_x = fpToKi(startCoords[0]) - this.input.bbox.x;
let start_y = fpToKi(startCoords[1]) - this.input.bbox.y;
let arcParameters = arc_path.substring(mIndex + 1).replace(/ /g, " ").trim();
const parts = arcParameters.split(" ");
const [svg_rx, svg_ry, x_axis_rotation, large_arc, sweep, end_x_str, end_y_str] =
parts;
const [rx, ry] = rotate(fpToKi(svg_rx), fpToKi(svg_ry), 0);
const end_x = fpToKi(end_x_str) - this.input.bbox.x;
const end_y = fpToKi(end_y_str) - this.input.bbox.y;
let cx = 0.0,
cy = 0.0,
extent = 0.0;
if (ry !== 0) {
[cx, cy, extent] = computeArc(
start_x,
start_y,
rx,
ry,
parseFloat(x_axis_rotation),
large_arc === "1",
sweep === "1",
end_x,
end_y
);
}
const ki_arc = new KiFootprintArc({
start_x: cx,
start_y: cy,
end_x: end_x,
end_y: end_y,
angle: extent,
layers: KI_LAYERS.hasOwnProperty(ee_arc.layer_id)
? KI_LAYERS[ee_arc.layer_id]
: "F.Fab",
stroke_width: Math.max(fpToKi(ee_arc.stroke_width), 0.01),
});
this.output.arcs.push(ki_arc);
});
// For texts
this.input.texts.forEach(ee_text => {
const ki_text = new KiFootprintText({
pos_x: ee_text.center_x - this.input.bbox.x,
pos_y: ee_text.center_y - this.input.bbox.y,
orientation: angleToKi(ee_text.rotation),
text: ee_text.text,
layers: KI_LAYERS.hasOwnProperty(ee_text.layer_id)
? KI_LAYERS[ee_text.layer_id]
: "F.Fab",
font_size: Math.max(ee_text.font_size, 1),
thickness: Math.max(ee_text.stroke_width, 0.01),
display: ee_text.is_displayed === false ? " hide" : "",
mirror: "",
});
if (ee_text.type === "N") {
ki_text.layers = ki_text.layers.replace(".SilkS", ".Fab");
}
ki_text.mirror = ki_text.layers.startsWith("B") ? " mirror" : "";
this.output.texts.push(ki_text);
});
// calculate the bounding box for the footprint here
}
getKiFootprint() {
return this.output;
}
// Add new method to get content without writing to file
getContent(model_3d_path = "${KIPRJMOD}") {
const ki = this.output;
let ki_lib = "";
ki_lib += formatTemplate(KI_MODULE_INFO, {
package_lib: "easyeda2kicad",
package_name: ki.info.name,
edit: "5DC5F6A4",
});
if (ki.info.fp_type) {
ki_lib += formatTemplate(KI_FP_TYPE, {
component_type: ki.info.fp_type === "smd" ? "smd" : "through_hole",
});
}
// Get y_min and y_max to position component info.
const y_values = ki.pads.map(pad => pad.pos_y);
const y_low = Math.min(...y_values);
const y_high = Math.max(...y_values);
ki_lib += formatTemplate(KI_REFERENCE, { pos_x: "0", pos_y: pyStr(y_low - 4) });
ki_lib += formatTemplate(KI_PACKAGE_VALUE, {
package_name: ki.info.name,
pos_x: "0",
pos_y: pyStr(y_high + 4),
});
ki_lib += KI_FAB_REF;
// ---------------------------------------
let minY = Infinity;
let maxY = -Infinity;
let minX = Infinity;
let maxX = -Infinity;
let xPts = [];
let yPts = [];
const combinedTracks = ki.tracks.concat(ki.rectangles);
combinedTracks.forEach(track => {
for (let i = 0; i < track.points_start_x.length; i++) {
ki_lib += formatTemplate(KI_LINE, {
start_x: track.points_start_x[i],
start_y: track.points_start_y[i],
end_x: track.points_end_x[i],
end_y: track.points_end_y[i],
layers: track.layers,
stroke_width: track.stroke_width,
});
}
xPts = xPts.concat(track.points_start_x.concat(track.points_end_x));
yPts = yPts.concat(track.points_start_y.concat(track.points_end_y));
});
ki.pads.forEach(pad => {
ki_lib += formatTemplate(KI_PAD, pad);
xPts.push(pad.pos_x + pad.width / 2);
xPts.push(pad.pos_x - pad.width / 2);
yPts.push(pad.pos_y + pad.height / 2);
yPts.push(pad.pos_y - pad.height / 2);
});
ki.holes.forEach(hole => {
ki_lib += formatTemplate(KI_HOLE, hole);
xPts.push(hole.center_x + hole.radius);
xPts.push(hole.center_x - hole.radius);
yPts.push(hole.center_y + hole.radius);
yPts.push(hole.center_y - hole.radius);
});
ki.vias.forEach(via => {
ki_lib += formatTemplate(KI_VIA, via);
xPts.push(via.center_x + via.radius);
xPts.push(via.center_x - via.radius);
yPts.push(via.center_y + via.radius);
yPts.push(via.center_y - via.radius);
});
ki.circles.forEach(circle => {
ki_lib += formatTemplate(KI_CIRCLE, circle);
const radius = (circle.cx - circle.end_x);
if (radius > 1) {
xPts.push(circle.cx + radius);
xPts.push(circle.cx - radius);
yPts.push(circle.cy + radius);
yPts.push(circle.cy - radius);
}
});
ki.arcs.forEach(arc => {
ki_lib += formatTemplate(KI_ARC, arc);
const bbox = arc.getBoundingBox();
if (bbox.max_x - bbox.min_x > 2 && bbox.max_y - bbox.min_y > 2) {
xPts.push(bbox.max_x);
xPts.push(bbox.min_x);
yPts.push(bbox.max_y);
yPts.push(bbox.min_y);
}
});
ki.texts.forEach(text => {
ki_lib += formatTemplate(KI_TEXT, text);
});
// Update max/min values
for (const p of xPts) {
if (+p > maxX) {
maxX = +p;
}
if (+p < minX) {
minX = +p;
}
}
for (const p of yPts) {
if (+p > maxY) {
maxY = +p;
}
if (+p < minY) {
minY = +p;
}
}
const hasFiniteMaxMinValues = (isFinite(minY) && isFinite(minX) && isFinite(maxY) && isFinite(maxX));
if (ki.model_3d !== null && ki.model_3d !== undefined) {
ki_lib += formatTemplate(KI_MODEL_3D, {
file_3d: `/${model_3d_path}/${ki.model_3d.name}.wrl`,
pos_x: this.translation.x + (hasFiniteMaxMinValues ? (minX + maxX) / 2 : 0),
pos_y: this.translation.y - (hasFiniteMaxMinValues ? (minY + maxY) / 2 : 0),
pos_z: this.translation.z,
rot_x: ki.model_3d.rotation.x,
rot_y: ki.model_3d.rotation.y,
rot_z: ki.model_3d.rotation.z,
});
}
if (hasFiniteMaxMinValues) {
const margin = 0.5;
ki_lib += formatTemplate(KI_RECT, {
start_x: minX - margin,
start_y: minY - margin,
end_x: maxX + margin,
end_y: maxY + margin,
layers: 'F.CrtYd',
stroke_width: '0.05',
});
}
ki_lib += KI_END_FILE;
return ki_lib;
}
// Original export method now uses getContent()
export(footprint_full_path, model_3d_path) {
const content = this.getContent(model_3d_path);
fs.writeFileSync(footprint_full_path, content, { encoding: "utf8" });
}
}
module.exports = {
ExporterFootprintKicad,
};