io/svg-serializer/index.js

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