tweak-tools
Version:
Tweak your React projects until awesomeness
176 lines (160 loc) • 6.36 kB
text/typescript
import v8n from 'v8n'
import { mapArrayToKeys } from '../../utils'
import { sanitize } from '../Number/number-plugin'
import { normalizeKeyedNumberSettings } from './vector-utils'
import type { InputWithSettings } from '../../types'
import type {
VectorType,
Format,
InternalVectorSettings,
VectorSettings,
VectorTypeFromValueFormatAndKeys,
} from './vector-types'
export function getVectorSchema(dimension: number) {
// Check if vector matches dimension and that every item is a number
// prettier-ignore
const isVectorArray = v8n().array().length(dimension).every.number()
// check if vector is an object and that all its keys are finite numbers
const isVectorObject = (o: any) => {
if (!o || typeof o !== 'object') return false
const values = Object.values(o)
return values.length === dimension && values.every((v: any) => isFinite(v))
}
return (o: any) => {
return isVectorArray.test(o) || isVectorObject(o)
}
}
/**
* Returns 'array' if value is an array, 'object' if it's an object.
* @param value
* @returns
*/
export function getVectorType(value: VectorType): Format {
return Array.isArray(value) ? 'array' : 'object'
}
/**
* Converts a value to either an object or an array.
* @param value
* @param format
* @param keys
* @returns
*/
function convert<Value extends VectorType, F extends Format, K extends string>(
value: Value,
format: F,
keys: K[]
): VectorTypeFromValueFormatAndKeys<Value, F, K> {
if (getVectorType(value) === format) return value as any
return (format === 'array' ? Object.values(value) : mapArrayToKeys(value as number[], keys)) as any
}
/**
* This function will check if the value argument is the full vector or only
* a slice of it. If it's only a slice, then it will merge it with the
* previousValue, preserving ratio if settings.lock is set.
* @param value
* @param settings
* @param previousValue
* @returns
*/
export const sanitizeVector = <K extends string, F extends Format>(
value: VectorType<K>,
settings: InternalVectorSettings<K, K[], F>,
previousValue: any
): VectorType<K, F> => {
// 1. We convert the value to a keyed object
const _value = convert(value, 'object', settings.keys) as any
for (let key in _value) _value[key] = sanitize(_value[key], settings[key as K])
// 2. We extract the keys
const _valueKeys = Object.keys(_value)
// will be the new value
let newValue: any = {}
// 3.a. if _value includes all keys of the vector input then _value is
// complete and can be considered as the new value.
if (_valueKeys.length === settings.keys.length) newValue = _value
// 3.b if _value is only a slice of the vector, therefore we need to merge
// it with the previous value.
else {
// 3.b.i We convert the previous value to an object.
const _previousValue = convert(previousValue, 'object', settings.keys) as any
// 3.b.i.1 if there's only one key and lock is true we keep the aspect ratio
// on all keys, then we keep the aspect ratio on all keys.
if (_valueKeys.length === 1 && settings.locked) {
// the only key from _value, which holds the base value for aspect ratio.
const lockedKey = _valueKeys[0]
// the base value for aspect ratio.
const lockedCoordinate = _value[lockedKey]
// the previous base value for aspect ratio
const previousLockedCoordinate = _previousValue[lockedKey]
// iterate through the previous value keys
const ratio = previousLockedCoordinate !== 0 ? lockedCoordinate / previousLockedCoordinate : 1
for (let key in _previousValue) {
// if key is the lockedKey, then its new value should be the locked
// value.
if (key === lockedKey) newValue[lockedKey] = lockedCoordinate
// if not then the other key should change by the same ratio as the
// lockedValue
else newValue[key] = _previousValue[key] * ratio
}
} else {
// _value is incomplete so we merge the previous value with the new one
newValue = { ..._previousValue, ..._value }
}
}
return convert(newValue, settings.format, settings.keys) as any
}
/**
* Formats a vector into an object to be displayed.
* @param value
* @param settings
* @returns
*/
export const formatVector = <K extends string, F extends Format>(
value: VectorType<K>,
settings: InternalVectorSettings<K, K[], F>
) => convert(value, 'object', settings.keys)
/**
* Checks if an object matches the format of the settings object for a number
* input.
* @param o
* @returns
*/
const isNumberSettings = (o?: object) => !!o && ('step' in o || 'min' in o || 'max' in o)
/**
* Normalizes a vector by extracting its keys and creating a number settings
* for each of them.
* @param _value
* @param settings
* @param defaultKeys
* @returns
*/
export function normalizeVector<Value extends VectorType, K extends string>(
value: Value,
settings: VectorSettings<Value, K>,
defaultKeys: K[] = []
) {
const { lock = false, ..._settings } = settings
const format: Format = Array.isArray(value) ? 'array' : 'object'
const keys = format === 'object' ? Object.keys(value) : defaultKeys
const _value = convert(value, 'object', keys)
// vector can accept either { value: { x, y }, { x: settings, y: settings } }
// or { value: { x, y }, { settings } } where settings will apply to both keys
// merged settings will recognize a unified settings and dispatch it to all keys
const mergedSettings = isNumberSettings(_settings)
? keys.reduce((acc, k) => Object.assign(acc, { [k]: _settings }), {})
: _settings
const numberSettings = normalizeKeyedNumberSettings(_value, mergedSettings)
return {
value: (format === 'array' ? value : _value) as Value,
settings: { ...numberSettings, format, keys, lock, locked: false },
}
}
export function getVectorPlugin<K extends string>(defaultKeys: K[]) {
return {
schema: getVectorSchema(defaultKeys.length),
normalize: <Value extends VectorType>({ value, ...settings }: InputWithSettings<Value, VectorSettings<Value, K>>) =>
normalizeVector(value, settings, defaultKeys),
format: (value: any, settings: InternalVectorSettings) => formatVector(value, settings),
sanitize: (value: any, settings: InternalVectorSettings, prevValue: any) =>
sanitizeVector(value, settings, prevValue),
}
}