1
0
Fork 0
This commit is contained in:
Enoch Riese 2023-06-06 11:25:53 -05:00
parent 15c4201906
commit 1ea470aa87
5 changed files with 18 additions and 517 deletions

View file

@ -54,7 +54,7 @@ const generateCutLayouts = (pattern, Design, settings, format, t, ui) => {
// get the materials from the already drafted base pattern // get the materials from the already drafted base pattern
const materials = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics( const materials = pattern.setStores[pattern.activeSet].cutlist.getCutFabrics(
pattern.settings[0] pattern.settings[0]
) || ['material'] ) || ['fabric']
if (!materials.length) return if (!materials.length) return
const isImperial = settings.units === 'imperial' const isImperial = settings.units === 'imperial'

View file

@ -14,23 +14,23 @@ export const ExportDraft = ({ gist, design, app }) => {
setLink(false) setLink(false)
setError(false) setError(false)
setFormat(format) setFormat(format)
handleExport( // handleExport(
format, // format,
gist, // gist,
design, // design,
t, // t,
app, // app,
(e) => { // (e) => {
if (e.data.link) { // if (e.data.link) {
setLink(e.data.link) // setLink(e.data.link)
} // }
}, // },
(e) => { // (e) => {
if (e.data?.error) { // if (e.data?.error) {
setError(true) // setError(true)
} // }
} // }
) // )
} }
return ( return (

View file

@ -1,123 +0,0 @@
import { useTranslation } from 'next-i18next'
import { ClearIcon } from 'shared/components/icons.mjs'
import get from 'lodash.get'
const Triangle = ({ transform = 'translate(0,0)', fill = 'currentColor' }) => (
<path
strokeLinecap="round"
strokeLinejoin="round"
transform={transform}
style={{ fill }}
transform-origin="12 12"
d="M1 12m9 3m-6 4h2c3 0 3 -3 3-3L9 3c-0-1.732 -2.25-2.6125 -3.325 -.77L2 16c-.77 1.333.192 3 1.732 3z"
/>
)
const Line = () => (
<path strokeLinecap="round" strokeLinejoin="round" transform="translate(12, 2)" d="M0 0L0 20" />
)
const FlipIconInner = ({ x = 0, y = 0, rotate = 0, ...style }) => (
<g transform={`translate(${x},${y}) rotate(${rotate})`} transform-origin="12 12" style={style}>
<Triangle fill="none" transform="translate(0, 2.5)" />
<path strokeLinecap="round" strokeLinejoin="round" transform="translate(12, 2)" d="M0 0L0 20" />
<Line />
<Triangle transform="scale(-1,1) translate(0,2.5)" />
</g>
)
const RotateIconInner = ({ flipX = false }) => (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
transform={flipX ? 'scale(-1,1)' : ''}
transform-origin="12 12"
/>
)
const rectSize = 24
const Button = ({ onClickCb, transform, Icon, children }) => {
const _onClick = (event) => {
event.stopPropagation()
onClickCb(event)
}
return (
<g className="svg-layout-button group" transform={transform}>
<rect width={rectSize} height={rectSize} className="button" />
<Icon />
<text className="invisible group-hover:visible text-xl">{children}</text>
<rect width={rectSize} height={rectSize} onClick={_onClick} className="fill-transparent" />
</g>
)
}
export const ShowButtonsToggle = ({ gist, layoutSetType, updateGist }) => {
const { t } = useTranslation('workbench')
const path = ['_state', 'layout', layoutSetType, 'showButtons']
const showButtons = get(gist, path, true)
const setShowButtons = () => updateGist(path, !showButtons)
return (
<label htmlFor="showButtons" className="label">
<span className="mr-2">{t('showButtons')}</span>
<input
type="checkbox"
className="toggle toggle-primary"
checked={showButtons}
onChange={setShowButtons}
/>
</label>
)
}
/** buttons for manipulating the part */
export const Buttons = ({ transform, flip, rotate, resetPart, rotate90 }) => {
const { t } = useTranslation('workbench')
return (
<g transform={transform}>
{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={() => <ClearIcon wrapped={false} />}
>
{t('toolbar.resetPart')}
</Button>
<Button
onClickCb={() => rotate90(-1)}
transform={`translate(${rectSize * -2.7}, ${rectSize / -2})`}
Icon={RotateIconInner}
>
{t('toolbar.rotateCCW')}
</Button>
<Button
onClickCb={() => flip('y')}
transform={`translate(${rectSize * 0.6}, ${rectSize / -2})`}
Icon={() => <FlipIconInner rotate="270" />}
>
{t('toolbar.flipY')}
</Button>
<Button
onClickCb={() => flip('x')}
transform={`translate(${rectSize * -1.6}, ${rectSize / -2})`}
Icon={FlipIconInner}
>
{t('toolbar.flipX')}
</Button>
<Button
onClickCb={() => rotate90()}
transform={`translate(${rectSize * 1.7}, ${rectSize / -2})`}
Icon={() => <RotateIconInner flipX={true} />}
>
{t('toolbar.rotateCW')}
</Button>
</g>
)
}

View file

@ -1,116 +0,0 @@
import { useRef } from 'react'
import { Stack } from './stack.mjs'
import { SvgWrapper } from '../../pattern/svg.mjs'
import { PartInner } from '../../pattern/part.mjs'
import get from 'lodash.get'
export const Draft = (props) => {
const {
patternProps,
gist,
updateGist,
app,
bgProps = {},
fitLayoutPart = false,
layoutType = 'printingLayout',
layoutSetType = 'forPrinting',
} = props
const svgRef = useRef(null)
if (!patternProps) return null
// keep a fresh copy of the layout because we might manipulate it without saving to the gist
const layoutPath = ['layouts'].concat(layoutType)
let layout = get(patternProps.settings[0], layoutPath) || {
...patternProps.autoLayout,
width: patternProps.width,
height: patternProps.height,
}
// Helper method to update part layout and re-calculate width * height
const updateLayout = (name, config, history = true) => {
// Start creating new layout
const newLayout = { ...layout }
newLayout.stacks[name] = config
// Pattern topLeft and bottomRight
let topLeft = { x: 0, y: 0 }
let bottomRight = { x: 0, y: 0 }
for (const pname in patternProps.stacks) {
if (pname == props.layoutPart && !fitLayoutPart) continue
let partLayout = newLayout.stacks[pname]
// Pages part does not have its topLeft and bottomRight set by core since it's added post-draft
if (partLayout?.tl) {
// set the pattern extremes
topLeft.x = Math.min(topLeft.x, partLayout.tl.x)
topLeft.y = Math.min(topLeft.y, partLayout.tl.y)
bottomRight.x = Math.max(bottomRight.x, partLayout.br.x)
bottomRight.y = Math.max(bottomRight.y, partLayout.br.y)
}
}
newLayout.width = bottomRight.x - topLeft.x
newLayout.height = bottomRight.y - topLeft.y
newLayout.bottomRight = bottomRight
newLayout.topLeft = topLeft
if (history) {
updateGist(layoutPath, newLayout, history)
} 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
layout = newLayout
}
}
const viewBox = layout.topLeft
? `${layout.topLeft.x} ${layout.topLeft.y} ${layout.width} ${layout.height}`
: false
// We need to make sure the `pages` part is at the bottom of the pile
// so we can drag-drop all parts on top of it.
// Bottom in SVG means we need to draw it first
const stacks = [
<PartInner
{...{
part: patternProps.parts[0][props.layoutPart],
partName: props.layoutPart,
gist,
skipGrid: true,
}}
key={props.layoutPart}
/>,
]
// then make a stack component for each remaining stack
for (var stackName in patternProps.stacks) {
if (stackName === props.layoutPart) {
continue
}
let stack = patternProps.stacks[stackName]
const stackPart = (
<Stack
{...{
key: stackName,
stackName,
stack,
layout,
app,
gist,
updateLayout,
isLayoutPart: stackName === props.layoutPart,
layoutSetType: layoutSetType,
}}
/>
)
stacks.push(stackPart)
}
return (
<SvgWrapper {...{ patternProps, gist, viewBox }} ref={svgRef}>
<rect x="0" y="0" width={layout.width} height={layout.height} {...bgProps} />
{stacks}
</SvgWrapper>
)
}

View file

@ -1,260 +0,0 @@
/*
* This React component is a long way from perfect, but it's a start for
* handling custom layouts.
*
* There are a few reasons that (at least in my opinion) implementing this is non-trivial:
*
* 1) React re-render vs DOM updates
*
* For performance reasons, we can't re-render with React when the user drags a
* pattern part (or rotates it). It would kill performance.
* So, we don't re-render with React upon dragging/rotating, but instead manipulate
* the DOM directly.
*
* So far so good, but of course we don't want a pattern that's only correctly laid
* out in the DOM. We want to update the pattern gist so that the new layout is stored.
* For this, we re-render with React on the end of the drag (or rotate).
*
* Handling this balance between DOM updates and React re-renders is a first contributing
* factor to why this component is non-trivial
*
* 2) SVG vs DOM coordinates
*
* When we drag or rotate with the mouse, all the events are giving us coordinates of
* where the mouse is in the DOM.
*
* The layout uses coordinates from the embedded SVG which are completely different.
*
* We run `getScreenCTM().inverse()` on the svg element to pass to `matrixTransform` on a `DOMPointReadOnly` for dom to svg space conversions.
*
* 3) Part-level transforms
*
* All parts use their center as the transform-origin to simplify transforms, especially flipping and rotating.
*
* 4) Bounding box
*
* We use `getBoundingClientRect` rather than `getBBox` because it provides more data and factors in the transforms.
* We then use our `domToSvg` function to move the points back into the SVG space.
*
*
* Known issues
* - currently none
*
* 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 { generateStackTransform, getTransformedBounds } from '@freesewing/core'
import { Part } from '../../draft/part.mjs'
import { getProps, angle } from '../../draft/utils.mjs'
import { drag } from 'd3-drag'
import { select } from 'd3-selection'
import { Buttons } from './buttons.mjs'
import get from 'lodash.get'
export const Stack = (props) => {
const { layout, stack, stackName, gist } = props
const stackLayout = layout.stacks?.[stackName]
const stackExists = typeof stackLayout?.move?.x !== 'undefined'
// Use a ref for direct DOM manipulation
const stackRef = useRef(null)
const centerRef = useRef(null)
const innerRef = useRef(null)
// 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 && !props.isLayoutPart) {
updateLayout(false)
}
}, [stackRef, stackLayout])
// Initialize drag handler
useEffect(() => {
// don't drag the pages
if (props.isLayoutPart || !stackExists) return
handleDrag(select(stackRef.current))
}, [rotate, stackRef, stackLayout])
// // Don't just assume this makes sense
if (!stackExists) return null
// These are kept as vars because re-rendering on drag would kill performance
// Managing the difference between re-render and direct DOM updates makes this
// whole thing a bit tricky to wrap your head around
let translateX = stackLayout.move.x
let translateY = stackLayout.move.y
let stackRotation = stackLayout.rotate || 0
let rotation = stackRotation
let flipX = !!stackLayout.flipX
let flipY = !!stackLayout.flipY
const center = {
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 = () => {
// get the transform attributes
const transforms = generateStackTransform(translateX, translateY, rotation, flipX, flipY, stack)
const me = select(stackRef.current)
me.attr('transform', transforms.join(' '))
return transforms
}
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
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
rotation = stackRotation + newRotation
} else {
translateX = event.x
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) updateLayout()
didDrag = false
})
/** reset the part's transforms */
const resetPart = () => {
rotation = 0
flipX = 0
flipY = 0
updateLayout()
}
/** toggle between dragging and rotating */
const toggleDragRotate = () => {
// only respond if the part should be able to drag/rotate
if (!stackRef.current || props.isLayoutPart) {
return
}
setRotate(!rotate)
}
/** update the layout either locally or in the gist */
const updateLayout = (history = true) => {
/** don't mess with what we don't lay out */
if (!stackRef.current || props.isLayoutPart) 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
props.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
updateLayout()
}
/** method to rotate 90 degrees */
const rotate90 = (direction = 1) => {
if (flipX) direction *= -1
if (flipY) direction *= -1
rotation += 90 * direction
updateLayout()
}
// don't render if the part is empty
// if (Object.keys(part.snippets).length === 0 && Object.keys(part.paths).length === 0) return null;
const showButtons = get(gist, ['_state', 'layout', props.layoutSetType, 'showButtons'], true)
return (
<g id={`stack-${stackName}`} {...getProps(stack)} ref={stackRef}>
<g id={`stack-inner-${stackName}`} ref={innerRef}>
{stack.parts.map((part) => (
<Part {...{ part, partName: part.name, gist }} key={part.name}></Part>
))}
</g>
{!props.isLayoutPart && (
<>
<text x={center.x} y={center.y} ref={centerRef} />
<rect
x={stack.topLeft.x}
y={stack.topLeft.y}
width={stack.width}
height={stack.height}
className={`layout-rect ${rotate ? 'rotate' : 'move'}`}
id={`${stackName}-layout-rect`}
onClick={toggleDragRotate}
/>
{showButtons ? (
<Buttons
transform={`translate(${center.x}, ${center.y}) rotate(${-rotation}) scale(${
flipX ? -1 : 1
},${flipY ? -1 : 1})`}
flip={flip}
rotate={rotate}
setRotate={setRotate}
resetPart={resetPart}
rotate90={rotate90}
partName={stackName}
/>
) : null}
</>
)}
</g>
)
}