UNPKG

@jscad/modeling

Version:

Constructive Solid Geometry (CSG) Library for JSCAD

90 lines (78 loc) 4 kB
const flatten = require('../../utils/flatten') const padArrayToLength = require('../../utils/padArrayToLength') const measureAggregateBoundingBox = require('../../measurements/measureAggregateBoundingBox') const { translate } = require('./translate') const validateOptions = (options) => { if (!Array.isArray(options.modes) || options.modes.length > 3) throw new Error('align(): modes must be an array of length <= 3') options.modes = padArrayToLength(options.modes, 'none', 3) 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"') if (!Array.isArray(options.relativeTo) || options.relativeTo.length > 3) throw new Error('align(): relativeTo must be an array of length <= 3') options.relativeTo = padArrayToLength(options.relativeTo, 0, 3) 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.') if (typeof options.grouped !== 'boolean') throw new Error('align(): grouped must be a boolean value.') return options } const populateRelativeToFromBounds = (relativeTo, modes, bounds) => { for (let i = 0; i < 3; i++) { if (relativeTo[i] == null) { if (modes[i] === 'center') { relativeTo[i] = (bounds[0][i] + bounds[1][i]) / 2 } else if (modes[i] === 'max') { relativeTo[i] = bounds[1][i] } else if (modes[i] === 'min') { relativeTo[i] = bounds[0][i] } } } return relativeTo } const alignGeometries = (geometry, modes, relativeTo) => { const bounds = measureAggregateBoundingBox(geometry) const translation = [0, 0, 0] for (let i = 0; i < 3; i++) { if (modes[i] === 'center') { translation[i] = relativeTo[i] - (bounds[0][i] + bounds[1][i]) / 2 } else if (modes[i] === 'max') { translation[i] = relativeTo[i] - bounds[1][i] } else if (modes[i] === 'min') { translation[i] = relativeTo[i] - bounds[0][i] } } return translate(translation, geometry) } /** * Align the boundaries of the given geometries using the given options. * @param {Object} options - options for aligning * @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". * @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. * @param {Boolean} [options.grouped = false] - if true, transform all geometries by the same amount, maintaining the relative positions to each other. * @param {...Object} geometries - the geometries to align * @return {Object|Array} the aligned geometry, or a list of aligned geometries * @alias module:modeling/transforms.align * * @example * let alignedGeometries = align({modes: ['min', 'center', 'none'], relativeTo: [10, null, 10], grouped: true }, geometries) */ const align = (options, ...geometries) => { const defaults = { modes: ['center', 'center', 'min'], relativeTo: [0, 0, 0], grouped: false } options = Object.assign({}, defaults, options) options = validateOptions(options) let { modes, relativeTo, grouped } = options geometries = flatten(geometries) if (geometries.length === 0) throw new Error('align(): No geometries were provided to act upon') if (relativeTo.filter((val) => val == null).length) { const bounds = measureAggregateBoundingBox(geometries) relativeTo = populateRelativeToFromBounds(relativeTo, modes, bounds) } if (grouped) { geometries = alignGeometries(geometries, modes, relativeTo) } else { geometries = geometries.map((geometry) => alignGeometries(geometry, modes, relativeTo)) } return geometries.length === 1 ? geometries[0] : geometries } module.exports = align