353 lines
10 KiB
JavaScript
353 lines
10 KiB
JavaScript
|
/*
|
||
|
## License
|
||
|
|
||
|
Copyright (c) 2016 Z3 Development https://github.com/z3dev
|
||
|
2017 Mark 'kaosat-dev' Moissette
|
||
|
|
||
|
* The upgrades (direct CSG output from this deserializer) and refactoring
|
||
|
have been very kindly sponsored by [Copenhagen Fabrication / Stykka](https://www.stykka.com/)***
|
||
|
|
||
|
All code released under MIT license
|
||
|
*/
|
||
|
|
||
|
const sax = require('sax')
|
||
|
const {CAG} = require('@jscad/csg')
|
||
|
|
||
|
const {cagLengthX, cagLengthY} = require('./helpers')
|
||
|
const {svgSvg, svgRect, svgCircle, svgGroup, svgLine, svgPath, svgEllipse, svgPolygon, svgPolyline, svgUse} = require('./svgElementHelpers')
|
||
|
const shapesMapCsg = require('./shapesMapCsg')
|
||
|
const shapesMapJscad = require('./shapesMapJscad')
|
||
|
// FIXE: should these be kept here ? any risk of side effects ?
|
||
|
let svgUnitsX
|
||
|
let svgUnitsY
|
||
|
let svgUnitsV
|
||
|
// processing controls
|
||
|
let svgObjects = [] // named objects
|
||
|
let svgGroups = [] // groups of objects
|
||
|
let svgInDefs = false // svg DEFS element in process
|
||
|
let svgObj = null // svg in object form
|
||
|
let svgUnitsPmm = [1, 1]
|
||
|
|
||
|
const objectify = function (group) {
|
||
|
const level = svgGroups.length
|
||
|
// add this group to the heiarchy
|
||
|
svgGroups.push(group)
|
||
|
// create an indent for the generated code
|
||
|
let i = level
|
||
|
while (i > 0) {
|
||
|
i--
|
||
|
}
|
||
|
|
||
|
let lnCAG = new CAG()
|
||
|
|
||
|
const params = {
|
||
|
svgUnitsPmm,
|
||
|
svgUnitsX,
|
||
|
svgUnitsY,
|
||
|
svgUnitsV,
|
||
|
level,
|
||
|
svgGroups
|
||
|
}
|
||
|
// generate code for all objects
|
||
|
for (i = 0; i < group.objects.length; i++) {
|
||
|
const obj = group.objects[i]
|
||
|
let onCAG = shapesMapCsg(obj, objectify, params)
|
||
|
|
||
|
if ('fill' in obj) {
|
||
|
// FIXME when CAG supports color
|
||
|
// code += indent+on+' = '+on+'.setColor(['+obj.fill[0]+','+obj.fill[1]+','+obj.fill[2]+']);\n';
|
||
|
}
|
||
|
if ('transforms' in obj) {
|
||
|
// NOTE: SVG specifications require that transforms are applied in the order given.
|
||
|
// But these are applied in the order as required by CSG/CAG
|
||
|
let tr
|
||
|
let ts
|
||
|
let tt
|
||
|
|
||
|
for (let j = 0; j < obj.transforms.length; j++) {
|
||
|
const t = obj.transforms[j]
|
||
|
if ('rotate' in t) { tr = t }
|
||
|
if ('scale' in t) { ts = t }
|
||
|
if ('translate' in t) { tt = t }
|
||
|
}
|
||
|
if (ts !== null) {
|
||
|
const x = ts.scale[0]
|
||
|
const y = ts.scale[1]
|
||
|
onCAG = onCAG.scale([x, y])
|
||
|
}
|
||
|
if (tr !== null) {
|
||
|
const z = 0 - tr.rotate
|
||
|
onCAG = onCAG.rotateZ(z)
|
||
|
}
|
||
|
if (tt !== null) {
|
||
|
const x = cagLengthX(tt.translate[0], svgUnitsPmm, svgUnitsX)
|
||
|
const y = (0 - cagLengthY(tt.translate[1], svgUnitsPmm, svgUnitsY))
|
||
|
onCAG = onCAG.translate([x, y])
|
||
|
}
|
||
|
}
|
||
|
lnCAG = lnCAG.union(onCAG)
|
||
|
}
|
||
|
|
||
|
// remove this group from the hiearchy
|
||
|
svgGroups.pop()
|
||
|
|
||
|
return lnCAG
|
||
|
}
|
||
|
|
||
|
const codify = function (group) {
|
||
|
const level = svgGroups.length
|
||
|
// add this group to the heiarchy
|
||
|
svgGroups.push(group)
|
||
|
// create an indent for the generated code
|
||
|
var indent = ' '
|
||
|
var i = level
|
||
|
while (i > 0) {
|
||
|
indent += ' '
|
||
|
i--
|
||
|
}
|
||
|
// pre-code
|
||
|
var code = ''
|
||
|
if (level === 0) {
|
||
|
code += 'function main(params) {\n'
|
||
|
}
|
||
|
var ln = 'cag' + level
|
||
|
code += indent + 'var ' + ln + ' = new CAG();\n'
|
||
|
|
||
|
// generate code for all objects
|
||
|
for (i = 0; i < group.objects.length; i++) {
|
||
|
const obj = group.objects[i]
|
||
|
const on = ln + i
|
||
|
|
||
|
const params = {
|
||
|
level,
|
||
|
indent,
|
||
|
ln,
|
||
|
on,
|
||
|
svgUnitsPmm,
|
||
|
svgUnitsX,
|
||
|
svgUnitsY,
|
||
|
svgUnitsV,
|
||
|
svgGroups
|
||
|
}
|
||
|
|
||
|
let tmpCode = shapesMapJscad(obj, codify, params)
|
||
|
code += tmpCode
|
||
|
|
||
|
if ('fill' in obj) {
|
||
|
// FIXME when CAG supports color
|
||
|
// code += indent+on+' = '+on+'.setColor(['+obj.fill[0]+','+obj.fill[1]+','+obj.fill[2]+']);\n';
|
||
|
}
|
||
|
if ('transforms' in obj) {
|
||
|
// NOTE: SVG specifications require that transforms are applied in the order given.
|
||
|
// But these are applied in the order as required by CSG/CAG
|
||
|
let tr
|
||
|
let ts
|
||
|
let tt
|
||
|
|
||
|
for (let j = 0; j < obj.transforms.length; j++) {
|
||
|
var t = obj.transforms[j]
|
||
|
if ('rotate' in t) { tr = t }
|
||
|
if ('scale' in t) { ts = t }
|
||
|
if ('translate' in t) { tt = t }
|
||
|
}
|
||
|
if (ts !== null && ts !== undefined) {
|
||
|
const x = ts.scale[0]
|
||
|
const y = ts.scale[1]
|
||
|
code += indent + on + ' = ' + on + '.scale([' + x + ',' + y + ']);\n'
|
||
|
}
|
||
|
if (tr !== null && tr !== undefined) {
|
||
|
console.log('tr', tr)
|
||
|
const z = 0 - tr.rotate
|
||
|
code += indent + on + ' = ' + on + '.rotateZ(' + z + ');\n'
|
||
|
}
|
||
|
if (tt !== null && tt !== undefined) {
|
||
|
const x = cagLengthX(tt.translate[0], svgUnitsPmm, svgUnitsX)
|
||
|
const y = (0 - cagLengthY(tt.translate[1], svgUnitsPmm, svgUnitsY))
|
||
|
code += indent + on + ' = ' + on + '.translate([' + x + ',' + y + ']);\n'
|
||
|
}
|
||
|
}
|
||
|
code += indent + ln + ' = ' + ln + '.union(' + on + ');\n'
|
||
|
}
|
||
|
// post-code
|
||
|
if (level === 0) {
|
||
|
code += indent + 'return ' + ln + ';\n'
|
||
|
code += '}\n'
|
||
|
}
|
||
|
// remove this group from the hiearchy
|
||
|
svgGroups.pop()
|
||
|
|
||
|
return code
|
||
|
}
|
||
|
|
||
|
function createSvgParser (src, pxPmm) {
|
||
|
// create a parser for the XML
|
||
|
const parser = sax.parser(false, {trim: true, lowercase: false, position: true})
|
||
|
if (pxPmm !== undefined && pxPmm > parser.pxPmm) {
|
||
|
parser.pxPmm = pxPmm
|
||
|
}
|
||
|
// extend the parser with functions
|
||
|
parser.onerror = e => console.log('error: line ' + e.line + ', column ' + e.column + ', bad character [' + e.c + ']')
|
||
|
|
||
|
parser.onopentag = function (node) {
|
||
|
// console.log('opentag: '+node.name+' at line '+this.line+' position '+this.column);
|
||
|
const objMap = {
|
||
|
SVG: svgSvg,
|
||
|
G: svgGroup,
|
||
|
RECT: svgRect,
|
||
|
CIRCLE: svgCircle,
|
||
|
ELLIPSE: svgEllipse,
|
||
|
LINE: svgLine,
|
||
|
POLYLINE: svgPolyline,
|
||
|
POLYGON: svgPolygon,
|
||
|
PATH: svgPath,
|
||
|
USE: svgUse,
|
||
|
DEFS: () => { svgInDefs = true },
|
||
|
DESC: () => undefined, // ignored by design
|
||
|
TITLE: () => undefined, // ignored by design
|
||
|
STYLE: () => undefined, // ignored by design
|
||
|
undefined: () => console.log('Warning: Unsupported SVG element: ' + node.name)
|
||
|
}
|
||
|
let obj = objMap[node.name] ? objMap[node.name](node.attributes, {svgObjects, customPxPmm: pxPmm}) : null
|
||
|
|
||
|
// case 'SYMBOL':
|
||
|
// this is just like an embedded SVG but does NOT render directly, only named
|
||
|
// this requires another set of control objects
|
||
|
// only add to named objects for later USE
|
||
|
// break;
|
||
|
// console.log('node',node)
|
||
|
|
||
|
if (obj !== null) {
|
||
|
// add to named objects if necessary
|
||
|
if ('id' in obj) {
|
||
|
svgObjects[obj.id] = obj
|
||
|
}
|
||
|
if (obj.type === 'svg') {
|
||
|
// initial SVG (group)
|
||
|
svgGroups.push(obj)
|
||
|
// console.log('units', obj.unitsPmm)
|
||
|
svgUnitsPmm = obj.unitsPmm
|
||
|
svgUnitsX = obj.viewW
|
||
|
svgUnitsY = obj.viewH
|
||
|
svgUnitsV = obj.viewP
|
||
|
} else {
|
||
|
// add the object to the active group if necessary
|
||
|
if (svgGroups.length > 0 && svgInDefs === false) {
|
||
|
var group = svgGroups.pop()
|
||
|
if ('objects' in group) {
|
||
|
// console.log('push object ['+obj.type+']');
|
||
|
// console.log(JSON.stringify(obj));
|
||
|
// TBD apply presentation attributes from the group
|
||
|
group.objects.push(obj)
|
||
|
}
|
||
|
svgGroups.push(group)
|
||
|
}
|
||
|
if (obj.type === 'group') {
|
||
|
// add GROUPs to the stack
|
||
|
svgGroups.push(obj)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
parser.onclosetag = function (node) {
|
||
|
// console.log('closetag: '+node)
|
||
|
const popGroup = () => svgGroups.pop()
|
||
|
const objMap = {
|
||
|
SVG: popGroup,
|
||
|
DEFS: () => { svgInDefs = false },
|
||
|
USE: popGroup,
|
||
|
G: popGroup,
|
||
|
undefined: () => {}
|
||
|
}
|
||
|
const obj = objMap[node] ? objMap[node]() : undefined
|
||
|
|
||
|
// check for completeness
|
||
|
if (svgGroups.length === 0) {
|
||
|
svgObj = obj
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// parser.onattribute = function (attr) {};
|
||
|
// parser.ontext = function (t) {};
|
||
|
|
||
|
parser.onend = function () {
|
||
|
// console.log('SVG parsing completed')
|
||
|
}
|
||
|
// start the parser
|
||
|
parser.write(src).close()
|
||
|
return parser
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse the given SVG source and return a JSCAD script
|
||
|
* @param {string} src svg data as text
|
||
|
* @param {string} filename (optional) original filename of SVG source
|
||
|
* @param {object} options options (optional) anonymous object with:
|
||
|
* pxPmm {number: pixels per milimeter for calcuations
|
||
|
* version: {string} version number to add to the metadata
|
||
|
* addMetadata: {boolean} flag to enable/disable injection of metadata (producer, date, source)
|
||
|
*
|
||
|
* @return a CAG (2D CSG) object
|
||
|
*/
|
||
|
function deserializeToCSG (src, filename, options) {
|
||
|
filename = filename || 'svg'
|
||
|
const defaults = {pxPmm: require('./constants').pxPmm, version: '0.0.0', addMetaData: true}
|
||
|
options = Object.assign({}, defaults, options)
|
||
|
const {pxPmm} = options
|
||
|
|
||
|
// parse the SVG source
|
||
|
createSvgParser(src, pxPmm)
|
||
|
if (!svgObj) {
|
||
|
throw new Error('SVG parsing failed, no valid svg data retrieved')
|
||
|
}
|
||
|
|
||
|
return objectify(svgObj)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse the given SVG source and return a JSCAD script
|
||
|
* @param {string} src svg data as text
|
||
|
* @param {string} filename (optional) original filename of SVG source
|
||
|
* @param {object} options options (optional) anonymous object with:
|
||
|
* pxPmm {number: pixels per milimeter for calcuations
|
||
|
* version: {string} version number to add to the metadata
|
||
|
* addMetadata: {boolean} flag to enable/disable injection of metadata (producer, date, source)
|
||
|
* at the start of the file
|
||
|
* @return a CAG (2D CSG) object
|
||
|
*/
|
||
|
function translate (src, filename, options) {
|
||
|
filename = filename || 'svg'
|
||
|
const defaults = {pxPmm: require('./constants').pxPmm, version: '0.0.0', addMetaData: true}
|
||
|
options = Object.assign({}, defaults, options)
|
||
|
const {version, pxPmm, addMetaData} = options
|
||
|
|
||
|
// parse the SVG source
|
||
|
createSvgParser(src, pxPmm)
|
||
|
// convert the internal objects to JSCAD code
|
||
|
let code = addMetaData ? `//
|
||
|
// producer: OpenJSCAD.org ${version} SVG Importer
|
||
|
// date: ${new Date()}
|
||
|
// source: ${filename}
|
||
|
//
|
||
|
` : ''
|
||
|
|
||
|
if (!svgObj) {
|
||
|
throw new Error('SVG parsing failed, no valid svg data retrieved')
|
||
|
}
|
||
|
|
||
|
const scadCode = codify(svgObj)
|
||
|
code += scadCode
|
||
|
|
||
|
return code
|
||
|
}
|
||
|
|
||
|
const deserialize = function (src, filename, options) {
|
||
|
const defaults = {
|
||
|
output: 'jscad'
|
||
|
}
|
||
|
options = Object.assign({}, defaults, options)
|
||
|
return options.output === 'jscad' ? translate(src, filename, options) : deserializeToCSG(src, filename, options)
|
||
|
}
|
||
|
|
||
|
module.exports = {deserialize}
|