diff --git a/packages/i18n/src/locales/en/plugin/plugins/cutlist.yaml b/packages/i18n/src/locales/en/plugin/plugins/cutlist.yaml deleted file mode 100644 index 103bc6c1e35..00000000000 --- a/packages/i18n/src/locales/en/plugin/plugins/cutlist.yaml +++ /dev/null @@ -1,16 +0,0 @@ -canvas: Canvas -cut: Cut -cuttingLayout: Suggested Cutting Layout -fabric: Main Fabric -fabricSize: "{length} of {width} wide material" -heavyCanvas: Heavy Canvas -interfacing: Interfacing -lining: Lining -lmhCanvas: Light to Medium Hair Canvas -mirrored: mirrored -onFoldLower: on the fold -onFoldAndBias: folded on the bias -onBias: on the bias -plastic: Plastic -ribbing: Ribbing -edgeOfFabric: Edge of Fabric diff --git a/packages/react-components/src/pattern/part.mjs b/packages/react-components/src/pattern/part.mjs index 9d56af4a682..ece24d2f8b3 100644 --- a/packages/react-components/src/pattern/part.mjs +++ b/packages/react-components/src/pattern/part.mjs @@ -13,7 +13,7 @@ export const PartInner = forwardRef( path={part.paths[pathName]} topLeft={part.topLeft} bottomRight={part.bottomRight} - units={settings.units} + units={settings[0].units} {...{ stackName, partName, pathName, part, settings, components, t }} /> ))} diff --git a/sites/shared/components/workbench/exporting/export-handler.mjs b/sites/shared/components/workbench/exporting/export-handler.mjs index 4dd1258d915..4cf0626c76e 100644 --- a/sites/shared/components/workbench/exporting/export-handler.mjs +++ b/sites/shared/components/workbench/exporting/export-handler.mjs @@ -2,11 +2,11 @@ import Worker from 'web-worker' import fileSaver from 'file-saver' import { themePlugin } from '@freesewing/plugin-theme' import { pluginI18n } from '@freesewing/plugin-i18n' -import { pagesPlugin, fabricPlugin } from 'shared/plugins/plugin-layout-part.mjs' +import { pagesPlugin, materialPlugin } from 'shared/plugins/plugin-layout-part.mjs' import { pluginAnnotations } from '@freesewing/plugin-annotations' -import { cutLayoutPlugin } from '../layout/cut/plugin-cut-layout.mjs' -import { fabricSettingsOrDefault } from '../layout/cut/index.mjs' -import { useFabricLength } from '../layout/cut/settings.mjs' +import { cutLayoutPlugin } from 'shared/plugins/plugin-cut-layout.mjs' +import { materialSettingsOrDefault } from 'shared/components/workbench/views/cut/hooks.mjs' +import { useMaterialLength } from 'shared/components/workbench/views/cut/hooks.mjs' import { capitalize, formatMm } from 'shared/utils.mjs' import { defaultPrintSettings, @@ -14,7 +14,7 @@ import { } from 'shared/components/workbench/views/print/config.mjs' import get from 'lodash.get' -export const ns = ['plugin', 'common'] +export const ns = ['cut', 'plugin', 'common'] export const exportTypes = { exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'], exportForEditing: ['svg', 'pdf'], @@ -48,39 +48,39 @@ const themedPattern = (Design, settings, overwrite, format, t) => { * @param {Object} settings the settings * @param {string} format the export format this pattern will be prepared for * @param {function} t the i18n function - * @return {Object} a dictionary of svgs and related translation strings, keyed by fabric + * @return {Object} a dictionary of svgs and related translation strings, keyed by material */ const generateCutLayouts = (pattern, Design, settings, format, t, ui) => { - // get the fabrics from the already drafted base pattern - const fabrics = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics( + // get the materials from the already drafted base pattern + const materials = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics( pattern.settings[0] - ) || ['fabric'] - if (!fabrics.length) return + ) || ['material'] + if (!materials.length) return const isImperial = settings.units === 'imperial' const cutLayouts = {} - // each fabric - fabrics.forEach((f) => { - // get the settings and layout for that fabric - const fabricSettings = fabricSettingsOrDefault(settings.units, ui, f) - const fabricLayout = get(ui, ['layouts', 'cut', f], true) + // each material + materials.forEach((f) => { + // get the settings and layout for that material + const materialSettings = materialSettingsOrDefault(settings.units, ui, f) + const materialLayout = get(ui, ['layouts', 'cut', f], true) // make a new pattern - const fabricPattern = themedPattern(Design, settings, { layout: fabricLayout }, format, t) - // add cut layout plugin and fabric plugin - .use(cutLayoutPlugin(f, fabricSettings.grainDirection)) - .use(fabricPlugin({ ...fabricSettings, printStyle: true, setPatternSize: 'width' })) + const materialPattern = themedPattern(Design, settings, { layout: materialLayout }, format, t) + // add cut layout plugin and material plugin + .use(cutLayoutPlugin(f, materialSettings.grainDirection)) + .use(materialPlugin({ ...materialSettings, printStyle: true, setPatternSize: 'width' })) // draft and render - fabricPattern.draft() - const svg = fabricPattern.render() + materialPattern.draft() + const svg = materialPattern.render() // include translations cutLayouts[f] = { svg, - title: t('plugin:' + f), - dimensions: t('plugin:fabricSize', { - width: formatMm(fabricSettings.sheetWidth, settings.units, 'notags'), - length: useFabricLength(isImperial, fabricPattern.height, 'notags'), + title: t('cut:' + f), + dimensions: t('cut:materialSize', { + width: formatMm(materialSettings.sheetWidth, settings.units, 'notags'), + length: useMaterialLength(isImperial, materialPattern.height, 'notags'), interpolation: { escapeValue: false }, }), } @@ -160,7 +160,6 @@ export const handleExport = async ({ } try { - throw new Error('bad bad bad') // add pages to pdf exports if (format !== 'svg') { pattern.use( @@ -177,7 +176,7 @@ export const handleExport = async ({ design: capitalize(design), tagline: t('common:sloganCome') + '. ' + t('common:sloganStay'), url: window.location.href, - cuttingLayout: t('plugin:cuttingLayout'), + cuttingLayout: t('cut:cuttingLayout'), } } diff --git a/sites/shared/components/workbench/exporting/pdf-maker.mjs b/sites/shared/components/workbench/exporting/pdf-maker.mjs index 08979c6b715..8598821d72c 100644 --- a/sites/shared/components/workbench/exporting/pdf-maker.mjs +++ b/sites/shared/components/workbench/exporting/pdf-maker.mjs @@ -170,8 +170,8 @@ export class PdfMaker { } /** generate the title for a cutting layout page */ - async generateCutLayoutTitle(fabricTitle, fabricDimensions) { - this.addText(this.strings.cuttingLayout, 12, 2).addText(fabricTitle, 28) + async generateCutLayoutTitle(materialTitle, materialDimensions) { + this.addText(this.strings.cuttingLayout, 12, 2).addText(materialTitle, 28) this.pdf.lineWidth(1) this.pdf @@ -180,16 +180,16 @@ export class PdfMaker { .stroke() this.lineLevel += 5 - this.addText(fabricDimensions, 16) + this.addText(materialDimensions, 16) } /** generate all cutting layout pages */ async generateCutLayoutPages() { if (!this.pageSettings.cutlist || !this.cutLayouts) return - for (const fabric in this.cutLayouts) { + for (const material in this.cutLayouts) { this.nextPage() - const { title, dimensions, svg } = this.cutLayouts[fabric] + const { title, dimensions, svg } = this.cutLayouts[material] await this.generateCutLayoutTitle(title, dimensions) await this.generateSvgPage(svg) } diff --git a/sites/shared/components/workbench/index.mjs b/sites/shared/components/workbench/index.mjs index bef27f45400..8d351684bb6 100644 --- a/sites/shared/components/workbench/index.mjs +++ b/sites/shared/components/workbench/index.mjs @@ -15,8 +15,9 @@ import { ModalSpinner } from 'shared/components/modal/spinner.mjs' import { DraftView, ns as draftNs } from 'shared/components/workbench/views/draft/index.mjs' import { SaveView, ns as saveNs } from 'shared/components/workbench/views/save/index.mjs' import { PrintView, ns as printNs } from 'shared/components/workbench/views/print/index.mjs' +import { CutView, ns as cutNs } from 'shared/components/workbench/views/cut/index.mjs' -export const ns = ['account', 'workbench', ...draftNs, ...saveNs, ...printNs] +export const ns = ['account', 'workbench', ...draftNs, ...saveNs, ...printNs, ...cutNs] const defaultUi = { renderer: 'react', @@ -25,6 +26,7 @@ const defaultUi = { const views = { draft: DraftView, print: PrintView, + cut: CutView, } const draftViews = ['draft', 'test'] diff --git a/sites/shared/components/workbench/layout/cut/index.mjs b/sites/shared/components/workbench/layout/cut/index.mjs deleted file mode 100644 index 26ad576c549..00000000000 --- a/sites/shared/components/workbench/layout/cut/index.mjs +++ /dev/null @@ -1,125 +0,0 @@ -import { useTranslation } from 'next-i18next' -import { CutLayoutSettings } from './settings.mjs' -// import { Draft } from '../draft/index.mjs' -import { fabricPlugin } from 'shared/plugins/plugin-layout-part.mjs' -import { cutLayoutPlugin } from './plugin-cut-layout.mjs' -import { pluginAnnotations } from '@freesewing/plugin-annotations' -import { measurementAsMm } from 'shared/utils.mjs' -import { useEffect } from 'react' -import get from 'lodash.get' - -export const fabricSettingsOrDefault = (units, ui, fabric) => { - const isImperial = units === 'imperial' - const sheetHeight = measurementAsMm(isImperial ? 36 : 100, units) - const uiSettings = get(ui, ['cut', 'fabric', fabric], {}) - const sheetWidth = uiSettings.sheetWidth || measurementAsMm(isImperial ? 54 : 120, units) - const grainDirection = uiSettings.grainDirection === undefined ? 90 : uiSettings.grainDirection - - return { activeFabric: fabric, sheetWidth, grainDirection, sheetHeight } -} - -const activeFabricPath = ['_state', 'layout', 'forCutting', 'activeFabric'] -const useFabricSettings = (gist) => { - const activeFabric = get(gist, activeFabricPath) || 'fabric' - return fabricSettingsOrDefault(gist, activeFabric) -} - -const useFabricDraft = (gist, design, fabricSettings) => { - // get the appropriate layout for the view - const layout = - get(gist, ['layouts', gist._state.view, fabricSettings.activeFabric]) || gist.layout || true - // hand it separately to the design - const draft = new design({ ...gist, layout }) - - const layoutSettings = { - sheetWidth: fabricSettings.sheetWidth, - sheetHeight: fabricSettings.sheetHeight, - } - - let patternProps - try { - // add the fabric plugin to the draft - draft.use(fabricPlugin(layoutSettings)) - // add the cutLayout plugin - draft.use(cutLayoutPlugin(fabricSettings.activeFabric, fabricSettings.grainDirection)) - // also, pluginAnnotations and pluginFlip are needed - draft.use(pluginAnnotations) - - // draft the pattern - draft.draft() - patternProps = draft.getRenderProps() - } catch (err) { - console.log(err, gist) - } - - return { draft, patternProps } -} - -const useFabricList = (draft) => { - return draft.setStores[0].cutlist.getCutFabrics(draft.settings[0]) -} - -const bgProps = { fill: 'none' } -export const CutLayout = (props) => { - const { t } = useTranslation(['workbench', 'plugin']) - const { gist, design, updateGist } = props - - // disable xray - useEffect(() => { - if (gist?._state?.xray?.enabled) updateGist(['_state', 'xray', 'enabled'], false) - }) - - const fabricSettings = useFabricSettings(gist) - const { draft, patternProps } = useFabricDraft(gist, design, fabricSettings) - const fabricList = useFabricList(draft) - - const setCutFabric = (newFabric) => { - updateGist(activeFabricPath, newFabric) - } - - let name = design.designConfig.data.name - name = name.replace('@freesewing/', '') - - const settingsProps = { - gist, - updateGist, - patternProps, - unsetGist: props.unsetGist, - ...fabricSettings, - } - - return patternProps ? ( -
-

{t('layoutThing', { thing: name }) + ': ' + t('forCutting')}

- -
- {fabricList.length > 1 ? ( -
- {fabricList.map((title) => ( - - ))} -
- ) : null} - {/* */} -
-
- ) : null -} diff --git a/sites/shared/components/workbench/layout/cut/settings.mjs b/sites/shared/components/workbench/layout/cut/settings.mjs deleted file mode 100644 index 02543bebcea..00000000000 --- a/sites/shared/components/workbench/layout/cut/settings.mjs +++ /dev/null @@ -1,139 +0,0 @@ -import { ClearIcon, IconWrapper } from 'shared/components/icons.mjs' -import { useTranslation } from 'next-i18next' -import { formatFraction128, measurementAsMm, round, formatMm } from 'shared/utils.mjs' -import { ShowButtonsToggle } from '../draft/buttons.mjs' - -const SheetIcon = (props) => ( - - - -) - -const GrainIcon = (props) => ( - - - - - - - -) -const FabricSizer = ({ gist, updateGist, activeFabric, sheetWidth }) => { - const { t } = useTranslation(['workbench']) - - let val = formatMm(sheetWidth, gist.units, 'none') - // onChange - const update = (evt) => { - evt.stopPropagation() - let evtVal = evt.target.value - // set Val immediately so that the input reflects it - val = evtVal - - let useVal = measurementAsMm(evtVal, gist.units) - // only set to the gist if it's valid - if (!isNaN(useVal)) { - updateGist(['_state', 'layout', 'forCutting', 'fabric', activeFabric, 'sheetWidth'], useVal) - } - } - - return ( - - ) -} -export const GrainDirectionPicker = ({ grainDirection, activeFabric, updateGist }) => { - const { t } = useTranslation(['workbench']) - - return ( - - ) -} - -export const useFabricLength = (isImperial, height, format = 'none') => { - // regular conversion from mm to inches or cm - const unit = isImperial ? 25.4 : 10 - // conversion from inches or cm to yards or meters - const fabricUnit = isImperial ? 36 : 100 - // for fabric, these divisions are granular enough - const rounder = isImperial ? 16 : 10 - - // we convert the used fabric height to the right units so we can round it - const inFabricUnits = height / (fabricUnit * unit) - // we multiply it by the rounder, round it up, then divide by the rounder again to get the rounded amount - const roundCount = Math.ceil(rounder * inFabricUnits) / rounder - // format as a fraction for imperial, a decimal for metric - const count = isImperial ? formatFraction128(roundCount, format) : round(roundCount, 1) - - return `${count}${isImperial ? 'yds' : 'm'}` -} - -export const CutLayoutSettings = ({ - gist, - patternProps, - unsetGist, - updateGist, - activeFabric, - sheetWidth, - grainDirection, -}) => { - const { t } = useTranslation(['workbench']) - - const fabricLength = useFabricLength(gist.units === 'imperial', patternProps.height) - - return ( -
-
- - -
-
- - {fabricLength} -
-
- - -
-
- ) -} diff --git a/sites/shared/components/workbench/layout/print/orientation-picker.mjs b/sites/shared/components/workbench/layout/print/orientation-picker.mjs deleted file mode 100644 index ba20d81a6e2..00000000000 --- a/sites/shared/components/workbench/layout/print/orientation-picker.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import { PageIcon } from 'shared/components/icons.mjs' -import { useTranslation } from 'next-i18next' - -export const PageOrientationPicker = ({ gist, updateGist }) => { - const { t } = useTranslation(['workbench']) - - return ( - - ) -} diff --git a/sites/shared/components/workbench/layout/print/pagesize-picker.mjs b/sites/shared/components/workbench/layout/print/pagesize-picker.mjs deleted file mode 100644 index 5f586e59ed9..00000000000 --- a/sites/shared/components/workbench/layout/print/pagesize-picker.mjs +++ /dev/null @@ -1,58 +0,0 @@ -import { PageSizeIcon } from 'shared/components/icons.mjs' -import { useTranslation } from 'next-i18next' -import { Popout } from 'shared/components/popout.mjs' - -const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'] - -export const PageSizePicker = ({ gist, updateGist }) => { - const { t } = useTranslation(['workbench']) - const setSize = (size) => { - updateGist(['_state', 'layout', 'forPrinting', 'page', 'size'], size) - if (!gist._state?.layout?.forPrinting?.page?.orientation) { - updateGist(['_state', 'layout', 'forPrinting', 'page', 'orientation'], 'portrait') - } - } - - if ( - !gist._state?.layout?.forPrinting?.page?.size || - sizes.indexOf(gist._state.layout.forPrinting.page.size) === -1 - ) - return ( - -

{t('startBySelectingAThing', { thing: t('pageSize') })}

-
- {sizes.map((size) => ( - - ))} -
-
- ) - - return ( -
-
- - {t(`pageSize`)}: - {gist._state.layout.forPrinting.page.size} -
- -
- ) -} diff --git a/sites/shared/components/workbench/layout/print/settings.mjs b/sites/shared/components/workbench/layout/print/settings.mjs deleted file mode 100644 index beb60d37097..00000000000 --- a/sites/shared/components/workbench/layout/print/settings.mjs +++ /dev/null @@ -1,129 +0,0 @@ -import { PageSizePicker } from './pagesize-picker.mjs' -import { PageOrientationPicker } from './orientation-picker.mjs' -import { PrintIcon, RightIcon, ClearIcon, ExportIcon } from 'shared/components/icons.mjs' -import { useTranslation } from 'next-i18next' -import { ShowButtonsToggle } from '../draft/buttons.mjs' - -export const PrintLayoutSettings = (props) => { - const { t } = useTranslation(['workbench']) - let pages = props.draft?.setStores[0].get('pages') - if (!pages) return null - const { cols, rows, count } = pages - - const setMargin = (evt) => { - props.updateGist( - ['_state', 'layout', 'forPrinting', 'page', 'margin'], - parseInt(evt.target.value) - ) - } - - const setCoverPage = () => { - props.updateGist( - ['_state', 'layout', 'forPrinting', 'page', 'coverPage'], - !props.layoutSettings.coverPage - ) - } - - const setCutlist = () => { - props.updateGist( - ['_state', 'layout', 'forPrinting', 'page', 'cutlist'], - !props.layoutSettings.cutlist - ) - } - return ( -
-
-
- - - -
-
- - -
-
-
-
- - - -
-
- - {count} - | - - {cols} - | -
- -
- {rows} -
-
-
- ) -} diff --git a/sites/shared/components/workbench/menus/core-settings/config.mjs b/sites/shared/components/workbench/menus/core-settings/config.mjs index b605586c821..4e5387ab160 100644 --- a/sites/shared/components/workbench/menus/core-settings/config.mjs +++ b/sites/shared/components/workbench/menus/core-settings/config.mjs @@ -71,7 +71,7 @@ export const loadSettingsConfig = ({ units: { control: 1, // Show when control > 2 list: ['metric', 'imperial'], - dflt: units, + dflt: 'metric', choiceTitles: { metric: 'metric', imperial: 'imperial', diff --git a/sites/shared/components/workbench/menus/shared/index.mjs b/sites/shared/components/workbench/menus/shared/index.mjs index 74ec217ec4e..22b5cda31c3 100644 --- a/sites/shared/components/workbench/menus/shared/index.mjs +++ b/sites/shared/components/workbench/menus/shared/index.mjs @@ -1,7 +1,6 @@ import { useContext } from 'react' import { MenuItemGroup } from './menu-item.mjs' import { useTranslation } from 'next-i18next' -import { HelpIcon } from 'shared/components/icons.mjs' import { ModalWrapper } from 'shared/components/wrappers/modal.mjs' import { ModalContext } from 'shared/context/modal-context.mjs' diff --git a/sites/shared/components/workbench/menus/shared/inputs.mjs b/sites/shared/components/workbench/menus/shared/inputs.mjs index e80fd4edcc4..f32d174cbfa 100644 --- a/sites/shared/components/workbench/menus/shared/inputs.mjs +++ b/sites/shared/components/workbench/menus/shared/inputs.mjs @@ -85,9 +85,7 @@ export const ListToggle = ({ config, changed, updateFunc, name }) => { return ( @@ -276,4 +274,25 @@ export const MmInput = (props) => { } /** A placeholder for an input to handle constant values */ -export const ConstantInput = () =>

FIXME: Constant options are not implemented (yet)

+export const ConstantInput = ({ + type = 'number', + name, + current, + updateFunc, + t, + changed, + config, +}) => ( + <> +

{t(`${name}.d`)}

+ updateFunc([name], evt.target.value)} + /> + +) diff --git a/sites/shared/components/workbench/menus/shared/menu-item.mjs b/sites/shared/components/workbench/menus/shared/menu-item.mjs index d4fa9afe791..d2fbf7b152e 100644 --- a/sites/shared/components/workbench/menus/shared/menu-item.mjs +++ b/sites/shared/components/workbench/menus/shared/menu-item.mjs @@ -119,7 +119,7 @@ export const MenuItem = ({ className={open ? openButtonClass : 'btn btn-accent'} onClick={(evt) => { evt.stopPropagation() - updateFunc(name) + updateFunc([name]) }} > diff --git a/sites/shared/components/workbench/pattern/movable/index.mjs b/sites/shared/components/workbench/pattern/movable/index.mjs index da9f2db4b06..1a7024ad362 100644 --- a/sites/shared/components/workbench/pattern/movable/index.mjs +++ b/sites/shared/components/workbench/pattern/movable/index.mjs @@ -1,20 +1,11 @@ import { useRef } from 'react' -import { Stack } from './stack.mjs' -// import { SvgWrapper } from '../../pattern/svg.mjs' -// import { PartInner } from '../../pattern/part.mjs' import { PanZoomPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs' import { MovableStack } from './stack.mjs' -import get from 'lodash.get' export const MovablePattern = ({ - design, - pattern, renderProps, - patternConfig, - settings, showButtons = true, update, - bgProps = {}, fitImmovable = false, immovable = [], layoutPath, @@ -68,10 +59,6 @@ export const MovablePattern = ({ } } - const viewBox = layout.topLeft - ? `${layout.topLeft.x} ${layout.topLeft.y} ${layout.width} ${layout.height}` - : false - const sortedStacks = {} Object.keys(renderProps.stacks) .sort((a, b) => { @@ -90,13 +77,13 @@ export const MovablePattern = ({ {...{ stackName, stack, - settings, components, t, movable: !immovable.includes(stackName), layout: layout.stacks[stackName], updateLayout, showButtons, + settings, }} /> ) diff --git a/sites/shared/components/workbench/pattern/movable/stack.mjs b/sites/shared/components/workbench/pattern/movable/stack.mjs index 3da5d3f856f..17f5618adb5 100644 --- a/sites/shared/components/workbench/pattern/movable/stack.mjs +++ b/sites/shared/components/workbench/pattern/movable/stack.mjs @@ -43,25 +43,23 @@ * I've sort of left it at this because I'm starting to wonder if we should perhaps re-think * how custom layouts are supported in the core. And I would like to discuss this with the core team. */ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useCallback } from 'react' import { generateStackTransform, getTransformedBounds } from '@freesewing/core' -import { Stack } from 'pkgs/react-components/src/pattern/stack.mjs' import { getProps, angle } from 'pkgs/react-components/src/pattern/utils.mjs' import { drag } from 'd3-drag' import { select } from 'd3-selection' import { Buttons } from './transform-buttons.mjs' -import get from 'lodash.get' export const MovableStack = ({ stackName, stack, - settings, components, t, movable = true, layout, updateLayout, showButtons, + settings, }) => { const stackExists = !movable || typeof layout?.move?.x !== 'undefined' @@ -72,100 +70,80 @@ export const MovableStack = ({ // State variable to switch between moving or rotating the part const [rotate, setRotate] = useState(false) - // update the layout on mount - useEffect(() => { - // only update if there's a rendered part and it's not the pages or fabric part - if (stackRef.current && movable) { - updateStacklayout(false) - } - }, [stackRef, layout]) - - // Initialize drag handler - useEffect(() => { - // don't drag the pages - if (!movable || !stackExists) return - handleDrag(select(stackRef.current)) - }, [rotate, stackRef, layout]) - - // // Don't just assume this makes sense - if (!stackExists) return null - - if (!movable) return - - // These are kept as vars because re-rendering on drag would kill performance + // This is kept as state to avoid re-rendering on drag, which would kill performance + // It's a bit of an anti-pattern, but we'll directly manipulate the properties instead of updating the state // Managing the difference between re-render and direct DOM updates makes this // whole thing a bit tricky to wrap your head around - let translateX = layout.move.x - let translateY = layout.move.y - let stackRotation = layout.rotate || 0 - let rotation = stackRotation - let flipX = !!layout.flipX - let flipY = !!layout.flipY + const stackRotation = layout?.rotate || 0 + const [liveTransforms] = useState({ + translateX: layout?.move.x, + translateY: layout?.move.y, + rotation: stackRotation, + flipX: !!layout?.flipX, + flipY: !!layout?.flipY, + }) - const center = { + const center = stack.topLeft && { x: stack.topLeft.x + (stack.bottomRight.x - stack.topLeft.x) / 2, y: stack.topLeft.y + (stack.bottomRight.y - stack.topLeft.y) / 2, } - /** get the delta rotation from the start of the drag event to now */ - const getRotation = (event) => - angle(center, event.subject) - angle(center, { x: event.x, y: event.y }) - - const setTransforms = () => { + const setTransforms = useCallback(() => { // get the transform attributes + const { translateX, translateY, rotation, flipX, flipY } = liveTransforms const transforms = generateStackTransform(translateX, translateY, rotation, flipX, flipY, stack) const me = select(stackRef.current) me.attr('transform', transforms.join(' ')) return transforms - } + }, [liveTransforms, stackRef, stack]) - let didDrag = false - const handleDrag = drag() - // subject allows us to save data from the start of the event to use throughout event handing - .subject(function (event) { - return rotate - ? // if we're rotating, the subject is the mouse position - { x: event.x, y: event.y } - : // if we're moving, the subject is the part's x,y coordinates - { x: translateX, y: translateY } - }) - .on('drag', function (event) { - if (!event.dx && !event.dy) return + /** update the layout either locally or in the gist */ + const updateStacklayout = useCallback( + (history = true) => { + /** don't mess with what we don't lay out */ + if (!stackRef.current || !movable) return - if (rotate) { - let newRotation = getRotation(event) - // shift key to snap the rotation - if (event.sourceEvent.shiftKey) { - newRotation = Math.ceil(newRotation / 15) * 15 - } - // reverse the rotation direction one time per flip. if we're flipped both directions, rotation will be positive again - if (flipX) newRotation *= -1 - if (flipY) newRotation *= -1 + // set the transforms on the stack in order to calculate from the latest position + const transforms = setTransforms() - rotation = stackRotation + newRotation - } else { - translateX = event.x - translateY = event.y - } + // apply the transforms to the bounding box to get the new extents of the stack + const { tl, br } = getTransformedBounds(stack, transforms) - // a drag happened, so we should update the layout when we're done - didDrag = true - setTransforms() - }) - .on('end', function () { - // save to gist if anything actually changed - if (didDrag) updateStacklayout() + // update it on the draft component + updateLayout( + stackName, + { + move: { + x: liveTransforms.translateX, + y: liveTransforms.translateY, + }, + rotate: liveTransforms.rotation % 360, + flipX: liveTransforms.flipX, + flipY: liveTransforms.flipY, + tl, + br, + }, + history + ) + }, + [stackRef, setTransforms, updateLayout, liveTransforms, movable, stack, stackName] + ) - didDrag = false - }) + // update the layout on mount + useEffect(() => { + // only update if there's a rendered part and it's not an imovable part + if (stackRef.current && movable) { + updateStacklayout(false) + } + }, [stackRef, movable, updateStacklayout]) /** reset the part's transforms */ const resetPart = () => { - rotation = 0 - flipX = 0 - flipY = 0 + liveTransforms.rotation = 0 + liveTransforms.flipX = 0 + liveTransforms.flipY = 0 updateStacklayout() } @@ -179,58 +157,85 @@ export const MovableStack = ({ setRotate(!rotate) } - /** update the layout either locally or in the gist */ - const updateStacklayout = (history = true) => { - /** don't mess with what we don't lay out */ - if (!stackRef.current || !movable) return - - // set the transforms on the stack in order to calculate from the latest position - const transforms = setTransforms() - - // apply the transforms to the bounding box to get the new extents of the stack - const { tl, br } = getTransformedBounds(stack, transforms) - - // update it on the draft component - updateLayout( - stackName, - { - move: { - x: translateX, - y: translateY, - }, - rotate: rotation % 360, - flipX, - flipY, - tl, - br, - }, - history - ) - } - /** Method to flip (mirror) the part along the X or Y axis */ const flip = (axis) => { - if (axis === 'x') flipX = !flipX - else flipY = !flipY + if (axis === 'x') liveTransforms.flipX = !liveTransforms.flipX + else liveTransforms.flipY = !liveTransforms.flipY updateStacklayout() } /** method to rotate 90 degrees */ const rotate90 = (direction = 1) => { - if (flipX) direction *= -1 - if (flipY) direction *= -1 + if (liveTransforms.flipX) direction *= -1 + if (liveTransforms.flipY) direction *= -1 - rotation += 90 * direction + liveTransforms.rotation += 90 * direction updateStacklayout() } + /** get the delta rotation from the start of the drag event to now */ + const getRotation = (event) => + angle(center, event.subject) - angle(center, { x: event.x, y: event.y }) + + let didDrag = false + const handleDrag = + movable && + drag() + // subject allows us to save data from the start of the event to use throughout event handing + .subject(function (event) { + return rotate + ? // if we're rotating, the subject is the mouse position + { x: event.x, y: event.y } + : // if we're moving, the subject is the part's x,y coordinates + { x: liveTransforms.translateX, y: liveTransforms.translateY } + }) + .on('drag', function (event) { + if (!event.dx && !event.dy) return + + if (rotate) { + let newRotation = getRotation(event) + // shift key to snap the rotation + if (event.sourceEvent.shiftKey) { + newRotation = Math.ceil(newRotation / 15) * 15 + } + // reverse the rotation direction one time per flip. if we're flipped both directions, rotation will be positive again + if (liveTransforms.flipX) newRotation *= -1 + if (liveTransforms.flipY) newRotation *= -1 + + liveTransforms.rotation = stackRotation + newRotation + } else { + liveTransforms.translateX = event.x + liveTransforms.translateY = event.y + } + + // a drag happened, so we should update the layout when we're done + didDrag = true + setTransforms() + }) + .on('end', function () { + // save to gist if anything actually changed + if (didDrag) updateStacklayout() + + didDrag = false + }) + + // Initialize drag handler + useEffect(() => { + // don't drag the pages + if (!movable || !stackExists) return + handleDrag(select(stackRef.current)) + }, [stackRef, movable, stackExists, handleDrag]) + + // // Don't just assume this makes sense + if (!stackExists) return null + const { Group, Part } = components return ( {[...stack.parts].map((part, key) => ( - + ))} {movable && ( @@ -246,9 +251,11 @@ export const MovableStack = ({ /> {showButtons ? ( ( { ) } -export const ShowButtonsToggle = ({ gist, layoutSetType, updateGist }) => { +export const ShowButtonsToggle = ({ ui, update }) => { const { t } = useTranslation('workbench') - const path = ['_state', 'layout', layoutSetType, 'showButtons'] - const showButtons = get(gist, path, true) - const setShowButtons = () => updateGist(path, !showButtons) - + const hideButtons = (evt) => { + update.ui('hideMovableButtons', !evt.target.checked) + } return ( -