ducki
Version:
Simple CLI for working with KiCad projects.
220 lines • 7.99 kB
JavaScript
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