warp-grid
Version:
Create a complex grid, warped in 2D space and access data about its lines and cells
379 lines (335 loc) • 10.6 kB
text/typescript
import { CellBoundsOrder, InterpolationStrategy, LineStrategy } from './enums'
import { ValidationError } from './errors/ValidationError'
import type {
BoundingCurves,
Curve,
GetPointProps,
GridDefinitionWithDefaults,
Point,
Step,
StepDefinition,
} from './types'
import { mapObj } from './utils/functional'
import {
isArray,
isBoolean,
isFunction,
isInt,
isNil,
isNumber,
isPlainObj,
isUndefined,
isPixelNumberString,
} from './utils/is'
import { roundTo5 } from './utils/math'
import { getPixelStringNumericComponent } from './utils/steps'
// -----------------------------------------------------------------------------
// Const
// -----------------------------------------------------------------------------
const CURVE_NAMES = [`top`, `right`, `bottom`, `left`]
// -----------------------------------------------------------------------------
// Utils
// -----------------------------------------------------------------------------
const getPointsAreSame = (point1: Point, point2: Point): boolean => {
// Round the points to 5 decimal places before comparing to avoid rounding
// issues where the values are fractionally different
const roundedPoint1 = mapObj(roundTo5, point1)
const roundedPoint2 = mapObj(roundTo5, point2)
return (
roundedPoint1.x === roundedPoint2.x && roundedPoint1.y === roundedPoint2.y
)
}
const isValidPoint = (point: Point): point is Point =>
isPlainObj(point) && isNumber(point.x) && isNumber(point.y)
// -----------------------------------------------------------------------------
// Exports
// -----------------------------------------------------------------------------
const validateCurve = (curve: Curve, name: string): void => {
if (!isPlainObj(curve)) {
throw new Error(`Curve '${name}' must be an object`)
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const validateFunction = (func: Function, name: string): void => {
if (!isFunction(func)) {
throw new ValidationError(`${name} must be a function`)
}
}
export const validateT = (t: number): void => {
if (t < 0 || t > 1) {
throw new ValidationError(`t value must be between 0 and 1, but was '${t}'`)
}
}
export const validateStepNumber = (value: number): void => {
if (value < 0) {
throw new ValidationError(
`If step is a number, it must be a positive integer, but was '${value}'`
)
}
}
const validatePixelString = (value: string): void => {
const numericPortion = getPixelStringNumericComponent(value as string)
validateStepNumber(numericPortion)
}
const validateStep = (value: number | string | Step): void => {
if (isNumber(value)) {
validateStepNumber(value)
} else if (isPixelNumberString(value)) {
validatePixelString(value as string)
} else if (isPlainObj(value)) {
validateStepObj(value)
} else {
throw new ValidationError(
`A step value must be a non-negative number or a pixel string, but it was '${value}'`
)
}
}
const validateStepObj = (step: Step): void => {
validateStep(step.value)
}
const validateColumnsAndRows = (
columns: StepDefinition,
rows: StepDefinition
): void => {
if (isInt(columns)) {
validateStepNumber(columns)
} else if (isArray(columns)) {
columns.map((column: string | number | Step) => {
validateStep(column)
}, columns)
} else {
throw new ValidationError(
`columns must be an integer or an array, but it was '${columns}'`
)
}
if (isInt(rows)) {
validateStepNumber(rows)
} else if (isArray(rows)) {
rows.map((row: string | number | Step) => {
validateStep(row)
}, rows)
} else {
throw new ValidationError(
`rows must be an integer or an array, but it was '${columns}'`
)
}
}
const validateStartAndEndPoints = (
{ startPoint, endPoint }: { startPoint: Point; endPoint: Point },
name: string
): void => {
if (!isValidPoint(startPoint)) {
throw new Error(`Bounding curve '${name}' startPoint must be a valid point`)
}
if (!isValidPoint(endPoint)) {
throw new Error(`Bounding curve '${name}' endPoint must be a valid point`)
}
}
export const validateCurves = (boundingCurves: BoundingCurves): void => {
CURVE_NAMES.map((name) => {
const curve = boundingCurves[name as keyof BoundingCurves]
validateCurve(curve, name)
validateStartAndEndPoints(curve, name)
})
}
export const validateCornerPoints = (boundingCurves: BoundingCurves): void => {
if (
!getPointsAreSame(
boundingCurves.top.startPoint,
boundingCurves.left.startPoint
)
) {
throw new ValidationError(
`top curve startPoint and left curve startPoint must have same coordinates`
)
}
if (
!getPointsAreSame(
boundingCurves.bottom.startPoint,
boundingCurves.left.endPoint
)
) {
throw new ValidationError(
`bottom curve startPoint and left curve endPoint must have the same coordinates`
)
}
if (
!getPointsAreSame(
boundingCurves.top.endPoint,
boundingCurves.right.startPoint
)
) {
throw new ValidationError(
`top curve endPoint and right curve startPoint must have the same coordinates`
)
}
if (
!getPointsAreSame(
boundingCurves.bottom.endPoint,
boundingCurves.right.endPoint
)
) {
throw new ValidationError(
`bottom curve endPoint and right curve endPoint must have the same coordinates`
)
}
}
export const validateBoundingCurves = (
boundingCurves: BoundingCurves
): void => {
if (isNil(boundingCurves)) {
throw new ValidationError(`You must supply boundingCurves(Object)`)
}
if (!isPlainObj(boundingCurves)) {
throw new ValidationError(`boundingCurves must be an object`)
}
validateCurves(boundingCurves)
validateCornerPoints(boundingCurves)
}
export const validateGutterNumber = (gutter: number): void => {
if (gutter < 0) {
throw new ValidationError(
`Gutter must be a positive number, but was '${gutter}'`
)
}
}
export const validateGutter = (gutter: number | string): void => {
if (isNumber(gutter)) {
validateGutterNumber(gutter)
} else if (isPixelNumberString(gutter)) {
const numericPortion = getPixelStringNumericComponent(gutter)
validateGutterNumber(numericPortion)
} else {
throw new ValidationError(
`Gutter must be a number, or a pixel string, but was '${gutter}'`
)
}
}
export const validateGridDefinition = (
gridDefinition: GridDefinitionWithDefaults
): void => {
const {
rows,
columns,
gutter,
interpolationStrategy,
lineStrategy,
precision,
} = gridDefinition
if (isNil(columns)) {
throw new ValidationError(`You must supply grid.columns(Array or Int)`)
}
if (!isArray(columns) && !isInt(columns)) {
throw new ValidationError(
`grid.columns must be an Int, an Array of Ints and/or pixel strings, or an Array of objects`
)
}
if (isNil(rows)) {
throw new ValidationError(`You must supply grid.rows(Array or Int)`)
}
if (!isArray(rows) && !isInt(rows)) {
throw new ValidationError(
`grid.rows must be an Int, an Array of Ints and/or pixel strings, or an Array of objects`
)
}
validateColumnsAndRows(columns, rows)
if (!isNil(gutter)) {
if (isArray(gutter)) {
if (gutter.length > 2) {
throw new ValidationError(
`if grid.gutters is an Array it must have a length of 1 or 2`
)
}
gutter.map(validateGutter)
} else {
if (!isNumber(gutter) && !isPixelNumberString(gutter)) {
throw new ValidationError(
`grid.gutters must be an Int, a pixel-string, or an Array of Ints and/or pixel-strings`
)
}
validateGutter(gutter)
}
}
if (!isUndefined(interpolationStrategy)) {
if (isFunction(interpolationStrategy)) {
validateFunction(interpolationStrategy, `interpolationStrategy`)
} else if (isArray(interpolationStrategy)) {
validateFunction(interpolationStrategy[0], `interpolationStrategyU`)
validateFunction(interpolationStrategy[1], `interpolationStrategyV`)
} else {
const possibleValues = Object.values(InterpolationStrategy)
if (!possibleValues.includes(interpolationStrategy)) {
throw new ValidationError(
`Interpolation strategy '${interpolationStrategy}' is not recognised. Must be one of '${possibleValues}'`
)
}
}
}
if (!isUndefined(lineStrategy)) {
if (isArray(lineStrategy)) {
validateFunction(lineStrategy[0], `lineStrategyU`)
validateFunction(lineStrategy[1], `lineStrategyV`)
} else {
const possibleValues = Object.values(LineStrategy)
if (!possibleValues.includes(lineStrategy)) {
throw new ValidationError(
`Line strategy '${lineStrategy}' is not recognised. Must be one of '${possibleValues}'`
)
}
}
}
if (!isUndefined(precision)) {
if (!isInt(precision) || precision < 1) {
throw new ValidationError(
`Precision must be a positive integer greater than 0, but was '${precision}'`
)
}
}
// TODO: validate Bezier easing
}
export const validateGetPointArguments = (params: GetPointProps): void => {
mapObj((value, name) => {
if (value < 0 || value > 1) {
throw new ValidationError(
`${name} must be between 0 and 1, but was '${value}'`
)
}
}, params)
}
export const validateGetGridSquareArguments = (
x: number,
y: number,
columns: Step[],
rows: Step[]
): void => {
const columnCount = columns.length
const rowCount = rows.length
if (x < 0 || y < 0) {
throw new ValidationError(
`Coordinates must not be negative. You supplied x:'${x}' x y:'${y}'`
)
}
if (x >= columnCount) {
throw new ValidationError(
`X is zero-based and cannot be greater than number of columns - 1. Grid is '${columnCount}' columns wide and you passed x:'${x}'`
)
}
if (y >= rowCount) {
throw new ValidationError(
`Y is zero-based and cannot be greater than number of rows - 1. Grid is '${rowCount}' rows wide and you passed y:'${y}'`
)
}
}
export const validateGetAllCellBoundsArguments = (
makeBoundsCurvesSequential: boolean,
cellBoundsOrder: CellBoundsOrder
): void => {
if (!isBoolean(makeBoundsCurvesSequential)) {
throw new ValidationError(`makeBoundsCurvesSequential must be a boolean`)
}
if (!Object.values(CellBoundsOrder).includes(cellBoundsOrder)) {
throw new ValidationError(
`cellBoundsOrder must be one of ${Object.values(CellBoundsOrder)}`
)
}
}