diff --git a/packages/freesewing.shared/components/workbench/layout/draft.js b/packages/freesewing.shared/components/workbench/layout/draft.js index c17e87e4296..17859ef445e 100644 --- a/packages/freesewing.shared/components/workbench/layout/draft.js +++ b/packages/freesewing.shared/components/workbench/layout/draft.js @@ -9,40 +9,159 @@ import { drag } from 'd3-drag' import { select } from 'd3-selection' import { event } from 'd3-dispatch' +const Buttons = ({ transform, flip, rotate, setRotate, resetPart }) => { + const letter = 'F' + const style = { style: {fill: 'white', fontSize: 18, fontWeight: 'bold', textAnchor: 'middle'} } + + return ( + + {rotate + ? + : + } + + + {letter} + + flip('y')}> + + {letter} + + flip('x')}> + + {letter} + + + ) +} + +const dx = (pointA, pointB) => pointB.x - pointA.x +const dy = (pointA, pointB) => pointB.y - pointA.y +const rad2deg = radians => radians * 57.29577951308232 +const 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 generateTransform = (x, y, rot, flipX, flipY, part) => { + const anchor = { + x: 0, + y: 0 + } + const center = { + x: part.topLeft.x + (part.bottomRight.x - part.topLeft.x)/2, + y: part.topLeft.y + (part.bottomRight.y - part.topLeft.y)/2, + } + const dx = part.topLeft.x - center.x + const dy = part.topLeft.y - center.y + const transforms = [`translate(${x},${y})`] + if (flipX) transforms.push( + `translate(${center.x * -1}, ${center.y * -1})`, + 'scale(-1, 1)', + `translate(${center.x * -1 + 2 * dx}, ${center.y})` + ) + if (flipX) transforms.push( + `translate(${center.x * -1}, ${center.y * -1})`, + 'scale(1, -1)', + `translate(${center.x}, ${center.y * -1 + 2 * dy})`, + ) + if (rot) transforms.push( + `rotate(${rot}, ${center.x - anchor.x}, ${center.y - anchor.y})` + ) + + return transforms.join(' ') +} + const Part = props => { - if (typeof props.autoLayout?.move?.x === 'undefined') return null + console.log('rendering draft') + const { layout, gist, updateGist, name, part } = props + const partLayout= layout.parts[name] - const groupRef = useRef(null) - const [layout, setLayout] = useState({ - move: { - x: props.autoLayout.move.x, - y: props.autoLayout.move.y, - }, - rotate: 0, - flipX: false, - flipY: false, - }) + // Don't just assume this makes sense + if (typeof layout?.parts?.[name]?.move?.x === 'undefined') return null + // Use a ref for direct DOM manipulation + const partRef = useRef(null) + const centerRef = useRef(null) + + // State variable to switch between moving or rotating the part + const [rotate, setRotate] = useState(false) + + // Initialize drag handler useEffect(() => { - handleDrag(select(groupRef.current)) - }, []) + handleDrag(select(partRef.current)) + }, [rotate]) + + // These are kepts 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 = partLayout.move.x + let translateY = partLayout.move.y + let rotation = partLayout.rotate || 0 + let flipX = partLayout.flipX ? true : false + let flipY = partLayout.flipY ? true : false + let rotStart = { x: 0, y: 0 } + + const center = { + x: part.topLeft.x + (part.bottomRight.x - part.topLeft.x)/2, + y: part.topLeft.y + (part.bottomRight.y - part.topLeft.y)/2, + } - let translateX = 0; - let translateY = 0; const handleDrag = drag() .subject(function() { const me = select(this); return { x: translateX, y: translateY } }) + .on('start', function(event) { + rotStart = { x: event.x, y: event.y } + }) .on('drag', function(event) { + if (rotate) { + rotation = angle(rotStart, { x:event.x, y: event.y }) * -1 + } else { + translateX = event.x + translateY = event.y + } const me = select(this); - const transform = `translate(${event.x}, ${event.y})`; - translateX = event.x; - translateY = event.y; - me.attr('transform', transform); - }); + me.attr('transform', generateTransform(translateX, translateY, rotation, flipX, flipY, part)) + }) + .on('end', updateLayout) - const { partName, part, app, gist, updateGist } = props + const resetPart = () => { + rotation = 0 + flipX = 0 + flipY = 0 + updateLayout() + } + const toggleDragRotate = () => { + updateLayout() + setRotate(!rotate) + } + const updateLayout = () => { + console.log('updating layout') + props.updateGist( + ['layout', 'parts', name], + { + move: { + x: translateX, + y: translateY, + }, + rotate: rotation, + flipX, + flipY + } + ) + } + + // Method to flip (mirror) the part along the X or Y axis + const flip = axis => { + if (axis === 'x') flipX = !flipX + else flipY = !flipY + updateLayout() + } const grid = gist.paperless ? ( { width={part.width} height={part.height} className="grid" - fill={'url(#grid-' + partName + ')'} + fill={'url(#grid-' + eame + ')'} /> ) : null return ( - + {grid} { props.gist?._state?.xray?.enabled && - props.gist?._state?.xray?.reveal?.[partName] + props.gist?._state?.xray?.reveal?.[name] && } {Object.keys(part.paths).map((pathName) => ( @@ -91,14 +215,21 @@ const Part = props => { {...props} /> ))} + + {props.name !== 'pages' && } ) } @@ -106,34 +237,50 @@ const Part = props => { const Draft = props => { const { patternProps, gist, app, updateGist, unsetGist, bgProps={} } = props + const { layout=false } = gist - //useEffect(() => { - // if (!props.gist.layout) { - // //updateGist(['layout'], patternProps.autoLayout) - // console.log(patternProps.autoLayout) - // } - //}, []) + useEffect(() => { + if (!layout) { + // On the initial draft, core does the layout, so we set the layout to the auto-layout + // After this, core won't handle layout anymore. It's up to the user from this point onwards + // FIXME: Allow use the option to clear the layout again + updateGist(['layout'], { + ...patternProps.autoLayout, + width: patternProps.width, + height: patternProps.height + }) + } + }, [layout]) + + if (!patternProps || !layout) return null + + // 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 partList = Object.keys(patternProps.parts) - console.log(gist) return ( -
+
- {Object.keys(patternProps.parts).map((name) => ( - - ))} + {[ + partList.filter(name => name === 'pages'), + partList.filter(name => name !== 'pages'), + ].map(list => list.map(name => ( + + )))}
diff --git a/packages/freesewing.shared/components/workbench/layout/print/index.js b/packages/freesewing.shared/components/workbench/layout/print/index.js index 4e7d93d9c52..a813fa4bbae 100644 --- a/packages/freesewing.shared/components/workbench/layout/print/index.js +++ b/packages/freesewing.shared/components/workbench/layout/print/index.js @@ -29,7 +29,7 @@ const PrintLayout = props => { draft.draft() patternProps = draft.getRenderProps() } catch(err) { - console.log(err) + console.log(err, props.gist) } const bgProps = { fill: "url(#page)" } diff --git a/packages/freesewing.shared/components/workbench/layout/print/plugin.js b/packages/freesewing.shared/components/workbench/layout/print/plugin.js index 3242b225bfb..448ee55c48a 100644 --- a/packages/freesewing.shared/components/workbench/layout/print/plugin.js +++ b/packages/freesewing.shared/components/workbench/layout/print/plugin.js @@ -1,4 +1,3 @@ - const name = 'Pages Plugin' const version = '1.0.0' const sizes = { @@ -11,10 +10,6 @@ const sizes = { tabloid: [ 279.4, 431.8 ], } -const drawPage = (x, y, size, orientation) => { -// -} - const pagesPlugin = (size='a4', orientation='portrait') => ({ name, version, @@ -24,6 +19,10 @@ const pagesPlugin = (size='a4', orientation='portrait') => ({ pattern.parts.pages = pattern.Part('pages') // Keep part out of layout pattern.parts.pages.layout = false + // But add the part to the autoLayout property + pattern.autoLayout.parts.pages = { + move: { x: 0, y: 0 } + } // Add pages const { macro } = pattern.parts.pages.shorthand() const { height, width } = pattern @@ -70,6 +69,7 @@ const pagesPlugin = (size='a4', orientation='portrait') => ({ } // Store page count in part this.pages = { cols, rows, count: cols*rows } + } } }) diff --git a/packages/freesewing.shared/components/workbench/menu/view.js b/packages/freesewing.shared/components/workbench/menu/view.js index 1f79077d153..2efebcb661a 100644 --- a/packages/freesewing.shared/components/workbench/menu/view.js +++ b/packages/freesewing.shared/components/workbench/menu/view.js @@ -58,6 +58,11 @@ const View = props => { title: t('editThing', { thing: 'YAML' }), onClick: () => props.updateGist(['_state', 'view'], 'edit') }, + { + name: 'clear', + title: t('clearThing', { thing: 'YAML' }), + onClick: () => props.setGist(null) + }, ] return ( diff --git a/packages/freesewing.shared/styles/svg-freesewing-draft.css b/packages/freesewing.shared/styles/svg-freesewing-draft.css index 9b8c6dcf2fe..ad158cc0b32 100644 --- a/packages/freesewing.shared/styles/svg-freesewing-draft.css +++ b/packages/freesewing.shared/styles/svg-freesewing-draft.css @@ -61,6 +61,34 @@ svg.freesewing.pattern { .muted { opacity: 0.15; } + /* layout rectangles */ + .layout-rect { + fill: var(--pattern-canvas); + fill-opacity: 0.05; + } + .layout-rect:hover { + fill: var(--pattern-lining); + fill-opacity: 0.15; + } + .layout-rect.move:hover { + cursor: move; + } + .layout-rect.rotate:hover { + cursor: crosshair; + } + .svg-layout-button > rect.button { + fill: var(--pattern-note); + fill-opacity: 0.3; + stroke: none; + } + .svg-layout-button:hover > rect { + fill: var(--pattern-lining); + stroke: none; + fill-opacity: 1; + } + .svg-layout-button:hover { + cursor: pointer; + } /* Developer view */ g.develop.point {