From a875760994d528496c69417845836694cc6adbc0 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 25 Dec 2024 12:45:47 +0100 Subject: [PATCH] Xml based 3mf master (#131) Co-authored-by: Davor Hrg --- .../3mf-export-compact/package-lock.json | 4 +- file-format/3mf-export-compact/package.json | 2 +- file-format/3mf-export-compact/testSpeed.js | 45 +++++ file-format/3mf-export/README.md | 20 +- file-format/3mf-export/index.js | 184 +++++++++++------- file-format/3mf-export/module-fix.d.ts | 5 + file-format/3mf-export/package.json | 5 +- file-format/3mf-export/src/defMatrix.js | 5 + file-format/3mf-export/src/makeItem.js | 15 +- file-format/3mf-export/src/matrix2str.js | 6 +- file-format/3mf-export/src/pushHeader.js | 71 +++---- .../3mf-export/src/pushObjectComponent.js | 41 ++-- file-format/3mf-export/src/pushObjectMesh.js | 54 +++-- file-format/3mf-export/src/toDate3mf.js | 1 + file-format/3mf-export/testGen.js | 63 +++--- file-format/3mf-export/testSpeed.js | 45 +++++ file-format/3mf-export/tsconfig.json | 7 + file-format/3mf-export/xml-schema-3mf.d.ts | 83 ++++++++ package-lock.json | 47 +++++ 19 files changed, 476 insertions(+), 227 deletions(-) create mode 100644 file-format/3mf-export-compact/testSpeed.js create mode 100644 file-format/3mf-export/module-fix.d.ts create mode 100644 file-format/3mf-export/testSpeed.js create mode 100644 file-format/3mf-export/tsconfig.json create mode 100644 file-format/3mf-export/xml-schema-3mf.d.ts diff --git a/file-format/3mf-export-compact/package-lock.json b/file-format/3mf-export-compact/package-lock.json index 8212f6ef..015910d1 100644 --- a/file-format/3mf-export-compact/package-lock.json +++ b/file-format/3mf-export-compact/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@jscadui/3mf-export", + "name": "@jscadui/3mf-export-compact", "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@jscadui/3mf-export", + "name": "@jscadui/3mf-export-compact", "version": "0.1.0", "license": "MIT", "devDependencies": { diff --git a/file-format/3mf-export-compact/package.json b/file-format/3mf-export-compact/package.json index 59ee7dd1..a362262b 100644 --- a/file-format/3mf-export-compact/package.json +++ b/file-format/3mf-export-compact/package.json @@ -1,7 +1,7 @@ { "type": "module", "sideEffects": false, - "name": "@jscadui/3mf-export", + "name": "@jscadui/3mf-export-compact", "version": "0.5.0", "description": "3mf export", "main": "index.js", diff --git a/file-format/3mf-export-compact/testSpeed.js b/file-format/3mf-export-compact/testSpeed.js new file mode 100644 index 00000000..e14fc6b3 --- /dev/null +++ b/file-format/3mf-export-compact/testSpeed.js @@ -0,0 +1,45 @@ +// @ts-nocheck + +// if not in browser +import { to3dmodel, to3mfZipContentSimple } from './index.js' + +function multiply(arr, mult, func){ + let out = new func(arr.length * mult) + for(let i=0; i} children * * @typedef Child3MF - * @prop {string} objectID - * @prop {mat4} transform + * @prop {number} objectID + * @prop {import('./src/defMatrix.js').mat4} [transform] * * @typedef To3MF * @prop {Array} meshes - manually declare meshes * @prop {Array} [components] - components can combine items * @prop {Array} items - output items, each pointing to component or mesh with objectID - * @prop {number} precision - * @prop {import('./src/pushHeader.js').Header} header + * @prop {number} [precision] + * @prop {import('./src/pushHeader.js').Header} [header] */ /** * @param {To3MF} options - * @returns string + * @returns {string} */ export function to3dmodel({ meshes = [], components = [], items = [], precision = 17, header }) { - // items to be placed on the scene (build section of 3mf) - const out = [] + /** @type {import('./xml-schema-3mf.js').Xml3mf} */ + const data = { + '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' }, + model: genModel( + header ?? {}, + [ + //Mesh objects + ...meshes.map(({ id, vertices, indices, name }) => ({ + object: genObjectWithMesh(id, vertices, indices, precision, name) + })), + //Component objects + ...components.map(({ id, children, name }) => ({ + object: genObjectWithComponents(id, children, name) + })), + ], + { item: items.map(v => genItem(v.objectID, v.transform)) }, + ) + } - // tag is opened here - pushHeader(out, header) + const builder = new XMLBuilder({ + ignoreAttributes: false, + format: true, + suppressEmptyNode: true + }) - // #region resources - out.push(' \n') + return builder.build(data) +} - if (items.length == 0) { - console.error('3MF empty build! Include items or simple.') - } +/** + * Simple export provided meshes that have transform attached to them and autocreate items and pass to to3dmodel. + * @param {Array} meshes + * @param {import('./src/pushHeader.js').Header} [header] + * @param {number} [precision] + * @returns {string} + */ +export function to3dmodelSimple(meshes, header, precision) { + /** @type {Child3MF[]} */ + const items = meshes.map(({ id, transform }) => ({ objectID: id, transform })) - meshes.forEach(({ id, vertices, indices, name }) => pushObjectWithMesh(out, id, vertices, indices, precision, name)) + return to3dmodel({ meshes, items, header, precision }) +} - components.forEach(({ id, children, name }) => { - pushObjectWithComponents(out, id, children, name) - }) +/** + * @typedef {[string,Uint8Array,boolean]} ZipContent FileName, FileContent, CanBeCompressed + */ - out.push(' \n') - // #endregion +/** + * @param {To3MF} options + * @param {Uint8Array | undefined} [thumbnailPng] + * @returns {ZipContent[]} + */ +export function to3mfZipContent(options, thumbnailPng) { + /** @type {ZipContent[]} */ + const result = [] + const utf8Encoder = new TextEncoder() - out.push(`\n`) - items.forEach(({ objectID, transform }) => { - out.push(makeItem(objectID, transform)) - }) - out.push('\n') + result.push([fileForContentTypes.name, utf8Encoder.encode(fileForContentTypes.content), true]) - out.push('\n') // model tag was opened in the pushHeader() + const fileForRelThumbnail = new FileForRelThumbnail() + fileForRelThumbnail.add3dModel('3D/3dmodel.model') - return out.join('') + if (thumbnailPng !== undefined) { + result.push(['Metadata/thumbnail.png', thumbnailPng, false]) + fileForRelThumbnail.addThumbnail('Metadata/thumbnail.png') + } + + result.push(['3D/3dmodel.model', utf8Encoder.encode(to3dmodel(options)), true]) + result.push([fileForRelThumbnail.name, utf8Encoder.encode(fileForRelThumbnail.content), true]) + + return result } -/** Simple export provided meshes that have transform attached to them and autocreate items and pass to to3dmodel. - * - * @param {Array} meshes - * @param {import('./src/pushHeader.js').Header} header - * @param {number} precision - * @returns string +/** + * @param {{meshes:Array,header?:import('./src/pushHeader.js').Header,precision?:number}} options + * @param {Uint8Array | undefined} [thumbnailPng] + * @returns {ZipContent[]} */ -export function to3dmodelSimple(meshes, header, precision) { - const items = [] - meshes.forEach(({ id, transform }) => { - items.push({ objectID: id, transform }) - }) - return to3dmodel({ meshes, items, header, precision }) +export function to3mfZipContentSimple({ meshes, header, precision }, thumbnailPng) { + const items = meshes.map(({ id, transform }) => ({ objectID: id, transform })) + + return to3mfZipContent({ meshes, items, header, precision }, thumbnailPng) } /** File that describes file relationships inside a 3mf */ export class FileForRelThumbnail { - constructor() { - this.idSeq = 0 - this.lines = [ - ``, - ``, - ] - } - get name() { - return '_rels/.rels' - } - /** - * - * @param {*} target file path - * @param {*} xmlType xml schema url + idSeq = 0 + name = '_rels/.rels' + + /** @type {import('./xml-schema-3mf.js').Xml3mfRelationFile} */ + data = { + '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' }, + Relationships: { + '@_xmlns': 'http://schemas.openxmlformats.org/package/2006/relationships', + Relationship: [] + }, + } + + /** + * @param {string} target file path + * @param {string} xmlType xml schema url */ addRel(target, xmlType) { - this.lines.push(` `) - } - add3dModel(path) { - this.addRel(path, 'http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel') - } - addThumbnail(path) { - this.addRel(path, 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail') + this.data.Relationships.Relationship.push( + { '@_Target': target, "@_Id": `rel-${++this.idSeq}`, '@_Type': xmlType } + ) } + /** @param {string} path */ + add3dModel = (path) => this.addRel(path, 'http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel') + + /** @param {string} path */ + addThumbnail = (path) => this.addRel(path, 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail') + get content() { - return this.lines.join('\n') + `\n` + const builder = new XMLBuilder({ ignoreAttributes: false, format: true, suppressEmptyNode: true }) + return builder.build(this.data) } } diff --git a/file-format/3mf-export/module-fix.d.ts b/file-format/3mf-export/module-fix.d.ts new file mode 100644 index 00000000..b57e2523 --- /dev/null +++ b/file-format/3mf-export/module-fix.d.ts @@ -0,0 +1,5 @@ +//This is required for the import tree shaking hack +declare module 'fast-xml-parser/src/xmlbuilder/json2xml.js' { + import { XMLBuilder } from 'fast-xml-parser' + export default XMLBuilder +} \ No newline at end of file diff --git a/file-format/3mf-export/package.json b/file-format/3mf-export/package.json index 8789c262..59ee7dd1 100644 --- a/file-format/3mf-export/package.json +++ b/file-format/3mf-export/package.json @@ -26,5 +26,8 @@ "@types/node": "18.11.9", "@trivago/prettier-plugin-sort-imports": "~3.3.0" }, - "license": "MIT" + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^4.5.0" + } } diff --git a/file-format/3mf-export/src/defMatrix.js b/file-format/3mf-export/src/defMatrix.js index bca4fa08..7c13f8c8 100644 --- a/file-format/3mf-export/src/defMatrix.js +++ b/file-format/3mf-export/src/defMatrix.js @@ -1,4 +1,9 @@ +/** + * @typedef {number []} mat4 + */ + /* prettier-ignore */ +/** @type {mat4} */ export const defMatrix = [ 1, 0, 0, 0, 0, 1, 0, 0, diff --git a/file-format/3mf-export/src/makeItem.js b/file-format/3mf-export/src/makeItem.js index 7020129b..558aff95 100644 --- a/file-format/3mf-export/src/makeItem.js +++ b/file-format/3mf-export/src/makeItem.js @@ -1,10 +1,11 @@ -import { defMatrix } from './defMatrix.js' import { matrix2str } from './matrix2str.js' + /** - * - * @param {string} id - must start with 1, can not be zero by spec - * @param {mat4} matrix - * @returns + * @param {number} id - must start with 1, can not be zero by spec + * @param {import('./defMatrix.js').mat4} [matrix] + * @returns {import('../xml-schema-3mf.js').Xml3mfItem} */ -export const makeItem = (id = 1, matrix = defMatrix) => - ` \n` +export const genItem = (id, matrix) => ({ + '@_objectid': id, + '@_transform': matrix !== undefined ? matrix2str(matrix) : undefined, +}) \ No newline at end of file diff --git a/file-format/3mf-export/src/matrix2str.js b/file-format/3mf-export/src/matrix2str.js index 8cac7c21..ca223202 100644 --- a/file-format/3mf-export/src/matrix2str.js +++ b/file-format/3mf-export/src/matrix2str.js @@ -1,5 +1,5 @@ -/** transform for attribute as specifiend in 3mf format +/** transform for attribute as specified in 3mf format * * When objects need to be transformed for rotation, scaling, or translation purposes, * row-major affine 3D matrices (4x4) are used. The matrix SHOULD NOT be singular or nearly singular. @@ -9,8 +9,8 @@ * matrices have the form "m00 m01 m02 m10 m11 m12 m20 m21 m22 m30 m31 m32" * where each value is a decimal number of arbitrary precision. * - * @param {mat4} matrix - * @return string tarnsform attribute value + * @param {import("./defMatrix").mat4} m + * @return string transform attribute value */ export const matrix2str = m=>{ let str = '' diff --git a/file-format/3mf-export/src/pushHeader.js b/file-format/3mf-export/src/pushHeader.js index 7f0433c2..ea1db966 100644 --- a/file-format/3mf-export/src/pushHeader.js +++ b/file-format/3mf-export/src/pushHeader.js @@ -2,42 +2,47 @@ import { toDate3mf } from './toDate3mf.js' /** * @typedef Header - * @prop {'micron'|'millimeter'|'centimeter'|'inch'|'foot'|'meter'} unit + * @prop {import('../xml-schema-3mf.js').Xml3mfUnit} [unit] * @prop {string} [title] * @prop {string} [author] * @prop {string} [description] * @prop {string} [application] - * @prop {string} [creationDate] + * @prop {Date} [creationDate] * @prop {string} [license] - * @prop {string} [modificationDate] - * - * - * @param {Array} out - * @param {Header} param1 + * @prop {string} [copyright] + * @prop {Date} [modificationDate] + * @prop {string} [rating] */ -export function pushHeader(out,{ - unit = 'millimeter', - title = 'jscad model', - author = '', - description = '', - application = 'jscad', - creationDate = new Date(), - license = '', - modificationDate, -}={}) { - out.push( - ` - - 1 - ${title} - ${author} - ${description || title} - - ${license} - - ${toDate3mf(creationDate)} - ${toDate3mf(modificationDate || creationDate)} - ${application} - `, - ) -} + +/** + * @param {Header} header + * @param {import('../xml-schema-3mf.js').Xml3mfResource[]} resources + * @param {import('../xml-schema-3mf.js').Xml3mfBuild} build + * @returns {import('../xml-schema-3mf.js').Xml3mfModel} + */ +export const genModel = (header, resources, build) => { + /** @type {import('../xml-schema-3mf.js').Xml3mfModel} */ + const result = { + '@_unit': header.unit ?? 'millimeter', + '@_xml:lang': 'en-US', + '@_xmlns': 'http://schemas.microsoft.com/3dmanufacturing/core/2015/02', + '@_xmlns:slic3rpe': 'http://schemas.slic3r.org/3mf/2017/06', + metadata: [ + { '@_name': 'slic3rpe:Version3mf', '#text': '1' }, + ], + resources, + build, + } + + if (header.title !== undefined) result.metadata.push({ '@_name': 'Title', '#text': header.title }) + if (header.author !== undefined) result.metadata.push({ '@_name': 'Designer', '#text': header.author }) + if (header.description !== undefined) result.metadata.push({ '@_name': 'Description', '#text': header.description }) + if (header.copyright !== undefined) result.metadata.push({ '@_name': 'Copyright', '#text': header.copyright }) + if (header.license !== undefined) result.metadata.push({ '@_name': 'LicenseTerms', '#text': header.license }) + if (header.rating !== undefined) result.metadata.push({ '@_name': 'Rating', '#text': header.rating }) + if (header.application !== undefined) result.metadata.push({ '@_name': 'Application', '#text': header.application }) + result.metadata.push({ '@_name': 'CreationDate', '#text': toDate3mf(header.creationDate ?? new Date()) }) + result.metadata.push({ '@_name': 'ModificationDate', '#text': toDate3mf(header.modificationDate ?? new Date()) }) + + return result +} \ No newline at end of file diff --git a/file-format/3mf-export/src/pushObjectComponent.js b/file-format/3mf-export/src/pushObjectComponent.js index 249b3a42..48ecc695 100644 --- a/file-format/3mf-export/src/pushObjectComponent.js +++ b/file-format/3mf-export/src/pushObjectComponent.js @@ -1,30 +1,19 @@ -import {defMatrix} from './defMatrix.js' import {matrix2str} from './matrix2str.js' /** - * - * @param {Array} out - * @param {string} id - * @param {Array} children - * @param {string} name + * @param {number} id + * @param {import('../index.js').Child3MF[]} components + * @param {string} [name] + * @return {import('../xml-schema-3mf').Xml3mfComponentObject} */ -export function pushObjectWithComponents(out, id, children, name) { - out.push(`\n`) - out.push(` \n`) - children.forEach( - ({objectID, transform}) => {addComp(out, objectID, transform)}) - out.push(` \n`) - out.push(`\n`) -} - -/** - * - * @param {*} out - * @param {*} id - must start with 1, can not be zero by spec - * @param {*} matrix - */ -const addComp = (out, id = 1, matrix = defMatrix) => { - out.push( - ` \n`) -} +export const genObjectWithComponents = (id, components, name) => ({ + '@_id': id, + '@_type': 'model', + '@_name': name, + components: { + component: components.map(({ objectID, transform }) => ({ + '@_objectid': objectID, + '@_transform': transform !== undefined ? matrix2str(transform) : undefined, + })) + } +}) \ No newline at end of file diff --git a/file-format/3mf-export/src/pushObjectMesh.js b/file-format/3mf-export/src/pushObjectMesh.js index b2819c86..41d98109 100644 --- a/file-format/3mf-export/src/pushObjectMesh.js +++ b/file-format/3mf-export/src/pushObjectMesh.js @@ -1,41 +1,39 @@ /** - * - * @param {Arrray} out - * @param {string} id + * @param {number} id * @param {Float32Array} vertices * @param {Uint32Array} indices * @param {number} precision * @param {string} [name] - * @returns + * @return {import('../xml-schema-3mf').Xml3mfMeshObject} */ -export function pushObjectWithMesh( - out, id, vertices, indices, precision, name) { - out.push(` - - -`) - +export const genObjectWithMesh = (id, vertices, indices, precision, name) => { + /** @type { import('../xml-schema-3mf').Xml3mfVertex[]} */ + const xmlVertex = [] for (let i = 0; i < vertices.length; i += 3) { - out.push(` \n`) + xmlVertex.push({ + '@_x': vertices[i].toPrecision(precision), + '@_y': vertices[i + 1].toPrecision(precision), + '@_z': vertices[i + 2].toPrecision(precision), + }) } - out.push(` - -`) - + /** @type { import('../xml-schema-3mf').Xml3mfTriangle[]} */ + const xmlTriangles = [] for (let i = 0; i < indices.length; i += 3) { - out.push(` \n`) + xmlTriangles.push({ + '@_v1': indices[i], + '@_v2': indices[i + 1], + '@_v3': indices[i + 2], + }) } - - out.push(` - - -`) - - return out + return { + '@_id': id, + '@_type': 'model', + '@_name': name, + mesh: { + vertices: { vertex: xmlVertex }, + triangles: { triangle: xmlTriangles }, + }, + } } \ No newline at end of file diff --git a/file-format/3mf-export/src/toDate3mf.js b/file-format/3mf-export/src/toDate3mf.js index f85249b5..0e35c365 100644 --- a/file-format/3mf-export/src/toDate3mf.js +++ b/file-format/3mf-export/src/toDate3mf.js @@ -1 +1,2 @@ +/** @param {Date} d */ export const toDate3mf = d => (d ? d.toISOString().substring(0, 10) : '') diff --git a/file-format/3mf-export/testGen.js b/file-format/3mf-export/testGen.js index 9f6aab5c..da80cfcd 100644 --- a/file-format/3mf-export/testGen.js +++ b/file-format/3mf-export/testGen.js @@ -1,22 +1,21 @@ // if not in browser import { Blob } from 'buffer' -import { Zip, ZipDeflate, ZipPassThrough, strToU8 } from 'fflate' +import { Zip, ZipDeflate, ZipPassThrough } from 'fflate' import { readFileSync, writeFileSync } from 'fs' -import { fileForContentTypes, FileForRelThumbnail, to3dmodel, to3dmodelSimple } from './index.js' +import { to3mfZipContentSimple } from './index.js' -const fileForRelThumbnail = new FileForRelThumbnail() //#region hardcoded cube data -let vertices = [ +let vertices = new Float32Array([ -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -] +]) -let indices = [ +let indices = new Uint32Array([ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23, -] +]) //#endregion const zipParts = [] @@ -32,53 +31,41 @@ const zip = new Zip(async (err, dat, final) => { } }) -let modelStr = to3dmodelSimple([{ vertices, indices, id: '1' }]) -addToZip(zip, '3D/3dmodel.model', modelStr) -fileForRelThumbnail.add3dModel('3D/3dmodel.model') -let thumb = readFileSync('testThumbnail.png') -const pngPreviewFile = new ZipPassThrough('Metadata/thumbnail.png') -zip.add(pngPreviewFile) -pngPreviewFile.push(thumb, true) -fileForRelThumbnail.addThumbnail('Metadata/thumbnail.png') +const thumb = readFileSync('testThumbnail.png') +const zipContents = to3mfZipContentSimple({ + meshes: [{ vertices, indices, id: 1 }], + header: { application: 'jscad.app', title: 'jscad model' } +}, thumb) -let staticFiles = [fileForContentTypes, fileForRelThumbnail] -staticFiles.forEach(({ name, content }) => addToZip(zip, name, content)) +for (const [fileName, fileContent, canBeCompressed] of zipContents) { + const fileStream = canBeCompressed ? new ZipDeflate(fileName, { level: 9 }) : new ZipPassThrough(fileName) + zip.add(fileStream) + fileStream.push(fileContent, true) +} zip.end() -function addToZip(zip, name, content) { - const zf = new ZipDeflate(name, { level: 9 }) - zip.add(zf) - zf.push(strToU8(content), true) -} - -/** //example how to generate thumb from canvas and add it in fflate -const pngPreviewFile = new fflate.ZipPassThrough('Metadata/thumbnail.png'); -zip.add(pngPreviewFile); -pngPreviewFile.push(cavassToPngA8(canvas), true); +/** example how to generate thumb from canvas +* @param {HTMLCanvasElement} canvas */ -function cavassToPngA8(canvas) { +function canvasToPngA8(canvas) { let url = canvas.toDataURL('image/png') url = url.substring(url.indexOf(',') + 1) - // strToU8 function from fflate - return strToU8(url) - // string to Uint8Array taken from stackoverflow, and should work in browser - return new Uint8Array( - atob(url) - .split('') - .map(c => c.charCodeAt(0)), - ) + //Convert to utf8 Uint8Array + return new TextEncoder().encode(url) } -/** intentionally not part of the lib, you may or may not need it in your export code */ +/** intentionally not part of the lib, you may or may not need it in your export code +* @param {*} blob +*/ async function blobToArrayBuffer(blob) { if ('arrayBuffer' in blob) return await blob.arrayBuffer() return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) - reader.onerror = () => reject + reader.onerror = (e) => reject(e) reader.readAsArrayBuffer(blob) }) } diff --git a/file-format/3mf-export/testSpeed.js b/file-format/3mf-export/testSpeed.js new file mode 100644 index 00000000..e14fc6b3 --- /dev/null +++ b/file-format/3mf-export/testSpeed.js @@ -0,0 +1,45 @@ +// @ts-nocheck + +// if not in browser +import { to3dmodel, to3mfZipContentSimple } from './index.js' + +function multiply(arr, mult, func){ + let out = new func(arr.length * mult) + for(let i=0; i=0.10.0" } }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -10389,6 +10415,12 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "license": "MIT" + }, "node_modules/style-mod": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz", @@ -19762,6 +19794,7 @@ "@trivago/prettier-plugin-sort-imports": "~3.3.0", "@types/node": "18.11.9", "esbuild": "^0.16.7", + "fast-xml-parser": "^4.5.0", "fflate": "0.8.0" }, "dependencies": { @@ -21572,6 +21605,7 @@ "@jscad/csg": "0.7.0", "@jscad/io": "2.4.7", "@jscad/modeling": "2.12.2", + "@jscadui/3mf-export": "*", "@jscadui/format-jscad": "*", "@jscadui/format-threejs": "*", "@jscadui/fs-provider": "*", @@ -25957,6 +25991,14 @@ } } }, + "fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "requires": { + "strnum": "^1.0.5" + } + }, "faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -28884,6 +28926,11 @@ "acorn": "^8.8.0" } }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "style-mod": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz",