support fractions and comma decimal separators
This commit is contained in:
parent
7a48cafe22
commit
8781e60350
8 changed files with 309 additions and 140 deletions
|
@ -1,8 +1,9 @@
|
||||||
import { isDegreeMeasurement } from 'config/measurements.mjs'
|
import { isDegreeMeasurement } from 'config/measurements.mjs'
|
||||||
import { measurementAsMm, formatMm } from 'shared/utils.mjs'
|
import { measurementAsMm, formatMm, measurementAsUnits } from 'shared/utils.mjs'
|
||||||
import { Collapse } from 'shared/components/collapse.mjs'
|
import { Collapse } from 'shared/components/collapse.mjs'
|
||||||
import { PlusIcon, EditIcon } from 'shared/components/icons.mjs'
|
import { PlusIcon, EditIcon } from 'shared/components/icons.mjs'
|
||||||
import { useState } from 'react'
|
import { NumberInput } from 'shared/components/workbench/menus/shared/inputs.mjs'
|
||||||
|
import { useState, useCallback } from 'react'
|
||||||
export const ns = ['account']
|
export const ns = ['account']
|
||||||
|
|
||||||
const Mval = ({ m, val = false, imperial = false, className = '' }) =>
|
const Mval = ({ m, val = false, imperial = false, className = '' }) =>
|
||||||
|
@ -101,34 +102,35 @@ export const MeasieInput = ({
|
||||||
}) => {
|
}) => {
|
||||||
const isDegree = isDegreeMeasurement(m)
|
const isDegree = isDegreeMeasurement(m)
|
||||||
const factor = isDegree ? 1 : mset.imperial ? 25.4 : 10
|
const factor = isDegree ? 1 : mset.imperial ? 25.4 : 10
|
||||||
|
const units = mset.imperial ? 'imperial' : 'metric'
|
||||||
|
const [val, setVal] = useState(() => {
|
||||||
|
const measie = mset.measies?.[m]
|
||||||
|
if (!measie) return ''
|
||||||
|
if (isDegree) return measie
|
||||||
|
return measurementAsUnits(measie, units)
|
||||||
|
})
|
||||||
|
|
||||||
const isValValid = (val) =>
|
const [valid, setValid] = useState(null)
|
||||||
typeof val === 'undefined' || val === '' ? null : val != false && !isNaN(val)
|
|
||||||
const isValid = (newVal) => (typeof newVal === 'undefined' ? isValValid(val) : isValValid(newVal))
|
|
||||||
|
|
||||||
const [val, setVal] = useState(mset.measies?.[m] / factor || '')
|
|
||||||
const [valid, setValid] = useState(isValid(mset.measies?.[m] / factor || ''))
|
|
||||||
|
|
||||||
// Update onChange
|
// Update onChange
|
||||||
const update = (evt) => {
|
const update = useCallback(
|
||||||
setVal(evt.target.value)
|
(validVal, rawVal) => {
|
||||||
|
setValid(validVal)
|
||||||
|
setVal(validVal || rawVal)
|
||||||
|
|
||||||
const useVal = isDegree
|
if (validVal && typeof onUpdate === 'function') {
|
||||||
? evt.target.value
|
const useVal = isDegree ? validVal : measurementAsMm(validVal, units)
|
||||||
: measurementAsMm(evt.target.value, mset.imperial ? 'imperial' : 'metric')
|
onUpdate(m, useVal)
|
||||||
const validUpdate = isValid(useVal)
|
}
|
||||||
setValid(validUpdate)
|
},
|
||||||
|
[isDegree, setValid, setVal, onUpdate, units]
|
||||||
if (validUpdate && typeof onUpdate === 'function') {
|
)
|
||||||
onUpdate(m, useVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
// FIXME
|
// FIXME
|
||||||
startLoading()
|
startLoading()
|
||||||
const measies = {}
|
const measies = {}
|
||||||
measies[m] = val * factor
|
measies[m] = isDegree ? val : measurementAsMm(val, units)
|
||||||
const result = await backend.updateSet(mset.id, { measies })
|
const result = await backend.updateSet(mset.id, { measies })
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
refresh()
|
refresh()
|
||||||
|
@ -137,7 +139,7 @@ export const MeasieInput = ({
|
||||||
stopLoading()
|
stopLoading()
|
||||||
}
|
}
|
||||||
|
|
||||||
const fraction = (i, base) => update({ target: { value: Math.floor(val) + i / base } })
|
const fraction = (i, base) => update(Math.floor(('' + val).split(/[\s\.]/)[0]) + i / base)
|
||||||
|
|
||||||
if (!m) return null
|
if (!m) return null
|
||||||
|
|
||||||
|
@ -147,48 +149,41 @@ export const MeasieInput = ({
|
||||||
<label className="shrink-0 grow max-w-full">
|
<label className="shrink-0 grow max-w-full">
|
||||||
{children}
|
{children}
|
||||||
<span className="input-group">
|
<span className="input-group">
|
||||||
<input
|
<NumberInput
|
||||||
type="number"
|
className={`border-r-0 w-full`}
|
||||||
step={mset.imperial && !isDegree ? 0.03125 : 0.1}
|
|
||||||
className={`
|
|
||||||
input input-bordered text-base-content border-r-0 w-full
|
|
||||||
${valid === false && 'input-error'}
|
|
||||||
${valid === true && 'input-success'}
|
|
||||||
`}
|
|
||||||
value={val}
|
value={val}
|
||||||
onChange={update}
|
onUpdate={update}
|
||||||
|
onMount={setValid}
|
||||||
/>
|
/>
|
||||||
{mset.imperial ? (
|
<span
|
||||||
<span
|
className={`bg-transparent border-y w-20
|
||||||
className={`bg-transparent border-y w-20
|
${valid === false && 'border-error text-neutral-content'}
|
||||||
${valid === false && 'border-error text-neutral-content'}
|
${valid && 'border-success text-neutral-content'}
|
||||||
${valid === true && 'border-success text-neutral-content'}
|
${valid === null && 'border-base-200 text-base-content'}
|
||||||
${valid === null && 'border-base-200 text-base-content'}
|
`}
|
||||||
`}
|
>
|
||||||
>
|
<Mval
|
||||||
<Mval
|
imperial={mset.imperial}
|
||||||
imperial={true}
|
val={isDegree ? val : measurementAsMm(val, units)}
|
||||||
val={val * (isDegree ? 1 : 25.4)}
|
m={m}
|
||||||
m={m}
|
className="text-base-content bg-transparent text-success text-xs font-bold p-0"
|
||||||
className="text-base-content bg-transparent text-success text-xs font-bold p-0"
|
/>
|
||||||
/>
|
</span>
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<span
|
<span
|
||||||
role="img"
|
role="img"
|
||||||
className={`bg-transparent border-y
|
className={`bg-transparent border-y
|
||||||
${valid === false && 'border-error text-neutral-content'}
|
${valid === false && 'border-error text-neutral-content'}
|
||||||
${valid === true && 'border-success text-neutral-content'}
|
${valid && 'border-success text-neutral-content'}
|
||||||
${valid === null && 'border-base-200 text-base-content'}
|
${valid === null && 'border-base-200 text-base-content'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{valid === true && '👍'}
|
{valid && '👍'}
|
||||||
{valid === false && '🤔'}
|
{valid === false && '🤔'}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`w-14 text-center
|
className={`w-14 text-center
|
||||||
${valid === false && 'bg-error text-neutral-content'}
|
${valid === false && 'bg-error text-neutral-content'}
|
||||||
${valid === true && 'bg-success text-neutral-content'}
|
${valid && 'bg-success text-neutral-content'}
|
||||||
${valid === null && 'bg-base-200 text-base-content'}
|
${valid === null && 'bg-base-200 text-base-content'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
// Components
|
// Components
|
||||||
import { OptionsIcon } from 'shared/components/icons.mjs'
|
import { OptionsIcon } from 'shared/components/icons.mjs'
|
||||||
import { optionsMenuStructure, optionType } from 'shared/utils.mjs'
|
import { optionsMenuStructure, optionType } from 'shared/utils.mjs'
|
||||||
|
@ -50,6 +51,8 @@ const DesignOption = ({ config, settings, control, ...rest }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDocsPath = (option) =>
|
||||||
|
`designs/${design}/options${option ? '/' + option.toLowerCase() : ''}`
|
||||||
/**
|
/**
|
||||||
* The design options menu
|
* The design options menu
|
||||||
* @param {String} options.design the name of the design
|
* @param {String} options.design the name of the design
|
||||||
|
@ -71,9 +74,11 @@ export const DesignOptions = ({
|
||||||
DynamicDocs = false,
|
DynamicDocs = false,
|
||||||
}) => {
|
}) => {
|
||||||
const menuNs = [`o_${design}`, ...ns]
|
const menuNs = [`o_${design}`, ...ns]
|
||||||
const optionsMenu = optionsMenuStructure(patternConfig.options)
|
const optionsMenu = useMemo(() => optionsMenuStructure(patternConfig.options), [patternConfig])
|
||||||
const getDocsPath = (option) =>
|
const updateFunc = useCallback(
|
||||||
`designs/${design}/options${option ? '/' + option.toLowerCase() : ''}`
|
(name, value) => update.settings(['options', ...name], value),
|
||||||
|
[update]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkbenchMenu
|
<WorkbenchMenu
|
||||||
|
@ -91,7 +96,7 @@ export const DesignOptions = ({
|
||||||
language,
|
language,
|
||||||
ns: menuNs,
|
ns: menuNs,
|
||||||
passProps: { settings, patternConfig },
|
passProps: { settings, patternConfig },
|
||||||
updateFunc: (name, value) => update.settings(['options', ...name], value),
|
updateFunc,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { useCallback, useMemo, useState, useEffect } from 'react'
|
import { useCallback, useMemo, useState, useEffect, useRef } from 'react'
|
||||||
import { round, measurementAsMm, measurementAsUnits, formatFraction128 } from 'shared/utils.mjs'
|
import {
|
||||||
|
round,
|
||||||
|
measurementAsMm,
|
||||||
|
measurementAsUnits,
|
||||||
|
formatFraction128,
|
||||||
|
fractionToDecimal,
|
||||||
|
} from 'shared/utils.mjs'
|
||||||
import { ChoiceButton } from 'shared/components/choice-button.mjs'
|
import { ChoiceButton } from 'shared/components/choice-button.mjs'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
||||||
|
@ -12,27 +18,141 @@ import debounce from 'lodash.debounce'
|
||||||
* Inputs that deal with more specific use cases should wrap one of the above base inputs
|
* Inputs that deal with more specific use cases should wrap one of the above base inputs
|
||||||
*******************************************************************************************/
|
*******************************************************************************************/
|
||||||
|
|
||||||
|
/** Regex to validate that an input is a number */
|
||||||
|
const numberInputMatchers = {
|
||||||
|
0: /^\-?[0-9]*[.,eE]?[0-9]+$/, // match a single decimal separator
|
||||||
|
1: /^\-?[0-9]*(\s?[0-9]+\/|[.,eE])?[0-9]+$/, // match a single decimal separator or fraction
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and parse a value that should be a number
|
||||||
|
* @param {any} val the value to validate
|
||||||
|
* @param {Boolean} allowFractions should fractions be considered valid input?
|
||||||
|
* @param {Number} min the minimum allowable value
|
||||||
|
* @param {Number} max the maximum allowable value
|
||||||
|
* @return {null|false|Number} null if the value is empty,
|
||||||
|
* false if the value is invalid,
|
||||||
|
* or the value parsed to a number if it is valid
|
||||||
|
*/
|
||||||
|
const validateVal = (val, allowFractions = true, min = -Infinity, max = Infinity) => {
|
||||||
|
// if it's empty, we're neutral
|
||||||
|
if (typeof val === 'undefined' || val === '') return null
|
||||||
|
|
||||||
|
// make sure it's a string
|
||||||
|
val = ('' + val).trim()
|
||||||
|
|
||||||
|
// get the appropriate match pattern and check for a match
|
||||||
|
const matchPattern = numberInputMatchers[Number(allowFractions)]
|
||||||
|
if (!val.match(matchPattern)) return false
|
||||||
|
|
||||||
|
// replace comma with period
|
||||||
|
const parsedVal = val.replace(',', '.')
|
||||||
|
// if fractions are allowed, parse for fractions, otherwise use the number as a value
|
||||||
|
const useVal = allowFractions ? fractionToDecimal(parsedVal) : parsedVal
|
||||||
|
|
||||||
|
// check that it's a number and it's in the range
|
||||||
|
if (isNaN(useVal) || useVal > max || useVal < min) return false
|
||||||
|
|
||||||
|
// all checks passed. return the parsed value
|
||||||
|
return useVal
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A number input that accepts comma or period decimal separators.
|
||||||
|
* Because our use case is almost never going to include thousands, we're using a very simple way of accepting commas:
|
||||||
|
* The validator checks for the presence of a single comma or period followed by numbers
|
||||||
|
* The parser replaces a single comma with a period
|
||||||
|
*
|
||||||
|
* optionally accepts fractions
|
||||||
|
* @param {Number} options.val the value of the input
|
||||||
|
* @param {Function} options.onUpdate a function to handle when the value is updated to a valid value
|
||||||
|
* @param {Boolean} options.fractions should the input allow fractional input
|
||||||
|
*/
|
||||||
|
export const NumberInput = ({
|
||||||
|
value,
|
||||||
|
onUpdate,
|
||||||
|
onMount,
|
||||||
|
className,
|
||||||
|
fractions = true,
|
||||||
|
min = -Infinity,
|
||||||
|
max = Infinity,
|
||||||
|
}) => {
|
||||||
|
const valid = useRef(validateVal(value, fractions, min, max))
|
||||||
|
|
||||||
|
// this is the change handler that will be debounced by the debounce handler
|
||||||
|
// we check validity inside this debounced function because
|
||||||
|
// we need to call the debounce handler on change regardless of validity
|
||||||
|
// if we don't, the displayed value won't update
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newVal) => {
|
||||||
|
// only actually update if the value is valid
|
||||||
|
if (typeof onUpdate === 'function') {
|
||||||
|
onUpdate(valid.current, newVal)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onUpdate, valid]
|
||||||
|
)
|
||||||
|
|
||||||
|
// get a debounce handler
|
||||||
|
const { debouncedHandleChange, displayVal } = useDebouncedHandlers({ handleChange, val: value })
|
||||||
|
|
||||||
|
// onChange
|
||||||
|
const onChange = useCallback(
|
||||||
|
(evt) => {
|
||||||
|
const newVal = evt.target.value
|
||||||
|
// set validity so it will display
|
||||||
|
valid.current = validateVal(newVal, fractions, min, max)
|
||||||
|
|
||||||
|
// handle the change
|
||||||
|
debouncedHandleChange(newVal)
|
||||||
|
},
|
||||||
|
[debouncedHandleChange, fractions, min, max, valid]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof onMount === 'function') {
|
||||||
|
console.log('mount', valid.current)
|
||||||
|
onMount(valid)
|
||||||
|
}
|
||||||
|
}, [onMount])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="number"
|
||||||
|
className={`input ${className || 'input-sm input-bordered grow text-base-content'}
|
||||||
|
${valid.current === false && 'input-error'}
|
||||||
|
${valid.current && 'input-success'}
|
||||||
|
`}
|
||||||
|
value={displayVal}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** A component that shows a number input to edit a value */
|
/** A component that shows a number input to edit a value */
|
||||||
const EditCount = (props) => (
|
const EditCount = (props) => {
|
||||||
<div className="form-control mb-2 w-full">
|
const onUpdate = useCallback(
|
||||||
<label className="label">
|
(validVal) => {
|
||||||
<span className="label-text text-base-content">{props.min}</span>
|
if (validVal !== null && validVal !== false) props.handleChange(validVal)
|
||||||
<span className="label-text font-bold text-base-content">{props.current}</span>
|
},
|
||||||
<span className="label-text text-base-content">{props.max}</span>
|
[props.handleChange]
|
||||||
</label>
|
)
|
||||||
<label className="input-group input-group-sm">
|
|
||||||
<input
|
return (
|
||||||
type="number"
|
<div className="form-control mb-2 w-full">
|
||||||
className={`
|
<label className="label">
|
||||||
input input-sm input-bordered grow text-base-content
|
<span className="label-text text-base-content">{props.min}</span>
|
||||||
`}
|
<span className="label-text font-bold text-base-content">{props.current}</span>
|
||||||
value={props.current}
|
<span className="label-text text-base-content">{props.max}</span>
|
||||||
onChange={props.handleChange}
|
</label>
|
||||||
/>
|
<label className="input-group input-group-sm">
|
||||||
<span className="text-base-content font-bold">#</span>
|
<NumberInput value={props.current} onUpdate={onUpdate} min={props.min} max={props.max} />
|
||||||
</label>
|
<span className="text-base-content font-bold">#</span>
|
||||||
</div>
|
</label>
|
||||||
)
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook to get the change handler for an input.
|
* A hook to get the change handler for an input.
|
||||||
|
@ -138,12 +258,12 @@ export const BoolInput = (props) => {
|
||||||
return <ListInput {...props} config={boolConfig} />
|
return <ListInput {...props} config={boolConfig} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDebouncedHandlers = ({ handleChange, changed, current, config }) => {
|
export const useDebouncedHandlers = ({ handleChange = () => {}, val }) => {
|
||||||
// hold onto what we're showing as the value so that the input doesn't look unresponsive
|
// hold onto what we're showing as the value so that the input doesn't look unresponsive
|
||||||
const [displayVal, setDisplayVal] = useState(changed ? current : config.dflt)
|
const [displayVal, setDisplayVal] = useState(val)
|
||||||
|
|
||||||
// the debounce function needs to be it's own memoized value so we can flush it on unmount
|
// the debounce function needs to be it's own memoized value so we can flush it on unmount
|
||||||
const debouncer = useMemo(() => debounce(handleChange, 200), [handleChange])
|
const debouncer = useMemo(() => debounce(handleChange, 300), [handleChange])
|
||||||
|
|
||||||
// this is the change handler
|
// this is the change handler
|
||||||
const debouncedHandleChange = useCallback(
|
const debouncedHandleChange = useCallback(
|
||||||
|
@ -161,8 +281,8 @@ export const useDebouncedHandlers = ({ handleChange, changed, current, config })
|
||||||
|
|
||||||
// set the display val to the current value when it gets changed
|
// set the display val to the current value when it gets changed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisplayVal(changed ? current : config.dflt)
|
setDisplayVal(val)
|
||||||
}, [changed, current, config])
|
}, [val])
|
||||||
|
|
||||||
return { debouncedHandleChange, displayVal }
|
return { debouncedHandleChange, displayVal }
|
||||||
}
|
}
|
||||||
|
@ -203,9 +323,7 @@ export const SliderInput = ({
|
||||||
|
|
||||||
const { debouncedHandleChange, displayVal } = useDebouncedHandlers({
|
const { debouncedHandleChange, displayVal } = useDebouncedHandlers({
|
||||||
handleChange,
|
handleChange,
|
||||||
current,
|
val: changed ? current : config.dflt,
|
||||||
changed,
|
|
||||||
config,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -216,7 +334,7 @@ export const SliderInput = ({
|
||||||
<EditCount
|
<EditCount
|
||||||
{...{
|
{...{
|
||||||
current: displayVal,
|
current: displayVal,
|
||||||
handleChange: (evt) => debouncedHandleChange(Number(evt.target.value)),
|
handleChange,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
t,
|
t,
|
||||||
|
@ -254,20 +372,44 @@ export const SliderInput = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* round a value to the correct number of decimal places to display all supplied digits after multiplication
|
||||||
|
* this is a workaround for floating point errors
|
||||||
|
* examples:
|
||||||
|
* roundPct(0.72, 100) === 72
|
||||||
|
* roundPct(7.5, 0.01) === 0.075
|
||||||
|
* roundPct(7.50, 0.01) === 0.0750
|
||||||
|
* @param {Number} num the number to be operated on
|
||||||
|
* @param {Number} factor the number to multiply by
|
||||||
|
* @return {Number} the given num multiplied by the factor, rounded appropriately
|
||||||
|
*/
|
||||||
|
const roundPct = (num, factor) => {
|
||||||
|
// stringify
|
||||||
|
const str = '' + num
|
||||||
|
// get the index of the decimal point in the number
|
||||||
|
const decimalIndex = str.indexOf('.')
|
||||||
|
// get the number of places the factor moves the decimal point
|
||||||
|
const factorPlaces = factor > 0 ? Math.ceil(Math.log10(factor)) : Math.floor(Math.log10(factor))
|
||||||
|
// the number of places needed is the number of digits that exist after the decimal minus the number of places the decimal point is being moved
|
||||||
|
const numPlaces = Math.max(0, str.length - (decimalIndex + factorPlaces))
|
||||||
|
return round(num * factor, numPlaces)
|
||||||
|
}
|
||||||
|
|
||||||
/** A {@see SliderInput} to handle percentage values */
|
/** A {@see SliderInput} to handle percentage values */
|
||||||
export const PctInput = ({ current, changed, updateFunc, config, ...rest }) => {
|
export const PctInput = ({ current, changed, updateFunc, config, ...rest }) => {
|
||||||
const factor = 100
|
const factor = 100
|
||||||
let pctCurrent = changed ? current * factor : current
|
let pctCurrent = changed ? roundPct(current, factor) : current
|
||||||
const pctUpdateFunc = useCallback(
|
const pctUpdateFunc = useCallback(
|
||||||
(path, newVal) => updateFunc(path, newVal === undefined ? undefined : newVal / factor),
|
(path, newVal) =>
|
||||||
[updateFunc, factor]
|
updateFunc(path, newVal === undefined ? undefined : roundPct(newVal, 1 / factor)),
|
||||||
|
[updateFunc]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SliderInput
|
<SliderInput
|
||||||
{...{
|
{...{
|
||||||
...rest,
|
...rest,
|
||||||
config: { ...config, dflt: config.dflt * factor },
|
config: { ...config, dflt: roundPct(config.dflt, factor) },
|
||||||
current: pctCurrent,
|
current: pctCurrent,
|
||||||
updateFunc: pctUpdateFunc,
|
updateFunc: pctUpdateFunc,
|
||||||
suffix: '%',
|
suffix: '%',
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Hooks
|
// Hooks
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { useView } from 'shared/hooks/use-view.mjs'
|
import { useView } from 'shared/hooks/use-view.mjs'
|
||||||
import { usePatternSettings } from 'shared/hooks/use-pattern-settings.mjs'
|
import { usePatternSettings } from 'shared/hooks/use-pattern-settings.mjs'
|
||||||
|
@ -103,32 +103,34 @@ export const Workbench = ({ design, Design, DynamicDocs }) => {
|
||||||
}, [Design, settings.measurements, mounted, view, setView])
|
}, [Design, settings.measurements, mounted, view, setView])
|
||||||
|
|
||||||
// Helper methods for settings/ui updates
|
// Helper methods for settings/ui updates
|
||||||
const update = {
|
const update = useMemo(
|
||||||
settings: (path, val) => setSettings(objUpdate({ ...settings }, path, val)),
|
() => ({
|
||||||
ui: (path, val) => setUi(objUpdate({ ...ui }, path, val)),
|
settings: (path, val) =>
|
||||||
toggleSa: () => {
|
setSettings((curSettings) => objUpdate({ ...curSettings }, path, val)),
|
||||||
const sa = settings.samm || (account.imperial ? 15.3125 : 10)
|
ui: (path, val) => setUi((curUi) => objUpdate({ ...curUi }, path, val)),
|
||||||
if (settings.sabool)
|
toggleSa: () => {
|
||||||
setSettings(
|
setSettings((curSettings) => {
|
||||||
objUpdate({ ...settings }, [
|
const sa = curSettings.samm || (account.imperial ? 15.3125 : 10)
|
||||||
[['sabool'], 0],
|
|
||||||
[['sa'], 0],
|
if (curSettings.sabool)
|
||||||
[['samm'], sa],
|
return objUpdate({ ...curSettings }, [
|
||||||
])
|
[['sabool'], 0],
|
||||||
)
|
[['sa'], 0],
|
||||||
else {
|
[['samm'], sa],
|
||||||
const sa = settings.samm || (account.imperial ? 15.3125 : 10)
|
])
|
||||||
setSettings(
|
else {
|
||||||
objUpdate({ ...settings }, [
|
return objUpdate({ ...curSettings }, [
|
||||||
[['sabool'], 1],
|
[['sabool'], 1],
|
||||||
[['sa'], sa],
|
[['sa'], sa],
|
||||||
[['samm'], sa],
|
[['samm'], sa],
|
||||||
])
|
])
|
||||||
)
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
setControl: controlState.update,
|
setControl: controlState.update,
|
||||||
}
|
}),
|
||||||
|
[setSettings, setUi, account, controlState]
|
||||||
|
)
|
||||||
|
|
||||||
// wait for mount. this helps prevent hydration issues
|
// wait for mount. this helps prevent hydration issues
|
||||||
if (!mounted) return <ModalSpinner />
|
if (!mounted) return <ModalSpinner />
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const MeasiesEditor = ({ Design, settings, update }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<h2>{t('editCurrentTitle')}</h2>
|
||||||
|
<p>{t('editCurrentDesc')}</p>
|
||||||
{Design.patternConfig.measurements.map((m) => (
|
{Design.patternConfig.measurements.map((m) => (
|
||||||
<MeasieInput {...{ t, m, mset, onUpdate }} key={m}>
|
<MeasieInput {...{ t, m, mset, onUpdate }} key={m}>
|
||||||
<span className="label">{t(m)}</span>
|
<span className="label">{t(m)}</span>
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const MeasiesView = ({ design, Design, settings, update, missingMeasureme
|
||||||
[['units'], set.imperial ? 'imperial' : 'metric'],
|
[['units'], set.imperial ? 'imperial' : 'metric'],
|
||||||
])
|
])
|
||||||
setView('draft')
|
setView('draft')
|
||||||
toast.success(t('updatedMeasurements'))
|
toast.success(t('appliedMeasies'))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
changeMeasies: Change Pattern Measurements
|
changeMeasies: Change Pattern Measurements
|
||||||
editCurrent: Edit Current Measurements
|
editCurrent: Edit Current Measurements
|
||||||
|
editCurrentTitle: Edit Pattern Measurements
|
||||||
|
editCurrentDesc: Changes you make here will not be saved to your measurements sets, and will only affect this pattern.
|
||||||
chooseNew: Choose a New Measurements Set
|
chooseNew: Choose a New Measurements Set
|
||||||
weLackSomeMeasies: We lack { nr } measurements to create this pattern
|
weLackSomeMeasies: We lack { nr } measurements to create this pattern
|
||||||
youCanPickOrEnter: You can either pick a measurements set, or enter them by hand, but we cannot proceed without these measurements.
|
youCanPickOrEnter: You can either pick a measurements set, or enter them by hand, but we cannot proceed without these measurements.
|
||||||
measiesOk: We have all required measurements to create this pattern.
|
measiesOk: We have all required measurements to create this pattern.
|
||||||
|
appliedMeasies: We applied a new measurements set to this pattern.
|
||||||
|
|
|
@ -151,39 +151,60 @@ export const getCrumbs = (app, slug = false) => {
|
||||||
|
|
||||||
/** convert a millimeter value to a Number value in the given units */
|
/** convert a millimeter value to a Number value in the given units */
|
||||||
export const measurementAsUnits = (mmValue, units = 'metric') =>
|
export const measurementAsUnits = (mmValue, units = 'metric') =>
|
||||||
mmValue / (units === 'imperial' ? 25.4 : 10)
|
round(mmValue / (units === 'imperial' ? 25.4 : 10), 3)
|
||||||
|
|
||||||
|
/** convert a value that may contain a fraction to a decimal */
|
||||||
|
export const fractionToDecimal = (value) => {
|
||||||
|
// if it's just a number, return it
|
||||||
|
if (!isNaN(value)) return value
|
||||||
|
|
||||||
|
// keep a running total
|
||||||
|
let total = 0
|
||||||
|
|
||||||
|
// split by spaces
|
||||||
|
let chunks = String(value).split(' ')
|
||||||
|
if (chunks.length > 2) return Number.NaN // too many spaces to parse
|
||||||
|
|
||||||
|
// a whole number with a fraction
|
||||||
|
if (chunks.length === 2) {
|
||||||
|
// shift the whole number from the array
|
||||||
|
const whole = Number(chunks.shift())
|
||||||
|
// if it's not a number, return NaN
|
||||||
|
if (isNaN(whole)) return Number.NaN
|
||||||
|
// otherwise add it to the total
|
||||||
|
total += whole
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we have only one chunk to parse
|
||||||
|
let fraction = chunks[0]
|
||||||
|
|
||||||
|
// split it to get numerator and denominator
|
||||||
|
let fChunks = fraction.trim().split('/')
|
||||||
|
// not really a fraction. return NaN
|
||||||
|
if (fChunks.length !== 2 || fChunks[1] === '') return Number.NaN
|
||||||
|
|
||||||
|
// do the division
|
||||||
|
let num = Number(fChunks[0])
|
||||||
|
let denom = Number(fChunks[1])
|
||||||
|
if (isNaN(num) || isNaN(denom)) return NaN
|
||||||
|
return total + num / denom
|
||||||
|
}
|
||||||
|
|
||||||
export const measurementAsMm = (value, units = 'metric') => {
|
export const measurementAsMm = (value, units = 'metric') => {
|
||||||
if (typeof value === 'number') return value * (units === 'imperial' ? 25.4 : 10)
|
if (typeof value === 'number') return value * (units === 'imperial' ? 25.4 : 10)
|
||||||
|
|
||||||
if (value.endsWith('.')) return false
|
if (String(value).endsWith('.')) return false
|
||||||
|
|
||||||
if (units === 'metric') {
|
if (units === 'metric') {
|
||||||
value = Number(value)
|
value = Number(value)
|
||||||
if (isNaN(value)) return false
|
if (isNaN(value)) return false
|
||||||
return value * 10
|
return value * 10
|
||||||
} else {
|
} else {
|
||||||
const imperialFractionToMm = (value) => {
|
const decimal = fractionToDecimal(value)
|
||||||
let chunks = value.trim().split('/')
|
if (isNaN(decimal)) return false
|
||||||
if (chunks.length !== 2 || chunks[1] === '') return false
|
return decimal * 24.5
|
||||||
let num = Number(chunks[0])
|
|
||||||
let denom = Number(chunks[1])
|
|
||||||
if (isNaN(num) || isNaN(denom)) return false
|
|
||||||
else return (num * 25.4) / denom
|
|
||||||
}
|
|
||||||
let chunks = value.split(' ')
|
|
||||||
if (chunks.length === 1) {
|
|
||||||
let val = chunks[0]
|
|
||||||
if (!isNaN(Number(val))) return Number(val) * 25.4
|
|
||||||
else return imperialFractionToMm(val)
|
|
||||||
} else if (chunks.length === 2) {
|
|
||||||
let inches = Number(chunks[0])
|
|
||||||
if (isNaN(inches)) return false
|
|
||||||
let fraction = imperialFractionToMm(chunks[1])
|
|
||||||
if (fraction === false) return false
|
|
||||||
return inches * 25.4 + fraction
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue