Skip to content

Commit

Permalink
fork 3mf-export
Browse files Browse the repository at this point in the history
  • Loading branch information
hrgdavor committed Dec 17, 2024
1 parent a647f68 commit f5c4c6c
Show file tree
Hide file tree
Showing 19 changed files with 2,801 additions and 0 deletions.
4 changes: 4 additions & 0 deletions file-format/3mf-export-compact/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
testfile.3mf
cjs
iife
esm
17 changes: 17 additions & 0 deletions file-format/3mf-export-compact/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"semi": false,
"tabWidth": 2,
"arrowParens": "avoid",
"printWidth": 120,

"endOfLine": "auto",

"singleQuote": true,

"trailingComma": "all",

"importOrder": ["^components/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}
48 changes: 48 additions & 0 deletions file-format/3mf-export-compact/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 3mf export MVP

This is a fork from `3mf-export` to allow it to move forward with a robust XML library.

`3mf-export-comapct`

- is less robust for XML (may break xml if some attribute velues are not sanitised)
- has no dependencies
- could be preferred by some people because of small footprint
- I want to keep it as proud example of using arrays and join to gain nice performance.


[![npm version](https://badge.fury.io/js/@jscadui%2F3mf-export-compact.svg)](https://www.npmjs.com/package/@jscadui%2F3mf-export-compact) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

This is a set of functions to generate 3mf content for exporting 3d models and optionally embedding a thumbnail.

reference: https://github.com/3MFConsortium/spec_core/blob/master/3MF%20Core%20Specification.md

Format is defined by openxmlformats.

3mf file is a zip file that you need to produce using a zip library of your choice.

# object ids > 0

lib3mf cannot recognize things with id 0, so it is recommended to not use zero as id

# contents of said zip file

File names (with path) and metadata are hardcoded as it is not important for the purpose of exporting a model.

File names
- `[Content_Types].xml` - static file describing content types [src/staticFiles.js]
- `_rels/.rels` - [src/staticFiles.js]
- `/3D/3dmodel.model` - you must use this exact path to store model data
- `/Metadata/thumbnail.png` - you must use this exact path to store the optional thumbnail


# sample code to generate 3mf

Checkout the code from github and run `npm install` then `node testGen.js`.

Sample code to generate 3mf file is in [testGen.js]. It uses `fflate` which is intentionally set as
devDependency so others can generate the zip file with own preferred library.

To demonstrate that it is simple to include a thumbnail, I am using one already generated by my old jscad prototype
where this 3mf code is from. Final usage will of course be to take a snapshot from canvas while exporting the mesh.
Sample of such code is left in [testGen.js] but unused.
![testThumbnail.png](testThumbnail.png)
7 changes: 7 additions & 0 deletions file-format/3mf-export-compact/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@



## 0.5.0

- `_rels/.rels` content is no longer a static file from this import: `fileForRelThumbnail`, use `FileForRelThumbnail` class to generate it

161 changes: 161 additions & 0 deletions file-format/3mf-export-compact/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { XMLBuilder } from 'fast-xml-parser'
import { genItem } from './src/makeItem.js'
import { genModel } from './src/pushHeader.js'
import { genObjectWithComponents } from './src/pushObjectComponent.js'
import { genObjectWithMesh } from './src/pushObjectMesh.js'
import { fileForContentTypes } from './src/staticFiles.js'

export * from './src/staticFiles.js'

/**
* @typedef Mesh3MF
* @prop {number} id
* @prop {Float32Array} vertices
* @prop {Uint32Array} indices
* @prop {string} [name]
*
* @typedef Mesh3MFSimpleExt
* @prop {import('./src/defMatrix.js').mat4} [transform]
*
* @typedef {Mesh3MF & Mesh3MFSimpleExt} Mesh3MFSimple
*
* @typedef Component3MF
* @prop {number} id
* @prop {string} name
* @prop {Array<Child3MF>} children
*
* @typedef Child3MF
* @prop {number} objectID
* @prop {import('./src/defMatrix.js').mat4} [transform]
*
* @typedef To3MF
* @prop {Array<Mesh3MF>} meshes - manually declare meshes
* @prop {Array<Component3MF>} [components] - components can combine items
* @prop {Array<Child3MF>} items - output items, each pointing to component or mesh with objectID
* @prop {number} [precision]
* @prop {import('./src/pushHeader.js').Header} [header]
*/

/**
* @param {To3MF} options
* @returns {string}
*/
export function to3dmodel({ meshes = [], components = [], items = [], precision = 17, header }) {
/** @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)) },
)
}

const builder = new XMLBuilder({
ignoreAttributes: false,
format: true,
suppressEmptyNode: true
})

return builder.build(data)
}

/**
* Simple export provided meshes that have transform attached to them and autocreate items and pass to to3dmodel.
* @param {Array<Mesh3MFSimple>} 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 }))

return to3dmodel({ meshes, items, header, precision })
}

/**
* @typedef {[string,Uint8Array,boolean]} ZipContent FileName, FileContent, CanBeCompressed
*/

/**
* @param {To3MF} options
* @param {Uint8Array | undefined} [thumbnailPng]
* @returns {ZipContent[]}
*/
export function to3mfZipContent(options, thumbnailPng) {
/** @type {ZipContent[]} */
const result = []
const utf8Encoder = new TextEncoder()

result.push([fileForContentTypes.name, utf8Encoder.encode(fileForContentTypes.content), true])

const fileForRelThumbnail = new FileForRelThumbnail()
fileForRelThumbnail.add3dModel('3D/3dmodel.model')

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
}

/**
* @param {{meshes:Array<Mesh3MFSimple>,header?:import('./src/pushHeader.js').Header,precision?:number}} options
* @param {Uint8Array | undefined} [thumbnailPng]
* @returns {ZipContent[]}
*/
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 {
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.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() {
const builder = new XMLBuilder({ ignoreAttributes: false, format: true, suppressEmptyNode: true })
return builder.build(this.data)
}
}
Loading

0 comments on commit f5c4c6c

Please sign in to comment.