1
0
Fork 0

wip: Work on layout view in editor

This commit is contained in:
joostdecock 2025-02-09 18:20:35 +01:00
parent 752dd69c50
commit b58535b972
13 changed files with 638 additions and 172 deletions

View file

@ -1,7 +1,7 @@
// Dependencies // Dependencies
import { missingMeasurements, flattenFlags } from '../lib/index.mjs' import { missingMeasurements, flattenFlags } from '../lib/index.mjs'
// Hooks // Hooks
import React, { useState } from 'react' import React, { useState, useEffect } from 'react'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
import { useDesignTranslation } from '@freesewing/react/hooks/useDesignTranslation' import { useDesignTranslation } from '@freesewing/react/hooks/useDesignTranslation'
// Components // Components
@ -21,7 +21,9 @@ import {
MenuIcon, MenuIcon,
OptionsIcon, OptionsIcon,
PaperlessIcon, PaperlessIcon,
PrintIcon,
ResetAllIcon, ResetAllIcon,
ResetIcon,
RightIcon, RightIcon,
RocketIcon, RocketIcon,
RotateIcon, RotateIcon,
@ -38,6 +40,7 @@ import { ButtonFrame } from '@freesewing/react/components/Input'
import { DesignOptionsMenu } from './menus/DesignOptionsMenu.mjs' import { DesignOptionsMenu } from './menus/DesignOptionsMenu.mjs'
import { CoreSettingsMenu } from './menus/CoreSettingsMenu.mjs' import { CoreSettingsMenu } from './menus/CoreSettingsMenu.mjs'
import { UiPreferencesMenu } from './menus/UiPreferencesMenu.mjs' import { UiPreferencesMenu } from './menus/UiPreferencesMenu.mjs'
import { LayoutSettingsMenu } from './menus/LayoutMenu.mjs'
import { FlagsAccordionEntries } from './Flag.mjs' import { FlagsAccordionEntries } from './Flag.mjs'
import { UndoStep } from './views/UndosView.mjs' import { UndoStep } from './views/UndosView.mjs'
@ -50,6 +53,7 @@ const headerMenuIcons = {
right: RightIcon, right: RightIcon,
settings: SettingsIcon, settings: SettingsIcon,
ui: UiIcon, ui: UiIcon,
layout: PrintIcon,
} }
export const HeaderMenuIcon = (props) => { export const HeaderMenuIcon = (props) => {
@ -508,8 +512,106 @@ export const HeaderMenuViewMenu = (props) => {
) )
} }
export const HeaderMenuLayoutView = (props) => (
<>
<HeaderMenuDropdown
{...props}
id="layoutOptions"
tooltip="These options are specific to this design. You can use them to customize your pattern in a variety of ways."
toggle={
<>
<HeaderMenuIcon name="layout" extraClasses="tw-text-secondary" />
<span className="tw-hidden lg:tw-inline">Print Settings</span>
</>
}
>
<LayoutSettingsMenu {...props} />
</HeaderMenuDropdown>
<HeaderMenuLayoutViewIcons {...props} />
</>
)
export const HeaderMenuLayoutViewIcons = (props) => {
const { pattern, update, state } = props
const [tweaks, setTweaks] = useState(0)
useEffect(() => {
/*
* When the layout is reset, the UI won't update to changes
* unless we apply them on the first change
*/
if (
tweaks === 0 &&
typeof props.state.ui?.layout === 'object' &&
typeof props.state.settings?.layout !== 'object'
)
applyLayout()
setTweaks(tweaks + 1)
}, [props.state.ui.layout])
const applyLayout = () => {
setTweaks(-1)
update.settings('layout', state.ui.layout)
}
const resetLayout = () => {
setTweaks(-1)
update.ui('layout', true)
update.settings('layout', true)
}
const pages = pattern.setStores[0].get('pages', {})
const format = state.ui.print?.pages?.size
? state.ui.print.pages.size
: state.settings?.units === 'imperial'
? 'letter'
: 'a4'
const { cols, rows, count } = pages
const blank = cols * rows - count
return (
<>
<Tooltip tip="Number of pages required for the current layout">
<span className="tw-px-1 tw-font-bold tw-text-sm tw-block tw-h-8 tw-py-1 tw-opacity-80">
<span className="">
{count} pages
<span className="tw-pl-1 tw-text-xs tw-font-medium">
({cols}x{rows}, {blank} blank)
</span>
</span>
</span>
</Tooltip>
<Tooltip tip="Apply this layout to the pattern">
<button
className="tw-daisy-btn tw-daisy-btn-ghost tw-daisy-btn-sm tw-px-1 disabled:tw-bg-transparent tw-text-secondary"
onClick={applyLayout}
disabled={tweaks === 0}
>
Apply Layout
</button>
</Tooltip>
<Tooltip tip="Generate a PDF that you can print">
<button
className="tw-daisy-btn tw-daisy-btn-ghost tw-daisy-btn-sm tw-px-1 disabled:tw-bg-transparent tw-text-secondary"
onClick={() => update.view('export')}
>
<PrintIcon />
</button>
</Tooltip>
<Tooltip tip="Reset the custom layout">
<button
className="tw-daisy-btn tw-daisy-btn-ghost tw-daisy-btn-sm tw-px-1 disabled:tw-bg-transparent tw-text-secondary"
onClick={resetLayout}
>
<ResetIcon />
</button>
</Tooltip>
</>
)
}
const headerMenus = { const headerMenus = {
draft: HeaderMenuDraftView, draft: HeaderMenuDraftView,
layout: HeaderMenuLayoutView,
//HeaderMenuDraftViewDesignOptions, //HeaderMenuDraftViewDesignOptions,
//HeaderMenuDraftViewCoreSettings, //HeaderMenuDraftViewCoreSettings,
//HeaderMenuDraftViewUiPreferences, //HeaderMenuDraftViewUiPreferences,

View file

@ -1,19 +1,26 @@
import React, { useRef } from 'react' import React, { useRef, useState, useEffect, useCallback } from 'react'
import { PanZoomPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs' import { ZoomablePattern } from './ZoomablePattern.mjs'
import { MovableStack } from './stack.mjs' import { generateStackTransform, getTransformedBounds } from '@freesewing/core'
import { getProps } from '@freesewing/react/components/Pattern'
import { FlipIcon, RotateIcon, ResetIcon } from '@freesewing/react/components/Icon'
import { drag } from 'd3-drag'
import { select } from 'd3-selection'
//import { Buttons } from './transform-buttons.mjs'
export const MovablePattern = ({ export const MovablePattern = ({
renderProps, renderProps,
showButtons = true, state,
update, update,
fitImmovable = false, fitImmovable = false,
immovable = [], immovable = [],
layoutPath, t,
}) => { }) => {
const svgRef = useRef(null) const svgRef = useRef(null)
if (!renderProps) return null if (!renderProps) return null
// keep a fresh copy of the layout because we might manipulate it without saving to the gist /* keep a fresh copy of the layout because we might manipulate it without
* update the state
*/
let layout = let layout =
renderProps.settings[0].layout === true renderProps.settings[0].layout === true
? { ? {
@ -52,9 +59,15 @@ export const MovablePattern = ({
newLayout.topLeft = topLeft newLayout.topLeft = topLeft
if (history) { if (history) {
update.ui(layoutPath, newLayout) update.ui('layout', newLayout)
} else { } else {
// we don't put it in the gist if it shouldn't contribute to history because we need some of the data calculated here for rendering purposes on the initial layout, but we don't want to actually save a layout until the user manipulates it. This is what allows the layout to respond appropriately to settings changes. Once the user has starting playing with the layout, all bets are off /* we don't put it in the gist if it shouldn't contribute to history
* because we need some of the data calculated here for rendering
* purposes on the initial layout, but we don't want to actually save a
* layout until the user manipulates it. This is what allows the layout
* to respond appropriately to settings changes. Once the user has
* starting playing with the layout, all bets are off
*/
layout = newLayout layout = newLayout
} }
} }
@ -82,20 +95,22 @@ export const MovablePattern = ({
movable: !immovable.includes(stackName), movable: !immovable.includes(stackName),
layout: layout.stacks[stackName], layout: layout.stacks[stackName],
updateLayout, updateLayout,
showButtons,
settings, settings,
state,
}} }}
/> />
) )
return ( return (
<PanZoomPattern <div className="" style={{ height: 'calc(100vh - 12rem)' }}>
<ZoomablePattern
{...{ {...{
renderProps: sortedRenderProps, renderProps: sortedRenderProps,
components: { Stack }, components: { Stack },
}} }}
ref={svgRef} ref={svgRef}
/> />
</div>
) )
} }
@ -141,13 +156,6 @@ export const MovablePattern = ({
* more data and factors in the transforms. We then use our `domToSvg` * more data and factors in the transforms. We then use our `domToSvg`
* function to move the points back into the SVG space. * function to move the points back into the SVG space.
*/ */
//import { useRef, useState, useEffect, useCallback } from 'react'
//import { generateStackTransform, getTransformedBounds } from '@freesewing/core'
//import { getProps } from 'pkgs/react-components/src/pattern/utils.mjs'
//import { angle } from '../utils.mjs'
//import { drag } from 'd3-drag'
//import { select } from 'd3-selection'
//import { Buttons } from './transform-buttons.mjs'
export const MovableStack = ({ export const MovableStack = ({
stackName, stackName,
@ -157,8 +165,8 @@ export const MovableStack = ({
movable = true, movable = true,
layout, layout,
updateLayout, updateLayout,
showButtons,
settings, settings,
state,
}) => { }) => {
const stackExists = !movable || typeof layout?.move?.x !== 'undefined' const stackExists = !movable || typeof layout?.move?.x !== 'undefined'
@ -198,7 +206,7 @@ export const MovableStack = ({
return transforms return transforms
}, [liveTransforms, stackRef, stack]) }, [liveTransforms, stackRef, stack])
/** update the layout either locally or in the gist */ /** update the layout either locally or in the state */
const updateStacklayout = useCallback( const updateStacklayout = useCallback(
(history = true) => { (history = true) => {
/** don't mess with what we don't lay out */ /** don't mess with what we don't lay out */
@ -327,9 +335,12 @@ export const MovableStack = ({
}, [stackRef, movable, stackExists, handleDrag]) }, [stackRef, movable, stackExists, handleDrag])
// // Don't just assume this makes sense // // Don't just assume this makes sense
if (!stackExists) return null if (!stackExists) {
return null
}
const { Group, Part } = components const { Group, Part } = components
return ( return (
<Group id={`stack-${stackName}`} {...getProps(stack)} ref={stackRef}> <Group id={`stack-${stackName}`} {...getProps(stack)} ref={stackRef}>
<Group id={`stack-inner-${stackName}`} ref={innerRef}> <Group id={`stack-inner-${stackName}`} ref={innerRef}>
@ -348,7 +359,6 @@ export const MovableStack = ({
id={`${stackName}-layout-rect`} id={`${stackName}-layout-rect`}
onClick={toggleDragRotate} onClick={toggleDragRotate}
/> />
{showButtons ? (
<Buttons <Buttons
transform={`translate(${center.x}, ${ transform={`translate(${center.x}, ${
center.y center.y
@ -361,10 +371,108 @@ export const MovableStack = ({
resetPart={resetPart} resetPart={resetPart}
rotate90={rotate90} rotate90={rotate90}
partName={stackName} partName={stackName}
iconSize={state.ui?.layout?.iconSize}
/> />
) : null}
</> </>
)} )}
</Group> </Group>
) )
} }
function dx(pointA, pointB) {
return pointB.x - pointA.x
}
function dy(pointA, pointB) {
return pointB.y - pointA.y
}
function rad2deg(radians) {
return radians * 57.29577951308232
}
function angle(pointA, pointB) {
let rad = Math.atan2(-1 * dy(pointA, pointB), dx(pointA, pointB))
while (rad < 0) rad += 2 * Math.PI
return rad2deg(rad)
}
const rectSize = 24
const Button = ({ onClickCb, transform, Icon, children, title = '' }) => {
const _onClick = (event) => {
event.stopPropagation()
onClickCb(event)
}
return (
<g transform={transform} className="svg-layout-button group">
<title>{title}</title>
<rect width={rectSize} height={rectSize} className="button" rx="2" ry="2" />
<Icon className="group-hover:tw-text-primary-content" />
<rect width={rectSize} height={rectSize} onClick={_onClick} className="tw-fill-transparent" />
</g>
)
}
export const ShowButtonsToggle = ({ ui, update }) => {
const hideButtons = (evt) => {
update.ui('hideMovableButtons', !evt.target.checked)
}
return (
<label className="label cursor-pointer">
<span className="label-text text-lg mr-2">{t('showMovableButtons')}</span>
<input
type="checkbox"
className="toggle toggle-primary"
checked={!ui.hideMovableButtons}
onChange={hideButtons}
/>
</label>
)
}
/** buttons for manipulating the part */
export const Buttons = ({ transform, flip, rotate, resetPart, rotate90, iconSize }) => {
return (
<g transform={transform}>
<g style={{ transform: `scale(${iconSize || 0.5}` }}>
{rotate ? (
<circle cx="0" cy="0" r="50" className="stroke-2xl muted" />
) : (
<path d="M -50, 0 l 100,0 M 0,-50 l 0,100" className="stroke-2xl muted" />
)}
<Button
onClickCb={resetPart}
transform={`translate(${rectSize / -2}, ${rectSize / -2})`}
Icon={() => <ResetIcon wrapped={0} />}
title="Reset part orientation"
/>
<Button
onClickCb={() => rotate90(-1)}
transform={`translate(${rectSize * -2.7}, ${rectSize / -2})`}
Icon={() => <RotateIcon wrapped={0} style={{}} />}
title="Rotate part clockwise"
/>
<Button
onClickCb={() => flip('y')}
transform={`translate(${rectSize * 0.6}, ${rectSize / -2})`}
Icon={() => (
<FlipIcon style={{ transform: 'rotate(90deg) translate(0, -24px)' }} wrapped={0} />
)}
title="Flip part top/bottom"
/>
<Button
onClickCb={() => flip('x')}
transform={`translate(${rectSize * -1.6}, ${rectSize / -2})`}
Icon={() => <FlipIcon style={{}} wrapped={0} />}
title="Flip part left/right"
/>
<Button
onClickCb={() => rotate90()}
transform={`translate(${rectSize * 1.7}, ${rectSize / -2})`}
Icon={() => <RotateIcon transform="scale(-1,1), translate(-24,0)" wrapped={0} />}
title="Rotate part counter-clockwise"
/>
</g>
</g>
)
}

View file

@ -6,7 +6,7 @@ import { Pattern } from '@freesewing/react/components/Pattern'
* A pattern you can pan and zoom * A pattern you can pan and zoom
*/ */
export const ZoomablePattern = forwardRef(function ZoomablePatternRef(props, ref) { export const ZoomablePattern = forwardRef(function ZoomablePatternRef(props, ref) {
const { renderProps, rotate } = props const { renderProps, rotate, components = {} } = props
const { onTransformed, setZoomFunctions } = useContext(ZoomContext) const { onTransformed, setZoomFunctions } = useContext(ZoomContext)
return ( return (
@ -26,7 +26,7 @@ export const ZoomablePattern = forwardRef(function ZoomablePatternRef(props, ref
> >
{props.children || ( {props.children || (
<Pattern <Pattern
{...{ renderProps }} {...{ renderProps, components }}
ref={ref} ref={ref}
className={`freesewing pattern tw-w-full ${rotate ? 'tw--rotate-90' : ''}`} className={`freesewing pattern tw-w-full ${rotate ? 'tw--rotate-90' : ''}`}
/> />

View file

@ -192,6 +192,7 @@ export const MenuMmInput = (props) => {
const imperial = units === 'imperial' const imperial = units === 'imperial'
const mmUpdateHandler = useCallback( const mmUpdateHandler = useCallback(
(path, newCurrent) => { (path, newCurrent) => {
console.log('mmUpdateHandler', { path, newCurrent })
const calcCurrent = const calcCurrent =
typeof newCurrent === 'undefined' ? undefined : measurementAsMm(newCurrent, units) typeof newCurrent === 'undefined' ? undefined : measurementAsMm(newCurrent, units)
updateHandler(path, calcCurrent) updateHandler(path, calcCurrent)
@ -199,7 +200,9 @@ export const MenuMmInput = (props) => {
[updateHandler, units] [updateHandler, units]
) )
// add a default step that's appropriate to the unit. can be overwritten by config /*
* Set a default step that matches the unit
*/
const defaultStep = units === 'imperial' ? 0.125 : 0.1 const defaultStep = units === 'imperial' ? 0.125 : 0.1
return ( return (
@ -209,8 +212,8 @@ export const MenuMmInput = (props) => {
config: { config: {
step: defaultStep, step: defaultStep,
...config, ...config,
min: imperial ? config.min / 25.4 : config.min, min: imperial ? config.min / 25.4 : config.min / 10,
max: imperial ? config.max / 25.4 : config.min, max: imperial ? config.max / 25.4 : config.max / 10,
dflt: measurementAsUnits(config.dflt, units), dflt: measurementAsUnits(config.dflt, units),
}, },
current: current === undefined ? undefined : measurementAsUnits(current, units), current: current === undefined ? undefined : measurementAsUnits(current, units),
@ -391,7 +394,7 @@ export const MenuEditOption = (props) => {
<div className="tw-daisy-form-control tw-mb-2 tw-w-full"> <div className="tw-daisy-form-control tw-mb-2 tw-w-full">
<div className="tw-daisy-label tw-font-medium tw-text-accent"> <div className="tw-daisy-label tw-font-medium tw-text-accent">
<label className="tw-daisy-label-text"> <label className="tw-daisy-label-text">
<em>Enter a custom value</em> {units} <em>Enter a custom value</em>
</label> </label>
{type === 'pct' && typeof config.fromAbs === 'function' ? ( {type === 'pct' && typeof config.fromAbs === 'function' ? (
<label className="tw-daisy-label-text"> <label className="tw-daisy-label-text">

View file

@ -0,0 +1,134 @@
// Dependencies
import React from 'react'
import { defaultPrintSettings, handleExport } from '../../lib/export/index.mjs'
import { tilerPlugin } from '../../lib/export/plugin-tiler.mjs'
import { capitalize, get } from '@freesewing/utils'
import { draft, menuLayoutSettingsStructure } from '../../lib/index.mjs'
// Components
import { ZoomablePattern } from '../ZoomablePattern.mjs'
import { PatternLayout } from '../PatternLayout.mjs'
import { MovablePattern } from '../MovablePattern.mjs'
import { Accordion } from '../Accordion.mjs'
import {
CompareIcon,
PageSizeIcon,
PatternIcon,
PrintIcon,
} from '@freesewing/react/components/Icon'
import { MenuBoolInput, MenuMmInput, MenuListInput, MenuPctInput } from '../menus/Input.mjs'
import { MenuBoolValue, MenuMmValue, MenuListValue, MenuPctOptionValue } from '../menus/Value.mjs'
import { MenuItem, MenuItemGroup } from './Container.mjs'
import { MenuHighlightValue } from './Value.mjs'
export const LayoutSettingsMenu = ({ update, state, Design }) => {
const structure = menuLayoutSettingsStructure()
const drillProps = { Design, state, update }
const inputs = {
size: (props) => <MenuListInput {...drillProps} {...props} />,
orientation: (props) => <MenuListInput {...drillProps} {...props} />,
margin: (props) => <MenuMmInput {...drillProps} {...props} />,
coverPage: (props) => <MenuBoolInput {...drillProps} {...props} />,
iconSize: (props) => <MenuPctInput {...drillProps} {...props} />,
}
const values = {
size: ({ current, changed, config }) => (
<MenuHighlightValue changed={changed}>
{capitalize(current ? current : config.dflt)}
</MenuHighlightValue>
),
orientation: ({ current, changed }) => (
<MenuHighlightValue changed={changed}>
<PatternIcon
className={`tw-w-6 tw-h-6 tw-text-inherit ${current === 'landscape' ? 'tw--rotate-90' : ''}`}
/>
</MenuHighlightValue>
),
margin: MenuMmValue,
coverPage: MenuBoolValue,
iconSize: MenuPctOptionValue,
}
return (
<MenuItemGroup
{...{
structure,
ux: state.ui?.ux,
currentValues: state.ui.layout || {},
isFirst: true,
name: 'UI Preferences',
passProps: {
ux: state.ui?.ux,
settings: state.settings,
patternConfig: Design.patternConfig,
},
updateHandler: (key, val) => update.ui(['layout', key], val),
isDesignOptionsGroup: false,
state,
Design,
inputs,
values,
}}
/>
)
}
/*
const PrintActions = ({ state, update }) => (
<SubAccordion
items={[
[
<div className="tw-w-full tw-flex tw-flex-row tw-gap2 tw-justify-between" key={1}>
<div className="tw-flex tw-flex-row tw-items-center tw-gap-2">
<LeftRightIcon />
<span>{'workbench:partTransfo'}</span>
</div>
{state.ui.hideMovableButtons ? <BoolNoIcon /> : <BoolYesIcon />}
</div>,
<ListInput
key={2}
update={() => update.state.ui('hideMovableButtons', state.ui.hideMovableButtons ? false : true)}
label={
<span className="tw-text-base tw-font-normal">{'workbench:partTransfoDesc'}</span>
}
list={[
{
val: true,
label: 'workbench:partTransfoNo',
desc: 'workbench:partTransfoNoDesc',
},
{
val: false,
label: 'workbench:partTransfoYes',
desc: 'workbench:partTransfoYesDesc',
},
]}
current={state.ui.hideMovableButtons ? true : false}
/>,
'partTransfo',
],
[
<div className="tw-w-full tw-flex tw-flex-row tw-gap2 tw-justify-between" key={1}>
<div className="tw-flex tw-flex-row tw-items-center tw-gap-2">
<ResetIcon />
<span>{'workbench:resetPrintLayout'}</span>
</div>
<WarningIcon />
</div>,
<Fragment key={2}>
<p>{'workbench:resetPrintLayoutDesc'}</p>
<button
className={`${horFlexClasses} tw-btn tw-btn-warning tw-btn-outline tw-w-full`}
onClick={() => update.ui(['layouts', 'print'])}
>
<ResetIcon />
<span>{'workbench:resetPrintLayout'}</span>
</button>
</Fragment>,
'reset',
],
]}
/>
)
*/

View file

@ -13,15 +13,11 @@ export const UiPreferencesMenu = ({ update, state, Design }) => {
const drillProps = { Design, state, update } const drillProps = { Design, state, update }
const inputs = { const inputs = {
ux: (props) => <MenuUxSettingInput {...drillProps} {...props} />, ux: (props) => <MenuUxSettingInput {...drillProps} {...props} />,
//aside: (props) => <MenuListInput {...drillProps} {...props} />,
//kiosk: (props) => <MenuListInput {...drillProps} {...props} />,
rotate: (props) => <MenuListInput {...drillProps} {...props} />, rotate: (props) => <MenuListInput {...drillProps} {...props} />,
renderer: (props) => <MenuListInput {...drillProps} {...props} />, renderer: (props) => <MenuListInput {...drillProps} {...props} />,
} }
const values = { const values = {
ux: (props) => <span>{state.ui.ux}/5</span>, ux: (props) => <span>{state.ui.ux}/5</span>,
//aside: MenuListValue,
//kiosk: MenuListValue,
rotate: MenuListValue, rotate: MenuListValue,
renderer: MenuListValue, renderer: MenuListValue,
} }

View file

@ -151,9 +151,10 @@ export const ExportView = (props) => {
) )
} }
function exportPattern(props) { export function exportPattern(props) {
props.setLink(false) if (props.setLink) props.setLink(false)
props.setFormat(props.format) if (props.setFormat) props.setFormat(props.format)
handleExport({ handleExport({
...props, ...props,
onComplete: (e) => (e.data?.link ? props.setLink(e.data.link) : null), onComplete: (e) => (e.data?.link ? props.setLink(e.data.link) : null),

View file

@ -1,142 +1,45 @@
// Dependencies // Dependencies
import { defaultPrintSettings, printSettingsPath, handleExport } from '../../lib/export/index.mjs'
import { tilerPlugin } from '../../lib/export/plugin-tiler.mjs'
import { get } from '@freesewing/utils'
import { draft } from '../../lib/index.mjs'
import React from 'react' import React from 'react'
//import { import { defaultPrintSettings, handleExport } from '../../lib/export/index.mjs'
// handleExport, import { tilerPlugin } from '../../lib/export/plugin-tiler.mjs'
// ns as exportNs, import { capitalize, get } from '@freesewing/utils'
//} from 'shared/components/workbench/exporting/export-handler.mjs' import { draft } from '../../lib/index.mjs'
//import { pagesPlugin } from 'shared/plugins/plugin-layout-part.mjs'
//import get from 'lodash.get'
//import { defaultPrintSettings, printSettingsPath } from './config.mjs'
//// Hooks
//import { useContext } from 'react'
//import { useTranslation } from 'next-i18next'
//// Context
//import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Components // Components
import { ZoomablePattern } from '../ZoomablePattern.mjs' import { ZoomablePattern } from '../ZoomablePattern.mjs'
import { PatternLayout } from '../PatternLayout.mjs' import { PatternLayout } from '../PatternLayout.mjs'
//import { MovablePattern } from 'shared/components/workbench/pattern/movable/index.mjs' import { MovablePattern } from '../MovablePattern.mjs'
//import { PrintMenu, ns as menuNs } from './menu.mjs' import { Accordion } from '../Accordion.mjs'
//import { PatternWithMenu, ns as wrapperNs } from '../pattern-with-menu.mjs' import { CompareIcon, PrintIcon } from '@freesewing/react/components/Icon'
import { MenuBoolInput, MenuMmInput, MenuListInput } from '../menus/Input.mjs'
import { MenuBoolValue, MenuMmValue, MenuListValue } from '../menus/Value.mjs'
export const LayoutView = (props) => { export const LayoutView = (props) => {
// design,
// pattern,
// patternConfig,
// settings,
// setSettings,
// ui,
// update,
// language,
// account,
// Design,
//}) => {
const { config, state, update, Design } = props const { config, state, update, Design } = props
const defaultSettings = defaultPrintSettings(state.settings?.units) const { ui, settings } = state
const defaultSettings = defaultPrintSettings(settings?.units)
// Settings for the tiler plugin // Settings for the tiler plugin
const pageSettings = { const pageSettings = {
...defaultSettings, ...defaultSettings,
...get(state.ui, printSettingsPath, {}), ...get(state.ui, 'layout', {}),
} }
/* /*
* Now draft the pattern * Now draft the pattern
*/ */
const { pattern, failure, errors } = draft(Design, state.settings, [tilerPlugin(pageSettings)]) const { pattern, failure, errors } = draft(Design, settings, [tilerPlugin(pageSettings)])
if (failure) return <p>Draft failed. FIXME: Handle this gracefully.</p> if (failure) return <p>Draft failed. FIXME: Handle this gracefully.</p>
const renderProps = pattern.getRenderProps()
const exportIt = () => {
update.startLoading('layout', { msg: 'Generating PDF' })
handleExport({
format: pageSettings.size,
settings,
design,
t,
Design,
ui,
startLoading: loading.startLoading,
stopLoading: loading.stopLoading,
onComplete: () => {
setLoadingStatus([true, 'pdfReady', true, true])
},
onError: (err) => {
setLoadingStatus([true, 'pdfFailed', true, true])
console.log(err)
},
})
}
const output = ( const output = (
<MovablePattern <MovablePattern
{...{ {...{
renderProps,
update, update,
renderProps: pattern.getRenderProps(),
immovable: ['pages'], immovable: ['pages'],
layoutPath: ['layouts', 'print'], state,
showButtons: !ui.hideMovableButtons,
}} }}
/> />
//<ZoomablePattern
// renderProps={renderProps}
// patternLocale="en"
// rotate={state.ui.rotate}
///>
) )
return <PatternLayout {...{ update, Design, output, state, pattern, config }} /> return <PatternLayout {...{ update, Design, output, state, pattern, config }} />
return (
<>
<PatternWithMenu
noHeader
{...{
settings,
ui,
update,
control: account.control,
account,
design,
setSettings,
title: (
<h2 className="text-center lg:text-left capitalize">{t('workbench:printLayout')}</h2>
),
pattern: (
<MovablePattern
{...{
renderProps,
update,
immovable: ['pages'],
layoutPath: ['layouts', 'print'],
showButtons: !ui.hideMovableButtons,
}}
/>
),
menu: (
<PrintMenu
{...{
design,
pattern,
patternConfig,
setSettings,
settings,
ui,
update,
language,
account,
exportIt,
}}
/>
),
}}
/>
</>
)
} }

View file

@ -6,7 +6,7 @@ import { DraftView } from './DraftView.mjs'
import { SaveView } from './SaveView.mjs' import { SaveView } from './SaveView.mjs'
import { ExportView } from './ExportView.mjs' import { ExportView } from './ExportView.mjs'
import { UndosView } from './UndosView.mjs' import { UndosView } from './UndosView.mjs'
//import { LayoutView } from './LayoutView.mjs' import { LayoutView } from './LayoutView.mjs'
import { ErrorIcon } from '@freesewing/react/components/Icon' import { ErrorIcon } from '@freesewing/react/components/Icon'
import { import {
OptionsIcon, OptionsIcon,
@ -58,7 +58,7 @@ export const View = (props) => {
if (view === 'save') return <SaveView {...props} /> if (view === 'save') return <SaveView {...props} />
if (view === 'export') return <ExportView {...props} /> if (view === 'export') return <ExportView {...props} />
if (view === 'undos') return <UndosView {...props} /> if (view === 'undos') return <UndosView {...props} />
//if (view === 'layout') return <LayoutView {...props} /> if (view === 'layout') return <LayoutView {...props} />
/* /*
viewComponents: { viewComponents: {
draft: 'DraftView', draft: 'DraftView',
@ -109,7 +109,7 @@ export const viewLabels = {
d: 'Shows detailed timing of the pattern being drafted, allowing you to find bottlenecks in performance', d: 'Shows detailed timing of the pattern being drafted, allowing you to find bottlenecks in performance',
}, },
layout: { layout: {
t: 'Pattern Layout', t: 'Print Layout',
d: 'Organize your pattern parts to minimize paper use', d: 'Organize your pattern parts to minimize paper use',
}, },
save: { save: {

View file

@ -10,6 +10,7 @@ import {
menuCoreSettingsStructure, menuCoreSettingsStructure,
} from './core-settings.mjs' } from './core-settings.mjs'
import { findOption, getOptionStructure, menuDesignOptionsStructure } from './design-options.mjs' import { findOption, getOptionStructure, menuDesignOptionsStructure } from './design-options.mjs'
import { menuLayoutSettingsStructure } from './layout-settings.mjs'
import { import {
addUndoStep, addUndoStep,
cloneObject, cloneObject,
@ -68,6 +69,8 @@ export {
findOption, findOption,
getOptionStructure, getOptionStructure,
menuDesignOptionsStructure, menuDesignOptionsStructure,
// layout-settings.mjs
menuLayoutSettingsStructure,
// editor.mjs // editor.mjs
addUndoStep, addUndoStep,
cloneObject, cloneObject,

View file

@ -0,0 +1,122 @@
import React from 'react'
import { defaultConfig } from '../config/index.mjs'
import { linkClasses } from '@freesewing/utils'
import {
CoverPageIcon,
MenuIcon,
KioskIcon,
RotateIcon,
RocketIcon,
UxIcon,
PageMarginIcon,
PageOrientationIcon,
PageSizeIcon,
PatternIcon,
ScaleIcon,
} from '@freesewing/react/components/Icon'
const UiDocsLink = ({ item }) => (
<a href={`/docs/about/site/draft/#${item.toLowerCase()}`} className={`${linkClasses} tw-px-2`}>
Learn more
</a>
)
const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'legal', 'tabloid']
const defaultPrintSettings = (units) => ({
size: units === 'imperial' ? 'letter' : 'a4',
orientation: 'portrait',
margin: units === 'imperial' ? 12.7 : 10,
coverPage: true,
})
export function menuLayoutSettingsStructure(units) {
const defaults = defaultPrintSettings(units)
const sizeTitles = {
a4: 'A4',
a3: 'A3',
a2: 'A2',
a1: 'A1',
a0: 'A0',
letter: 'Letter',
legal: 'Legal',
tabloid: 'Tabloid',
}
return {
size: {
dense: true,
title: 'Paper Size',
about: (
<span>
This control the pages overlay that helps you see how your pattern spans the pages. This
does not limit your export options, you can still export in a variety of paper sizes.
</span>
),
ux: 1,
list: Object.keys(sizeTitles),
choiceTitles: sizeTitles,
valueTitles: sizeTitles,
dflt: defaults.size,
icon: PageSizeIcon,
},
orientation: {
dense: true,
title: 'Page Orientation',
about: (
<span>Landscape or Portrait. Try both to see which yields the least amount of pages.</span>
),
ux: 1,
list: ['portrait', 'landscape'],
choiceTitles: {
portrait: (
<div className="tw-flex tw-flex-row tw-items-center tw-gap-4">
<PatternIcon className="tw-h-5 tw-w-5" />
<span className="tw-grow">Portrait (tall)</span>
</div>
),
landscape: (
<div className="tw-flex tw-flex-row tw-items-center tw-gap-4">
<PatternIcon className="tw-h-5 tw-w-5 tw--rotate-90" />
<span className="tw-grow">Landscape (wide)</span>
</div>
),
},
icon: PageOrientationIcon,
},
margin: {
dense: true,
title: 'Page Margin',
min: units === 'imperial' ? 2.5 : 5,
max: 25,
dflt: defaults.margin,
icon: PageMarginIcon,
ux: 1,
},
coverPage: {
dense: true,
ux: 1,
icon: CoverPageIcon,
title: 'Cover Page',
about:
'The cover page includes information about the pattern and an overview of how to assemble the pages.',
list: [0, 1],
choiceTitles: {
0: 'Do not include a cover page',
1: 'Include a cover page',
},
dflt: 0,
},
iconSize: {
dense: true,
ux: 1,
icon: ScaleIcon,
title: 'Icon Size',
about:
'Controls the size of the icons that allow you to rotate/flip individual pattern parts',
min: 10,
dflt: 0.5,
step: 1,
max: 200,
},
}
}

View file

@ -1,6 +1,10 @@
import React from 'react' import React from 'react'
import { logoPath } from '@freesewing/config' import { logoPath } from '@freesewing/config'
// Used in several icons
const page =
'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z'
/* /*
* Used inside the pattern editor * Used inside the pattern editor
*/ */
@ -153,6 +157,16 @@ export const CopyIcon = (props) => (
</IconWrapper> </IconWrapper>
) )
// Looks like a page with a smiley on it
export const CoverPageIcon = (props) => (
<IconWrapper {...props}>
<path d={page} />
<circle cx="9" cy="12" r="1" />
<circle cx="14" cy="12" r="1" />
<path d="M 9 16 C 11 18 12 18 14 16" />
</IconWrapper>
)
// Looks like a museum building // Looks like a museum building
export const CuratedMeasurementsSetIcon = (props) => ( export const CuratedMeasurementsSetIcon = (props) => (
<IconWrapper {...props}> <IconWrapper {...props}>
@ -268,6 +282,13 @@ export const FlagIcon = (props) => (
</IconWrapper> </IconWrapper>
) )
// Looks lik a flag
export const FlipIcon = (props) => (
<IconWrapper {...props}>
<path d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
</IconWrapper>
)
// Looks like skully // Looks like skully
export const FreeSewingIcon = (props) => ( export const FreeSewingIcon = (props) => (
<IconWrapper {...props} stroke={0} fill> <IconWrapper {...props} stroke={0} fill>
@ -452,6 +473,36 @@ export const OptionsIcon = (props) => (
</IconWrapper> </IconWrapper>
) )
// Looks like a page with a margin drawn around
export const PageMarginIcon = (props) => (
<IconWrapper {...props}>
<path
d="M 4.5 2.5 v 19.2 h 15 v -13.3 h-3 v 10.3 h-9 v-13.2 h 6 v-3 z"
strokeWidth={0.1}
stroke="none"
fill="currentColor"
fillOpacity="0.666"
/>
<path d={page} />
</IconWrapper>
)
// Looks like a portrait and landscape page stacked
export const PageOrientationIcon = (props) => (
<IconWrapper {...props}>
<path d={page} transform="scale(-1 1) translate(-21 0)" />
<path d="M 16.5 7.75 h 5 v 14 h-5" />
</IconWrapper>
)
// Looks like two differently sizes pages stacked
export const PageSizeIcon = (props) => (
<IconWrapper {...props}>
<path d={page} />
<path d={page} transform="scale(0.666) translate(3, 11)" />
</IconWrapper>
)
// Looks like a grid // Looks like a grid
export const PaperlessIcon = (props) => ( export const PaperlessIcon = (props) => (
<IconWrapper {...props}> <IconWrapper {...props}>
@ -466,7 +517,7 @@ export const PaperlessIcon = (props) => (
// Looks like a page // Looks like a page
export const PatternIcon = (props) => ( export const PatternIcon = (props) => (
<IconWrapper {...props}> <IconWrapper {...props}>
<path d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path d={page} />
</IconWrapper> </IconWrapper>
) )
@ -536,7 +587,12 @@ export const RocketIcon = (props) => (
// Looks like two arrows in a circular layout // Looks like two arrows in a circular layout
export const RotateIcon = (props) => ( export const RotateIcon = (props) => (
<IconWrapper {...props}> <IconWrapper {...props}>
<path d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /> <path
strokeLinecap="round"
strokeLinejoin="round"
d="M 19.5,12 C 19.5,10.768 19.454,9.547 19.362,8.338 19.21576,6.3582806 17.641719,4.7842398 15.662,4.638 14.504476,4.5506731 13.344609,4.5048098 12.184624,4.5004103 M 19.5,12 l 3,-3 m -3,3 -3,-3 m -12,3 c 0,1.232 0.046,2.453 0.138,3.662 0.1462398,1.979719 1.7202806,3.55376 3.7,3.7 1.295324,0.09777 2.593584,0.143587 3.891661,0.13746 M 4.5,12 l 3,3 m -3,-3 -3,3"
{...props}
/>
</IconWrapper> </IconWrapper>
) )

View file

@ -278,6 +278,44 @@ svg.freesewing.pattern .muted {
opacity: 0.15; opacity: 0.15;
} }
/* layout rectangles */
svg.freesewing.pattern .layout-rect {
fill: var(--pattern-canvas);
fill-opacity: 0.05;
}
svg.freesewing.pattern .layout-rect:hover {
fill: var(--pattern-lining);
fill-opacity: 0.15;
}
svg.freesewing.pattern .layout-rect.move:hover {
cursor: move;
}
svg.freesewing.pattern .layout-rect.rotate:hover {
cursor: crosshair;
}
svg.freesewing.pattern .svg-layout-button {
fill: var(--pattern-note);
fill-opacity: 0;
stroke: none;
}
svg.freesewing.pattern .svg-layout-button:hover {
fill: var(--pattern-note);
fill-opacity: 1;
stroke: none;
cursor: pointer;
}
svg.freesewing.pattern .svg-layout-button path {
stroke-width: calc(var(--pattern-stroke) * 2 * var(--pattern-scale));
color: var(--pattern-contrast);
}
svg.freesewing.pattern .svg-layout-button:hover path {
stroke: #fff;
}
svg.freesewing.pattern .svg-layout-button:hover > rect.button {
fill: var(--pattern-contrast);
stroke: none;
}
/* Developer view */ /* Developer view */
g.develop.point { g.develop.point {
circle.center { circle.center {