modeling/src/operations/extrusions/extrudeFromSlices.js

const mat4 = require('../../maths/mat4')

const geom2 = require('../../geometries/geom2')
const geom3 = require('../../geometries/geom3')
const poly3 = require('../../geometries/poly3')

const slice = require('./slice')
const repairSlice = require('./slice/repair')

const extrudeWalls = require('./extrudeWalls')

const defaultCallback = (progress, index, base) => {
  let baseSlice = null
  if (geom2.isA(base)) baseSlice = slice.fromSides(geom2.toSides(base))
  if (poly3.isA(base)) baseSlice = slice.fromPoints(poly3.toPoints(base))

  return progress === 0 || progress === 1 ? slice.transform(mat4.fromTranslation(mat4.create(), [0, 0, progress]), baseSlice) : null
}

/**
 * Extrude a solid from the slices as returned by the callback function.
 * @see slice
 *
 * @param {Object} options - options for extrude
 * @param {Integer} [options.numberOfSlices=2] the number of slices to be generated by the callback
 * @param {Boolean} [options.capStart=true] the solid should have a cap at the start
 * @param {Boolean} [options.capEnd=true] the solid should have a cap at the end
 * @param {Boolean} [options.close=false] the solid should have a closing section between start and end
 * @param {Boolean} [options.repair=true] - repair gaps in the geometry
 * @param {Function} [options.callback] the callback function that generates each slice
 * @param {Object} base - the base object which is used to create slices (see the example for callback information)
 * @return {geom3} the extruded shape
 * @alias module:modeling/extrusions.extrudeFromSlices
 *
 * @example
 * // Parameters:
 * //   progress : the percent complete [0..1]
 * //   index : the index of the current slice [0..numberOfSlices - 1]
 * //   base : the base object as given
 * // Return Value:
 * //   slice or null (to skip)
 * const callback = (progress, index, base) => {
 *   ...
 *   return slice
 * }
 */
const extrudeFromSlices = (options, base) => {
  const defaults = {
    numberOfSlices: 2,
    capStart: true,
    capEnd: true,
    close: false,
    repair: true,
    callback: defaultCallback
  }
  const { numberOfSlices, capStart, capEnd, close, repair, callback: generate } = Object.assign({ }, defaults, options)

  if (numberOfSlices < 2) throw new Error('numberOfSlices must be 2 or more')

  // Repair gaps in the base slice
  if (repair) {
    // note: base must be a slice, if base is geom2 this doesn't repair
    base = repairSlice(base)
  }

  const sMax = numberOfSlices - 1

  let startSlice = null
  let endSlice = null
  let prevSlice = null
  let polygons = []
  for (let s = 0; s < numberOfSlices; s++) {
    // invoke the callback function to get the next slice
    // NOTE: callback can return null to skip the slice
    const currentSlice = generate(s / sMax, s, base)

    if (currentSlice) {
      if (!slice.isA(currentSlice)) throw new Error('the callback function must return slice objects')

      const edges = slice.toEdges(currentSlice)
      if (edges.length === 0) throw new Error('the callback function must return slices with one or more edges')

      if (prevSlice) {
        polygons = polygons.concat(extrudeWalls(prevSlice, currentSlice))
      }

      // save start and end slices for caps if necessary
      if (s === 0) startSlice = currentSlice
      if (s === (numberOfSlices - 1)) endSlice = currentSlice

      prevSlice = currentSlice
    }
  }

  if (capEnd) {
    // create a cap at the end
    const endPolygons = slice.toPolygons(endSlice)
    polygons = polygons.concat(endPolygons)
  }
  if (capStart) {
    // create a cap at the start
    const startPolygons = slice.toPolygons(startSlice).map(poly3.invert)
    polygons = polygons.concat(startPolygons)
  }
  if (!capStart && !capEnd) {
    // create walls between end and start slices
    if (close && !slice.equals(endSlice, startSlice)) {
      polygons = polygons.concat(extrudeWalls(endSlice, startSlice))
    }
  }
  return geom3.create(polygons)
}

module.exports = extrudeFromSlices