-
Notifications
You must be signed in to change notification settings - Fork 558
Expand file tree
/
Copy pathindex.js
More file actions
211 lines (177 loc) · 6.84 KB
/
index.js
File metadata and controls
211 lines (177 loc) · 6.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/*
JSCAD Object to SVG Format Serialization
## License
Copyright (c) 2018 JSCAD Organization https://github.com/jscad
All code released under MIT license
Notes:
1) geom2 conversion to:
SVG GROUP containing a continous SVG PATH that contains the outlines of the geometry
2) geom3 conversion to:
none
3) path2 conversion to:
SVG GROUP containing a SVG PATH for each path
*/
/**
* Serializer of JSCAD geometries to SVG source (XML).
*
* The serialization of the following geometries are possible.
* - serialization of 2D geometry (geom2) to SVG path (a continous path containing the outlines of the geometry)
* - serialization of 2D geometry (path2) to SVG path
*
* Colors are added to SVG shapes when found on the geometry.
* Special attributes (id and class) are added to SVG shapes when found on the geometry.
*
* @module io/svg-serializer
* @example
* const { serializer, mimeType } = require('@jscad/svg-serializer')
*/
const { geometries, maths, measurements, utils } = require('@jscad/modeling')
const stringify = require('onml/lib/stringify')
const version = require('./package.json').version
const mimeType = 'image/svg+xml'
/**
* Serialize the give objects to SVG code (XML).
* @see https://www.w3.org/TR/SVG/Overview.html
* @param {Object} options - options for serialization, REQUIRED
* @param {String} [options.unit='mm'] - unit of design; em, ex, px, in, cm, mm, pt, pc
* @param {Function} [options.statusCallback] - call back function for progress ({ progress: 0-100 })
* @param {Object|Array} objects - objects to serialize as SVG
* @returns {Array} serialized contents, SVG code (XML string)
* @alias module:io/svg-serializer.serialize
* @example
* const geometry = primitives.square()
* const svgData = serializer({unit: 'mm'}, geometry)
*/
const serialize = (options, ...objects) => {
const defaults = {
unit: 'mm', // em | ex | px | in | cm | mm | pt | pc
decimals: 10000,
version,
statusCallback: null
}
options = Object.assign({}, defaults, options)
objects = utils.flatten(objects)
// convert only 2D geometries
const objects2d = objects.filter((object) => geometries.geom2.isA(object) || geometries.path2.isA(object))
if (objects2d.length === 0) throw new Error('only 2D geometries can be serialized to SVG')
if (objects.length !== objects2d.length) console.warn('some objects could not be serialized to SVG')
options.statusCallback && options.statusCallback({ progress: 0 })
// get the lower and upper bounds of ALL convertable objects
const bounds = getBounds(objects2d)
let width = 0
let height = 0
if (bounds) {
width = Math.round((bounds[1][0] - bounds[0][0]) * options.decimals) / options.decimals
height = Math.round((bounds[1][1] - bounds[0][1]) * options.decimals) / options.decimals
}
let body = ['svg',
{
width: width + options.unit,
height: height + options.unit,
viewBox: ('0 0 ' + width + ' ' + height),
fill: 'none',
'fill-rule': 'evenodd',
'stroke-width': '0.1px',
version: '1.1',
baseProfile: 'tiny',
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:xlink': 'http://www.w3.org/1999/xlink'
}
]
if (bounds) {
body = body.concat(convertObjects(objects2d, bounds, options))
}
const svg = `<?xml version="1.0" encoding="UTF-8"?>
<!-- Created by JSCAD SVG Serializer -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">
${stringify(body, 2)}`
options.statusCallback && options.statusCallback({ progress: 100 })
return [svg]
}
/*
* Measure the bounds of the given objects, which is required to offset all points to positive X/Y values.
*/
const getBounds = (objects) => {
const allbounds = measurements.measureBoundingBox(objects)
if (objects.length === 1) return allbounds
// create a sum of the bounds
const sumofbounds = allbounds.reduce((sum, bounds) => {
maths.vec3.min(sum[0], sum[0], bounds[0])
maths.vec3.max(sum[1], sum[1], bounds[1])
return sum
}, [[0, 0, 0], [0, 0, 0]])
return sumofbounds
}
const convertObjects = (objects, bounds, options) => {
const xoffset = 0 - bounds[0][0] // offset to X=0
const yoffset = 0 - bounds[1][1] // offset to Y=0
const contents = []
objects.forEach((object, i) => {
options.statusCallback && options.statusCallback({ progress: 100 * i / objects.length })
if (geometries.geom2.isA(object)) {
contents.push(convertGeom2(object, [xoffset, yoffset], options))
}
if (geometries.path2.isA(object)) {
contents.push(convertPaths([object], [xoffset, yoffset], options))
}
})
return contents
}
const reflect = (x, y, px, py) => {
const ox = x - px
const oy = y - py
if (x === px && y === px) return [x, y]
if (x === px) return [x, py - (oy)]
if (y === py) return [px - (-ox), y]
return [px - (-ox), py - (oy)]
}
const convertGeom2 = (object, offsets, options) => {
const outlines = geometries.geom2.toOutlines(object)
const paths = outlines.map((outline) => geometries.path2.fromPoints({ closed: true }, outline))
options.color = 'black' // SVG initial color
if (object.color) options.color = convertColor(object.color)
options.id = null
if (object.id) options.id = object.id
options.class = null
if (object.class) options.class = object.class
return convertToContinousPath(paths, offsets, options)
}
const convertToContinousPath = (paths, offsets, options) => {
let instructions = ''
paths.forEach((path) => (instructions += convertPath(path, offsets, options)))
const d = { fill: options.color, d: instructions }
if (options.id) d.id = options.id
if (options.class) d.class = options.class
return ['g', ['path', d]]
}
const convertPaths = (paths, offsets, options) => paths.reduce((res, path, i) => {
const d = { d: convertPath(path, offsets, options) }
if (path.color) d.stroke = convertColor(path.color)
if (path.id) d.id = path.id
if (path.class) d.class = path.class
return res.concat([['path', d]])
}, ['g'])
const convertPath = (path, offsets, options) => {
let str = ''
const numpointsClosed = path.points.length + (path.isClosed ? 1 : 0)
for (let pointindex = 0; pointindex < numpointsClosed; pointindex++) {
let pointindexwrapped = pointindex
if (pointindexwrapped >= path.points.length) pointindexwrapped -= path.points.length
const point = path.points[pointindexwrapped]
const offpoint = [point[0] + offsets[0], point[1] + offsets[1]]
const svgpoint = reflect(offpoint[0], offpoint[1], 0, 0)
const x = Math.round(svgpoint[0] * options.decimals) / options.decimals
const y = Math.round(svgpoint[1] * options.decimals) / options.decimals
if (pointindex > 0) {
str += `L${x} ${y}`
} else {
str += `M${x} ${y}`
}
}
return str
}
const convertColor = (color) => `rgba(${color[0] * 255},${color[1] * 255},${color[2] * 255},${color[3]})`
module.exports = {
serialize,
mimeType
}