1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | const sax = require('sax')
|
14 |
|
15 | const { colors, transforms } = require('@jscad/modeling')
|
16 | const { toArray } = require('@jscad/array-utils')
|
17 |
|
18 | const version = require('./package.json').version
|
19 |
|
20 | const { cagLengthX, cagLengthY } = require('./helpers')
|
21 | const { svgSvg, svgRect, svgCircle, svgGroup, svgLine, svgPath, svgEllipse, svgPolygon, svgPolyline, svgUse } = require('./svgElementHelpers')
|
22 | const shapesMapGeometry = require('./shapesMapGeometry')
|
23 | const shapesMapJscad = require('./shapesMapJscad')
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | const deserialize = (options, input) => {
|
45 | const defaults = {
|
46 | filename: 'svg',
|
47 | addMetaData: true,
|
48 | output: 'script',
|
49 | pxPmm: require('./constants').pxPmm,
|
50 | target: '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 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | const instantiate = (src, options) => {
|
69 | const { pxPmm } = options
|
70 |
|
71 | options && options.statusCallback && options.statusCallback({ progress: 0 })
|
72 |
|
73 |
|
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 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | const translate = (src, options) => {
|
98 | const { filename, version, pxPmm, addMetaData } = options
|
99 |
|
100 | options && options.statusCallback && options.statusCallback({ progress: 0 })
|
101 |
|
102 |
|
103 | createSvgParser(src, pxPmm)
|
104 | if (!svgObj) {
|
105 | throw new Error('SVG parsing failed, no valid svg data retrieved')
|
106 | }
|
107 |
|
108 |
|
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 |
|
128 | let svgUnitsX
|
129 | let svgUnitsY
|
130 | let svgUnitsV
|
131 |
|
132 | const svgObjects = []
|
133 | const svgGroups = []
|
134 | const svgDefs = []
|
135 | let svgInDefs = false
|
136 | let svgObj
|
137 | let svgUnitsPmm = [1, 1]
|
138 |
|
139 |
|
140 |
|
141 |
|
142 | const objectify = (options, group) => {
|
143 | const { target, segments } = options
|
144 | const level = svgGroups.length
|
145 |
|
146 | svgGroups.push(group)
|
147 |
|
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 |
|
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 |
|
172 |
|
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 |
|
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
|
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 |
|
219 | svgGroups.pop()
|
220 |
|
221 | return geometries
|
222 | }
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | const codify = (options, group) => {
|
228 | const { target, segments } = options
|
229 | const level = svgGroups.length
|
230 |
|
231 | svgGroups.push(group)
|
232 |
|
233 | let indent = ' '
|
234 | let i = level
|
235 | while (i > 0) {
|
236 | indent += ' '
|
237 | i--
|
238 | }
|
239 |
|
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 |
|
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 |
|
271 |
|
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 |
|
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
|
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 |
|
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 |
|
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 |
|
317 | if (level === 0) {
|
318 | code += indent + 'return ' + ln + '\n'
|
319 | code += '}\n'
|
320 | }
|
321 |
|
322 | svgGroups.pop()
|
323 |
|
324 | return code
|
325 | }
|
326 |
|
327 | const createSvgParser = (src, pxPmm) => {
|
328 |
|
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 |
|
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,
|
350 | TITLE: () => undefined,
|
351 | STYLE: () => undefined,
|
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 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 | if (obj) {
|
364 |
|
365 | if ('id' in obj) {
|
366 | svgObjects[obj.id] = obj
|
367 | }
|
368 | if (obj.type === 'svg') {
|
369 |
|
370 | svgGroups.push(obj)
|
371 | svgUnitsPmm = obj.unitsPmm
|
372 | svgUnitsX = obj.viewW
|
373 | svgUnitsY = obj.viewH
|
374 | svgUnitsV = obj.viewP
|
375 | } else {
|
376 |
|
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 |
|
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 |
|
424 | if (svgGroups.length === 0) {
|
425 | svgObj = obj
|
426 | }
|
427 | }
|
428 |
|
429 |
|
430 |
|
431 |
|
432 | parser.onend = function () {
|
433 | }
|
434 |
|
435 | parser.write(src).close()
|
436 | return parser
|
437 | }
|
438 |
|
439 | const extension = 'svg'
|
440 |
|
441 | module.exports = {
|
442 | deserialize,
|
443 | extension
|
444 | }
|