UNPKG

warp-grid

Version:

Create a complex grid, warped in 2D space and access data about its lines and cells

1 lines 82.1 kB
{"version":3,"file":"index.cjs","sources":["../src/enums.ts","../src/utils/easing.ts","../src/interpolate/pointOnCurve/interpolatePointOnCurveEvenlySpacedEasedFactory.ts","../src/interpolate/pointOnCurve/interpolatePointOnCurveLinearEasedFactory.ts","../src/errors/ValidationError.ts","../src/utils/functional.ts","../src/utils/is.ts","../src/utils/steps.ts","../src/utils/math.ts","../src/validation.ts","../src/utils/matrix.ts","../src/utils/bezier.ts","../src/getGridApi.ts","../src/interpolate/curves/straight.ts","../src/interpolate/curves/curved.ts","../src/api.ts"],"sourcesContent":["// -----------------------------------------------------------------------------\n// Enums\n// -----------------------------------------------------------------------------\n\nexport enum InterpolationStrategy {\n LINEAR = `linear`,\n EVEN = `even`,\n}\n\nexport enum LineStrategy {\n STRAIGHT_LINES = `straightLines`,\n CURVES = `curves`,\n}\n\nexport enum CellBoundsOrder {\n TTB_LTR = `topToBottom-leftToRight`,\n TTB_RTL = `topToBottom-rightToLeft`,\n BTT_LTR = `bottomToTop-leftToRight`,\n BTT_RTL = `bottomToTop-rightToLeft`,\n LTR_TTB = `leftToRight-topToBottom`,\n LTR_BTT = `leftToRight-bottomToTop`,\n RTL_TTB = `rightToLeft-topToBottom`,\n RTL_BTT = `rightToLeft-bottomToTop`,\n}\n","import BezierEasing from 'bezier-easing'\n\nimport type {\n BezierEasingParams,\n Curve,\n InterpolatePointOnCurveFactory,\n} from '../types'\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nconst wrapInterpolatePointOnCurveWithEasing =\n (\n interpolatePointOnCurve: InterpolatePointOnCurveFactory\n ): InterpolatePointOnCurveFactory =>\n (config: { bezierEasing: BezierEasingParams; precision: number }) => {\n // Use a named const for better debugging\n const interpolatePointOnCurveWrappedWithEasing = (\n t: number,\n curve: Curve\n ) => {\n // Create an easing function\n const ease = BezierEasing(...config.bezierEasing)\n const tEased = ease(t)\n return interpolatePointOnCurve(config)(tEased, curve)\n }\n return interpolatePointOnCurveWrappedWithEasing\n }\n\nexport default wrapInterpolatePointOnCurveWithEasing\n","import { interpolatePointOnCurveEvenlySpacedFactory } from 'coons-patch'\n\nimport wrapInterpolatePointOnCurveWithEasing from '../../utils/easing'\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nconst interpolatePointOnCurveEvenlySpacedEasedFactory =\n wrapInterpolatePointOnCurveWithEasing(\n interpolatePointOnCurveEvenlySpacedFactory\n )\n\nexport default interpolatePointOnCurveEvenlySpacedEasedFactory\n","import { interpolatePointOnCurveLinearFactory } from 'coons-patch'\n\nimport wrapInterpolatePointOnCurveWithEasing from '../../utils/easing'\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nconst interpolatePointOnCurveLinearEasedFactory =\n wrapInterpolatePointOnCurveWithEasing(interpolatePointOnCurveLinearFactory)\n\nexport default interpolatePointOnCurveLinearEasedFactory\n","export class ValidationError extends Error {\n constructor(message = `A validation error occurred`) {\n super(message)\n this.message = message\n }\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// These are very general-purpose functions, so any is appropriate here.\n\nimport type { ObjectWithStringKeys } from '../types'\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nexport const times = (f: (i: number) => any, n: number): any[] => {\n const result = []\n for (let i = 0; i < n; i++) {\n result.push(f(i))\n }\n return result\n}\n\n// The values of each object key will be the same as the return value of f (T)\nexport const mapObj = <T, R = Record<string, T>>(\n f: (value: any, key: string, idx: number) => T,\n o: ObjectWithStringKeys\n): R => {\n return Object.keys(o).reduce((acc, key, idx) => {\n const value = o[key]\n return {\n ...acc,\n [key]: f(value, key, idx),\n }\n }, {}) as R\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n// These are very general-purpose functions, so any is appropriate here.\n\n// -----------------------------------------------------------------------------\n// Const\n// -----------------------------------------------------------------------------\n\nconst VALID_GUTTER_REGEXP = /^\\d*\\.?\\d+px$/\n\n// -----------------------------------------------------------------------------\n// Utils\n// -----------------------------------------------------------------------------\n\nconst isType = (type: string, value: any): boolean => typeof value === type\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nexport const isBoolean = (value: any): value is boolean =>\n isType(`boolean`, value)\n\nexport const isArray = (value: any): value is any[] => Array.isArray(value)\n\nexport const isInt = (value: any): value is number => Number.isInteger(value)\n\nexport const isNumber = (value: any): value is number =>\n !isNaN(value) && isType(`number`, value)\n\nexport const isUndefined = (value: any): value is undefined =>\n isType(`undefined`, value)\n\nexport const isNull = (value: any): value is null => value === null\n\nexport const isNil = (value: any): value is null | undefined =>\n isUndefined(value) || isNull(value)\n\nexport const isString = (value: any): value is string =>\n isType(`string`, value) || value instanceof String\n\nexport const isPlainObj = (value: any): value is object =>\n !isNull(value) && isType(`object`, value) && value.constructor === Object\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\nexport const isFunction = (value: any): value is Function =>\n isType(`function`, value)\n\nexport const isPixelNumberString = (value: unknown) => {\n return isString(value) && VALID_GUTTER_REGEXP.test(value)\n}\n","import type {\n CurveLengths,\n Step,\n UnprocessedStep,\n StepDefinition,\n} from '../types'\nimport { times } from './functional'\nimport { isInt, isNumber, isPlainObj, isPixelNumberString, isNil } from './is'\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\ninterface SideData {\n start: number\n curveLength: number\n nonAbsoluteRatio: number\n}\n\n// -----------------------------------------------------------------------------\n// Utils\n// -----------------------------------------------------------------------------\n\n// If its 0px default it to 0, otherwise return unchanged.\nconst getProcessedStepValue = (step: number | string): number | string => {\n if (step === `0px`) {\n return 0\n }\n return step\n}\n\nconst ensureArray = (unprocessedSteps: StepDefinition): UnprocessedStep[] =>\n isInt(unprocessedSteps) ? times(() => 1, unprocessedSteps) : unprocessedSteps\n\nconst ensureObjectsAndProcess = (steps: UnprocessedStep[]) =>\n steps.map((step: UnprocessedStep): Step => {\n if (isPlainObj(step)) {\n return {\n ...step,\n value: getProcessedStepValue(step.value),\n }\n } else {\n return {\n value: getProcessedStepValue(step),\n }\n }\n })\n\nconst insertGutters = (steps: Step[], gutter: number | string): Step[] => {\n const lastStepIndex = steps.length - 1\n const hasGutter = isGutterNonZero(gutter)\n\n return steps.reduce((acc: Step[], step: Step, idx: number): Step[] => {\n const isLastStep = idx === lastStepIndex\n const nextStep = steps[idx + 1]\n const hasNextStep = !isNil(nextStep)\n const nextStepIsGutter = hasNextStep && nextStep.isGutter\n const shouldAddGutter =\n hasGutter && !isLastStep && !step.isGutter && !nextStepIsGutter\n\n // Insert a gutter step if we have gutters and are not the last step\n\n return shouldAddGutter\n ? [...acc, step, { value: gutter, isGutter: true }]\n : [...acc, step]\n }, [])\n}\n\nconst addStepRatioValues = (steps: Step[]) =>\n steps.reduce((total: number, step: Step) => {\n const { value } = step\n // Only add steps with values that are not absolute\n return isNumber(value) ? total + value : total\n }, 0)\n\nconst addStepAbsoluteValues = (steps: Step[]) =>\n steps.reduce((acc, step) => {\n const { value } = step\n return isPixelNumberString(value)\n ? acc + getPixelStringNumericComponent(value as string)\n : acc\n }, 0)\n\nconst getCounts = (columns: Step[], rows: Step[]) => ({\n columnsTotalCount: columns.length,\n rowsTotalCount: rows.length,\n})\n\nconst getTotalRatioValues = (columns: Step[], rows: Step[]) => ({\n columnsTotalRatioValue: addStepRatioValues(columns),\n rowsTotalRatioValue: addStepRatioValues(rows),\n})\n\nconst getAbsoluteRatio = (value: string, totalLength: number): number => {\n return getPixelStringNumericComponent(value) / totalLength\n}\n\nconst getNonAbsoluteSpaceRatios = (\n columns: Step[],\n rows: Step[],\n curveLengths: CurveLengths\n) => {\n const totalAbsoluteSizeU = addStepAbsoluteValues(columns)\n const totalAbsoluteSizeV = addStepAbsoluteValues(rows)\n\n const totalAbsoluteRatioTop = totalAbsoluteSizeU / curveLengths.top\n const totalAbsoluteRatioBottom = totalAbsoluteSizeU / curveLengths.bottom\n const totalAbsoluteRatioLeft = totalAbsoluteSizeV / curveLengths.left\n const totalAbsoluteRatioRight = totalAbsoluteSizeV / curveLengths.right\n\n const nonAbsoluteSpaceTopRatio = Math.max(1 - totalAbsoluteRatioTop, 0)\n const nonAbsoluteSpaceBottomRatio = Math.max(1 - totalAbsoluteRatioBottom, 0)\n\n // Prevent this from becoming a negative number\n const nonAbsoluteSpaceLeftRatio = Math.max(1 - totalAbsoluteRatioLeft, 0)\n const nonAbsoluteSpaceRightRatio = Math.max(1 - totalAbsoluteRatioRight, 0)\n\n return {\n nonAbsoluteSpaceTopRatio,\n nonAbsoluteSpaceBottomRatio,\n nonAbsoluteSpaceLeftRatio,\n nonAbsoluteSpaceRightRatio,\n }\n}\n\nconst getSize = (\n step: Step,\n totalNonAbsoluteValue: number,\n sideData: SideData\n): number => {\n // If step value is absolute, we use the ratio of the value to curve length\n if (isPixelNumberString(step.value)) {\n return getAbsoluteRatio(step.value as string, sideData.curveLength)\n }\n\n // Otherwise we use the ratio of the value to the non-absolute space\n const stepRatio = (step.value as number) / totalNonAbsoluteValue\n return stepRatio * sideData.nonAbsoluteRatio\n}\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nexport function getPixelStringNumericComponent(value: string): number {\n return Number(value.split(`px`)[0])\n}\n\nexport const isGutterNonZero = (gutter: number | string) =>\n (isNumber(gutter) && gutter > 0) || isPixelNumberString(gutter)\n\nexport const getEndValues = (\n step: Step,\n totalRatioValue: number,\n sideData: SideData,\n sideDataOpposite: SideData\n) => {\n const size = getSize(step, totalRatioValue, sideData)\n const uOppositeSize = getSize(step, totalRatioValue, sideDataOpposite)\n\n const end = sideData.start + size\n const oppositeEnd = sideDataOpposite.start + uOppositeSize\n return [end, oppositeEnd]\n}\n\nexport const processSteps = ({\n steps,\n gutter,\n}: {\n steps: StepDefinition\n gutter: number | string\n}): Step[] => {\n const stepsArray = ensureArray(steps)\n const stepsArrayOfObjs = ensureObjectsAndProcess(stepsArray)\n return insertGutters(stepsArrayOfObjs, gutter)\n}\n\nexport const getStepData = (\n columns: Step[],\n rows: Step[],\n curveLengths: CurveLengths\n) => {\n const processedColumns = columns\n const processedRows = rows\n\n return {\n ...getCounts(columns, rows),\n ...getTotalRatioValues(columns, rows),\n ...getNonAbsoluteSpaceRatios(columns, rows, curveLengths),\n processedColumns,\n processedRows,\n }\n}\n\nexport const getNonGutterSteps = (steps: Step[]) =>\n steps.filter((step: Step) => !step.isGutter)\n","// -----------------------------------------------------------------------------\n// Const\n// -----------------------------------------------------------------------------\n\nconst BINOMIAL_COEFFICIENTS = [[1], [1, 1]]\n\n// -----------------------------------------------------------------------------\n// Utils\n// -----------------------------------------------------------------------------\n\nconst roundToN = (n: number, value: number): number => Number(value.toFixed(n))\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nexport const roundTo5 = (value: number) => roundToN(5, value)\n\nexport const binomial = (n: number, k: number): number => {\n if (n === 0) {\n return 1\n }\n\n const lut = BINOMIAL_COEFFICIENTS\n\n while (n >= lut.length) {\n const lutLength = lut.length\n const nextRow = [1]\n for (let i = 1, prev = lutLength - 1; i < lutLength; i++) {\n nextRow[i] = lut[prev][i - 1] + lut[prev][i]\n }\n nextRow[lutLength] = 1\n lut.push(nextRow)\n }\n\n return lut[n][k]\n}\n","import { CellBoundsOrder, InterpolationStrategy, LineStrategy } from './enums'\nimport { ValidationError } from './errors/ValidationError'\nimport type {\n BoundingCurves,\n Curve,\n GetPointProps,\n GridDefinitionWithDefaults,\n Point,\n Step,\n StepDefinition,\n} from './types'\nimport { mapObj } from './utils/functional'\nimport {\n isArray,\n isBoolean,\n isFunction,\n isInt,\n isNil,\n isNumber,\n isPlainObj,\n isUndefined,\n isPixelNumberString,\n} from './utils/is'\nimport { roundTo5 } from './utils/math'\nimport { getPixelStringNumericComponent } from './utils/steps'\n\n// -----------------------------------------------------------------------------\n// Const\n// -----------------------------------------------------------------------------\n\nconst CURVE_NAMES = [`top`, `right`, `bottom`, `left`]\n\n// -----------------------------------------------------------------------------\n// Utils\n// -----------------------------------------------------------------------------\n\nconst getPointsAreSame = (point1: Point, point2: Point): boolean => {\n // Round the points to 5 decimal places before comparing to avoid rounding\n // issues where the values are fractionally different\n const roundedPoint1 = mapObj(roundTo5, point1)\n const roundedPoint2 = mapObj(roundTo5, point2)\n\n return (\n roundedPoint1.x === roundedPoint2.x && roundedPoint1.y === roundedPoint2.y\n )\n}\n\nconst isValidPoint = (point: Point): point is Point =>\n isPlainObj(point) && isNumber(point.x) && isNumber(point.y)\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nconst validateCurve = (curve: Curve, name: string): void => {\n if (!isPlainObj(curve)) {\n throw new Error(`Curve '${name}' must be an object`)\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\nconst validateFunction = (func: Function, name: string): void => {\n if (!isFunction(func)) {\n throw new ValidationError(`${name} must be a function`)\n }\n}\n\nexport const validateT = (t: number): void => {\n if (t < 0 || t > 1) {\n throw new ValidationError(`t value must be between 0 and 1, but was '${t}'`)\n }\n}\n\nexport const validateStepNumber = (value: number): void => {\n if (value < 0) {\n throw new ValidationError(\n `If step is a number, it must be a positive integer, but was '${value}'`\n )\n }\n}\n\nconst validatePixelString = (value: string): void => {\n const numericPortion = getPixelStringNumericComponent(value as string)\n validateStepNumber(numericPortion)\n}\n\nconst validateStep = (value: number | string | Step): void => {\n if (isNumber(value)) {\n validateStepNumber(value)\n } else if (isPixelNumberString(value)) {\n validatePixelString(value as string)\n } else if (isPlainObj(value)) {\n validateStepObj(value)\n } else {\n throw new ValidationError(\n `A step value must be a non-negative number or a pixel string, but it was '${value}'`\n )\n }\n}\n\nconst validateStepObj = (step: Step): void => {\n validateStep(step.value)\n}\n\nconst validateColumnsAndRows = (\n columns: StepDefinition,\n rows: StepDefinition\n): void => {\n if (isInt(columns)) {\n validateStepNumber(columns)\n } else if (isArray(columns)) {\n columns.map((column: string | number | Step) => {\n validateStep(column)\n }, columns)\n } else {\n throw new ValidationError(\n `columns must be an integer or an array, but it was '${columns}'`\n )\n }\n\n if (isInt(rows)) {\n validateStepNumber(rows)\n } else if (isArray(rows)) {\n rows.map((row: string | number | Step) => {\n validateStep(row)\n }, rows)\n } else {\n throw new ValidationError(\n `rows must be an integer or an array, but it was '${columns}'`\n )\n }\n}\n\nconst validateStartAndEndPoints = (\n { startPoint, endPoint }: { startPoint: Point; endPoint: Point },\n name: string\n): void => {\n if (!isValidPoint(startPoint)) {\n throw new Error(`Bounding curve '${name}' startPoint must be a valid point`)\n }\n if (!isValidPoint(endPoint)) {\n throw new Error(`Bounding curve '${name}' endPoint must be a valid point`)\n }\n}\n\nexport const validateCurves = (boundingCurves: BoundingCurves): void => {\n CURVE_NAMES.map((name) => {\n const curve = boundingCurves[name as keyof BoundingCurves]\n validateCurve(curve, name)\n validateStartAndEndPoints(curve, name)\n })\n}\n\nexport const validateCornerPoints = (boundingCurves: BoundingCurves): void => {\n if (\n !getPointsAreSame(\n boundingCurves.top.startPoint,\n boundingCurves.left.startPoint\n )\n ) {\n throw new ValidationError(\n `top curve startPoint and left curve startPoint must have same coordinates`\n )\n }\n\n if (\n !getPointsAreSame(\n boundingCurves.bottom.startPoint,\n boundingCurves.left.endPoint\n )\n ) {\n throw new ValidationError(\n `bottom curve startPoint and left curve endPoint must have the same coordinates`\n )\n }\n\n if (\n !getPointsAreSame(\n boundingCurves.top.endPoint,\n boundingCurves.right.startPoint\n )\n ) {\n throw new ValidationError(\n `top curve endPoint and right curve startPoint must have the same coordinates`\n )\n }\n if (\n !getPointsAreSame(\n boundingCurves.bottom.endPoint,\n boundingCurves.right.endPoint\n )\n ) {\n throw new ValidationError(\n `bottom curve endPoint and right curve endPoint must have the same coordinates`\n )\n }\n}\n\nexport const validateBoundingCurves = (\n boundingCurves: BoundingCurves\n): void => {\n if (isNil(boundingCurves)) {\n throw new ValidationError(`You must supply boundingCurves(Object)`)\n }\n\n if (!isPlainObj(boundingCurves)) {\n throw new ValidationError(`boundingCurves must be an object`)\n }\n validateCurves(boundingCurves)\n validateCornerPoints(boundingCurves)\n}\n\nexport const validateGutterNumber = (gutter: number): void => {\n if (gutter < 0) {\n throw new ValidationError(\n `Gutter must be a positive number, but was '${gutter}'`\n )\n }\n}\n\nexport const validateGutter = (gutter: number | string): void => {\n if (isNumber(gutter)) {\n validateGutterNumber(gutter)\n } else if (isPixelNumberString(gutter)) {\n const numericPortion = getPixelStringNumericComponent(gutter)\n validateGutterNumber(numericPortion)\n } else {\n throw new ValidationError(\n `Gutter must be a number, or a pixel string, but was '${gutter}'`\n )\n }\n}\n\nexport const validateGridDefinition = (\n gridDefinition: GridDefinitionWithDefaults\n): void => {\n const {\n rows,\n columns,\n gutter,\n interpolationStrategy,\n lineStrategy,\n precision,\n } = gridDefinition\n\n if (isNil(columns)) {\n throw new ValidationError(`You must supply grid.columns(Array or Int)`)\n }\n\n if (!isArray(columns) && !isInt(columns)) {\n throw new ValidationError(\n `grid.columns must be an Int, an Array of Ints and/or pixel strings, or an Array of objects`\n )\n }\n\n if (isNil(rows)) {\n throw new ValidationError(`You must supply grid.rows(Array or Int)`)\n }\n\n if (!isArray(rows) && !isInt(rows)) {\n throw new ValidationError(\n `grid.rows must be an Int, an Array of Ints and/or pixel strings, or an Array of objects`\n )\n }\n\n validateColumnsAndRows(columns, rows)\n\n if (!isNil(gutter)) {\n if (isArray(gutter)) {\n if (gutter.length > 2) {\n throw new ValidationError(\n `if grid.gutters is an Array it must have a length of 1 or 2`\n )\n }\n gutter.map(validateGutter)\n } else {\n if (!isNumber(gutter) && !isPixelNumberString(gutter)) {\n throw new ValidationError(\n `grid.gutters must be an Int, a pixel-string, or an Array of Ints and/or pixel-strings`\n )\n }\n validateGutter(gutter)\n }\n }\n\n if (!isUndefined(interpolationStrategy)) {\n if (isFunction(interpolationStrategy)) {\n validateFunction(interpolationStrategy, `interpolationStrategy`)\n } else if (isArray(interpolationStrategy)) {\n validateFunction(interpolationStrategy[0], `interpolationStrategyU`)\n validateFunction(interpolationStrategy[1], `interpolationStrategyV`)\n } else {\n const possibleValues = Object.values(InterpolationStrategy)\n if (!possibleValues.includes(interpolationStrategy)) {\n throw new ValidationError(\n `Interpolation strategy '${interpolationStrategy}' is not recognised. Must be one of '${possibleValues}'`\n )\n }\n }\n }\n\n if (!isUndefined(lineStrategy)) {\n if (isArray(lineStrategy)) {\n validateFunction(lineStrategy[0], `lineStrategyU`)\n validateFunction(lineStrategy[1], `lineStrategyV`)\n } else {\n const possibleValues = Object.values(LineStrategy)\n if (!possibleValues.includes(lineStrategy)) {\n throw new ValidationError(\n `Line strategy '${lineStrategy}' is not recognised. Must be one of '${possibleValues}'`\n )\n }\n }\n }\n\n if (!isUndefined(precision)) {\n if (!isInt(precision) || precision < 1) {\n throw new ValidationError(\n `Precision must be a positive integer greater than 0, but was '${precision}'`\n )\n }\n }\n\n // TODO: validate Bezier easing\n}\n\nexport const validateGetPointArguments = (params: GetPointProps): void => {\n mapObj((value, name) => {\n if (value < 0 || value > 1) {\n throw new ValidationError(\n `${name} must be between 0 and 1, but was '${value}'`\n )\n }\n }, params)\n}\n\nexport const validateGetGridSquareArguments = (\n x: number,\n y: number,\n columns: Step[],\n rows: Step[]\n): void => {\n const columnCount = columns.length\n const rowCount = rows.length\n\n if (x < 0 || y < 0) {\n throw new ValidationError(\n `Coordinates must not be negative. You supplied x:'${x}' x y:'${y}'`\n )\n }\n\n if (x >= columnCount) {\n throw new ValidationError(\n `X is zero-based and cannot be greater than number of columns - 1. Grid is '${columnCount}' columns wide and you passed x:'${x}'`\n )\n }\n\n if (y >= rowCount) {\n throw new ValidationError(\n `Y is zero-based and cannot be greater than number of rows - 1. Grid is '${rowCount}' rows wide and you passed y:'${y}'`\n )\n }\n}\n\nexport const validateGetAllCellBoundsArguments = (\n makeBoundsCurvesSequential: boolean,\n cellBoundsOrder: CellBoundsOrder\n): void => {\n if (!isBoolean(makeBoundsCurvesSequential)) {\n throw new ValidationError(`makeBoundsCurvesSequential must be a boolean`)\n }\n\n if (!Object.values(CellBoundsOrder).includes(cellBoundsOrder)) {\n throw new ValidationError(\n `cellBoundsOrder must be one of ${Object.values(CellBoundsOrder)}`\n )\n }\n}\n","/*------------------------------------------------------------------------------\n * All code in this file is based on work by Pomax on curve fitting, and all\n * credit is due to him.\n *\n * That work can be found here: https://pomax.github.io/bezierinfo/#curvefitting\n * ---------------------------------------------------------------------------*/\n\nimport matrix, { Matrix } from 'matrix-js'\n\nimport { times } from './functional'\nimport { binomial } from './math'\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nexport const getRatioMatrix = (\n ratios: number[]\n): { tMatrix: Matrix; tMatrixTransposed: Matrix } => {\n const ratioLength = ratios.length\n const data = times((i) => ratios.map((v) => v ** i), ratioLength)\n const tMatrix = matrix(data)\n const tMatrixTransposed = matrix(tMatrix.trans())\n return { tMatrix, tMatrixTransposed }\n}\n\nexport const getBasisMatrix = (numberOfPoints: number): Matrix => {\n let basisMatrix = matrix(matrix.gen(0).size(numberOfPoints, numberOfPoints))\n\n for (let i = 0; i < numberOfPoints; i++) {\n const value = binomial(numberOfPoints - 1, i)\n const ret = basisMatrix.set(i, i).to(value)\n basisMatrix = matrix(ret)\n }\n\n for (let c = 0, r; c < numberOfPoints; c++) {\n for (r = c + 1; r < numberOfPoints; r++) {\n const sign = (r + c) % 2 === 0 ? 1 : -1\n const value = binomial(r, c) * basisMatrix(r, r)\n basisMatrix = matrix(basisMatrix.set(r, c).to(sign * value))\n }\n }\n\n return basisMatrix\n}\n","/*------------------------------------------------------------------------------\n * The fitCurveToPoints function is based on work by Pomax on curve fitting,\n * and all credit is due to him.\n *\n * That work can be found here: https://pomax.github.io/bezierinfo/#curvefitting\n * ---------------------------------------------------------------------------*/\n\nimport matrix from 'matrix-js'\n\nimport type { Curve, Point } from '../types'\nimport { getBasisMatrix, getRatioMatrix } from './matrix'\nimport { Bezier } from 'bezier-js'\n\n// -----------------------------------------------------------------------------\n// Utils\n// -----------------------------------------------------------------------------\n\nconst getBezierFromCurve = ({\n startPoint,\n endPoint,\n controlPoint1,\n controlPoint2,\n}: Curve): Bezier =>\n new Bezier(\n startPoint.x,\n startPoint.y,\n controlPoint1.x,\n controlPoint1.y,\n controlPoint2.x,\n controlPoint2.y,\n endPoint.x,\n endPoint.y\n )\n\nconst pointsToCurve = (points: Point[]): Curve => ({\n startPoint: points[0],\n controlPoint1: points[1],\n controlPoint2: points[2],\n endPoint: points[3],\n})\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nexport const fitCubicBezierToPoints = (\n points: Point[],\n ratios: number[]\n): Curve => {\n const numberOfPoints = points.length\n const { tMatrix, tMatrixTransposed } = getRatioMatrix(ratios)\n const basisMatrix = getBasisMatrix(numberOfPoints)\n const basisMatrixInverted = matrix(basisMatrix.inv())\n const ratioMultipliedMatrix = matrix(tMatrix.prod(tMatrixTransposed))\n const invertedRatioMultipliedMatrix = matrix(ratioMultipliedMatrix.inv())\n const step1 = matrix(invertedRatioMultipliedMatrix.prod(tMatrix))\n const step2 = matrix(basisMatrixInverted.prod(step1))\n const X = matrix(points.map((v: Point) => [v.x]))\n const x = step2.prod(X)\n const Y = matrix(points.map((v: Point) => [v.y]))\n const y = step2.prod(Y)\n const result = x.map((r: number[], i: number) => ({ x: r[0], y: y[i][0] }))\n return pointsToCurve(result)\n}\n\nexport function getBezierCurveLength(curve: Curve): number {\n return getBezierFromCurve(curve).length()\n}\n","import { coonsPatch } from 'coons-patch'\nimport memoize from 'fast-memoize'\n\nimport { CellBoundsOrder } from './enums'\nimport type {\n BoundingCurves,\n BoundingCurvesWithMeta,\n Curve,\n GetAllCellBoundsProps,\n GetPointProps,\n GridApi,\n InterpolateLineU,\n InterpolateLineV,\n InterpolatePointOnCurve,\n InterpolationParamsU,\n InterpolationParamsV,\n ObjectWithStringKeys,\n LinesByAxis,\n Point,\n Step,\n} from './types'\nimport { getEndValues, getNonGutterSteps, getStepData } from './utils/steps'\nimport {\n validateGetAllCellBoundsArguments,\n validateGetPointArguments,\n validateGetGridSquareArguments,\n} from './validation'\nimport { getBezierCurveLength } from './utils/bezier'\nimport { mapObj } from './utils/functional'\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n// Internal\ninterface GetAPiConfig {\n interpolatePointOnCurveU: InterpolatePointOnCurve\n interpolatePointOnCurveV: InterpolatePointOnCurve\n interpolateLineU: InterpolateLineU\n interpolateLineV: InterpolateLineV\n}\n\ninterface GetStepIdxIncludingGuttersAcc {\n isComplete?: boolean\n value: number\n}\n\n// -----------------------------------------------------------------------------\n// Utils\n// -----------------------------------------------------------------------------\n\nconst getCurveReversed = (curve: Curve) => {\n return {\n startPoint: curve.endPoint,\n endPoint: curve.startPoint,\n controlPoint1: curve.controlPoint2,\n controlPoint2: curve.controlPoint1,\n }\n}\n\nconst getAreStepsVerticalFirst = (cellBoundsOrder: CellBoundsOrder): boolean =>\n [\n CellBoundsOrder.TTB_LTR,\n CellBoundsOrder.TTB_RTL,\n CellBoundsOrder.BTT_LTR,\n CellBoundsOrder.BTT_RTL,\n ].includes(cellBoundsOrder)\n\nconst getIsOuterReversed = (\n cellBoundsOrder: CellBoundsOrder,\n isVerticalFirst: boolean\n): boolean => {\n // If we are vertical first, anything starting with BTT is reversed\n // If we are not, anything starting with RTL is reversed\n const reversedEntries = isVerticalFirst\n ? [CellBoundsOrder.BTT_LTR, CellBoundsOrder.BTT_RTL]\n : [CellBoundsOrder.RTL_TTB, CellBoundsOrder.RTL_BTT]\n\n return reversedEntries.includes(cellBoundsOrder)\n}\n\nconst getIsInnerReversed = (\n cellBoundsOrder: CellBoundsOrder,\n isVerticalFirst: boolean\n): boolean => {\n // If we are vertical first, anything ending with RTL is reversed\n // If we are not, anything ending with BTT is reversed\n const reversedEntries = isVerticalFirst\n ? [CellBoundsOrder.TTB_RTL, CellBoundsOrder.BTT_RTL]\n : [CellBoundsOrder.RTL_BTT, CellBoundsOrder.LTR_BTT]\n\n return reversedEntries.includes(cellBoundsOrder)\n}\n\nconst clampT = (t: number): number => Math.min(Math.max(t, 0), 1)\n\nconst mapClampT = <T>(o: ObjectWithStringKeys) =>\n mapObj<number, ObjectWithStringKeys>(clampT, o) as T\n\n// Deal with case where there are multiple gutters in a row\nconst previousWasAlsoGutter = (steps: Step[], stepIdx: number): boolean =>\n !!(\n stepIdx > 0 &&\n steps[stepIdx - 1].isGutter &&\n steps[stepIdx] &&\n steps[stepIdx].isGutter\n )\n\n// We need to account for gutters when calculating the index of the step\nconst getStepIdxIncludingGutters = (stepIdx: number, steps: Step[]) =>\n steps.reduce<GetStepIdxIncludingGuttersAcc>(\n (acc, step, idx) => {\n if (acc.isComplete) {\n return acc\n }\n\n if (step.isGutter) {\n return acc\n } else {\n if (acc.value === stepIdx) {\n return { value: idx, isComplete: true }\n }\n return {\n value: acc.value + 1,\n }\n }\n },\n { value: 0 }\n ).value\n\nexport const getAllCellCornerPoints = (curves: Curve[][]) =>\n curves.reduce<Point[]>((acc: Point[], items: Curve[]) => {\n const subItems: Point[] = items.reduce<Point[]>(\n (acc: Point[], curve: Curve) => [\n ...acc,\n curve.startPoint,\n curve.endPoint,\n ],\n []\n )\n return [...acc, ...subItems]\n }, [])\n\nconst iterateOverSteps = (\n rows: Step[],\n columns: Step[],\n cellBoundsOrder: CellBoundsOrder,\n getBoundingCurves: (\n rowIdx: number,\n columnIdx: number\n ) => BoundingCurvesWithMeta\n) => {\n const nonGutterRows = getNonGutterSteps(rows)\n const nonGutterColumns = getNonGutterSteps(columns)\n\n // If cellBoundsOrder starts with either TTB or BTT then outer steps are\n // vertical, so we are vertical first\n const isVerticalFirst = getAreStepsVerticalFirst(cellBoundsOrder)\n\n const outerSteps = isVerticalFirst ? nonGutterRows : nonGutterColumns\n const innerSteps = isVerticalFirst ? nonGutterColumns : nonGutterRows\n\n const outerStepsLength = outerSteps.length\n const innerStepsLength = innerSteps.length\n\n const isOuterReversed = getIsOuterReversed(cellBoundsOrder, isVerticalFirst)\n const isInnerReversed = getIsInnerReversed(cellBoundsOrder, isVerticalFirst)\n\n const items = []\n for (let i = 0; i < outerStepsLength; i++) {\n const outerIdxResolved = isOuterReversed ? outerStepsLength - i - 1 : i\n\n for (let j = 0; j < innerStepsLength; j++) {\n const innerIdxResolved = isInnerReversed ? innerStepsLength - j - 1 : j\n\n const rowIdx = isVerticalFirst ? innerIdxResolved : outerIdxResolved\n const columnIdx = isVerticalFirst ? outerIdxResolved : innerIdxResolved\n items.push(getBoundingCurves(rowIdx, columnIdx))\n }\n }\n return items\n}\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a grid API instance with the specified configuration.\n * @param boundingCurves The curves defining the grid boundaries.\n * @param columns Array of steps defining column structure.\n * @param rows Array of steps defining row structure.\n * @param gutter The horizontal and vertical spacing between cells.\n * @param config Object containing interpolation functions for grid calculations.\n * @returns API interacting with the defined grid.\n * @throws ValidationError When grid boundaries are invalid or interpolation functions are missing.\n */\nconst getApi = (\n boundingCurves: BoundingCurves,\n columns: Step[],\n rows: Step[],\n {\n interpolatePointOnCurveU,\n interpolatePointOnCurveV,\n interpolateLineU,\n interpolateLineV,\n }: GetAPiConfig\n): GridApi => {\n // Cache lengths of bounding curves for use in API\n const curveLengths = {\n top: getBezierCurveLength(boundingCurves.top),\n left: getBezierCurveLength(boundingCurves.left),\n bottom: getBezierCurveLength(boundingCurves.bottom),\n right: getBezierCurveLength(boundingCurves.right),\n }\n\n const {\n processedColumns,\n processedRows,\n columnsTotalCount,\n rowsTotalCount,\n columnsTotalRatioValue,\n rowsTotalRatioValue,\n nonAbsoluteSpaceLeftRatio,\n nonAbsoluteSpaceRightRatio,\n nonAbsoluteSpaceTopRatio,\n nonAbsoluteSpaceBottomRatio,\n } = getStepData(columns, rows, curveLengths)\n\n const getPoint = memoize((params: GetPointProps): Point => {\n validateGetPointArguments(params)\n return coonsPatch(boundingCurves, params, {\n interpolatePointOnCurveU,\n interpolatePointOnCurveV,\n })\n })\n\n // Horizontal lines\n // Outer array represents rows, inner array represents horizontal line\n // sections for each column within that row\n const getLinesXAxis = memoize((): Curve[][] => {\n const curves = []\n let vStart = 0\n let vOppositeStart = 0\n\n // Short circuit if we are only 1x1 and just return bounds\n if (columnsTotalCount === 1 && rowsTotalCount === 1) {\n return [[boundingCurves.top], [boundingCurves.bottom]]\n }\n\n // Otherwise work our way through the grid building all the horizontal lines\n // that make up a row.\n for (let rowIdx = 0; rowIdx <= rowsTotalCount; rowIdx++) {\n const lineSections: Curve[] = []\n let uStart = 0\n let uOppositeStart = 0\n const row = processedRows[rowIdx]\n\n processedColumns.map((column) => {\n const [uEnd, uOppositeEnd] = getEndValues(\n column,\n columnsTotalRatioValue,\n {\n start: uStart,\n curveLength: curveLengths.top,\n nonAbsoluteRatio: nonAbsoluteSpaceTopRatio,\n },\n {\n start: uOppositeStart,\n curveLength: curveLengths.bottom,\n nonAbsoluteRatio: nonAbsoluteSpaceBottomRatio,\n }\n )\n\n const isFirstRowAndRowIsGutter = rowIdx === 0 && row.isGutter\n\n if (\n !column.isGutter &&\n !isFirstRowAndRowIsGutter &&\n !previousWasAlsoGutter(rows, rowIdx)\n ) {\n const paramsClamped: InterpolationParamsU =\n mapClampT<InterpolationParamsU>({\n uStart,\n uEnd,\n vStart,\n uOppositeStart,\n uOppositeEnd,\n vOppositeStart,\n })\n const curve = interpolateLineU(\n boundingCurves,\n paramsClamped,\n interpolatePointOnCurveU,\n interpolatePointOnCurveV\n )\n\n lineSections.push(curve)\n }\n // Only update after we have saved the curve\n uStart = uEnd\n uOppositeStart = uOppositeEnd\n })\n\n curves.push(lineSections)\n\n if (rowIdx < rowsTotalCount) {\n const row = processedRows[rowIdx]\n const [vEnd, vOppositeEnd] = getEndValues(\n row,\n rowsTotalRatioValue,\n {\n start: vStart,\n curveLength: curveLengths.left,\n nonAbsoluteRatio: nonAbsoluteSpaceLeftRatio,\n },\n {\n start: vOppositeStart,\n curveLength: curveLengths.right,\n nonAbsoluteRatio: nonAbsoluteSpaceRightRatio,\n }\n )\n vStart = vEnd\n vOppositeStart = vOppositeEnd\n }\n }\n return curves\n })\n\n // Vertical lines Outer array represents columns, inner array represents line\n // sections for each row within that column\n const getLinesYAxis = memoize((): Curve[][] => {\n const curves = []\n let uStart = 0\n let uOppositeStart = 0\n\n // Short circuit if we are only 1x1 and just return the bounds\n if (columnsTotalCount === 1 && rowsTotalCount === 1) {\n return [[boundingCurves.left], [boundingCurves.right]]\n }\n\n // Otherwise work our way through the grid building all the vertical lines\n // that make up a column.\n for (let columnIdx = 0; columnIdx <= columnsTotalCount; columnIdx++) {\n const lineSections: Curve[] = []\n let vStart = 0\n let vOppositeStart = 0\n const column = processedColumns[columnIdx]\n\n processedRows.map((row) => {\n const [vEnd, vOppositeEnd] = getEndValues(\n row,\n rowsTotalRatioValue,\n {\n start: vStart,\n curveLength: curveLengths.left,\n nonAbsoluteRatio: nonAbsoluteSpaceLeftRatio,\n },\n {\n start: vOppositeStart,\n curveLength: curveLengths.right,\n nonAbsoluteRatio: nonAbsoluteSpaceRightRatio,\n }\n )\n\n const isFirstColumnAndColumnIsGutter =\n columnIdx === 0 && column.isGutter\n\n if (\n !row.isGutter &&\n !isFirstColumnAndColumnIsGutter &&\n !previousWasAlsoGutter(columns, columnIdx)\n ) {\n const paramsClamped: InterpolationParamsV =\n mapClampT<InterpolationParamsV>({\n vStart,\n vEnd,\n uStart,\n vOppositeStart,\n vOppositeEnd,\n uOppositeStart,\n })\n\n const curve = interpolateLineV(\n boundingCurves,\n paramsClamped,\n interpolatePointOnCurveU,\n interpolatePointOnCurveV\n )\n\n lineSections.push(curve)\n }\n\n // Only update after we have saved the curve\n vStart = vEnd\n vOppositeStart = vOppositeEnd\n })\n\n curves.push(lineSections)\n\n // Calculate the position of the next column\n if (columnIdx < columnsTotalCount) {\n const column = processedColumns[columnIdx]\n\n const [uEnd, uOppositeEnd] = getEndValues(\n column,\n columnsTotalRatioValue,\n {\n start: uStart,\n curveLength: curveLengths.top,\n nonAbsoluteRatio: nonAbsoluteSpaceTopRatio,\n },\n {\n start: uOppositeStart,\n curveLength: curveLengths.bottom,\n nonAbsoluteRatio: nonAbsoluteSpaceBottomRatio,\n }\n )\n\n uStart = uEnd\n uOppositeStart = uOppositeEnd\n }\n }\n return curves\n })\n\n const getLines = memoize((): LinesByAxis => {\n return {\n xAxis: getLinesXAxis(),\n yAxis: getLinesYAxis(),\n }\n })\n\n const getIntersections = memoize((): Point[] => {\n const { xAxis } = getLines()\n\n const intersections = getAllCellCornerPoints(xAxis)\n return intersections\n })\n\n const getCellBounds = memoize(\n (\n columnIdx: number,\n rowIdx: number,\n { makeBoundsCurvesSequential = false }: GetAllCellBoundsProps = {}\n ): BoundingCurvesWithMeta => {\n const nonGutterColumns = getNonGutterSteps(columns)\n const nonGutterRows = getNonGutterSteps(rows)\n validateGetGridSquareArguments(\n columnIdx,\n rowIdx,\n nonGutterColumns,\n nonGutterRows\n )\n\n // We need to account for gutters when calculating the index of the column\n // const columnIdxAccountingForGutters = getIdx(columnIdx,\n // processedColumns)\n const rowIdxIncludingGutters = getStepIdxIncludingGutters(\n rowIdx,\n processedRows\n )\n const columnIdxIncludingGutters = getStepIdxIncludingGutters(\n columnIdx,\n processedColumns\n )\n\n const { xAxis, yAxis } = getLines()\n\n const top = xAxis[rowIdxIncludingGutters][columnIdx]\n const bottom = xAxis[rowIdxIncludingGutters + 1][columnIdx]\n const left = yAxis[columnIdxIncludingGutters][rowIdx]\n const right = yAxis[columnIdxIncludingGutters + 1][rowIdx]\n\n return {\n meta: {\n row: rowIdx,\n column: columnIdx,\n },\n top,\n bottom: makeBoundsCurvesSequential ? getCurveReversed(bottom) : bottom,\n left: makeBoundsCurvesSequential ? getCurveReversed(left) : left,\n right,\n }\n }\n )\n\n const getAllCellBounds = memoize(\n ({\n makeBoundsCurvesSequential = false,\n cellBoundsOrder = CellBoundsOrder.TTB_LTR,\n }: GetAllCellBoundsProps = {}): BoundingCurvesWithMeta[] => {\n validateGetAllCellBoundsArguments(\n makeBoundsCurvesSequential,\n cellBoundsOrder\n )\n\n return iterateOverSteps(\n rows,\n columns,\n cellBoundsOrder,\n (rowIdx: number, columnIdx: number) =>\n getCellBounds(rowIdx, columnIdx, {\n makeBoundsCurvesSequential,\n })\n )\n }\n )\n\n return {\n getPoint,\n getLinesXAxis,\n getLinesYAxis,\n getLines,\n getIntersections,\n getCellBounds,\n getAllCellBounds,\n }\n}\n\nexport default getApi\n","import { coonsPatch } from 'coons-patch'\n\nimport type {\n BoundingCurves,\n Curve,\n InterpolatePointOnCurve,\n InterpolationParamsU,\n InterpolationParamsV,\n} from '../../types'\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\n/**\n * Interpolates a straight line along the U axis of a surface defined by\n * bounding curves.\n *\n * @param {BoundingCurves} boundingCurves - An object containing curves that\n * define the surface boundaries.\n * @param {InterpolationParamsU} params - Parameters describing curve start and\n * end points\n * @param {InterpolatePointOnCurveU} interpolatePointOnCurveU - A function to\n * interpolate points on the U axis.\n * @param {InterpolatePointOnCurveU} interpolatePointOnCurveV - A function to\n * interpolate points on the V axis.\n * @returns {Curve} The interpolated straight line as a cubic Bezier curve.\n *\n * @group Interpolation\n */\nexport const interpolateStraightLineU = (\n boundingCurves: BoundingCurves,\n {\n uStart,\n uEnd,\n uOppositeStart,\n uOppositeEnd,\n vStart,\n vOppositeStart,\n }: InterpolationParamsU,\n interpolatePointOnCurveU: InterpolatePointOnCurve,\n interpolatePointOnCurveV: InterpolatePointOnCurve\n): Curve => {\n const startPoint = coonsPatch(\n boundingCurves,\n {\n u: uStart,\n v: vStart,\n uOpposite: uOppositeStart,\n vOpposite: vOppositeStart,\n },\n { interpolatePointOnCurveU, interpolatePointOnCurveV }\n )\n\n const endPoint = coonsPatch(\n boundingCurves,\n { u: uEnd, v: vStart, uOpposite: uOppositeEnd, vOpposite: vOppositeStart },\n { interpolatePointOnCurveU, interpolatePointOnCurveV }\n )\n\n // Set the control points to the start and end points so the line is straight\n return {\n startPoint,\n endPoint,\n controlPoint1: startPoint,\n controlPoint2: endPoint,\n }\n}\n\n/**\n * Interpolates a straight line along the V axis of a surface defined by\n * bounding curves.\n *\n * @param {BoundingCurves} boundingCurves - An object containing curves that\n * define the surface boundaries.\n * @param {InterpolationParamsU} params - Parameters describing curve start and\n * end points\n * @param {InterpolatePointOnCurveU} interpolatePointOnCurveU - A function to\n * interpolate points on the U axis.\n * @param {InterpolatePointOnCurveU} interpolatePointOnCurveV - A function to\n * interpolate points on the V axis.\n * @returns {Curve} The interpolated straight line as a cubic Bezier curve.\n *\n * @group Interpolation\n */\nexport const interpolateStraightLineV = (\n boundingCurves: BoundingCurves,\n {\n vStart,\n vEnd,\n vOppositeStart,\n vOppositeEnd,\n uStart,\n uOppositeStart,\n }: InterpolationParamsV,\n interpolatePointOnCurveU: InterpolatePointOnCurve,\n interpolatePointOnCurveV: InterpolatePointOnCurve\n): Curve => {\n const startPoint = coonsPatch(\n boundingCurves,\n {\n u: uStart,\n v: vStart,\n uOpposite: uOppositeStart,\n vOpposite: vOppositeStart,\n },\n { interpolatePointOnCurveU, interpolatePointOnCurveV }\n )\n\n const endPoint = coonsPatch(\n boundingCurves,\n { u: uStart, v: vEnd, uOpposite: uOppositeStart, vOpposite: vOppositeEnd },\n { interpolatePointOnCurveU, interpolatePointOnCurveV }\n )\n\n // Set the control points to the start and end points so the line is straight\n return {\n startPoint,\n endPoint,\n controlPoint1: startPoint,\n controlPoint2: endPoint,\n }\n}\n","import type {\n BoundingCurves,\n Curve,\n InterpolatePointOnCurve,\n InterpolationParamsU,\n InterpolationParamsV,\n} from '../../types'\nimport { fitCubicBezierToPoints } from '../../utils/bezier'\nimport { interpolateStraightLineU, interpolateStraightLineV } from './straight'\nimport { coonsPatch } from 'coons-patch'\n\n// -----------------------------------------------------------------------------\n// Const\n// -----------------------------------------------------------------------------\n\nconst T_MIDPOINT_1 = 0.25\nconst T_MIDPOINT_2 = 0.75\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\n/**\n * Interpolates a cubic Bezier curve along the U direction of a surface defined\n * by bounding curves.\n *\n * @param {BoundingCurves} boundingCurves - An object containing curves that\n * define the surface boundaries.\n * @param {number} uStart - The starting parameter along the U direction.\n * @param {number} uSize - The size of the step along the U direction.\n * @param {number} uEnd - The ending parameter along the U direction.\n * @param {number} vStart - The starting parameter along the V direction.\n * @param {InterpolatePointOnCurve} interpolatePointOnCurveU - A function to\n * interpolate points on the U axis.\n * @param {InterpolatePointOnCurve} interpolatePointOnCurveV - A function to\n * interpolate points on the V axis.\n * @returns {Curve} The interpolated cubic Bezier curve.\n *\n * @group Interpolation\n */\nexport const interpolateCurveU = (\n boundingCurves: BoundingCurves,\n {\n uStart,\n uEnd,\n vStart,\n vOppositeStart,\n uOppositeEnd,\n uOppositeStart,\n }: InterpolationParamsU,\n interpolatePointOnCurveU: InterpolatePointOnCurve,\n interpolatePointOnCurveV: InterpolatePointOnCurve\n): Curve => {\n const uSize = uEnd - uStart\n const uOppositeSize = uOpposit