214 lines
7.5 KiB
JavaScript
214 lines
7.5 KiB
JavaScript
// Dependencies
|
|
import React, { useMemo } from 'react'
|
|
import {
|
|
sample,
|
|
missingMeasurements,
|
|
menuDesignOptionsStructure,
|
|
bundlePatternTranslations,
|
|
} from '../../lib/index.mjs'
|
|
import { measurements as measurementsTranslations } from '@freesewing/i18n'
|
|
import { orderBy } from '@freesewing/utils'
|
|
// Components
|
|
import { Null } from '@freesewing/react/components/Null'
|
|
import { ZoomablePattern } from '../ZoomablePattern.mjs'
|
|
import { PatternLayout } from '../PatternLayout.mjs'
|
|
import { HeaderMenu } from '../HeaderMenu.mjs'
|
|
import { H1, H3, H4, H5 } from '@freesewing/react/components/Heading'
|
|
import { OptionsIcon, MeasurementsIcon } from '@freesewing/react/components/Icon'
|
|
|
|
/**
|
|
* The test view allows users to test options and measurements
|
|
*
|
|
* @param (object) props - All the props
|
|
* @param {function} props.config - The editor configuration
|
|
* @param {function} props.Design - The design constructor
|
|
* @param {array} props.missingMeasurements - List of missing measurements for the current design
|
|
* @param {object} props.state - The ViewWrapper state object
|
|
* @param {object} props.state.settings - The current settings
|
|
* @param {object} props.update - Helper object for updating the ViewWrapper state
|
|
* @return {function} DraftView - React component
|
|
*/
|
|
export const TestView = ({ Design, state, update, config }) => {
|
|
/*
|
|
* Don't trust that we have all measurements
|
|
*
|
|
* We do not need to change the view here. That is done in the central
|
|
* ViewWrapper componenet. However, checking the measurements against
|
|
* the design takes a brief moment, so this component will typically
|
|
* render before that happens, and if measurements are missing it will
|
|
* throw and error.
|
|
*
|
|
* So when measurements are missing, we just return here and the view
|
|
* will switch on the next render loop.
|
|
*/
|
|
if (missingMeasurements(state)) return <Null />
|
|
|
|
const { settings } = state
|
|
if (settings?.sample) {
|
|
/*
|
|
* When testing/sampling one design, and then switching the editor to a different design,
|
|
* we run the risk that settings.sample holds invalid configuration. Like testing an unused
|
|
* measurement, or non-existing option.
|
|
* So here we try to check for that and only test/sample if things make sense
|
|
*/
|
|
if (
|
|
(settings.sample.type === 'option' &&
|
|
Object.keys(Design.patternConfig.options).includes(settings.sample.option)) ||
|
|
(settings.sample.type === 'measurement' &&
|
|
[
|
|
...Design.patternConfig.measurements,
|
|
...Design.patternConfig.optionalMeasurements,
|
|
].includes(settings.sample.measurement)) ||
|
|
settings.sample.type === 'models'
|
|
) {
|
|
const { pattern } = sample(Design, settings)
|
|
const renderProps = pattern.getRenderProps()
|
|
const strings = bundlePatternTranslations(pattern.designConfig.data.id)
|
|
const output = (
|
|
<ZoomablePattern
|
|
renderProps={renderProps}
|
|
patternLocale={state.locale || 'en'}
|
|
rotate={state.ui.rotate}
|
|
strings={strings}
|
|
/>
|
|
)
|
|
|
|
return <PatternLayout {...{ update, Design, output, state, pattern, config }} />
|
|
} else console.log('Not sampling', settings.sample)
|
|
}
|
|
|
|
// Translated measurements
|
|
const trm = orderBy(
|
|
Design.patternConfig.measurements.map((m) => ({ m, t: measurementsTranslations[m] })),
|
|
't',
|
|
'ASC'
|
|
)
|
|
const tom = orderBy(
|
|
Design.patternConfig.optionalMeasurements.map((m) => ({ m, t: measurementsTranslations[m] })),
|
|
't',
|
|
'ASC'
|
|
)
|
|
const btnClasses =
|
|
'tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs tw:flex tw:flex-row tw:items-center tw:justify-between tw:w-full tw:max-w-64'
|
|
|
|
return (
|
|
<>
|
|
<HeaderMenu state={state} {...{ config, update, Design }} />
|
|
<div className="tw:m-auto tw:mt-8 tw:max-w-4xl tw:px-4 tw:mb-8">
|
|
<H1>Test Pattern</H1>
|
|
<div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:lg:gap-4">
|
|
<div className="tw:flex tw:flex-col tw:gap-4">
|
|
<H3>Test Design Options</H3>
|
|
<SampleOptionsMenu {...{ Design, state, update }} />
|
|
</div>
|
|
{trm.length > 0 ? (
|
|
<div className="tw:flex tw:flex-col tw:gap-4">
|
|
<H3>Test Measurements</H3>
|
|
<H4>Required Measurements</H4>
|
|
<div className="">
|
|
{trm.map(({ t, m }) => (
|
|
<button
|
|
key={m}
|
|
className={btnClasses}
|
|
onClick={() =>
|
|
update.settings(['sample'], { type: 'measurement', measurement: m })
|
|
}
|
|
>
|
|
<MeasurementsIcon className="tw:w-4 tw:h-4" />
|
|
<span>{t}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
{tom.length > 0 ? (
|
|
<div className="tw:flex tw:flex-col tw:gap-4">
|
|
<H4>Optional Measurements</H4>
|
|
<div className="">
|
|
{tom.map(({ t, m }) => (
|
|
<button
|
|
key={m}
|
|
className={btnClasses}
|
|
onClick={() =>
|
|
update.settings(['sample'], { type: 'measurement', measurement: m })
|
|
}
|
|
>
|
|
<MeasurementsIcon className="tw:w-4 tw:h-4" />
|
|
<span>{t}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const SampleOptionsMenu = ({ Design, state, update }) => {
|
|
const { settings = {} } = state // Guard against undefined settings
|
|
const structure = useMemo(
|
|
() =>
|
|
menuDesignOptionsStructure(
|
|
Design.designConfig.data.id,
|
|
Design.patternConfig.options,
|
|
settings
|
|
),
|
|
[Design.designConfig.data.id, Design.patternConfig, settings]
|
|
)
|
|
|
|
return <SampleOptionsSubMenu structure={structure} update={update} />
|
|
}
|
|
|
|
const SampleOptionsSubMenu = ({ structure, update, level = 1 }) => {
|
|
const output = []
|
|
const btnClasses =
|
|
'tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs tw:flex tw:flex-row tw:items-center tw:justify-between tw:w-full tw:max-w-64'
|
|
/*
|
|
* Show entries alphabetic, but force groups last, and advanced last among them
|
|
*/
|
|
const titles = Object.keys(structure)
|
|
.filter((key) => key !== 'isGroup')
|
|
.sort()
|
|
const order = [
|
|
...titles.filter((key) => key !== 'advanced'),
|
|
...titles.filter((key) => key === 'advanced'),
|
|
]
|
|
// Non-groups first
|
|
for (const name of order) {
|
|
const struct = structure[name]
|
|
if (!struct.isGroup)
|
|
output.push(
|
|
<button
|
|
key={name}
|
|
className={btnClasses}
|
|
onClick={() => update.settings(['sample'], { type: 'option', option: name })}
|
|
>
|
|
<OptionsIcon className="tw:w-4 tw:h-4" />
|
|
<span>{struct.title}</span>
|
|
</button>
|
|
)
|
|
}
|
|
// Then groups
|
|
for (const name of order) {
|
|
const struct = structure[name]
|
|
if (struct.isGroup) {
|
|
output.push(
|
|
<H5 key={name}>
|
|
<span className="tw:capitalize">{name}</span>
|
|
</H5>
|
|
)
|
|
output.push(
|
|
<SampleOptionsSubMenu
|
|
structure={struct}
|
|
update={update}
|
|
level={level + 1}
|
|
key={name + 's'}
|
|
/>
|
|
)
|
|
}
|
|
}
|
|
|
|
return <div className="tw:pl-2 tw:border-l-2">{output}</div>
|
|
}
|