UNPKG

14.1 kBJavaScriptView Raw
1/*
2## License
3
4Copyright (c) 2016 Z3 Development https://github.com/z3dev
5 2017 Mark 'kaosat-dev' Moissette
6
7The upgrades (direct geometry instantiation from this deserializer) and refactoring
8have been very kindly sponsored by [Copenhagen Fabrication / Stykka](https://www.stykka.com/)
9
10All code released under MIT license
11*/
12
13const sax = require('sax')
14
15const { colors, transforms } = require('@jscad/modeling')
16const { toArray } = require('@jscad/array-utils')
17
18const version = require('./package.json').version
19
20const { cagLengthX, cagLengthY } = require('./helpers')
21const { svgSvg, svgRect, svgCircle, svgGroup, svgLine, svgPath, svgEllipse, svgPolygon, svgPolyline, svgUse } = require('./svgElementHelpers')
22const shapesMapGeometry = require('./shapesMapGeometry')
23const shapesMapJscad = require('./shapesMapJscad')
24
25/**
26 * Deserializer of STL data to JSCAD geometries.
27 * @module io/stl-deserializer
28 * @example
29 * const { deserializer, extension } = require('@jscad/stl-deserializer')
30 */
31
32/**
33 * Parse the given SVG data and return either a JSCAD script or a set of geometries
34 * @param {Object} options - options used during deserializing, REQUIRED
35 * @param {string} [options.filename='svg'] - filename of original SVG source
36 * @param {string} [options.version='0.0.0'] - version number to add to the metadata
37 * @param {boolean} [options.addMetadata=true] - toggle injection of metadata at the start of the script
38 * @param {string} [options.output='script'] - either 'script' or 'geometry' to set desired output
39 * @param {float} [options.pxPmm] - custom pixels per mm unit
40 * @param {string} input - SVG data
41 * @return {string|[object]} either a string (script) or a set of objects (geometry)
42 * @alias module:io/svg-deserializer.deserialize
43 */
44const deserialize = (options, input) => {
45 const defaults = {
46 filename: 'svg',
47 addMetaData: true,
48 output: 'script',
49 pxPmm: require('./constants').pxPmm,
50 target: 'path', // target - 'geom2' or 'path'
51 version,
52 segments: 32
53 }
54 options = Object.assign({}, defaults, options)
55 return options.output === 'script' ? translate(input, options) : instantiate(input, options)
56}
57
58/**
59 * Parse the given SVG source and return a set of geometries.
60 * @param {string} src svg data as text
61 * @param {object} options options (optional) anonymous object with:
62 * pxPmm {number} pixels per milimeter for calcuations
63 * version: {string} version number to add to the metadata
64 * addMetadata: {boolean} flag to enable/disable injection of metadata (producer, date, source)
65 *
66 * @return {[geometry]} a set of geometries
67 */
68const instantiate = (src, options) => {
69 const { pxPmm } = options
70
71 options && options.statusCallback && options.statusCallback({ progress: 0 })
72
73 // parse the SVG source
74 createSvgParser(src, pxPmm)
75 if (!svgObj) {
76 throw new Error('SVG parsing failed, no valid svg data retrieved')
77 }
78
79 options && options.statusCallback && options.statusCallback({ progress: 50 })
80
81 const result = objectify(options, svgObj)
82
83 options && options.statusCallback && options.statusCallback({ progress: 100 })
84 return result
85}
86
87/**
88 * Parse the given SVG source and return a JSCAD script
89 * @param {string} src svg data as text
90 * @param {object} options options (optional) anonymous object with:
91 * pxPmm {number: pixels per milimeter for calcuations
92 * version: {string} version number to add to the metadata
93 * addMetadata: {boolean} flag to enable/disable injection of metadata (producer, date, source)
94 * at the start of the file
95 * @return {string} a string (JSCAD script)
96 */
97const translate = (src, options) => {
98 const { filename, version, pxPmm, addMetaData } = options
99
100 options && options.statusCallback && options.statusCallback({ progress: 0 })
101
102 // parse the SVG source
103 createSvgParser(src, pxPmm)
104 if (!svgObj) {
105 throw new Error('SVG parsing failed, no valid svg data retrieved')
106 }
107
108 // convert the internal objects to JSCAD code
109 let code = addMetaData ? `//
110 // producer: JSCAD SVG Deserializer ${version}
111 // date: ${new Date()}
112 // source: ${filename}
113 //
114` : ''
115 code += 'const { colors, geometries, primitives, transforms } = require(\'@jscad/modeling\')\n\n'
116
117 options && options.statusCallback && options.statusCallback({ progress: 50 })
118
119 const scadCode = codify(options, svgObj)
120 code += scadCode
121 code += '\nmodule.exports = { main }'
122
123 options && options.statusCallback && options.statusCallback({ progress: 100 })
124 return code
125}
126
127// FIXME: should these be kept here ? any risk of side effects ?
128let svgUnitsX
129let svgUnitsY
130let svgUnitsV
131// processing controls
132const svgObjects = [] // named objects
133const svgGroups = [] // groups of objects
134const svgDefs = [] // defined objects
135let svgInDefs = false // svg DEFS element in process
136let svgObj // svg in object form
137let svgUnitsPmm = [1, 1]
138
139/*
140 * Convert the given group (of objects) into geometries
141 */
142const objectify = (options, group) => {
143 const { target, segments } = options
144 const level = svgGroups.length
145 // add this group to the heiarchy
146 svgGroups.push(group)
147 // create an indent for the generated code
148 let i = level
149 while (i > 0) {
150 i--
151 }
152
153 let geometries = []
154
155 const params = {
156 svgUnitsPmm,
157 svgUnitsX,
158 svgUnitsY,
159 svgUnitsV,
160 level,
161 target,
162 svgGroups,
163 segments
164 }
165 // apply base level attributes to all shapes
166 for (i = 0; i < group.objects.length; i++) {
167 const obj = group.objects[i]
168 let shapes = toArray(shapesMapGeometry(obj, objectify, params))
169 shapes = shapes.map((shape) => {
170 if ('transforms' in obj) {
171 // NOTE: SVG specifications require that transforms are applied in the order given.
172 // But these are applied in the order as required by JSCAD
173 let rotateAttribute = null
174 let scaleAttribute = null
175 let translateAttribute = null
176
177 for (let j = 0; j < obj.transforms.length; j++) {
178 const t = obj.transforms[j]
179 if ('rotate' in t) { rotateAttribute = t }
180 if ('scale' in t) { scaleAttribute = t }
181 if ('translate' in t) { translateAttribute = t }
182 }
183 if (scaleAttribute !== null) {
184 let x = Math.abs(scaleAttribute.scale[0])
185 let y = Math.abs(scaleAttribute.scale[1])
186 shape = transforms.scale([x, y, 1], shape)
187 // and mirror if necessary
188 x = scaleAttribute.scale[0]
189 y = scaleAttribute.scale[1]
190 if (x < 0) {
191 shape = transforms.mirrorX(shape)
192 }
193 if (y < 0) {
194 shape = transforms.mirrorY(shape)
195 }
196 }
197 if (rotateAttribute !== null) {
198 const z = 0 - rotateAttribute.rotate * 0.017453292519943295 // radians
199 shape = transforms.rotateZ(z, shape)
200 }
201 if (translateAttribute !== null) {
202 const x = cagLengthX(translateAttribute.translate[0], svgUnitsPmm, svgUnitsX)
203 const y = (0 - cagLengthY(translateAttribute.translate[1], svgUnitsPmm, svgUnitsY))
204 shape = transforms.translate([x, y, 0], shape)
205 }
206 }
207 if (target === 'path' && obj.stroke) {
208 shape = colors.colorize([obj.stroke[0], obj.stroke[1], obj.stroke[2], 1], shape)
209 }
210 if (target === 'geom2' && obj.fill) {
211 shape = colors.colorize([obj.fill[0], obj.fill[1], obj.fill[2], 1], shape)
212 }
213 return shape
214 })
215 geometries = geometries.concat(shapes)
216 }
217
218 // remove this group from the hiearchy
219 svgGroups.pop()
220
221 return geometries
222}
223
224/*
225 * Convert the given group into JSCAD script
226 */
227const codify = (options, group) => {
228 const { target, segments } = options
229 const level = svgGroups.length
230 // add this group to the heiarchy
231 svgGroups.push(group)
232 // create an indent for the generated code
233 let indent = ' '
234 let i = level
235 while (i > 0) {
236 indent += ' '
237 i--
238 }
239 // pre-code
240 let code = ''
241 if (level === 0) {
242 code += 'function main(params) {\n let levels = {}\n let paths = {}\n let parts\n'
243 }
244 const ln = 'levels.l' + level
245 code += `${indent}${ln} = []\n`
246
247 // generate code for all objects
248 for (i = 0; i < group.objects.length; i++) {
249 const obj = group.objects[i]
250 const on = 'paths.p' + i
251
252 const params = {
253 level,
254 indent,
255 ln,
256 on,
257 svgUnitsPmm,
258 svgUnitsX,
259 svgUnitsY,
260 svgUnitsV,
261 svgGroups,
262 target,
263 segments
264 }
265
266 const tmpCode = shapesMapJscad(obj, codify, params)
267 code += tmpCode
268
269 if ('transforms' in obj) {
270 // NOTE: SVG specifications require that transforms are applied in the order given.
271 // But these are applied in the order as required by JSCAD
272 let rotateAttribute = null
273 let scaleAttribute = null
274 let translateAttribute = null
275
276 for (let j = 0; j < obj.transforms.length; j++) {
277 const t = obj.transforms[j]
278 if ('rotate' in t) { rotateAttribute = t }
279 if ('scale' in t) { scaleAttribute = t }
280 if ('translate' in t) { translateAttribute = t }
281 }
282 if (scaleAttribute !== null) {
283 let x = Math.abs(scaleAttribute.scale[0])
284 let y = Math.abs(scaleAttribute.scale[1])
285 code += `${indent}${on} = transforms.scale([${x}, ${y}, 1], ${on})\n`
286 // and mirror if necessary
287 x = scaleAttribute.scale[0]
288 y = scaleAttribute.scale[1]
289 if (x < 0) {
290 code += `${indent}${on} = transforms.mirrorX(${on})\n`
291 }
292 if (y < 0) {
293 code += `${indent}${on} = transforms.mirrorY(${on})\n`
294 }
295 }
296 if (rotateAttribute !== null) {
297 const z = 0 - rotateAttribute.rotate * 0.017453292519943295 // radians
298 code += `${indent}${on} = transforms.rotateZ(${z}, ${on})\n`
299 }
300 if (translateAttribute !== null) {
301 const x = cagLengthX(translateAttribute.translate[0], svgUnitsPmm, svgUnitsX)
302 const y = (0 - cagLengthY(translateAttribute.translate[1], svgUnitsPmm, svgUnitsY))
303 code += `${indent}${on} = transforms.translate([${x}, ${y}, 0], ${on})\n`
304 }
305 }
306 if (target === 'path' && obj.stroke) {
307 // for path, only use the supplied SVG stroke color
308 code += `${indent}${on} = colors.colorize([${obj.stroke[0]}, ${obj.stroke[1]}, ${obj.stroke[2]}, 1], ${on})\n`
309 }
310 if (target === 'geom2' && obj.fill) {
311 // for geom2, only use the supplied SVG fill color
312 code += `${indent}${on} = colors.colorize([${obj.fill[0]}, ${obj.fill[1]}, ${obj.fill[2]}, 1], ${on})\n`
313 }
314 code += `${indent}${ln} = ${ln}.concat(${on})\n\n`
315 }
316 // post-code
317 if (level === 0) {
318 code += indent + 'return ' + ln + '\n'
319 code += '}\n'
320 }
321 // remove this group from the hiearchy
322 svgGroups.pop()
323
324 return code
325}
326
327const createSvgParser = (src, pxPmm) => {
328 // create a parser for the XML
329 const parser = sax.parser(false, { trim: true, lowercase: false, position: true })
330 if (pxPmm !== undefined && pxPmm > parser.pxPmm) {
331 parser.pxPmm = pxPmm
332 }
333 // extend the parser with functions
334 parser.onerror = (e) => console.log('error: line ' + e.line + ', column ' + e.column + ', bad character [' + e.c + ']')
335
336 parser.onopentag = function (node) {
337 const objMap = {
338 SVG: svgSvg,
339 G: svgGroup,
340 RECT: svgRect,
341 CIRCLE: svgCircle,
342 ELLIPSE: svgEllipse,
343 LINE: svgLine,
344 POLYLINE: svgPolyline,
345 POLYGON: svgPolygon,
346 PATH: svgPath,
347 USE: svgUse,
348 DEFS: () => { svgInDefs = true; return undefined },
349 DESC: () => undefined, // ignored by design
350 TITLE: () => undefined, // ignored by design
351 STYLE: () => undefined, // ignored by design
352 undefined: () => console.log('Warning: Unsupported SVG element: ' + node.name)
353 }
354 node.attributes.position = [parser.line + 1, parser.column + 1]
355 const obj = objMap[node.name] ? objMap[node.name](node.attributes, { svgObjects, customPxPmm: pxPmm }) : undefined
356
357 // case 'SYMBOL':
358 // this is just like an embedded SVG but does NOT render directly, only named
359 // this requires another set of control objects
360 // only add to named objects for later USE
361 // break;
362
363 if (obj) {
364 // add to named objects if necessary
365 if ('id' in obj) {
366 svgObjects[obj.id] = obj
367 }
368 if (obj.type === 'svg') {
369 // initial SVG (group)
370 svgGroups.push(obj)
371 svgUnitsPmm = obj.unitsPmm
372 svgUnitsX = obj.viewW
373 svgUnitsY = obj.viewH
374 svgUnitsV = obj.viewP
375 } else {
376 // add the object to the active group if necessary
377 if (svgInDefs === true) {
378 if (svgDefs.length > 0) {
379 const group = svgDefs.pop()
380 if ('objects' in group) {
381 group.objects.push(obj)
382 }
383 svgDefs.push(group)
384 }
385 if (obj.type === 'group') {
386 svgDefs.push(obj)
387 }
388 } else {
389 if (svgGroups.length > 0) {
390 const group = svgGroups.pop()
391 if ('objects' in group) {
392 // TBD apply presentation attributes from the group
393 group.objects.push(obj)
394 }
395 svgGroups.push(group)
396 }
397 if (obj.type === 'group') {
398 svgGroups.push(obj)
399 }
400 }
401 }
402 }
403 }
404
405 parser.onclosetag = function (node) {
406 const popGroup = () => {
407 if (svgInDefs === true) {
408 return svgDefs.pop()
409 } else {
410 return svgGroups.pop()
411 }
412 }
413
414 const objMap = {
415 SVG: popGroup,
416 DEFS: () => { svgInDefs = false },
417 USE: popGroup,
418 G: popGroup,
419 undefined: () => {}
420 }
421 const obj = objMap[node] ? objMap[node]() : undefined
422
423 // check for completeness
424 if (svgGroups.length === 0) {
425 svgObj = obj
426 }
427 }
428
429 // parser.onattribute = function (attr) {};
430 // parser.ontext = function (t) {};
431
432 parser.onend = function () {
433 }
434 // start the parser
435 parser.write(src).close()
436 return parser
437}
438
439const extension = 'svg'
440
441module.exports = {
442 deserialize,
443 extension
444}