io/amf-serializer/index.js

/*
JSCAD Object to AMF (XML) Format Serialization

## License

Copyright (c) 2018 JSCAD Organization https://github.com/jscad

All code released under MIT license

Notes:
1) geom2 conversion to:
     none
2) geom3 conversion to:
     mesh
3) path2 conversion to:
     none

TBD
1) support zip output
*/

/**
 * Serializer of JSCAD geometries to AMF source data (XML)
 *
 * The serialization of the following geometries are possible.
 * - serialization of 3D geometry (geom3) to AMF object (a unique mesh containing both vertices and volumes)
 *
 * Colors are added to volumes when found on the 3D geometry.
 * Colors are added to triangles when found on individual polygons.
 *
 * @module io/amf-serializer
 * @example
 * const { serializer, mimeType } = require('@jscad/amf-serializer')
 */

const stringify = require('onml/lib/stringify')

const { geometries, modifiers } = require('@jscad/modeling')

const { flatten, toArray } = require('@jscad/array-utils')

const mimeType = 'application/amf+xml'

/**
 * Serialize the give objects (geometry) to AMF source data (XML).
 * @param {Object} options - options for serialization
 * @param {String} [options.unit='millimeter'] - unit of design; millimeter, inch, feet, meter or micrometer
 * @param {Function} [options.statusCallback] - call back function for progress ({ progress: 0-100 })
 * @param {...Object} objects - objects to serialize into AMF source data
 * @returns {Array} serialized contents, AMF source data(XML)
 * @alias module:io/amf-serializer.serialize
 * @example
 * const geometry = primitives.cube()
 * const amfData = serializer({unit: 'meter'}, geometry)
 */
const serialize = (options, ...objects) => {
  const defaults = {
    statusCallback: null,
    unit: 'millimeter' // millimeter, inch, feet, meter or micrometer
  }
  options = Object.assign({}, defaults, options)

  objects = flatten(objects)

  // convert only 3D geometries
  let objects3d = objects.filter((object) => geometries.geom3.isA(object))

  if (objects3d.length === 0) throw new Error('only 3D geometries can be serialized to AMF')
  if (objects.length !== objects3d.length) console.warn('some objects could not be serialized to AMF')

  // convert to triangles
  objects3d = toArray(modifiers.generalize({ snap: true, triangulate: true }, objects3d))

  options.statusCallback && options.statusCallback({ progress: 0 })

  // construct the contents of the XML
  let body = ['amf',
    {
      unit: options.unit,
      version: '1.1'
    },
    ['metadata', { type: 'author' }, 'Created by JSCAD']
  ]
  body = body.concat(translateObjects(objects3d, options))

  // convert the contents to AMF (XML) format
  const amf = `<?xml version="1.0" encoding="UTF-8"?>
${stringify(body, 2)}`

  options && options.statusCallback && options.statusCallback({ progress: 100 })

  return [amf]
}

const translateObjects = (objects, options) => {
  const contents = []
  objects.forEach((object, i) => {
    const polygons = geometries.geom3.toPolygons(object)
    if (polygons.length > 0) {
      options.id = i
      contents.push(convertToObject(object, options))
    }
  })
  return contents
}

const convertToObject = (object, options) => {
  const contents = ['object', { id: options.id }, convertToMesh(object, options)]
  return contents
}

const convertToMesh = (object, options) => {
  let contents = ['mesh', {}, convertToVertices(object, options)]
  contents = contents.concat(convertToVolumes(object, options))
  return contents
}

/*
 * This section converts each 3D geometry to a list of vertex / coordinates
 */

const convertToVertices = (object, options) => {
  const contents = ['vertices', {}]

  const vertices = []
  const polygons = geometries.geom3.toPolygons(object)
  polygons.forEach((polygon) => {
    for (let i = 0; i < polygon.vertices.length; i++) {
      vertices.push(convertToVertex(polygon.vertices[i], options))
    }
  })

  return contents.concat(vertices)
}

const convertToVertex = (vertex, options) => {
  const contents = ['vertex', {}, convertToCoordinates(vertex, options)]
  return contents
}

const convertToCoordinates = (vertex, options) => {
  const contents = ['coordinates', {}, ['x', {}, vertex[0]], ['y', {}, vertex[1]], ['z', {}, vertex[2]]]
  return contents
}

/*
 * This section converts each 3D geometry to a list of volumes consisting of indexes into the list of vertices
 */

const convertToVolumes = (object, options) => {
  const objectcolor = convertColor(object.color)
  const polygons = geometries.geom3.toPolygons(object)

  const contents = []

  let volume = ['volume', {}]

  // add color specification if available
  if (objectcolor) {
    volume.push(objectcolor)
  }

  let vcount = 0
  polygons.forEach((polygon) => {
    if (polygon.vertices.length < 3) {
      return
    }

    const triangles = convertToTriangles(polygon, vcount, options)

    volume = volume.concat(triangles)

    vcount += polygon.vertices.length
  })
  contents.push(volume)
  return contents
}

const convertColor = (color) => {
  if (color) {
    if (color.length < 4) color.push(1.0)
    return ['color', {}, ['r', {}, color[0]], ['g', {}, color[1]], ['b', {}, color[2]], ['a', {}, color[3]]]
  }
  return null
}

const convertToColor = (polygon, options) => {
  const color = polygon.color
  return convertColor(color)
}

const convertToTriangles = (polygon, index, options) => {
  const polycolor = convertToColor(polygon, options)

  // making sure they are all triangles (triangular polygons)
  const contents = []
  for (let i = 0; i < polygon.vertices.length - 2; i++) {
    if (polycolor) {
      contents.push(['triangle', {}, polycolor, ['v1', {}, index], ['v2', {}, (index + i + 1)], ['v3', {}, (index + i + 2)]])
    } else {
      contents.push(['triangle', {}, ['v1', {}, index], ['v2', {}, (index + i + 1)], ['v3', {}, (index + i + 2)]])
    }
  }
  return contents
}

module.exports = {
  serialize,
  mimeType
}