UNPKG

kipm

Version:

KiCad component package manager

729 lines (662 loc) 22.1 kB
// parameters_kicad_symbol.js function dedent(text) { // Split the text into lines const lines = text.split('\n'); // Determine the minimum indentation (ignoring blank lines) let minIndent = Infinity; for (const line of lines) { const trimmed = line.trim(); if (trimmed.length === 0) continue; // Skip empty or whitespace-only lines const match = line.match(/^(\s*)/); if (match) { const indentLength = match[1].length; if (indentLength < minIndent) { minIndent = indentLength; } } } // If no non-blank lines were found, return the original text if (minIndent === Infinity) { return text; } // Remove the common leading whitespace from each line const dedented = lines.map(line => { // Only remove if the line has at least minIndent whitespace at the beginning return line.startsWith(' '.repeat(minIndent)) ? line.slice(minIndent) : line; }).join('\n'); return dedented; } function toFixedPreserveSign(num, decimals) { // Check for negative zero using Object.is if (Object.is(num, -0)) { // Return a string with the negative sign explicitly return '-' + (0).toFixed(decimals); } return num.toFixed(decimals); } // -------------------- ENUMS & CONFIGURATIONS -------------------- // KicadVersion const KicadVersion = { v5: "v5", v6: "v6", v6_99: "v6_99", }; Object.freeze(KicadVersion); // KiPinType const KiPinType = { _input: "_input", output: "output", bidirectional: "bidirectional", tri_state: "tri_state", passive: "passive", free: "free", unspecified: "unspecified", power_in: "power_in", power_out: "power_out", open_collector: "open_collector", open_emitter: "open_emitter", no_connect: "no_connect", }; Object.freeze(KiPinType); // KiPinStyle const KiPinStyle = { line: "line", inverted: "inverted", clock: "clock", inverted_clock: "inverted_clock", input_low: "input_low", clock_low: "clock_low", output_low: "output_low", edge_clock_high: "edge_clock_high", non_logic: "non_logic", }; Object.freeze(KiPinStyle); // KiBoxFill const KiBoxFill = { none: "none", outline: "outline", background: "background", }; Object.freeze(KiBoxFill); // Config V5 (dimensions in mil) const KiExportConfigV5 = { PIN_LENGTH: 100, PIN_SPACING: 100, PIN_NUM_SIZE: 50, PIN_NAME_SIZE: 50, PIN_NAME_OFFSET: 40, DEFAULT_BOX_LINE_WIDTH: 0, FIELD_FONT_SIZE: 60, FIELD_OFFSET_START: 200, FIELD_OFFSET_INCREMENT: 100, }; Object.freeze(KiExportConfigV5); // Mapping dictionaries for V5 const ki_pin_type_v5_format = { [KiPinType._input]: "I", [KiPinType.output]: "O", [KiPinType.bidirectional]: "B", [KiPinType.tri_state]: "T", [KiPinType.passive]: "P", [KiPinType.free]: "U", [KiPinType.unspecified]: "U", [KiPinType.power_in]: "W", [KiPinType.power_out]: "W", [KiPinType.open_collector]: "C", [KiPinType.open_emitter]: "E", [KiPinType.no_connect]: "N", }; const ki_pin_style_v5_format = { [KiPinStyle.line]: "", [KiPinStyle.inverted]: "I", [KiPinStyle.clock]: "C", [KiPinStyle.inverted_clock]: "F", [KiPinStyle.input_low]: "L", [KiPinStyle.clock_low]: "CL", [KiPinStyle.output_low]: "V", [KiPinStyle.edge_clock_high]: "C", [KiPinStyle.non_logic]: "X", }; const ki_pin_orientation_v5_format = { "0": "L", "90": "D", "180": "R", "270": "U", }; const ki_box_fill_v5_format = { [KiBoxFill.none]: "N", [KiBoxFill.outline]: "F", [KiBoxFill.background]: "f", }; // Config V6 (dimensions in mm) const KiExportConfigV6 = { PIN_LENGTH: 2.54, PIN_SPACING: 2.54, PIN_NUM_SIZE: 1.27, PIN_NAME_SIZE: 1.27, DEFAULT_BOX_LINE_WIDTH: 0, PROPERTY_FONT_SIZE: 1.27, FIELD_OFFSET_START: 5.08, FIELD_OFFSET_INCREMENT: 2.54, }; Object.freeze(KiExportConfigV6); // -------------------- HELPER FUNCTIONS -------------------- // Simple indent helper that adds a given number of spaces to each line. function indent(str, numSpaces) { const spaces = " ".repeat(numSpaces); return str .split("\n") .map((line) => (line !== "" ? '' + spaces + '' + line : line)) .join("\n"); } function sanitize_fields(name) { return name.replace(/ /g, "").replace(/\//g, "_"); } function apply_text_style(text, kicad_version) { if (text.endsWith("#")) { text = kicad_version === KicadVersion.v6 ? `~{${text.slice(0, -1)}}` : `~${text.slice(0, -1)}~`; } return text; } function apply_pin_name_style(pin_name, kicad_version) { return pin_name .split("/") .map((txt) => apply_text_style(txt, kicad_version)) .join("/"); } // -------------------- INFO HEADER -------------------- class KiSymbolInfo { constructor({ name, prefix, package: pkg, manufacturer, datasheet, lcsc_id, jlc_id, y_low = 0, y_high = 0, }) { this.name = name; this.prefix = prefix; this.package = pkg; this.manufacturer = manufacturer; this.datasheet = datasheet; this.lcsc_id = lcsc_id; this.jlc_id = jlc_id; this.y_low = y_low; this.y_high = y_high; } export_v5() { let field_offset_y = KiExportConfigV5.FIELD_OFFSET_START; let header = [ `DEF ${sanitize_fields(this.name)} ${this.prefix} 0 ${KiExportConfigV5.PIN_NAME_OFFSET} Y Y 1 L N`, `F0 "${this.prefix}" 0 ${this.y_high + field_offset_y} ${KiExportConfigV5.FIELD_FONT_SIZE} H V C CNN`, `F1 "${this.name}" 0 ${this.y_low - field_offset_y} ${KiExportConfigV5.FIELD_FONT_SIZE} H V C CNN`, ]; if (this.package) { field_offset_y += KiExportConfigV5.FIELD_OFFSET_INCREMENT; header.push( `F2 "${this.package}" 0 ${this.y_low - field_offset_y} ${KiExportConfigV5.FIELD_FONT_SIZE} H I C CNN` ); } if (this.datasheet) { field_offset_y += KiExportConfigV5.FIELD_OFFSET_INCREMENT; header.push( `F3 "${this.datasheet}" 0 ${this.y_low - field_offset_y} ${KiExportConfigV5.FIELD_FONT_SIZE} H I C CNN` ); } if (this.manufacturer) { header.push(`F4 "${this.manufacturer}" 0 0 0 H I C CNN "Manufacturer"`); } if (this.lcsc_id) { header.push(`F6 "${this.lcsc_id}" 0 0 0 H I C CNN "LCSC Part"`); } if (this.jlc_id) { header.push(`F7 "${this.jlc_id}" 0 0 0 H I C CNN "JLC Part"`); } header.push("DRAW\n"); return header.join("\n"); } export_v6() { const property_template = ( key, value, id_, pos_y, font_size, style, hide ) => { return dedent(` (property "${key}" "${value}" (id ${id_}) (at 0 ${parseFloat(pos_y).toFixed(2)} 0) (effects (font (size ${font_size} ${font_size}) ${style}) ${hide}) ) `); }; let field_offset_y = KiExportConfigV6.FIELD_OFFSET_START; let header = [ property_template( "Reference", this.prefix, 0, this.y_high + field_offset_y, KiExportConfigV6.PROPERTY_FONT_SIZE, "", "" ), property_template( "Value", this.name, 1, this.y_low - field_offset_y, KiExportConfigV6.PROPERTY_FONT_SIZE, "", "" ), ]; if (this.package) { field_offset_y += KiExportConfigV6.FIELD_OFFSET_INCREMENT; header.push( property_template( "Footprint", this.package, 2, this.y_low - field_offset_y, KiExportConfigV6.PROPERTY_FONT_SIZE, "", "hide" ) ); } if (this.datasheet) { field_offset_y += KiExportConfigV6.FIELD_OFFSET_INCREMENT; header.push( property_template( "Datasheet", this.datasheet, 3, this.y_low - field_offset_y, KiExportConfigV6.PROPERTY_FONT_SIZE, "", "hide" ) ); } if (this.manufacturer) { field_offset_y += KiExportConfigV6.FIELD_OFFSET_INCREMENT; header.push( property_template( "Manufacturer", this.manufacturer, 4, this.y_low - field_offset_y, KiExportConfigV6.PROPERTY_FONT_SIZE, "", "hide" ) ); } if (this.lcsc_id) { field_offset_y += KiExportConfigV6.FIELD_OFFSET_INCREMENT; header.push( property_template( "LCSC Part", this.lcsc_id, 5, this.y_low - field_offset_y, KiExportConfigV6.PROPERTY_FONT_SIZE, "", "hide" ) ); } if (this.jlc_id) { field_offset_y += KiExportConfigV6.FIELD_OFFSET_INCREMENT; header.push( property_template( "JLC Part", this.jlc_id, 6, this.y_low - field_offset_y, KiExportConfigV6.PROPERTY_FONT_SIZE, "", "hide" ) ); } return header; } } // -------------------- PIN -------------------- class KiSymbolPin { constructor({ name, number, style, length, type, orientation, pos_x, pos_y }) { this.name = name; this.number = number; this.style = style; // Should be one of KiPinStyle values. this.length = length; this.type = type; // Should be one of KiPinType values. this.orientation = orientation; this.pos_x = pos_x; this.pos_y = pos_y; } export_v5() { const orientationKey = String(this.orientation); const orientationFormatted = Object.prototype.hasOwnProperty.call( ki_pin_orientation_v5_format, orientationKey ) ? ki_pin_orientation_v5_format[orientationKey] : ki_pin_orientation_v5_format["0"]; return `X ${apply_pin_name_style(this.name, KicadVersion.v5)} ${this.number} ${this.pos_x} ${this.pos_y} ${Math.round( this.length )} ${orientationFormatted} ${KiExportConfigV5.PIN_NUM_SIZE} ${KiExportConfigV5.PIN_NAME_SIZE} 1 1 ${ki_pin_type_v5_format[this.type]} ${ki_pin_style_v5_format[this.style]}\n`; } export_v6() { // Remove leading underscore if present. const typeName = this.type.startsWith("_") ? this.type.slice(1) : this.type; const styleName = this.style; return dedent(` (pin ${typeName} ${styleName} (at ${toFixedPreserveSign(this.pos_x, 2)} ${toFixedPreserveSign(this.pos_y, 2)} ${((180 + +this.orientation) % 360).toFixed(0)}) (length ${this.length}) (name "${apply_pin_name_style(this.name, KicadVersion.v6)}" (effects (font (size ${KiExportConfigV6.PIN_NAME_SIZE} ${KiExportConfigV6.PIN_NAME_SIZE})))) (number "${this.number}" (effects (font (size ${KiExportConfigV6.PIN_NUM_SIZE} ${KiExportConfigV6.PIN_NUM_SIZE})))) ) `); } } // -------------------- RECTANGLE -------------------- class KiSymbolRectangle { constructor({ pos_x0 = 0, pos_y0 = 0, pos_x1 = 0, pos_y1 = 0 } = {}) { this.pos_x0 = pos_x0; this.pos_y0 = pos_y0; this.pos_x1 = pos_x1; this.pos_y1 = pos_y1; } export_v5() { return `S ${Math.round(this.pos_x0)} ${Math.round(this.pos_y0)} ${Math.round( this.pos_x1 )} ${Math.round(this.pos_y1)} 1 1 ${KiExportConfigV5.DEFAULT_BOX_LINE_WIDTH} ${ki_box_fill_v5_format[KiBoxFill.background]}\n`; } export_v6() { return dedent(` (rectangle (start ${this.pos_x0.toFixed(2)} ${this.pos_y0.toFixed(2)}) (end ${this.pos_x1.toFixed(2)} ${this.pos_y1.toFixed(2)}) (stroke (width ${KiExportConfigV6.DEFAULT_BOX_LINE_WIDTH}) (type default) (color 0 0 0 0)) (fill (type ${KiBoxFill.background})) ) `); } } // -------------------- POLYGON -------------------- class KiSymbolPolygon { constructor({ points = [], points_number = 0, is_closed = false } = {}) { this.points = points; // Array of [x, y] pairs. this.points_number = points_number; this.is_closed = is_closed; } export_v5() { const flatPoints = this.points.flat().join(" "); return `P ${this.points_number} 1 1 ${KiExportConfigV5.DEFAULT_BOX_LINE_WIDTH} ${flatPoints} ${ this.is_closed ? ki_box_fill_v5_format[KiBoxFill.background] : ki_box_fill_v5_format[KiBoxFill.none] }\n`; } export_v6() { const polyline_path = this.points .map((pts) => `(xy ${toFixedPreserveSign(pts[0], 2)} ${toFixedPreserveSign(pts[1], 2)})`) .join(" "); return dedent(` (polyline (pts ${polyline_path} ) (stroke (width ${KiExportConfigV6.DEFAULT_BOX_LINE_WIDTH}) (type default) (color 0 0 0 0)) (fill (type ${this.is_closed ? KiBoxFill.background : KiBoxFill.none})) ) `); } } // -------------------- CIRCLE -------------------- class KiSymbolCircle { constructor({ pos_x = 0, pos_y = 0, radius = 0, background_filling = false } = {}) { this.pos_x = pos_x; this.pos_y = pos_y; this.radius = radius; this.background_filling = background_filling; } export_v5() { return `C ${Math.round(this.pos_x)} ${Math.round(this.pos_y)} ${Math.round( this.radius )} 1 1 ${KiExportConfigV5.DEFAULT_BOX_LINE_WIDTH} ${ this.background_filling ? ki_box_fill_v5_format[KiBoxFill.background] : ki_box_fill_v5_format[KiBoxFill.none] }\n`; } export_v6() { return dedent(` (circle (center ${this.pos_x.toFixed(2)} ${this.pos_y.toFixed(2)}) (radius ${this.radius.toFixed(2)}) (stroke (width ${KiExportConfigV6.DEFAULT_BOX_LINE_WIDTH}) (type default) (color 0 0 0 0)) (fill (type ${this.background_filling ? KiBoxFill.background : KiBoxFill.none})) ) `); } } // -------------------- ARC -------------------- class KiSymbolArc { constructor({ center_x = 0, center_y = 0, radius = 0, angle_start = 0, angle_end = 0, start_x = 0, start_y = 0, middle_x = 0, middle_y = 0, end_x = 0, end_y = 0, } = {}) { this.center_x = center_x; this.center_y = center_y; this.radius = radius; this.angle_start = angle_start; this.angle_end = angle_end; this.start_x = start_x; this.start_y = start_y; this.middle_x = middle_x; this.middle_y = middle_y; this.end_x = end_x; this.end_y = end_y; } export_v5() { return `A ${Math.round(this.center_x)} ${Math.round(this.center_y)} ${Math.round( this.radius )} ${Math.round(this.angle_start * 10)} ${Math.round( this.angle_end * 10 )} 1 1 ${KiExportConfigV5.DEFAULT_BOX_LINE_WIDTH} ${ this.angle_start === this.angle_end ? ki_box_fill_v5_format[KiBoxFill.background] : ki_box_fill_v5_format[KiBoxFill.none] } ${Math.round(this.start_x)} ${Math.round(this.start_y)} ${Math.round( this.end_x )} ${Math.round(this.end_y)}\n`; } export_v6() { return dedent(` (arc (start ${this.start_x.toFixed(2)} ${this.start_y.toFixed(2)}) (mid ${this.middle_x.toFixed(2)} ${this.middle_y.toFixed(2)}) (end ${this.end_x.toFixed(2)} ${this.end_y.toFixed(2)}) (stroke (width ${KiExportConfigV6.DEFAULT_BOX_LINE_WIDTH}) (type default) (color 0 0 0 0)) (fill (type ${this.angle_start === this.angle_end ? KiBoxFill.background : KiBoxFill.none})) ) `); } } // -------------------- BEZIER CURVE -------------------- class KiSymbolBezier { constructor({ points = [], points_number = 0, is_closed = false } = {}) { this.points = points; this.points_number = points_number; this.is_closed = is_closed; } export_v5() { const flatPoints = this.points.flat().join(" "); return `B ${this.points_number} 1 1 ${KiExportConfigV5.DEFAULT_BOX_LINE_WIDTH} ${flatPoints} ${ this.is_closed ? ki_box_fill_v5_format[KiBoxFill.background] : ki_box_fill_v5_format[KiBoxFill.none] }\n`; } export_v6() { const polyline_path = this.points .map((pts) => ` (xy ${pts[0]} ${pts[1]})`) .join(""); return dedent(` (gr_curve (pts${polyline_path}) (stroke (width ${KiExportConfigV6.DEFAULT_BOX_LINE_WIDTH}) (type default) (color 0 0 0 0)) (fill (type ${this.is_closed ? KiBoxFill.background : KiBoxFill.none})) ) `); } } // -------------------- SYMBOL -------------------- class KiSymbol { constructor({ info, pins = [], rectangles = [], circles = [], arcs = [], polygons = [], beziers = [], } = {}) { this.info = info; // Instance of KiSymbolInfo this.pins = pins; // Array of KiSymbolPin this.rectangles = rectangles; // Array of KiSymbolRectangle this.circles = circles; // Array of KiSymbolCircle this.arcs = arcs; // Array of KiSymbolArc this.polygons = polygons; // Array of KiSymbolPolygon this.beziers = beziers; // Array of KiSymbolBezier } export_handler(kicad_version) { if (this.pins.length > 0) { this.info.y_low = Math.min(...this.pins.map(pin => pin.pos_y)); this.info.y_high = Math.max(...this.pins.map(pin => pin.pos_y)); } else { this.info.y_low = 0; this.info.y_high = 0; } const data = {}; for (const key in this) { if (Object.prototype.hasOwnProperty.call(this, key)) { const value = this[key]; if (Array.isArray(value)) { data[key] = value.map((item) => typeof item[`export_v${kicad_version}`] === "function" ? item[`export_v${kicad_version}`]() : "" ); } else if ( value && typeof value[`export_v${kicad_version}`] === "function" ) { data[key] = value[`export_v${kicad_version}`](); } } } return data; } export_v5() { const exportData = this.export_handler("5"); const sym_info = exportData.info; // Gather all graphic items from all arrays (pins, rectangles, circles, arcs, polygons, beziers) const graphicItems = Object.keys(exportData) .filter((key) => key !== "info") .map((key) => exportData[key]) .flat(); return `# # ${sanitize_fields(this.info.name)} # ${sym_info}${graphicItems.join("")}ENDDRAW ENDDEF `; } export_v6() { const exportData = this.export_handler("6"); const sym_info = exportData.info; const sym_pins = exportData.pins; const graphicItems = Object.keys(exportData) .filter((key) => key !== "info" && key !== "pins") .map((key) => exportData[key]) .flat(); const r = indent(sym_info.join("\n"), 8); // console.log(({ r })); const x = ` (symbol "${sanitize_fields(this.info.name)}" (in_bom yes) (on_board yes) ${r} (symbol "${sanitize_fields(this.info.name)}_0_1" ${indent(graphicItems.join("\n"), 10)} ${indent(sym_pins.join("\n"), 10)} ) ) `; // console.log({ x: dedent(x) }); // Use dedent to remove unwanted leading spaces, and indent() to add nested indenting where desired. const a = indent(dedent(x), 2); // console.log({ a }); return a; } export(kicad_version) { let component_data = kicad_version === KicadVersion.v5 ? this.export_v5() : this.export_v6() || ""; // Remove extra blank lines. return component_data.replace(/\n\s*\n/g, "\n"); } } // -------------------- EXPORTS -------------------- module.exports = { KicadVersion, KiPinType, KiPinStyle, KiBoxFill, KiExportConfigV5, ki_pin_type_v5_format, ki_pin_style_v5_format, ki_pin_orientation_v5_format, ki_box_fill_v5_format, KiExportConfigV6, sanitize_fields, apply_text_style, apply_pin_name_style, KiSymbolInfo, KiSymbolPin, KiSymbolRectangle, KiSymbolPolygon, KiSymbolCircle, KiSymbolArc, KiSymbolBezier, KiSymbol, };