initial commit

This commit is contained in:
Matt McWilliams 2025-04-25 23:02:29 -04:00
commit fe1c0750a2
8 changed files with 1525 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules

169
dist/index.js vendored Normal file
View File

@ -0,0 +1,169 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const puppeteer_1 = __importDefault(require("puppeteer"));
const argparse_1 = require("argparse");
const path_1 = require("path");
const promises_1 = require("fs/promises");
const URL = 'https://mattmcw.github.io/plotter-vision/';
const DOWNLOADS = './downloads';
const WIDTH_DEFAULT = 1920;
const HEIGHT_DEFAULT = 1080;
let PROCESSING = false;
let PROCESSING_MONITOR = false;
let WAITING = false;
async function delay(ms) {
return new Promise((resolve, reject) => {
return setTimeout(resolve, ms);
});
}
function compare(before, after) {
let downloaded = null;
for (let file of after) {
if (before.indexOf(file) === -1) {
console.log(`Downloaded ${(0, path_1.join)(DOWNLOADS, file)}`);
downloaded = (0, path_1.resolve)((0, path_1.join)(DOWNLOADS, file));
break;
}
}
return downloaded;
}
async function dir(path) {
const files = await (0, promises_1.readdir)(path);
return files.filter((el) => { return (0, path_1.extname)(el) === '.svg'; });
}
async function clearDownloads() {
const files = await dir(DOWNLOADS);
for (let file of files) {
await (0, promises_1.unlink)((0, path_1.resolve)((0, path_1.join)(DOWNLOADS, file)));
}
}
function cli() {
const parser = new argparse_1.ArgumentParser({ description: 'STL to SVG', usage: '%(prog)s <options> [input] [output]' });
parser.add_argument('input', { type: 'str', help: 'Input STL to render' });
parser.add_argument('output', { type: 'str', help: 'Output SVG to write' });
parser.add_argument('--width', '-W', { type: 'int', default: WIDTH_DEFAULT, help: `Width of window to render STL (default: ${WIDTH_DEFAULT})` });
parser.add_argument('--height', '-H', { type: 'int', default: HEIGHT_DEFAULT, help: `Height of window to render STL (default: ${HEIGHT_DEFAULT})` });
parser.add_argument('--load-delay', '-l', { type: 'int', default: 0, help: 'Number of milliseconds to wait after loading (default: 0)' });
parser.add_argument('--save-delay', '-s', { type: 'int', default: 0, help: 'Number of milliseconds to wait before saving (default: 0)' });
parser.add_argument('--theta', '-t', { type: 'float', default: null, help: 'Theta of the camera' });
parser.add_argument('--psi', '-p', { type: 'float', default: null, help: 'Psi of the camera' });
parser.add_argument('--radius', '-r', { type: 'float', default: null, help: 'Radius of the camera' });
parser.add_argument('--center', '-c', { type: 'str', default: null, help: 'LookAt center' });
parser.add_argument('--no-display', '-n', { action: 'store_true', default: true, help: 'No display' });
return parser.parse_args();
}
async function download(page, dest, save) {
let before;
let after;
let downloaded = null;
before = await dir(DOWNLOADS);
//console.dir(before)
await page.click('#fileDownload');
console.log(`Clicked download`);
await delay(100);
if (save > 0) {
console.log(`Delaying for: ${save}ms`);
await delay(save);
}
while (downloaded === null) {
await delay(1);
after = await dir(DOWNLOADS);
downloaded = compare(before, after);
}
if (downloaded === null) {
console.warn(`Cannot move download file to destination: ${dest}`);
return;
}
await (0, promises_1.copyFile)(downloaded, dest);
await (0, promises_1.unlink)(downloaded);
console.dir(`${downloaded} => ${dest}`);
}
function startProcessing() {
PROCESSING = true;
PROCESSING_MONITOR = true;
}
async function waitForProcessing() {
while (PROCESSING) {
await delay(401);
}
return true;
}
async function main() {
startProcessing();
const args = cli();
console.dir(args);
const config = {
headless: !args.no_display,
args: [`--window-size=${args.width},${args.height}`]
};
await clearDownloads();
const browser = await puppeteer_1.default.launch(config);
const page = await browser.newPage();
let lookat = [0, 0, 0];
page.on('console', (message) => {
const text = message.text();
const type = message.type().substr(0, 3).toUpperCase();
if (type === 'LOG' && text.trim() === 'hidden processing') {
PROCESSING = true;
PROCESSING_MONITOR = true;
}
else if (type === 'LOG' && text.indexOf('hidden processing ') !== -1 && text.indexOf(' segments ') !== -1) {
PROCESSING_MONITOR = false;
setTimeout(() => {
if (!PROCESSING_MONITOR && PROCESSING) {
PROCESSING = false;
console.log('PROCESSING COMPLETE');
}
}, 100);
}
if (!text.startsWith('filtered ')) {
console.log(`${type} ${text}`);
}
});
const client = await page.target().createCDPSession();
await client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOADS,
});
await page.setViewport({ width: args.width, height: args.height });
await page.goto(URL, { waitUntil: 'networkidle2' });
await delay(100);
console.log(`Loading: ${args.input}`);
startProcessing();
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('#loadFileXml')
]);
await fileChooser.accept([args.input]);
startProcessing();
await waitForProcessing();
if (args.psi !== null || args.theta !== null) {
console.log(`Adjusting the camera rotation`);
startProcessing();
await page.evaluate(`cameraView(${args.theta === null ? 0 : args.theta}, ${args.psi === null ? 0 : args.psi});`);
}
if (args.radius !== null) {
console.log(`Adjusting the camera distance`);
startProcessing();
await page.evaluate(`camera_radius = ${args.radius}; reproject = true; vz = 0.00001;`); //vz = 0.00001;
}
if (args.center !== null) {
lookat = args.center.split(',').map((el) => { return parseFloat(el); });
await page.evaluate(`camera.lookat.x = ${lookat[0]};
camera.lookat.y = ${lookat[1]};
camera.lookat.z = ${lookat[2]};
vz = 0.00001;`);
}
if (args.load_delay > 0) {
console.log(`Delaying for: ${args.load_delay}ms`);
await delay(args.load_delay);
}
await waitForProcessing();
await download(page, args.output, args.save_delay);
await browser.close();
}
main();
//# sourceMappingURL=index.js.map

1
dist/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1121
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "plotter-vision-auto",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"compile": "./node_modules/.bin/tsc -p tsconfig.json"
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@types/argparse": "^2.0.17",
"@types/node": "^22.13.5",
"typescript": "^5.7.3"
},
"dependencies": {
"argparse": "^2.0.1",
"puppeteer": "^24.2.1"
}
}

0
src/globals.d.ts vendored Normal file
View File

192
src/index.ts Normal file
View File

@ -0,0 +1,192 @@
import puppeteer from 'puppeteer';
import type { LaunchOptions, Page } from 'puppeteer';
import { ArgumentParser } from 'argparse';
import { join, resolve, extname } from 'path';
import { readdir, copyFile, unlink } from 'fs/promises';
const URL : string = 'https://mattmcw.github.io/plotter-vision/';
const DOWNLOADS : string = './downloads';
const WIDTH_DEFAULT : number = 1920;
const HEIGHT_DEFAULT : number = 1080;
let PROCESSING : boolean = false;
let PROCESSING_MONITOR : boolean = false;
let WAITING : boolean = false;
async function delay (ms : number) {
return new Promise((resolve : Function, reject : Function) => {
return setTimeout(resolve, ms);
});
}
function compare (before : string[], after : string[]) : string {
let downloaded : string = null;
for (let file of after) {
if (before.indexOf(file) === -1) {
console.log(`Downloaded ${join(DOWNLOADS, file)}`);
downloaded = resolve(join(DOWNLOADS, file));
break;
}
}
return downloaded;
}
async function dir (path : string) : Promise<string[]> {
const files : string[] = await readdir(path);
return files.filter((el : string) => { return extname(el) === '.svg'; });
}
async function clearDownloads () {
const files : string[] = await dir(DOWNLOADS);
for (let file of files) {
await unlink(resolve(join(DOWNLOADS, file)));
}
}
function cli () : any {
const parser : ArgumentParser = new ArgumentParser({ description: 'STL to SVG', usage : '%(prog)s <options> [input] [output]' });
parser.add_argument('input', { type : 'str', help : 'Input STL to render' });
parser.add_argument('output', { type : 'str', help : 'Output SVG to write' });
parser.add_argument('--width', '-W', { type : 'int', default : WIDTH_DEFAULT, help : `Width of window to render STL (default: ${WIDTH_DEFAULT})` });
parser.add_argument('--height', '-H', { type : 'int', default : HEIGHT_DEFAULT, help : `Height of window to render STL (default: ${HEIGHT_DEFAULT})` });
parser.add_argument('--load-delay', '-l', { type : 'int', default : 0, help : 'Number of milliseconds to wait after loading (default: 0)' });
parser.add_argument('--save-delay', '-s', { type : 'int', default : 0, help : 'Number of milliseconds to wait before saving (default: 0)' });
parser.add_argument('--theta', '-t', { type : 'float', default : null, help : 'Theta of the camera' });
parser.add_argument('--psi', '-p', { type : 'float', default : null, help : 'Psi of the camera' });
parser.add_argument('--radius', '-r', { type : 'float', default : null, help : 'Radius of the camera' });
parser.add_argument('--center', '-c', { type : 'str', default : null, help : 'LookAt center' });
parser.add_argument('--no-display', '-n', { action : 'store_true', default : true, help : 'No display' });
return parser.parse_args();
}
async function download (page : Page, dest : string, save : number) {
let before : string[];
let after : string[];
let downloaded : string = null;
before = await dir(DOWNLOADS);
//console.dir(before)
await page.click('#fileDownload');
console.log(`Clicked download`);
await delay(100);
if (save > 0) {
console.log(`Delaying for: ${save}ms`);
await delay(save);
}
while (downloaded === null) {
await delay(1);
after = await dir(DOWNLOADS);
downloaded = compare(before, after);
}
if (downloaded === null) {
console.warn(`Cannot move download file to destination: ${dest}`)
return;
}
await copyFile(downloaded, dest);
await unlink(downloaded);
console.dir(`${downloaded} => ${dest}`);
}
function startProcessing () {
PROCESSING = true;
PROCESSING_MONITOR = true;
}
async function waitForProcessing () : Promise<boolean> {
while (PROCESSING) {
await delay(401);
}
return true;
}
async function main () {
startProcessing();
const args : any = cli();
console.dir(args)
const config : LaunchOptions = {
headless : !args.no_display,
args: [`--window-size=${args.width},${args.height}`]
};
await clearDownloads();
const browser = await puppeteer.launch(config);
const page = await browser.newPage();
let lookat : number[] = [0, 0, 0];
page.on('console', (message : any) => {
const text : string = message.text();
const type : string = message.type().substr(0, 3).toUpperCase();
if (type === 'LOG' && text.trim() === 'hidden processing') {
PROCESSING = true;
PROCESSING_MONITOR = true;
} else if (type === 'LOG' && text.indexOf('hidden processing ') !== -1 && text.indexOf(' segments ') !== -1) {
PROCESSING_MONITOR = false;
setTimeout(() => {
if (!PROCESSING_MONITOR && PROCESSING) {
PROCESSING = false;
console.log('PROCESSING COMPLETE');
}
}, 100)
}
if (!text.startsWith('filtered ')) {
console.log(`${type} ${text}`);
}
});
const client = await page.target().createCDPSession();
await client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOADS,
})
await page.setViewport({ width: args.width, height: args.height });
await page.goto(URL, { waitUntil: 'networkidle2' });
await delay(100);
console.log(`Loading: ${args.input}`);
startProcessing();
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('#loadFileXml')
]);
await fileChooser.accept([ args.input ]);
startProcessing();
await waitForProcessing();
if (args.psi !== null || args.theta !== null) {
console.log(`Adjusting the camera rotation`);
startProcessing();
await page.evaluate(`cameraView(${args.theta === null ? 0 : args.theta}, ${args.psi === null ? 0 : args.psi});`);
}
if (args.radius !== null) {
console.log(`Adjusting the camera distance`);
startProcessing();
await page.evaluate(`camera_radius = ${args.radius}; reproject = true; vz = 0.00001;`); //vz = 0.00001;
}
if (args.center !== null) {
lookat = args.center.split(',').map((el : string) => { return parseFloat(el); });
await page.evaluate(`camera.lookat.x = ${lookat[0]};
camera.lookat.y = ${lookat[1]};
camera.lookat.z = ${lookat[2]};
vz = 0.00001;`);
}
if (args.load_delay > 0) {
console.log(`Delaying for: ${args.load_delay}ms`);
await delay(args.load_delay);
}
await waitForProcessing();
await download(page, args.output, args.save_delay);
await browser.close();
}
main();

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "ES2020",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"removeComments" : false,
"baseUrl" : "dist",
"outDir": "./dist/",
"rootDir" : "./src/",
"paths" : {
}
},
"exclude" : [
"./dist"
]
}