1
0
Fork 0
freesewing/sites/shared/components/workbench/layout/draft/part.js
Joost De Cock 474188a05e fix(shared): Don't import from core source files
Importing from source files (of published packages) works fine
in the monorepo, but breaks outside of it.
Specifically, it breaks in the standalone development environment
which spins up the lab outside the monorepo.

For a situation like this, you need to make sure we have a
named export that's available from the built package.
2022-07-02 20:21:36 +02:00

229 lines
7.5 KiB
JavaScript

/*
* 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 {PartInner} from '../../draft/part'
import { generatePartTransform } from '@freesewing/core'
import { getProps, angle } from '../../draft/utils'
import { drag } from 'd3-drag'
import { select } from 'd3-selection'
import { useRef, useState, useEffect} from 'react'
/** buttons for manipulating the part */
const Buttons = ({ transform, flip, rotate, setRotate, resetPart }) => {
const letter = 'F'
const style = { style: {fill: 'white', fontSize: 18, fontWeight: 'bold', textAnchor: 'middle'} }
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" />
}
<g className="svg-layout-button" onClick={resetPart}>
<rect x="-10" y="-10" width="20" height="20" />
<text x="0" y="10" {...style}>{letter}</text>
</g>
<g className="svg-layout-button" onClick={() => flip('y')}>
<rect x="10" y="-10" width="20" height="20" className="button" />
<text x="20" y="10" {...style} transform="scale(1,-1)">{letter}</text>
</g>
<g className="svg-layout-button" onClick={() => flip('x')}>
<rect x="-30" y="-10" width="20" height="20" className="button" />
<text x="20" y="10" {...style} transform="scale(-1,1)">{letter}</text>
</g>
</g>
)
}
const Part = props => {
const { layout, name, part} = props
const partLayout = layout.parts?.[name]
// Don't just assume this makes sense
if (typeof partLayout?.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)
// update the layout on mount
useEffect(() => {
if (partRef.current) updateLayout()
}, [partRef])
// Initialize drag handler
useEffect(() => {
handleDrag(select(partRef.current))
}, [rotate, layout])
// 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 = partLayout.move.x
let translateY = partLayout.move.y
let partRotation = partLayout.rotate || 0;
let rotation = partRotation;
let flipX = !!partLayout.flipX
let flipY = !!partLayout.flipY
const center = {
x: part.topLeft.x + (part.bottomRight.x - part.topLeft.x)/2,
y: part.topLeft.y + (part.bottomRight.y - part.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 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 (rotate) {
let newRotation = getRotation(event);
// 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 = partRotation + newRotation
}
else {
translateX = event.x
translateY = event.y
}
// get the transform attributes
const transforms = generatePartTransform(translateX, translateY, rotation, flipX, flipY, part);
const me = select(this);
for (var t in transforms) {
me.attr(t, transforms[t])
}
})
.on('end', function(event) {
// save to gist
updateLayout()
})
const resetPart = () => {
rotation = 0
flipX = 0
flipY = 0
updateLayout()
}
const toggleDragRotate = () => {
updateLayout()
setRotate(!rotate)
}
const updateLayout = () => {
const partRect = partRef.current.getBoundingClientRect();
const matrix = partRef.current.ownerSVGElement.getScreenCTM().inverse();
const domToSvg = (point) => DOMPointReadOnly.fromPoint(point).matrixTransform(matrix)
// include the new top left and bottom right to ease calculating the pattern width and height
const tl = domToSvg({x: partRect.left, y: partRect.top});
const br = domToSvg({x: partRect.right, y: partRect.bottom});
props.updateLayout(name, {
move: {
x: translateX,
y: translateY,
},
rotate: rotation,
flipX,
flipY,
tl,
br
})
}
// Method to flip (mirror) the part along the X or Y axis
const flip = axis => {
if (axis === 'x') flipX = !flipX
else flipY = !flipY
updateLayout()
}
return (
<g
{...getProps(part)}
id={`part-${name}`}
ref={props.name === 'pages' ? null : partRef}
onClick={toggleDragRotate}
transform-origin={`${center.x} ${center.y}`}
>
{PartInner(props)}
{props.name !== 'pages' && <>
<text x={center.x} y={center.y} ref={centerRef} />
<rect
x={part.topLeft.x}
y={part.topLeft.y}
width={part.width}
height={part.height}
className={`layout-rect ${rotate ? 'rotate' : 'move'}`}
/>
<Buttons
transform={`translate(${part.topLeft.x + part.width/2}, ${part.topLeft.y + part.height/2})`}
flip={flip}
rotate={rotate}
setRotate={setRotate}
resetPart={resetPart}
/>
</>}
</g>
)
}
export default Part