diff --git a/packages/cli/README.md b/packages/cli/README.md index 618933d7d..8eeb8646e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -73,6 +73,16 @@ Examples: ```jscad mydesign.js -of amf # -- convert mydesign.js into mydesign.amf``` +For multi-part models, you can pass the `generateParts` flag `-gp` to output each part as a separate, numbered file: + +```jscad mydesign.js -gp # -- convert mydesign.js into mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` + +You may also pass the `zip` flag `-z` to zip generated files into one .zip file: + +```jscad mydesign.js -z # -- convert mydesign.js into mydesign.zip which contains: mydesign.stl``` + +```jscad mydesign.js -gp -z # -- convert mydesign.js into mydesign.zip which contains: mydesign-part-1-of-2.stl and mydesign-part-2-of-2.stl``` + The '-o' option can be used to control where the output will be placed. While, the '-of' option can be used to control the format of the output. diff --git a/packages/cli/cli.js b/packages/cli/cli.js index adc7c4160..f6c305240 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -22,6 +22,7 @@ // jscad name_plate.jscad --name "Just Me" --title "CEO" -o amf test.amf // const fs = require('fs') +const JSZip = require('jszip') const { formats } = require('@jscad/io/formats') @@ -33,7 +34,7 @@ const parseArgs = require('./src/parseArgs') // handle arguments (inputs, outputs, etc) const args = process.argv.splice(2) -let { inputFile, inputFormat, outputFile, outputFormat, params, addMetaData, inputIsDirectory } = parseArgs(args) +let { inputFile, inputFormat, outputFile, outputFormat, generateParts, zip, params, addMetaData, inputIsDirectory } = parseArgs(args) // outputs const output = determineOutputNameAndFormat(outputFormat, outputFile, inputFile) @@ -48,18 +49,64 @@ const clicolors = { black: '\u{1b}[0m' } -console.log(`${clicolors.blue}JSCAD: generating output ${clicolors.red} - from: ${clicolors.green} ${inputFile} ${clicolors.red} - to: ${clicolors.green} ${outputFile} ${clicolors.yellow}(${formats[outputFormat].description}) ${clicolors.black} -`) +const logFileOutput = (outputFile) => { + console.log(`${clicolors.blue}JSCAD: generating output ${clicolors.red} + from: ${clicolors.green} ${inputFile} ${clicolors.red} + to: ${clicolors.green} ${outputFile} ${clicolors.yellow}(${formats[outputFormat].description}) ${clicolors.black} + `) +} // read input data const src = fs.readFileSync(inputFile, inputFile.match(/\.stl$/i) ? 'binary' : 'UTF8') // -- convert from JSCAD script into the desired output format // -- and write it to disk -generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputFormat, version, addMetaData, inputIsDirectory }) - .then((outputData) => writeOutput(outputFile, outputData)) +generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputFormat, generateParts, version, addMetaData, inputIsDirectory }) + .then((outputData) => { + if (outputData instanceof Array) { + if (zip) { + const zip = new JSZip() + for (let i = 0; i < outputData.length; i++) { + const filename = outputFile.replace(/\.(\w+)$/, `-part-${i + 1}-of-${outputData.length}.$1`) + zip.file(filename, outputData[i].asBuffer()) + } + zip.generateAsync({ type: 'nodebuffer' }).then((content) => { + const zipFilename = outputFile.replace(/\.(\w+)$/, '.zip') + fs.writeFile(zipFilename, content, (err) => { + if (err) { + console.error(err) + } else { + logFileOutput(zipFilename) + } + }) + }) + } else { + for (let i = 0; i < outputData.length; i++) { + const filename = outputFile.replace(/\.(\w+)$/, `-part-${i + 1}-of-${outputData.length}.$1`) + logFileOutput(filename) + writeOutput(filename, outputData[i]) + } + } + } else { + if (zip) { + const zip = new JSZip() + zip.file(outputFile, outputData.asBuffer()) + zip.generateAsync({ type: 'nodebuffer' }).then((content) => { + const zipFilename = outputFile.replace(/\.(\w+)$/, '.zip') + fs.writeFile(zipFilename, content, (err) => { + if (err) { + console.error(err) + } else { + logFileOutput(zipFilename) + } + }) + }) + } else { + logFileOutput(outputFile) + writeOutput(outputFile, outputData) + } + } + }) .catch((error) => { console.error(error) process.exit(1) diff --git a/packages/cli/cli.parameters.test.js b/packages/cli/cli.parameters.test.js index 504acc281..0b6108992 100644 --- a/packages/cli/cli.parameters.test.js +++ b/packages/cli/cli.parameters.test.js @@ -1,4 +1,5 @@ const test = require('ava') +const JSZip = require('jszip') const path = require('path') const { execSync } = require('child_process') @@ -13,6 +14,12 @@ test.afterEach.always((t) => { try { if (t.context.outputPath) fs.unlinkSync(t.context.outputPath) } catch (err) {} + try { + if (t.context.outputPath2) fs.unlinkSync(t.context.outputPath2) + } catch (err) {} + try { + if (t.context.outputPath3) fs.unlinkSync(t.context.outputPath3) + } catch (err) {} try { if (t.context.folderPath) fs.rmdirSync(t.context.folderPath, { recursive: false }) @@ -32,7 +39,7 @@ test.beforeEach((t) => { // create a simple JSCAD script for input // the script should produce ALL geometry types -const createJscad = (id) => { +const createJscad = (id, multipart = false) => { const jscadScript = `// test script ${id} const { primitives } = require('@jscad/modeling') @@ -51,7 +58,7 @@ const main = (params) => { let ageom2 = primitives.ellipse() let ageom3 = primitives.ellipsoid() - return [apath2, ageom2, ageom3] + ${multipart ? `return [ageom3, ageom3, ageom3]` : `return [apath2, ageom2, ageom3]`} } module.exports = { main, getParameterDefinitions } @@ -207,3 +214,89 @@ test('cli (single input file, invalid jscad)', (t) => { execSync(cmd, { stdio: [0, 1, 2] }) }) }) + + +test('cli (single input file, multiple output files)', (t) => { + const testID = 7 + + const inputPath = createJscad(testID, true) + t.true(fs.existsSync(inputPath)) + + t.context.inputPath = inputPath + + const outputName = (partNum) => `./test${testID}-part-${partNum}-of-3.stl` + const outputPath1 = path.resolve(__dirname, outputName(1)) + const outputPath2 = path.resolve(__dirname, outputName(2)) + const outputPath3 = path.resolve(__dirname, outputName(3)) + t.false(fs.existsSync(outputPath1)) + t.false(fs.existsSync(outputPath2)) + t.false(fs.existsSync(outputPath3)) + + t.context.outputPath = outputPath1 + t.context.outputPath2 = outputPath2 + t.context.outputPath3 = outputPath3 + + const cliPath = t.context.cliPath + + const cmd = `node ${cliPath} ${inputPath} -gp` + execSync(cmd, { stdio: [0,1,2] }) + t.true(fs.existsSync(outputPath1)) + t.true(fs.existsSync(outputPath2)) + t.true(fs.existsSync(outputPath3)) +}) + +test('cli (single multipart input file, zipped output file)', async (t) => { + const testID = 8 + + const inputPath = createJscad(testID, true) + t.true(fs.existsSync(inputPath)) + + t.context.inputPath = inputPath + + const outputName = `./test${testID}.zip` + const outputPath = path.resolve(__dirname, outputName) + + t.false(fs.existsSync(outputPath)) + + t.context.outputPath = outputPath + + const cliPath = t.context.cliPath + + const cmd = `node ${cliPath} ${inputPath} -gp -z` + execSync(cmd, { stdio: [0,1,2] }) + t.true(fs.existsSync(outputPath)) + + // check contents of zip file + const data = await fs.promises.readFile(outputPath) + const content = await JSZip.loadAsync(data) + t.true(content.files[path.resolve(__dirname, `./test${testID}-part-1-of-3.stl`)] !== undefined) + t.true(content.files[path.resolve(__dirname, `./test${testID}-part-2-of-3.stl`)] !== undefined) + t.true(content.files[path.resolve(__dirname, `./test${testID}-part-3-of-3.stl`)] !== undefined) +}) + +test('cli (single input file, zipped output file)', async (t) => { + const testID = 9 + + const inputPath = createJscad(testID, true) + t.true(fs.existsSync(inputPath)) + + t.context.inputPath = inputPath + + const outputName = `./test${testID}.zip` + const outputPath = path.resolve(__dirname, outputName) + + t.false(fs.existsSync(outputPath)) + + t.context.outputPath = outputPath + + const cliPath = t.context.cliPath + + const cmd = `node ${cliPath} ${inputPath} -z` + execSync(cmd, { stdio: [0,1,2] }) + t.true(fs.existsSync(outputPath)) + + // check contents of zip file + const data = await fs.promises.readFile(outputPath) + const content = await JSZip.loadAsync(data) + t.true(content.files[path.resolve(__dirname, `./test${testID}.stl`)] !== undefined) +}) diff --git a/packages/cli/package.json b/packages/cli/package.json index 1172999a7..8df1624e2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,7 +38,8 @@ "@jscad/array-utils": "2.1.4", "@jscad/core": "2.6.6", "@jscad/io": "2.4.5", - "@jscad/modeling": "2.11.1" + "@jscad/modeling": "2.11.1", + "jszip": "^3.10.1" }, "devDependencies": { "ava": "3.15.0", diff --git a/packages/cli/src/generateOutputData.js b/packages/cli/src/generateOutputData.js index 329b0685b..0990bd379 100644 --- a/packages/cli/src/generateOutputData.js +++ b/packages/cli/src/generateOutputData.js @@ -19,10 +19,11 @@ const generateOutputData = (source, params, options) => { outputFormat: 'stl', inputFile: '', version: '', - addMetaData: true + addMetaData: true, + generateParts: false } options = Object.assign({}, defaults, options) - const { outputFormat, inputFile, inputFormat } = options + const { outputFormat, inputFile, inputFormat, generateParts } = options options.filename = inputFile // for deserializers @@ -62,6 +63,13 @@ const generateOutputData = (source, params, options) => { }) .then((solids) => { const serializerOptions = Object.assign({ format: outputFormat }, params) + if (generateParts) { + let blobs = [] + for (let i = 0; i < solids.length; i++) { + blobs.push(solidsAsBlob(solids[i], serializerOptions)) + } + return blobs + } return solidsAsBlob(solids, serializerOptions) }) } diff --git a/packages/cli/src/parseArgs.js b/packages/cli/src/parseArgs.js index c2dc9d4fa..577a6bb70 100644 --- a/packages/cli/src/parseArgs.js +++ b/packages/cli/src/parseArgs.js @@ -24,6 +24,8 @@ const parseArgs = (args) => { let inputFormat let outputFile let outputFormat + let generateParts = false + let zip = false const params = {} // parameters to feed the script if applicable let addMetaData = false // wether to add metadata to outputs or not : ie version info, timestamp etc let inputIsDirectory = false // did we pass in a folder or a file ? @@ -41,6 +43,10 @@ const parseArgs = (args) => { for (let i = 0; i < args.length; i++) { if (args[i] === '-of') { // -of outputFormat = args[++i] + } else if (args[i] === '-gp') { + generateParts = true + } else if (args[i] === '-z') { + zip = true } else if (args[i].match(/^-o(\S.+)/)) { // -o outputFile = args[i] outputFile = outputFile.replace(/^-o(\S+)$/, '$1') @@ -92,6 +98,8 @@ const parseArgs = (args) => { inputFormat, outputFile, outputFormat, + generateParts, + zip, params, addMetaData, inputIsDirectory