modeling/src/operations/transforms/align.js

  1. const flatten = require('../../utils/flatten')
  2. const padArrayToLength = require('../../utils/padArrayToLength')
  3. const measureAggregateBoundingBox = require('../../measurements/measureAggregateBoundingBox')
  4. const { translate } = require('./translate')
  5. const validateOptions = (options) => {
  6. if (!Array.isArray(options.modes) || options.modes.length > 3) throw new Error('align(): modes must be an array of length <= 3')
  7. options.modes = padArrayToLength(options.modes, 'none', 3)
  8. if (options.modes.filter((mode) => ['center', 'max', 'min', 'none'].includes(mode)).length !== 3) throw new Error('align(): all modes must be one of "center", "max" or "min"')
  9. if (!Array.isArray(options.relativeTo) || options.relativeTo.length > 3) throw new Error('align(): relativeTo must be an array of length <= 3')
  10. options.relativeTo = padArrayToLength(options.relativeTo, 0, 3)
  11. if (options.relativeTo.filter((alignVal) => (Number.isFinite(alignVal) || alignVal == null)).length !== 3) throw new Error('align(): all relativeTo values must be a number, or null.')
  12. if (typeof options.grouped !== 'boolean') throw new Error('align(): grouped must be a boolean value.')
  13. return options
  14. }
  15. const populateRelativeToFromBounds = (relativeTo, modes, bounds) => {
  16. for (let i = 0; i < 3; i++) {
  17. if (relativeTo[i] == null) {
  18. if (modes[i] === 'center') {
  19. relativeTo[i] = (bounds[0][i] + bounds[1][i]) / 2
  20. } else if (modes[i] === 'max') {
  21. relativeTo[i] = bounds[1][i]
  22. } else if (modes[i] === 'min') {
  23. relativeTo[i] = bounds[0][i]
  24. }
  25. }
  26. }
  27. return relativeTo
  28. }
  29. const alignGeometries = (geometry, modes, relativeTo) => {
  30. const bounds = measureAggregateBoundingBox(geometry)
  31. const translation = [0, 0, 0]
  32. for (let i = 0; i < 3; i++) {
  33. if (modes[i] === 'center') {
  34. translation[i] = relativeTo[i] - (bounds[0][i] + bounds[1][i]) / 2
  35. } else if (modes[i] === 'max') {
  36. translation[i] = relativeTo[i] - bounds[1][i]
  37. } else if (modes[i] === 'min') {
  38. translation[i] = relativeTo[i] - bounds[0][i]
  39. }
  40. }
  41. return translate(translation, geometry)
  42. }
  43. /**
  44. * Align the boundaries of the given geometries using the given options.
  45. * @param {Object} options - options for aligning
  46. * @param {Array} [options.modes = ['center', 'center', 'min']] - the point on the geometries to align to for each axis. Valid options are "center", "max", "min", and "none".
  47. * @param {Array} [options.relativeTo = [0,0,0]] - The point one each axis on which to align the geometries upon. If the value is null, then the corresponding value from the group's bounding box is used.
  48. * @param {Boolean} [options.grouped = false] - if true, transform all geometries by the same amount, maintaining the relative positions to each other.
  49. * @param {...Object} geometries - the geometries to align
  50. * @return {Object|Array} the aligned geometry, or a list of aligned geometries
  51. * @alias module:modeling/transforms.align
  52. *
  53. * @example
  54. * let alignedGeometries = align({modes: ['min', 'center', 'none'], relativeTo: [10, null, 10], grouped: true }, geometries)
  55. */
  56. const align = (options, ...geometries) => {
  57. const defaults = {
  58. modes: ['center', 'center', 'min'],
  59. relativeTo: [0, 0, 0],
  60. grouped: false
  61. }
  62. options = Object.assign({}, defaults, options)
  63. options = validateOptions(options)
  64. let { modes, relativeTo, grouped } = options
  65. geometries = flatten(geometries)
  66. if (geometries.length === 0) throw new Error('align(): No geometries were provided to act upon')
  67. if (relativeTo.filter((val) => val == null).length) {
  68. const bounds = measureAggregateBoundingBox(geometries)
  69. relativeTo = populateRelativeToFromBounds(relativeTo, modes, bounds)
  70. }
  71. if (grouped) {
  72. geometries = alignGeometries(geometries, modes, relativeTo)
  73. } else {
  74. geometries = geometries.map((geometry) => alignGeometries(geometry, modes, relativeTo))
  75. }
  76. return geometries.length === 1 ? geometries[0] : geometries
  77. }
  78. module.exports = align