@jscad/3mf-serializer 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [2.1.1](https://github.com/jscad/OpenJSCAD.org/compare/@jscad/3mf-serializer@2.1.0...@jscad/3mf-serializer@2.1.1) (2022-04-03)
7
+
8
+ **Note:** Version bump only for package @jscad/3mf-serializer
9
+
10
+
11
+
12
+
13
+
14
+ # 2.1.0 (2022-04-03)
15
+
16
+
17
+ ### Features
18
+
19
+ * **3mf-serializer:** new serializer from JSCAD geometry to 3MF packages ([6b51aed](https://github.com/jscad/OpenJSCAD.org/commit/6b51aed63ee40674822a04ced773564552689763))
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ The MIT License (MIT)
3
+
4
+ Copyright (c) 2017-2021 JSCAD Organization
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ ## @jscad/3mf-serializer
2
+
3
+ > Serializer of JSCAD geometries to 3MF shapes
4
+
5
+ [![npm version](https://badge.fury.io/js/%40jscad%2F3mf-serializer.svg)](https://badge.fury.io/js/%40jscad%2F3mf-serializer)
6
+ [![Build Status](https://travis-ci.org/jscad/io.svg)](https://travis-ci.org/jscad/3mf-serializer)
7
+
8
+ ## Overview
9
+
10
+ This serializer outputs a 'blobable' array of data from one or more JSCAD geometries. Only XML output is supported.
11
+ The array of data can either be used to create a Blob (`new Blob(blobable)`), or converted to a Node.js buffer.
12
+
13
+ The serialization of the following geometries are possible.
14
+ - serialization of 3D geometry (geom3) to 3MF mesh(s)
15
+
16
+ In addition, both color and name attributes are converted to 3MF attributes if found on the JSCAD geometry.
17
+
18
+ ## Table of Contents
19
+
20
+ - [Installation](#installation)
21
+ - [Usage](#usage)
22
+ - [Contribute](#contribute)
23
+ - [License](#license)
24
+
25
+ ## Installation
26
+
27
+ ```
28
+ npm install @jscad/3mf-serializer
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```javascript
34
+ const { serialize } = require('@jscad/3mf-serializer')
35
+
36
+ const rawData = serialize({unit: inch}, geometry)
37
+
38
+ //in browser (with browserify etc)
39
+ const blob = new Blob(rawData)
40
+
41
+ ```
42
+
43
+ ## Contribute
44
+
45
+ For questions about the API, please contact the [User Group](https://openjscad.xyz/forum.html)
46
+
47
+ Please see the README information of the OpenJSCAD.org project for how to submit bug reports or changes.
48
+
49
+ Small Note: If editing this README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
50
+
51
+ ## License
52
+
53
+ [The MIT License (MIT)](./LICENSE)
54
+ (unless specified otherwise)
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@jscad/3mf-serializer",
3
+ "version": "2.1.1",
4
+ "description": "3MF serializer for JSCAD project",
5
+ "repository": "https://github.com/jscad/OpenJSCAD.org",
6
+ "main": "src/index.js",
7
+ "scripts": {
8
+ "coverage": "nyc --reporter=html --reporter=text npm test",
9
+ "test": "ava --concurrency 3 --verbose --timeout 40000 './tests/*.test.js'"
10
+ },
11
+ "contributors": [
12
+ {
13
+ "name": "z3dev",
14
+ "url": "http://www.z3d.jp"
15
+ }
16
+ ],
17
+ "keywords": [
18
+ "openjscad",
19
+ "jscad",
20
+ "export",
21
+ "serializer",
22
+ "3mf"
23
+ ],
24
+ "license": "MIT",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@jscad/array-utils": "2.1.3",
30
+ "@jscad/modeling": "2.9.2",
31
+ "fflate": "0.7.3",
32
+ "onml": "1.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "ava": "3.15.0",
36
+ "nyc": "15.1.0"
37
+ },
38
+ "gitHead": "0cebde0166c104e3c08cc05d2c03d9defc7eca26"
39
+ }
package/src/index.js ADDED
@@ -0,0 +1,256 @@
1
+ /*
2
+ JSCAD Object to 3MF (XML) Format Serialization
3
+
4
+ ## License
5
+
6
+ Copyright (c) 2022 JSCAD Organization https://github.com/jscad
7
+
8
+ All code released under MIT license
9
+
10
+ Notes:
11
+ 1) geom2 conversion to:
12
+ none
13
+ 2) geom3 conversion to:
14
+ mesh
15
+ 3) path2 conversion to:
16
+ none
17
+ */
18
+
19
+ /**
20
+ * Serializer of JSCAD geometries to 3D manufacturing format (XML)
21
+ *
22
+ * The serialization of the following geometries are possible.
23
+ * - serialization of 3D geometry (geom3) to 3MF object (a unique mesh containing both vertices and triangles)
24
+ *
25
+ * Colors are added to base materials when found on the 3D geometry, i.e. attribute color.
26
+ * Names are added to meshs when found on the 3D geometry, i.e. attribute name.
27
+ *
28
+ * @module io/3mf-serializer
29
+ * @example
30
+ * const { serializer, mimeType } = require('@jscad/3mf-serializer')
31
+ */
32
+
33
+
34
+ const zipSync = require('fflate').zipSync
35
+ const strToU8 = require('fflate').strToU8
36
+
37
+ const stringify = require('onml/lib/stringify')
38
+
39
+ const { colors, geometries, modifiers } = require('@jscad/modeling')
40
+ const { flatten, toArray } = require('@jscad/array-utils')
41
+
42
+
43
+ const mimeType = 'model/3mf'
44
+ const fileExtension = '3mf'
45
+
46
+ /**
47
+ * Serialize the give objects to 3MF contents (XML) or 3MF packaging (OPC).
48
+ * @see https://3mf.io/specification/
49
+ * @param {Object} [options] - options for serialization
50
+ * @param {String} [options.unit='millimeter'] - unit of design; millimeter, inch, feet, meter or micrometer
51
+ * @param {Boolean} [options.metadata=true] - add metadata to 3MF contents, such at CreationDate
52
+ * @param {Array} [options.defaultcolor=[0,0,0,1]] - default color for objects
53
+ * @param {Boolean} [options.compress=true] - package and compress the contents
54
+ * @param {Object|Array} objects - objects to serialize into 3D manufacturing format
55
+ * @returns {Array} serialized contents, 3MF contents (XML) or 3MF packaging (ZIP)
56
+ * @example
57
+ * const geometry = primitives.cube()
58
+ * const package = serializer({unit: 'meter'}, geometry) // 3MF package, ZIP format
59
+ */
60
+ const serialize = (options, ...objects) => {
61
+ const defaults = {
62
+ unit: 'millimeter', // micron, millimeter, centimeter, inch, foot, meter
63
+ metadata: true,
64
+ defaultcolor: [255/255, 160/255, 0, 1], // JSCAD Orange
65
+ compress: true
66
+ }
67
+ options = Object.assign({}, defaults, options)
68
+
69
+ objects = flatten(objects)
70
+
71
+ // convert only 3D geometries
72
+ let objects3d = objects.filter((object) => geometries.geom3.isA(object))
73
+
74
+ if (objects3d.length === 0) throw new Error('only 3D geometries can be serialized to 3MF')
75
+ if (objects.length !== objects3d.length) console.warn('some objects could not be serialized to 3MF')
76
+
77
+ // convert to triangles
78
+ objects = toArray(modifiers.generalize({ snap: true, triangulate: true }, objects3d))
79
+
80
+ // construct the contents of the 3MF 'model'
81
+ const body = ['model',
82
+ {
83
+ unit: options.unit,
84
+ 'xml:lang': 'und'
85
+ },
86
+ ['metadata', { name: 'Application' }, 'JSCAD']
87
+ ]
88
+ if (options.metadata) {
89
+ body.push(['metadata', { name: 'CreationDate' }, new Date().toISOString()])
90
+ }
91
+ body.push(translateResources(objects, options))
92
+ body.push(translateBuild(objects, options))
93
+
94
+ // convert the contents to 3MF (XML) format
95
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
96
+ ${stringify(body, 2)}`
97
+
98
+ // compress and package the contents if requested
99
+ if (options.compress) {
100
+ const data = {
101
+ '3D': {
102
+ '3dmodel.model': strToU8(xml)
103
+ },
104
+ '_rels': {
105
+ '.rels': strToU8(rels)
106
+ },
107
+ '[Content_Types].xml': strToU8(contenttype)
108
+ }
109
+ const opts = {
110
+ comment: 'created by JSCAD'
111
+ }
112
+ const zipData = zipSync(data, opts)
113
+ return [zipData.buffer]
114
+ }
115
+ return [xml]
116
+ }
117
+
118
+ const contenttype = `<?xml version="1.0" encoding="UTF-8" ?>
119
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
120
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml">
121
+ </Default>
122
+ <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml">
123
+ </Default>
124
+ </Types>`
125
+
126
+ const rels = `<?xml version="1.0" encoding="UTF-8" ?>
127
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
128
+ <Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel">
129
+ </Relationship>
130
+ </Relationships>`
131
+
132
+ const translateResources = (objects, options) => {
133
+ let resources = ['resources', {}, translateMaterials(objects, options)]
134
+ resources = resources.concat(translateObjects(objects, options))
135
+ return resources
136
+ }
137
+
138
+ const translateMaterials = (objects, options) => {
139
+ let basematerials = ['basematerials', { id: '0' }]
140
+
141
+ const materials = []
142
+ objects.forEach((object, i) => {
143
+ let srgb = colors.rgbToHex(options.defaultcolor).toUpperCase()
144
+ if (object.color) {
145
+ srgb = colors.rgbToHex(object.color).toUpperCase()
146
+ }
147
+ materials.push(['base', { name: `mat${i}`, displaycolor: srgb }])
148
+ })
149
+
150
+ basematerials = basematerials.concat(materials)
151
+ return basematerials
152
+ }
153
+
154
+ const translateObjects = (objects, options) => {
155
+ const contents = []
156
+ objects.forEach((object, i) => {
157
+ if (geometries.geom3.isA(object)) {
158
+ const polygons = geometries.geom3.toPolygons(object)
159
+ if (polygons.length > 0) {
160
+ options.id = i
161
+ contents.push(convertToObject(object, options))
162
+ }
163
+ }
164
+ })
165
+ return contents
166
+ }
167
+
168
+ const translateBuild = (objects, options) => {
169
+ let build = ['build', { }]
170
+
171
+ const items = []
172
+ objects.forEach((object, i) => {
173
+ items.push(['item', { objectid: `${i + 1}` }])
174
+ })
175
+
176
+ build = build.concat(items)
177
+ return build
178
+ }
179
+
180
+ /*
181
+ * This section converts each 3D geometry to object / mesh
182
+ */
183
+
184
+ const convertToObject = (object, options) => {
185
+ const name = object.name ? object.name : `Part ${options.id}`
186
+ const contents = ['object', { id: `${options.id + 1}`, type: 'model', pid: '0', pindex: `${options.id}`, name: name }, convertToMesh(object, options)]
187
+ return contents
188
+ }
189
+
190
+ const convertToMesh = (object, options) => {
191
+ const contents = ['mesh', {}, convertToVertices(object, options), convertToVolumes(object, options)]
192
+ return contents
193
+ }
194
+
195
+ /*
196
+ * This section converts each 3D geometry to mesh vertices
197
+ */
198
+
199
+ const convertToVertices = (object, options) => {
200
+ const contents = ['vertices', {}]
201
+
202
+ const vertices = []
203
+ const polygons = geometries.geom3.toPolygons(object)
204
+ polygons.forEach((polygon) => {
205
+ for (let i = 0; i < polygon.vertices.length; i++) {
206
+ vertices.push(convertToVertex(polygon.vertices[i], options))
207
+ }
208
+ })
209
+
210
+ return contents.concat(vertices)
211
+ }
212
+
213
+ const convertToVertex = (vertex, options) => {
214
+ const contents = ['vertex', { x: vertex[0], y: vertex[1], z: vertex[2] }]
215
+ return contents
216
+ }
217
+
218
+ /*
219
+ * This section converts each 3D geometry to mesh triangles
220
+ */
221
+
222
+ const convertToVolumes = (object, options) => {
223
+ let n = 0
224
+ const polygons = geometries.geom3.toPolygons(object)
225
+
226
+ let contents = ['triangles', {}]
227
+ polygons.forEach((polygon) => {
228
+ if (polygon.vertices.length < 3) {
229
+ return
230
+ }
231
+
232
+ const triangles = convertToTriangles(polygon, n)
233
+
234
+ contents = contents.concat(triangles)
235
+
236
+ n += polygon.vertices.length
237
+ })
238
+ return contents
239
+ }
240
+
241
+ const convertToTriangles = (polygon, index) => {
242
+ const contents = []
243
+
244
+ // making sure they are all triangles (triangular polygons)
245
+ for (let i = 0; i < polygon.vertices.length - 2; i++) {
246
+ const triangle = ['triangle', { v1: index, v2: (index + i + 1), v3: (index + i + 2) }]
247
+ contents.push(triangle)
248
+ }
249
+ return contents
250
+ }
251
+
252
+ module.exports = {
253
+ serialize,
254
+ mimeType,
255
+ fileExtension
256
+ }
@@ -0,0 +1,335 @@
1
+ const test = require('ava')
2
+
3
+ const { colors, geometries, primitives } = require('@jscad/modeling')
4
+
5
+ const serializer = require('../src/index.js')
6
+
7
+ test('serialize (empty)', (t) => {
8
+ const emptyShape = geometries.geom3.create()
9
+ const buffer = serializer.serialize({ metadata: false, compress: false }, emptyShape)
10
+ t.deepEqual(buffer, expected1)
11
+ })
12
+
13
+ test('serialize (single)', (t) => {
14
+ const cube1 = primitives.cube()
15
+ const buffer = serializer.serialize({ metadata: false, compress: false }, cube1)
16
+ t.deepEqual(buffer, expected2)
17
+ })
18
+
19
+ test('serialize (single, color)', (t) => {
20
+ let cube1 = primitives.cube()
21
+ cube1 = colors.colorize([1.0, 0.0, 0.5, 0.8], cube1)
22
+ const buffer = serializer.serialize({ metadata: false, compress: false, unit: 'inch' }, cube1)
23
+ t.deepEqual(buffer, expected3)
24
+ })
25
+
26
+ test('serialize (multiple, color)', (t) => {
27
+ let cube1 = primitives.cuboid({ size: [4, 5, 6], center: [5, 5, 5] })
28
+ cube1 = colors.colorize([0.0, 0.0, 1.0, 0.8], cube1)
29
+ cube1.name = "CUBE A"
30
+ const cube2 = primitives.cube()
31
+ cube2.name = "CUBE B"
32
+ const buffer = serializer.serialize({ metadata: false, compress: false, defaultcolor: [1, 0, 0, 1] }, cube1, cube2)
33
+ t.deepEqual(buffer, expected4)
34
+ })
35
+
36
+ test('serialize (multiple, compress)', (t) => {
37
+ const cube1 = colors.colorize([1.0, 0.0, 0.5, 0.8], primitives.cube())
38
+ cube1.name = "CUBE A"
39
+ const cube2 = primitives.cuboid({ size: [4, 5, 6], center: [5, 5, 5] })
40
+ cube2.name = "CUBE B"
41
+
42
+ const results = serializer.serialize({ compress: true }, cube1, cube2)
43
+ t.is(results.length, 1)
44
+ const len = results[0].byteLength
45
+ t.true(len > 1600)
46
+ })
47
+
48
+ const expected1 = [
49
+ `<?xml version="1.0" encoding="UTF-8"?>
50
+ <model unit="millimeter" xml:lang="und">
51
+ <metadata name="Application">JSCAD</metadata>
52
+ <resources>
53
+ <basematerials id="0">
54
+ <base name="mat0" displaycolor="#FFA000FF"/>
55
+ </basematerials>
56
+ </resources>
57
+ <build>
58
+ <item objectid="1"/>
59
+ </build>
60
+ </model>
61
+ `
62
+ ]
63
+
64
+ const expected2 = [
65
+ `<?xml version="1.0" encoding="UTF-8"?>
66
+ <model unit="millimeter" xml:lang="und">
67
+ <metadata name="Application">JSCAD</metadata>
68
+ <resources>
69
+ <basematerials id="0">
70
+ <base name="mat0" displaycolor="#FFA000FF"/>
71
+ </basematerials>
72
+ <object id="1" type="model" pid="0" pindex="0" name="Part 0">
73
+ <mesh>
74
+ <vertices>
75
+ <vertex x="-1" y="-1" z="-1"/>
76
+ <vertex x="-1" y="-1" z="1"/>
77
+ <vertex x="-1" y="1" z="1"/>
78
+ <vertex x="-1" y="-1" z="-1"/>
79
+ <vertex x="-1" y="1" z="1"/>
80
+ <vertex x="-1" y="1" z="-1"/>
81
+ <vertex x="1" y="-1" z="-1"/>
82
+ <vertex x="1" y="1" z="-1"/>
83
+ <vertex x="1" y="1" z="1"/>
84
+ <vertex x="1" y="-1" z="-1"/>
85
+ <vertex x="1" y="1" z="1"/>
86
+ <vertex x="1" y="-1" z="1"/>
87
+ <vertex x="-1" y="-1" z="-1"/>
88
+ <vertex x="1" y="-1" z="-1"/>
89
+ <vertex x="1" y="-1" z="1"/>
90
+ <vertex x="-1" y="-1" z="-1"/>
91
+ <vertex x="1" y="-1" z="1"/>
92
+ <vertex x="-1" y="-1" z="1"/>
93
+ <vertex x="-1" y="1" z="-1"/>
94
+ <vertex x="-1" y="1" z="1"/>
95
+ <vertex x="1" y="1" z="1"/>
96
+ <vertex x="-1" y="1" z="-1"/>
97
+ <vertex x="1" y="1" z="1"/>
98
+ <vertex x="1" y="1" z="-1"/>
99
+ <vertex x="-1" y="-1" z="-1"/>
100
+ <vertex x="-1" y="1" z="-1"/>
101
+ <vertex x="1" y="1" z="-1"/>
102
+ <vertex x="-1" y="-1" z="-1"/>
103
+ <vertex x="1" y="1" z="-1"/>
104
+ <vertex x="1" y="-1" z="-1"/>
105
+ <vertex x="-1" y="-1" z="1"/>
106
+ <vertex x="1" y="-1" z="1"/>
107
+ <vertex x="1" y="1" z="1"/>
108
+ <vertex x="-1" y="-1" z="1"/>
109
+ <vertex x="1" y="1" z="1"/>
110
+ <vertex x="-1" y="1" z="1"/>
111
+ </vertices>
112
+ <triangles>
113
+ <triangle v1="0" v2="1" v3="2"/>
114
+ <triangle v1="3" v2="4" v3="5"/>
115
+ <triangle v1="6" v2="7" v3="8"/>
116
+ <triangle v1="9" v2="10" v3="11"/>
117
+ <triangle v1="12" v2="13" v3="14"/>
118
+ <triangle v1="15" v2="16" v3="17"/>
119
+ <triangle v1="18" v2="19" v3="20"/>
120
+ <triangle v1="21" v2="22" v3="23"/>
121
+ <triangle v1="24" v2="25" v3="26"/>
122
+ <triangle v1="27" v2="28" v3="29"/>
123
+ <triangle v1="30" v2="31" v3="32"/>
124
+ <triangle v1="33" v2="34" v3="35"/>
125
+ </triangles>
126
+ </mesh>
127
+ </object>
128
+ </resources>
129
+ <build>
130
+ <item objectid="1"/>
131
+ </build>
132
+ </model>
133
+ `
134
+ ]
135
+
136
+ const expected3 = [
137
+ `<?xml version="1.0" encoding="UTF-8"?>
138
+ <model unit="inch" xml:lang="und">
139
+ <metadata name="Application">JSCAD</metadata>
140
+ <resources>
141
+ <basematerials id="0">
142
+ <base name="mat0" displaycolor="#FF007FCC"/>
143
+ </basematerials>
144
+ <object id="1" type="model" pid="0" pindex="0" name="Part 0">
145
+ <mesh>
146
+ <vertices>
147
+ <vertex x="-1" y="-1" z="-1"/>
148
+ <vertex x="-1" y="-1" z="1"/>
149
+ <vertex x="-1" y="1" z="1"/>
150
+ <vertex x="-1" y="-1" z="-1"/>
151
+ <vertex x="-1" y="1" z="1"/>
152
+ <vertex x="-1" y="1" z="-1"/>
153
+ <vertex x="1" y="-1" z="-1"/>
154
+ <vertex x="1" y="1" z="-1"/>
155
+ <vertex x="1" y="1" z="1"/>
156
+ <vertex x="1" y="-1" z="-1"/>
157
+ <vertex x="1" y="1" z="1"/>
158
+ <vertex x="1" y="-1" z="1"/>
159
+ <vertex x="-1" y="-1" z="-1"/>
160
+ <vertex x="1" y="-1" z="-1"/>
161
+ <vertex x="1" y="-1" z="1"/>
162
+ <vertex x="-1" y="-1" z="-1"/>
163
+ <vertex x="1" y="-1" z="1"/>
164
+ <vertex x="-1" y="-1" z="1"/>
165
+ <vertex x="-1" y="1" z="-1"/>
166
+ <vertex x="-1" y="1" z="1"/>
167
+ <vertex x="1" y="1" z="1"/>
168
+ <vertex x="-1" y="1" z="-1"/>
169
+ <vertex x="1" y="1" z="1"/>
170
+ <vertex x="1" y="1" z="-1"/>
171
+ <vertex x="-1" y="-1" z="-1"/>
172
+ <vertex x="-1" y="1" z="-1"/>
173
+ <vertex x="1" y="1" z="-1"/>
174
+ <vertex x="-1" y="-1" z="-1"/>
175
+ <vertex x="1" y="1" z="-1"/>
176
+ <vertex x="1" y="-1" z="-1"/>
177
+ <vertex x="-1" y="-1" z="1"/>
178
+ <vertex x="1" y="-1" z="1"/>
179
+ <vertex x="1" y="1" z="1"/>
180
+ <vertex x="-1" y="-1" z="1"/>
181
+ <vertex x="1" y="1" z="1"/>
182
+ <vertex x="-1" y="1" z="1"/>
183
+ </vertices>
184
+ <triangles>
185
+ <triangle v1="0" v2="1" v3="2"/>
186
+ <triangle v1="3" v2="4" v3="5"/>
187
+ <triangle v1="6" v2="7" v3="8"/>
188
+ <triangle v1="9" v2="10" v3="11"/>
189
+ <triangle v1="12" v2="13" v3="14"/>
190
+ <triangle v1="15" v2="16" v3="17"/>
191
+ <triangle v1="18" v2="19" v3="20"/>
192
+ <triangle v1="21" v2="22" v3="23"/>
193
+ <triangle v1="24" v2="25" v3="26"/>
194
+ <triangle v1="27" v2="28" v3="29"/>
195
+ <triangle v1="30" v2="31" v3="32"/>
196
+ <triangle v1="33" v2="34" v3="35"/>
197
+ </triangles>
198
+ </mesh>
199
+ </object>
200
+ </resources>
201
+ <build>
202
+ <item objectid="1"/>
203
+ </build>
204
+ </model>
205
+ `
206
+ ]
207
+
208
+ const expected4 = [`<?xml version="1.0" encoding="UTF-8"?>
209
+ <model unit="millimeter" xml:lang="und">
210
+ <metadata name="Application">JSCAD</metadata>
211
+ <resources>
212
+ <basematerials id="0">
213
+ <base name="mat0" displaycolor="#0000FFCC"/>
214
+ <base name="mat1" displaycolor="#FF0000FF"/>
215
+ </basematerials>
216
+ <object id="1" type="model" pid="0" pindex="0" name="CUBE A">
217
+ <mesh>
218
+ <vertices>
219
+ <vertex x="3" y="2.5" z="2"/>
220
+ <vertex x="3" y="2.5" z="8"/>
221
+ <vertex x="3" y="7.5" z="8"/>
222
+ <vertex x="3" y="2.5" z="2"/>
223
+ <vertex x="3" y="7.5" z="8"/>
224
+ <vertex x="3" y="7.5" z="2"/>
225
+ <vertex x="7" y="2.5" z="2"/>
226
+ <vertex x="7" y="7.5" z="2"/>
227
+ <vertex x="7" y="7.5" z="8"/>
228
+ <vertex x="7" y="2.5" z="2"/>
229
+ <vertex x="7" y="7.5" z="8"/>
230
+ <vertex x="7" y="2.5" z="8"/>
231
+ <vertex x="3" y="2.5" z="2"/>
232
+ <vertex x="7" y="2.5" z="2"/>
233
+ <vertex x="7" y="2.5" z="8"/>
234
+ <vertex x="3" y="2.5" z="2"/>
235
+ <vertex x="7" y="2.5" z="8"/>
236
+ <vertex x="3" y="2.5" z="8"/>
237
+ <vertex x="3" y="7.5" z="2"/>
238
+ <vertex x="3" y="7.5" z="8"/>
239
+ <vertex x="7" y="7.5" z="8"/>
240
+ <vertex x="3" y="7.5" z="2"/>
241
+ <vertex x="7" y="7.5" z="8"/>
242
+ <vertex x="7" y="7.5" z="2"/>
243
+ <vertex x="3" y="2.5" z="2"/>
244
+ <vertex x="3" y="7.5" z="2"/>
245
+ <vertex x="7" y="7.5" z="2"/>
246
+ <vertex x="3" y="2.5" z="2"/>
247
+ <vertex x="7" y="7.5" z="2"/>
248
+ <vertex x="7" y="2.5" z="2"/>
249
+ <vertex x="3" y="2.5" z="8"/>
250
+ <vertex x="7" y="2.5" z="8"/>
251
+ <vertex x="7" y="7.5" z="8"/>
252
+ <vertex x="3" y="2.5" z="8"/>
253
+ <vertex x="7" y="7.5" z="8"/>
254
+ <vertex x="3" y="7.5" z="8"/>
255
+ </vertices>
256
+ <triangles>
257
+ <triangle v1="0" v2="1" v3="2"/>
258
+ <triangle v1="3" v2="4" v3="5"/>
259
+ <triangle v1="6" v2="7" v3="8"/>
260
+ <triangle v1="9" v2="10" v3="11"/>
261
+ <triangle v1="12" v2="13" v3="14"/>
262
+ <triangle v1="15" v2="16" v3="17"/>
263
+ <triangle v1="18" v2="19" v3="20"/>
264
+ <triangle v1="21" v2="22" v3="23"/>
265
+ <triangle v1="24" v2="25" v3="26"/>
266
+ <triangle v1="27" v2="28" v3="29"/>
267
+ <triangle v1="30" v2="31" v3="32"/>
268
+ <triangle v1="33" v2="34" v3="35"/>
269
+ </triangles>
270
+ </mesh>
271
+ </object>
272
+ <object id="2" type="model" pid="0" pindex="1" name="CUBE B">
273
+ <mesh>
274
+ <vertices>
275
+ <vertex x="-1" y="-1" z="-1"/>
276
+ <vertex x="-1" y="-1" z="1"/>
277
+ <vertex x="-1" y="1" z="1"/>
278
+ <vertex x="-1" y="-1" z="-1"/>
279
+ <vertex x="-1" y="1" z="1"/>
280
+ <vertex x="-1" y="1" z="-1"/>
281
+ <vertex x="1" y="-1" z="-1"/>
282
+ <vertex x="1" y="1" z="-1"/>
283
+ <vertex x="1" y="1" z="1"/>
284
+ <vertex x="1" y="-1" z="-1"/>
285
+ <vertex x="1" y="1" z="1"/>
286
+ <vertex x="1" y="-1" z="1"/>
287
+ <vertex x="-1" y="-1" z="-1"/>
288
+ <vertex x="1" y="-1" z="-1"/>
289
+ <vertex x="1" y="-1" z="1"/>
290
+ <vertex x="-1" y="-1" z="-1"/>
291
+ <vertex x="1" y="-1" z="1"/>
292
+ <vertex x="-1" y="-1" z="1"/>
293
+ <vertex x="-1" y="1" z="-1"/>
294
+ <vertex x="-1" y="1" z="1"/>
295
+ <vertex x="1" y="1" z="1"/>
296
+ <vertex x="-1" y="1" z="-1"/>
297
+ <vertex x="1" y="1" z="1"/>
298
+ <vertex x="1" y="1" z="-1"/>
299
+ <vertex x="-1" y="-1" z="-1"/>
300
+ <vertex x="-1" y="1" z="-1"/>
301
+ <vertex x="1" y="1" z="-1"/>
302
+ <vertex x="-1" y="-1" z="-1"/>
303
+ <vertex x="1" y="1" z="-1"/>
304
+ <vertex x="1" y="-1" z="-1"/>
305
+ <vertex x="-1" y="-1" z="1"/>
306
+ <vertex x="1" y="-1" z="1"/>
307
+ <vertex x="1" y="1" z="1"/>
308
+ <vertex x="-1" y="-1" z="1"/>
309
+ <vertex x="1" y="1" z="1"/>
310
+ <vertex x="-1" y="1" z="1"/>
311
+ </vertices>
312
+ <triangles>
313
+ <triangle v1="0" v2="1" v3="2"/>
314
+ <triangle v1="3" v2="4" v3="5"/>
315
+ <triangle v1="6" v2="7" v3="8"/>
316
+ <triangle v1="9" v2="10" v3="11"/>
317
+ <triangle v1="12" v2="13" v3="14"/>
318
+ <triangle v1="15" v2="16" v3="17"/>
319
+ <triangle v1="18" v2="19" v3="20"/>
320
+ <triangle v1="21" v2="22" v3="23"/>
321
+ <triangle v1="24" v2="25" v3="26"/>
322
+ <triangle v1="27" v2="28" v3="29"/>
323
+ <triangle v1="30" v2="31" v3="32"/>
324
+ <triangle v1="33" v2="34" v3="35"/>
325
+ </triangles>
326
+ </mesh>
327
+ </object>
328
+ </resources>
329
+ <build>
330
+ <item objectid="1"/>
331
+ <item objectid="2"/>
332
+ </build>
333
+ </model>
334
+ `
335
+ ]