UNPKG

ducki

Version:

Simple CLI for working with KiCad projects.

220 lines 7.99 kB
import fs from "fs"; import { dirname, join, resolve as _resolve } from "path"; import { randomBytes } from "crypto"; import GIFEncoder from "gif-encoder-2"; import { PNG } from "pngjs"; import { generateSpinFrames } from "./quat.js"; import { run } from "./run.js"; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); export function resolve(...path) { return _resolve(__dirname, "..", ...path); } function flatXYZ(xyz) { if (xyz == null) { return "0,0,0"; } return "'" + [ (xyz.x || 0).toFixed(2), (xyz.y || 0).toFixed(2), (xyz.z || 0).toFixed(2), ].join(",") + "'"; } function normalizeSize(size) { if (size == null) { return { width: 512, height: 512 }; } let { width, height } = size; if (width != null) { if (height == null) { height = width; } } else if (height == null) { if (width == null) { width = height; } } if (width == null) { width = 512; } if (height == null) { height = 512; } return { width, height }; } function searchDir(folder, ext) { ext = "." + ext; const result = []; for (const filename of fs.readdirSync(folder)) { if (filename[0] !== "_" && filename[0] !== "." && filename.endsWith(ext)) { result.push(filename); } } return result; } export class Project { constructor(cmd, projectFolder) { this.cmd = resolve(cmd); this.projectFolder = projectFolder; } _searchDir(ext) { return searchDir(this.projectFolder, ext); } get projectName() { const filename = this.projectFilename; return filename.substring(0, filename.length - ".kicad_pro".length); } get projectFilename() { const files = this._searchDir("kicad_pro"); if (files.length === 0) { throw new Error(`No project file found in ${JSON.stringify(this.projectFolder)}`); } if (files.length > 1) { throw new Error(`Too many project files found in ${JSON.stringify(this.projectFolder)}`); } return join(this.projectFolder, files[0]); ; } get pcbFilename() { const files = this._searchDir("kicad_pcb"); if (files.length === 0) { throw new Error(`No PCB file found in ${JSON.stringify(this.projectFolder)}`); } if (files.length > 1) { throw new Error(`Too many PCB files found in ${JSON.stringify(this.projectFolder)}`); } return join(this.projectFolder, files[0]); ; } get schFilename() { const filename = `${this.projectName}.kicad_sch`; if (this._searchDir("kicad_sch").indexOf(filename) >= 0) { return filename; } throw new Error("could not find root schematic?"); } mv(src, dst) { src = join(this.projectFolder, src); dst = join(this.projectFolder, dst); fs.renameSync(src, dst); } plot(folder, options) { if (options == null) { options = {}; } console.log("GENERATING:", folder); const args = [ "sch", "export", "svg", "--output", ".ducki-tmp" ]; args.push(this.schFilename); fs.mkdirSync(folder, { recursive: true }); const result = run(this.cmd, args); for (const line of result.stdout.split("\n")) { const prefix = "Plotted to '"; const suffix = "'."; if (!line.startsWith(prefix)) { continue; } const filename = line.substring(prefix.length, line.length - suffix.length); // Get the real filename (generally more concise and unique) const data = fs.readFileSync(filename).toString(); const match = data.match(/<desc>File: (.*?).kicad_sch<\/desc>/i); if (!match) { throw new Error(`missing desc`); } let name = match[1]; name = (name === this.projectName) ? "sch.svg" : (`sch-${name}.svg`); console.log("GENERATING:", join(folder, name)); fs.renameSync(filename, join(this.projectFolder, folder, name)); } } _render(filename, options) { filename = _resolve(this.projectFolder, filename); if (!filename.endsWith(".png") && !filename.endsWith(".jpg")) { throw new Error("Invalid image filename ${ JSON.stringify(filename) }"); } const args = [ "pcb", "render", "--output", filename ]; if (options.perspective) { args.push("--perspective"); } if (options.zoom) { args.push("--zoom", String(options.zoom)); } // if (options.pivot) { args.push("--pivot", flatXYZ(options.pivot)); } if (options.rotate) { args.push("--rotate", flatXYZ(options.rotate)); } // if (options.pan) { args.push("--pan", flatXYZ(options.pan)); } // @TODO: Why does Kicad always render images a bit pixels shy? const size = normalizeSize(options.size); args.push("--width", String(size.width)); args.push("--height", String(size.height)); args.push(this.pcbFilename); fs.mkdirSync(dirname(filename), { recursive: true }); const result = run(this.cmd, args); if (result.stdout.indexOf("Successfully created 3D render image") === -1) { console.log({ args, status: result.status, stdout: result.stdout, stderr: result.stderr }); throw new Error("Failed to render PCB"); } } render(filename, options) { console.log("GENERATING:", filename); return this._render(filename, options ?? {}); } flyaround(filename, options) { if (options == null) { options = {}; } console.log("GENERATING:", filename); const perspective = options.perspective ?? false; const zoom = options.zoom ?? 0.8; filename = _resolve(this.projectFolder, filename); const dr = 5; const size = normalizeSize(options.size); const tmp = `.ducki-tmp/temp-${randomBytes(16).toString("hex")}.png`; const getImage = (rotate) => { this._render(tmp, { perspective, rotate, size, zoom }); const data = fs.readFileSync(tmp); const img = PNG.sync.read(data); const bg = [0xb1, 0xb1, 0xc4]; // @TODO: expose in CLI for (let i = 0; i < img.data.length; i += 4) { const a = img.data[i + 3] / 255; if (a == 0) { img.data[i + 0] = bg[0]; img.data[i + 1] = bg[1]; img.data[i + 2] = bg[2]; } else if (a < 1) { img.data[i + 0] = Math.trunc((a * img.data[i + 0]) + ((1 - a) * bg[0])); img.data[i + 1] = Math.trunc((a * img.data[i + 1]) + ((1 - a) * bg[1])); img.data[i + 2] = Math.trunc((a * img.data[i + 2]) + ((1 - a) * bg[2])); } } return img; }; const rot = Object.assign({ x: 0, y: 0, z: 0 }, options.rotate); const rots = generateSpinFrames(rot, 360, 360 / dr); const img = getImage(rots[0]); const encoder = new GIFEncoder(img.width, img.height, 'neuquant', true); encoder.start(); encoder.setRepeat(0); encoder.setDelay(35); encoder.setQuality(10); encoder.addFrame(img.data); for (let i = 1; i < rots.length; i++) { encoder.addFrame(getImage(rots[i]).data); } encoder.finish(); fs.writeFileSync(filename, encoder.out.getData()); } } //# sourceMappingURL=project.js.map