UNPKG

saxi

Version:

Drive the AxiDraw pen plotter

255 lines 9.56 kB
/** * Starting point for the server app (Command Line Interface). * Execute also one-off instructions on the device. */ import { readFileSync } from "node:fs"; import { flattenSVG } from "flatten-svg"; import { Window } from "svgdom"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { replan } from "./massager.js"; import { PaperSize } from "./paper-size.js"; import { Device, defaultPlanOptions } from "./planning.js"; import { connectEBB, startServer } from "./server.js"; import { formatDuration } from "./util.js"; function parseSvg(svg) { const window = new Window(); window.document.documentElement.innerHTML = svg; return window.document.documentElement; } /** * Process command arguments. * @param argv */ export function cli(argv) { yargs(hideBin(process.argv)) .strict() .option("hardware", { describe: "select hardware type", choices: ["v3", "brushless", "nextdraw-2234"], default: "v3", coerce: (value) => value, }) .option("device", { alias: "d", describe: "device to connect to", type: "string", }) .command("plot <file>", "plot an svg, then exit", (yargs) => yargs .positional("file", { type: "string", description: "File to plot", }) .option("paper-size", { alias: "s", describe: "Paper size to use", coerce: (value) => { if (Object.hasOwn(PaperSize.standard, value)) { return PaperSize.standard[value]; } const m = /^([0-9]*(?:\.[0-9]+)?)\s*x\s*([0-9]*(?:\.[0-9]+)?)\s*(cm|mm|in)$/i.exec(String(value).trim()); if (m) { return new PaperSize({ x: Number(m[1]), y: Number(m[2]) }); } throw new Error(`Paper size should be a standard size (${Object.keys(PaperSize.standard).join(", ")}) or a custom size such as "100x100mm" or "16x10in"`); }, required: true, }) .option("landscape", { type: "boolean", description: "Place the long side of the paper on the x-axis", }) .option("portrait", { type: "boolean", description: "Place the short side of the paper on the x-axis", }) .option("margin", { describe: "Margin (in mm)", type: "number", default: defaultPlanOptions.marginMm, required: false, }) .option("pen-down-height", { describe: "Pen down height (%)", type: "number", default: defaultPlanOptions.penDownHeight, required: false, }) .option("pen-up-height", { describe: "Pen up height (%)", type: "number", default: defaultPlanOptions.penUpHeight, required: false, }) .option("pen-down-acceleration", { describe: "Acceleration when the pen is down (in mm/s^2)", type: "number", default: defaultPlanOptions.penDownAcceleration, required: false, }) .option("pen-down-max-velocity", { describe: "Maximum velocity when the pen is down (in mm/s)", type: "number", default: defaultPlanOptions.penDownMaxVelocity, required: false, }) .option("pen-down-cornering-factor", { describe: "Cornering factor when the pen is down", type: "number", default: defaultPlanOptions.penDownCorneringFactor, required: false, }) .option("pen-up-acceleration", { describe: "Acceleration when the pen is up (in mm/s^2)", type: "number", default: defaultPlanOptions.penUpAcceleration, required: false, }) .option("pen-up-max-velocity", { describe: "Maximum velocity when the pen is up (in mm/s)", type: "number", default: defaultPlanOptions.penUpMaxVelocity, required: false, }) .option("pen-drop-duration", { describe: "How long the pen takes to drop (in seconds)", type: "number", default: defaultPlanOptions.penDropDuration, required: false, }) .option("pen-lift-duration", { describe: "How long the pen takes to lift (in seconds)", type: "number", default: defaultPlanOptions.penLiftDuration, required: false, }) .option("sort-paths", { describe: "Re-order paths to minimize pen-up travel time", type: "boolean", default: true, }) .option("fit-page", { describe: "Re-scale and position the image to fit on the page", type: "boolean", default: true, }) .option("crop-to-margins", { describe: "Remove lines that fall outside the margins", type: "boolean", default: true, }) .option("minimum-path-length", { describe: "Remove paths that are shorter than this length (in mm)", type: "number", default: defaultPlanOptions.minimumPathLength, }) .option("point-join-radius", { describe: "Point-joining radius (in mm)", type: "number", default: defaultPlanOptions.pointJoinRadius, }) .option("path-join-radius", { describe: "Path-joining radius (in mm)", type: "number", default: defaultPlanOptions.pathJoinRadius, }) .option("rotate-drawing", { describe: "Rotate drawing (in degrees)", type: "number", default: defaultPlanOptions.rotateDrawing, }) .check((args) => { if (args.landscape && args.portrait) { throw new Error("Only one of --portrait and --landscape may be specified"); } return true; }), async (args) => { console.log("reading svg..."); const svg = readFileSync(args.file, "utf8"); console.log("parsing svg..."); const parsed = parseSvg(svg); console.log("flattening svg..."); const lines = flattenSVG(parsed, {}); console.log("generating motion plan..."); const paperSize = args.landscape ? args["paper-size"].landscape : args.portrait ? args["paper-size"].portrait : args["paper-size"]; const planOptions = { paperSize, marginMm: args.margin, hardware: args.hardware, selectedGroupLayers: new Set([]), // TODO selectedStrokeLayers: new Set([]), // TODO layerMode: "all", // TODO penUpHeight: args["pen-up-height"], penDownHeight: args["pen-down-height"], penDownAcceleration: args["pen-down-acceleration"], penDownMaxVelocity: args["pen-down-max-velocity"], penDownCorneringFactor: args["pen-down-cornering-factor"], penUpAcceleration: args["pen-up-acceleration"], penUpMaxVelocity: args["pen-up-max-velocity"], penDropDuration: args["pen-drop-duration"], penLiftDuration: args["pen-lift-duration"], sortPaths: args["sort-paths"], fitPage: args["fit-page"], cropToMargins: args["crop-to-margins"], rotateDrawing: args["rotate-drawing"], minimumPathLength: args["minimum-path-length"], pathJoinRadius: args["path-join-radius"], pointJoinRadius: args["point-join-radius"], }; const p = replan(lines, planOptions); console.log(`${p.motions.length} motions, estimated duration: ${formatDuration(p.duration())}`); console.log("connecting to plotter..."); const ebb = await connectEBB(args.hardware, args.device); if (!ebb) { console.error("Couldn't connect to device!"); process.exit(1); } console.log("plotting..."); const startTime = Date.now(); await ebb.executePlan(p); console.log(`done! took ${formatDuration((Date.now() - startTime) / 1000)}`); await ebb.close(); }) .command("pen [percent]", "put the pen to [percent]", (yargs) => yargs .positional("percent", { type: "number", description: "percent height between 0 and 100", required: true }) .check((args) => args.percent >= 0 && args.percent <= 100), async (args) => { console.log("connecting to plotter..."); const ebb = await connectEBB(args.hardware, args.device); if (!ebb) { console.error("Couldn't connect to device!"); process.exit(1); } const device = Device(ebb.hardware); await ebb.setPenHeight(device.penPctToPos(args.percent), 1000); console.log(`moving to ${args.percent}%...`); await ebb.close(); }) .command("$0", "run the saxi web server", (args) => args .option("port", { alias: "p", describe: "TCP port on which to listen", default: 9080, type: "number", }) .option("enable-cors", { describe: "enable cross-origin resource sharing (CORS)", default: false, type: "boolean", }) .option("max-payload-size", { describe: "maximum payload size to accept", default: "200mb", }) .option("svgio-api-key", { describe: "API Key - to enable AI image generation with SVG IO", default: "", }), (args) => { startServer(args.port, args.hardware, args.device, args["enable-cors"], args["max-payload-size"], args["svgio-api-key"]); }) .parse(argv); } //# sourceMappingURL=cli.js.map