feat(lab): First stab at custom layout
This adds a React component to handle custom layouts.
This React component is a long way from perfect, but it's a start.
There are two 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 updat 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.
So we need to make this translation and that adds complexity.
3) Part-level transforms
It's not just that the DOM coordinates and SVG coordinate system is different, each
part also has it's own transforms applied and because of this behaves as if they have
their own coordinate system.
In other words, a point (0,0) in the part is not the top-left corner of the page.
In the best-case scenario, it's the top-left corner of the part. But even this is
often not the case as parts will have transforms applied to them.
4) Flip along X or Y axis
Parts can be flipped along the X or Y axis to facilitate a custom layout.
This is handled in a transform, so the part's coordinate's don't actually change. They
are flipped late into the rendering process (by the browser displaying the SVG).
Handling this adds yet more mental overhead
5) Bounding box
While moving and rotating parts around is one thing. Recalculating the bounding box
(think auto-cropping the pattern) gets kinda complicated because of the reasons
outlined above.
We are currently handling a lot in the frontend code. It might be more elegant to move
some of this to core. For example, core expects the custom layout to set the widht and height
rather than figuring it out on its own as it does for auto-generated layouts.
Known issues
- Rotating gets a little weird sometimes as the part rotates around it's center in the
SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
used to calculate the angle of the rotation
- Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
- Rotation gets weird when a part is mirrored
- The bounding box update when a part is rotated is not entirely accurate
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.
2022-03-12 18:53:47 +01:00
|
|
|
/*
|
|
|
|
* This React component is a long way from perfect, but it's a start for
|
|
|
|
* handling custom layouts.
|
|
|
|
*
|
|
|
|
* There are two 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 updat 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.
|
|
|
|
* So we need to make this translation and that adds complexity.
|
|
|
|
*
|
|
|
|
* 3) Part-level transforms
|
|
|
|
*
|
|
|
|
* It's not just that the DOM coordinates and SVG coordinate system is different, each
|
|
|
|
* part also has it's own transforms applied and because of this behaves as if they have
|
|
|
|
* their own coordinate system.
|
|
|
|
*
|
|
|
|
* In other words, a point (0,0) in the part is not the top-left corner of the page.
|
|
|
|
* In the best-case scenario, it's the top-left corner of the part. But even this is
|
|
|
|
* often not the case as parts will have transforms applied to them.
|
|
|
|
*
|
|
|
|
* 4) Flip along X or Y axis
|
|
|
|
*
|
|
|
|
* Parts can be flipped along the X or Y axis to facilitate a custom layout.
|
|
|
|
* This is handled in a transform, so the part's coordinate's don't actually change. They
|
|
|
|
* are flipped late into the rendering process (by the browser displaying the SVG).
|
|
|
|
*
|
|
|
|
* Handling this adds yet more mental overhead
|
|
|
|
*
|
|
|
|
* 5) Bounding box
|
|
|
|
*
|
|
|
|
* While moving and rotating parts around is one thing. Recalculating the bounding box
|
|
|
|
* (think auto-cropping the pattern) gets kinda complicated because of the reasons
|
|
|
|
* outlined above.
|
|
|
|
*
|
|
|
|
* We are currently handling a lot in the frontend code. It might be more elegant to move
|
|
|
|
* some of this to core. For example, core expects the custom layout to set the widht and height
|
|
|
|
* rather than figuring it out on its own as it does for auto-generated layouts.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* Known issues
|
|
|
|
*
|
|
|
|
* - Rotating gets a little weird sometimes as the part rotates around it's center in the
|
|
|
|
* SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
|
|
|
|
* used to calculate the angle of the rotation
|
|
|
|
*
|
|
|
|
* - Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
|
|
|
|
*
|
|
|
|
* - Rotation gets weird when a part is mirrored
|
|
|
|
*
|
|
|
|
* - The bounding box update when a part is rotated is not entirely accurate
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
2022-02-25 08:30:03 +01:00
|
|
|
import { useEffect, useRef, useState } from 'react'
|
2022-02-20 18:46:21 +01:00
|
|
|
import Svg from '../draft/svg'
|
|
|
|
import Defs from '../draft/defs'
|
2022-02-25 08:30:03 +01:00
|
|
|
import Path from '../draft/path'
|
|
|
|
import Point from '../draft/point'
|
|
|
|
import Snippet from '../draft/snippet'
|
|
|
|
import { getProps } from '../draft/utils'
|
|
|
|
import { drag } from 'd3-drag'
|
|
|
|
import { select } from 'd3-selection'
|
|
|
|
|
2022-03-06 18:54:30 +01:00
|
|
|
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 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})`
|
|
|
|
)
|
2022-03-13 08:48:21 +01:00
|
|
|
if (flipY) transforms.push(
|
2022-03-06 18:54:30 +01:00
|
|
|
`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(' ')
|
|
|
|
}
|
|
|
|
|
2022-02-25 08:30:03 +01:00
|
|
|
const Part = props => {
|
2022-03-13 17:29:22 +01:00
|
|
|
const { layout, gist, name, part } = props
|
2022-03-06 18:54:30 +01:00
|
|
|
const partLayout= layout.parts[name]
|
|
|
|
|
|
|
|
// Don't just assume this makes sense
|
2022-03-13 14:37:17 +01:00
|
|
|
if (typeof layout.parts[name].move?.x === 'undefined') return null
|
2022-03-06 18:54:30 +01:00
|
|
|
|
|
|
|
// 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)
|
2022-02-25 08:30:03 +01:00
|
|
|
|
2022-03-06 18:54:30 +01:00
|
|
|
// Initialize drag handler
|
2022-02-25 08:30:03 +01:00
|
|
|
useEffect(() => {
|
2022-03-06 18:54:30 +01:00
|
|
|
handleDrag(select(partRef.current))
|
|
|
|
}, [rotate])
|
|
|
|
|
2022-03-13 14:33:10 +01:00
|
|
|
// These are kept as vars because re-rendering on drag would kill performance
|
2022-03-06 18:54:30 +01:00
|
|
|
// 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
|
feat(lab): First stab at custom layout
This adds a React component to handle custom layouts.
This React component is a long way from perfect, but it's a start.
There are two 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 updat 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.
So we need to make this translation and that adds complexity.
3) Part-level transforms
It's not just that the DOM coordinates and SVG coordinate system is different, each
part also has it's own transforms applied and because of this behaves as if they have
their own coordinate system.
In other words, a point (0,0) in the part is not the top-left corner of the page.
In the best-case scenario, it's the top-left corner of the part. But even this is
often not the case as parts will have transforms applied to them.
4) Flip along X or Y axis
Parts can be flipped along the X or Y axis to facilitate a custom layout.
This is handled in a transform, so the part's coordinate's don't actually change. They
are flipped late into the rendering process (by the browser displaying the SVG).
Handling this adds yet more mental overhead
5) Bounding box
While moving and rotating parts around is one thing. Recalculating the bounding box
(think auto-cropping the pattern) gets kinda complicated because of the reasons
outlined above.
We are currently handling a lot in the frontend code. It might be more elegant to move
some of this to core. For example, core expects the custom layout to set the widht and height
rather than figuring it out on its own as it does for auto-generated layouts.
Known issues
- Rotating gets a little weird sometimes as the part rotates around it's center in the
SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
used to calculate the angle of the rotation
- Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
- Rotation gets weird when a part is mirrored
- The bounding box update when a part is rotated is not entirely accurate
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.
2022-03-12 18:53:47 +01:00
|
|
|
let partRect
|
2022-03-06 18:54:30 +01:00
|
|
|
|
|
|
|
const center = {
|
|
|
|
x: part.topLeft.x + (part.bottomRight.x - part.topLeft.x)/2,
|
|
|
|
y: part.topLeft.y + (part.bottomRight.y - part.topLeft.y)/2,
|
|
|
|
}
|
2022-02-25 08:30:03 +01:00
|
|
|
const handleDrag = drag()
|
|
|
|
.subject(function() {
|
|
|
|
return { x: translateX, y: translateY }
|
|
|
|
})
|
2022-03-06 18:54:30 +01:00
|
|
|
.on('start', function(event) {
|
feat(lab): First stab at custom layout
This adds a React component to handle custom layouts.
This React component is a long way from perfect, but it's a start.
There are two 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 updat 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.
So we need to make this translation and that adds complexity.
3) Part-level transforms
It's not just that the DOM coordinates and SVG coordinate system is different, each
part also has it's own transforms applied and because of this behaves as if they have
their own coordinate system.
In other words, a point (0,0) in the part is not the top-left corner of the page.
In the best-case scenario, it's the top-left corner of the part. But even this is
often not the case as parts will have transforms applied to them.
4) Flip along X or Y axis
Parts can be flipped along the X or Y axis to facilitate a custom layout.
This is handled in a transform, so the part's coordinate's don't actually change. They
are flipped late into the rendering process (by the browser displaying the SVG).
Handling this adds yet more mental overhead
5) Bounding box
While moving and rotating parts around is one thing. Recalculating the bounding box
(think auto-cropping the pattern) gets kinda complicated because of the reasons
outlined above.
We are currently handling a lot in the frontend code. It might be more elegant to move
some of this to core. For example, core expects the custom layout to set the widht and height
rather than figuring it out on its own as it does for auto-generated layouts.
Known issues
- Rotating gets a little weird sometimes as the part rotates around it's center in the
SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
used to calculate the angle of the rotation
- Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
- Rotation gets weird when a part is mirrored
- The bounding box update when a part is rotated is not entirely accurate
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.
2022-03-12 18:53:47 +01:00
|
|
|
partRect = partRef.current.getBoundingClientRect()
|
2022-03-06 18:54:30 +01:00
|
|
|
})
|
2022-02-25 08:30:03 +01:00
|
|
|
.on('drag', function(event) {
|
feat(lab): First stab at custom layout
This adds a React component to handle custom layouts.
This React component is a long way from perfect, but it's a start.
There are two 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 updat 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.
So we need to make this translation and that adds complexity.
3) Part-level transforms
It's not just that the DOM coordinates and SVG coordinate system is different, each
part also has it's own transforms applied and because of this behaves as if they have
their own coordinate system.
In other words, a point (0,0) in the part is not the top-left corner of the page.
In the best-case scenario, it's the top-left corner of the part. But even this is
often not the case as parts will have transforms applied to them.
4) Flip along X or Y axis
Parts can be flipped along the X or Y axis to facilitate a custom layout.
This is handled in a transform, so the part's coordinate's don't actually change. They
are flipped late into the rendering process (by the browser displaying the SVG).
Handling this adds yet more mental overhead
5) Bounding box
While moving and rotating parts around is one thing. Recalculating the bounding box
(think auto-cropping the pattern) gets kinda complicated because of the reasons
outlined above.
We are currently handling a lot in the frontend code. It might be more elegant to move
some of this to core. For example, core expects the custom layout to set the widht and height
rather than figuring it out on its own as it does for auto-generated layouts.
Known issues
- Rotating gets a little weird sometimes as the part rotates around it's center in the
SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
used to calculate the angle of the rotation
- Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
- Rotation gets weird when a part is mirrored
- The bounding box update when a part is rotated is not entirely accurate
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.
2022-03-12 18:53:47 +01:00
|
|
|
if (rotate) rotation = angle(partRect, { x:event.x, y: event.y }) * -1
|
|
|
|
else {
|
2022-03-06 18:54:30 +01:00
|
|
|
translateX = event.x
|
|
|
|
translateY = event.y
|
|
|
|
}
|
2022-02-25 08:30:03 +01:00
|
|
|
const me = select(this);
|
2022-03-06 18:54:30 +01:00
|
|
|
me.attr('transform', generateTransform(translateX, translateY, rotation, flipX, flipY, part))
|
|
|
|
})
|
feat(lab): First stab at custom layout
This adds a React component to handle custom layouts.
This React component is a long way from perfect, but it's a start.
There are two 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 updat 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.
So we need to make this translation and that adds complexity.
3) Part-level transforms
It's not just that the DOM coordinates and SVG coordinate system is different, each
part also has it's own transforms applied and because of this behaves as if they have
their own coordinate system.
In other words, a point (0,0) in the part is not the top-left corner of the page.
In the best-case scenario, it's the top-left corner of the part. But even this is
often not the case as parts will have transforms applied to them.
4) Flip along X or Y axis
Parts can be flipped along the X or Y axis to facilitate a custom layout.
This is handled in a transform, so the part's coordinate's don't actually change. They
are flipped late into the rendering process (by the browser displaying the SVG).
Handling this adds yet more mental overhead
5) Bounding box
While moving and rotating parts around is one thing. Recalculating the bounding box
(think auto-cropping the pattern) gets kinda complicated because of the reasons
outlined above.
We are currently handling a lot in the frontend code. It might be more elegant to move
some of this to core. For example, core expects the custom layout to set the widht and height
rather than figuring it out on its own as it does for auto-generated layouts.
Known issues
- Rotating gets a little weird sometimes as the part rotates around it's center in the
SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
used to calculate the angle of the rotation
- Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
- Rotation gets weird when a part is mirrored
- The bounding box update when a part is rotated is not entirely accurate
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.
2022-03-12 18:53:47 +01:00
|
|
|
.on('end', function(event) {
|
|
|
|
updateLayout()
|
|
|
|
})
|
2022-03-06 18:54:30 +01:00
|
|
|
|
|
|
|
const resetPart = () => {
|
|
|
|
rotation = 0
|
|
|
|
flipX = 0
|
|
|
|
flipY = 0
|
|
|
|
updateLayout()
|
|
|
|
}
|
|
|
|
const toggleDragRotate = () => {
|
|
|
|
updateLayout()
|
|
|
|
setRotate(!rotate)
|
|
|
|
}
|
|
|
|
const updateLayout = () => {
|
feat(lab): First stab at custom layout
This adds a React component to handle custom layouts.
This React component is a long way from perfect, but it's a start.
There are two 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 updat 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.
So we need to make this translation and that adds complexity.
3) Part-level transforms
It's not just that the DOM coordinates and SVG coordinate system is different, each
part also has it's own transforms applied and because of this behaves as if they have
their own coordinate system.
In other words, a point (0,0) in the part is not the top-left corner of the page.
In the best-case scenario, it's the top-left corner of the part. But even this is
often not the case as parts will have transforms applied to them.
4) Flip along X or Y axis
Parts can be flipped along the X or Y axis to facilitate a custom layout.
This is handled in a transform, so the part's coordinate's don't actually change. They
are flipped late into the rendering process (by the browser displaying the SVG).
Handling this adds yet more mental overhead
5) Bounding box
While moving and rotating parts around is one thing. Recalculating the bounding box
(think auto-cropping the pattern) gets kinda complicated because of the reasons
outlined above.
We are currently handling a lot in the frontend code. It might be more elegant to move
some of this to core. For example, core expects the custom layout to set the widht and height
rather than figuring it out on its own as it does for auto-generated layouts.
Known issues
- Rotating gets a little weird sometimes as the part rotates around it's center in the
SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
used to calculate the angle of the rotation
- Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
- Rotation gets weird when a part is mirrored
- The bounding box update when a part is rotated is not entirely accurate
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.
2022-03-12 18:53:47 +01:00
|
|
|
props.updateLayout(name, {
|
|
|
|
move: {
|
|
|
|
x: translateX,
|
|
|
|
y: translateY,
|
|
|
|
},
|
|
|
|
rotate: rotation,
|
|
|
|
flipX,
|
|
|
|
flipY
|
|
|
|
})
|
2022-03-06 18:54:30 +01:00
|
|
|
}
|
2022-02-25 08:30:03 +01:00
|
|
|
|
2022-03-06 18:54:30 +01:00
|
|
|
// Method to flip (mirror) the part along the X or Y axis
|
|
|
|
const flip = axis => {
|
|
|
|
if (axis === 'x') flipX = !flipX
|
|
|
|
else flipY = !flipY
|
|
|
|
updateLayout()
|
|
|
|
}
|
2022-02-25 08:30:03 +01:00
|
|
|
|
|
|
|
const grid = gist.paperless ? (
|
|
|
|
<rect
|
|
|
|
x={part.topLeft.x}
|
|
|
|
y={part.topLeft.y}
|
|
|
|
width={part.width}
|
|
|
|
height={part.height}
|
|
|
|
className="grid"
|
2022-03-06 18:54:30 +01:00
|
|
|
fill={'url(#grid-' + eame + ')'}
|
2022-02-25 08:30:03 +01:00
|
|
|
/>
|
|
|
|
) : null
|
|
|
|
|
|
|
|
return (
|
2022-03-06 18:54:30 +01:00
|
|
|
<g
|
|
|
|
{...getProps(part)}
|
|
|
|
id={`part-${name}`}
|
|
|
|
ref={props.name === 'pages' ? null : partRef}
|
|
|
|
onClick={toggleDragRotate}
|
|
|
|
>
|
2022-02-25 08:30:03 +01:00
|
|
|
{grid}
|
|
|
|
{
|
|
|
|
props.gist?._state?.xray?.enabled &&
|
2022-03-06 18:54:30 +01:00
|
|
|
props.gist?._state?.xray?.reveal?.[name]
|
2022-02-25 08:30:03 +01:00
|
|
|
&& <XrayPart {...props} />
|
|
|
|
}
|
|
|
|
{Object.keys(part.paths).map((pathName) => (
|
|
|
|
<Path
|
|
|
|
key={pathName}
|
|
|
|
pathName={pathName}
|
|
|
|
path={part.paths[pathName]}
|
|
|
|
topLeft={props.part.topLeft}
|
|
|
|
bottomRight={props.part.bottomRight}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
{Object.keys(props.part.points).map((pointName) => (
|
|
|
|
<Point
|
|
|
|
key={pointName}
|
|
|
|
pointName={pointName}
|
|
|
|
point={props.part.points[pointName]}
|
|
|
|
topLeft={props.part.topLeft}
|
|
|
|
bottomRight={props.part.bottomRight}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
{Object.keys(props.part.snippets).map((snippetName) => (
|
|
|
|
<Snippet
|
|
|
|
key={snippetName}
|
|
|
|
snippetName={snippetName}
|
|
|
|
snippet={props.part.snippets[snippetName]}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
))}
|
2022-03-06 18:54:30 +01:00
|
|
|
<text x={center.x} y={center.y} ref={centerRef} />
|
2022-02-25 08:30:03 +01:00
|
|
|
<rect
|
|
|
|
x={part.topLeft.x}
|
|
|
|
y={part.topLeft.y}
|
|
|
|
width={part.width}
|
|
|
|
height={part.height}
|
2022-03-06 18:54:30 +01:00
|
|
|
className={`layout-rect ${rotate ? 'rotate' : 'move'}`}
|
2022-02-25 08:30:03 +01:00
|
|
|
/>
|
2022-03-06 18:54:30 +01:00
|
|
|
{props.name !== 'pages' && <Buttons
|
|
|
|
transform={`translate(${part.topLeft.x + part.width/2}, ${part.topLeft.y + part.height/2})`}
|
|
|
|
flip={flip}
|
|
|
|
rotate={rotate}
|
|
|
|
setRotate={setRotate}
|
|
|
|
resetPart={resetPart}
|
|
|
|
/>}
|
2022-02-25 08:30:03 +01:00
|
|
|
</g>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-02-20 18:46:21 +01:00
|
|
|
const Draft = props => {
|
2022-03-13 14:37:17 +01:00
|
|
|
const { patternProps, gist, updateGist ,app, bgProps={} } = props
|
2022-03-06 18:54:30 +01:00
|
|
|
const { layout=false } = gist
|
|
|
|
|
|
|
|
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
|
2022-02-20 18:46:21 +01:00
|
|
|
|
feat(lab): First stab at custom layout
This adds a React component to handle custom layouts.
This React component is a long way from perfect, but it's a start.
There are two 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 updat 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.
So we need to make this translation and that adds complexity.
3) Part-level transforms
It's not just that the DOM coordinates and SVG coordinate system is different, each
part also has it's own transforms applied and because of this behaves as if they have
their own coordinate system.
In other words, a point (0,0) in the part is not the top-left corner of the page.
In the best-case scenario, it's the top-left corner of the part. But even this is
often not the case as parts will have transforms applied to them.
4) Flip along X or Y axis
Parts can be flipped along the X or Y axis to facilitate a custom layout.
This is handled in a transform, so the part's coordinate's don't actually change. They
are flipped late into the rendering process (by the browser displaying the SVG).
Handling this adds yet more mental overhead
5) Bounding box
While moving and rotating parts around is one thing. Recalculating the bounding box
(think auto-cropping the pattern) gets kinda complicated because of the reasons
outlined above.
We are currently handling a lot in the frontend code. It might be more elegant to move
some of this to core. For example, core expects the custom layout to set the widht and height
rather than figuring it out on its own as it does for auto-generated layouts.
Known issues
- Rotating gets a little weird sometimes as the part rotates around it's center in the
SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
used to calculate the angle of the rotation
- Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
- Rotation gets weird when a part is mirrored
- The bounding box update when a part is rotated is not entirely accurate
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.
2022-03-12 18:53:47 +01:00
|
|
|
// Helper method to update part layout and re-calculate width * height
|
|
|
|
const updateLayout = (name, config) => {
|
|
|
|
// Start creating new layout
|
|
|
|
const newLayout = {...layout}
|
|
|
|
newLayout.parts[name] = config
|
|
|
|
newLayout.width = layout.width
|
|
|
|
newLayout.height = layout.height
|
|
|
|
// Pattern topLeft and bottomRight
|
|
|
|
let topLeft = { x: 0, y: 0 }
|
|
|
|
let bottomRight = { x: 0, y: 0 }
|
|
|
|
for (const [pname, part] of Object.entries(patternProps.parts)) {
|
|
|
|
// Pages part does not have its topLeft and bottomRight set by core since it's added post-draft
|
|
|
|
if (part.topLeft) {
|
|
|
|
// Find topLeft (tl) and bottomRight (br) of this part
|
|
|
|
const tl = {
|
|
|
|
x: part.topLeft.x + newLayout.parts[pname].move.x,
|
|
|
|
y: part.topLeft.y + newLayout.parts[pname].move.y
|
|
|
|
}
|
|
|
|
const br = {
|
|
|
|
x: part.bottomRight.x + newLayout.parts[pname].move.x,
|
|
|
|
y: part.bottomRight.y + newLayout.parts[pname].move.y
|
|
|
|
}
|
|
|
|
// Handle rotate
|
|
|
|
if (newLayout.parts[pname].rotate) {
|
|
|
|
// Angle to the corners
|
|
|
|
const center = {
|
|
|
|
x: part.topLeft.x + part.width/2,
|
|
|
|
y: part.topLeft.x + part.height/2,
|
|
|
|
}
|
|
|
|
const corners = {
|
|
|
|
tl: part.topLeft,
|
|
|
|
br: part.bottomRight,
|
|
|
|
}
|
|
|
|
const angles = {}
|
|
|
|
for (const corner in corners) angles[corner] = angle(center, corners[corner])
|
|
|
|
const delta = {}
|
|
|
|
const rotation = newLayout.parts[pname].rotate
|
|
|
|
for (const corner in corners) {
|
|
|
|
delta[corner] = {
|
|
|
|
x: part.width/2 * (Math.cos(angles[corner]) - Math.cos(angles[corner] + rotation)),
|
|
|
|
y: part.height/2 * (Math.sin(angles[corner]) - Math.sin(angles[corner] + rotation))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (delta.br.x > 0) br.x += delta.br.x
|
|
|
|
if (delta.br.y > 0) br.y += delta.br.y
|
|
|
|
if (delta.tl.x < 0) tl.x -= delta.tl.x
|
|
|
|
if (delta.tl.y < 0) tl.y -= delta.tl.y
|
|
|
|
}
|
|
|
|
if (tl.x < topLeft.x) topLeft.x = tl.x
|
|
|
|
if (tl.y < topLeft.y) topLeft.y = tl.y
|
|
|
|
if (br.x > bottomRight.x) bottomRight.x = br.x
|
|
|
|
if (br.y > bottomRight.y) bottomRight.y = br.y
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newLayout.width = bottomRight.x - topLeft.x
|
|
|
|
newLayout.height = bottomRight.y - topLeft.y
|
|
|
|
updateGist(['layout'], newLayout)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-03-06 18:54:30 +01:00
|
|
|
// 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)
|
2022-02-25 08:30:03 +01:00
|
|
|
|
2022-02-20 18:46:21 +01:00
|
|
|
return (
|
2022-03-06 18:54:30 +01:00
|
|
|
<div className="my-8 w-11/12 m-auto border-2 border-dotted border-base-content shadow">
|
2022-02-20 18:46:21 +01:00
|
|
|
<Svg {...patternProps} embed={gist.embed}>
|
|
|
|
<Defs {...patternProps} />
|
|
|
|
<style>{`:root { --pattern-scale: ${gist.scale || 1}}`}</style>
|
|
|
|
<g>
|
|
|
|
<rect x="0" y="0" width={patternProps.width} height={patternProps.height} {...bgProps} />
|
2022-03-06 18:54:30 +01:00
|
|
|
{[
|
|
|
|
partList.filter(name => name === 'pages'),
|
|
|
|
partList.filter(name => name !== 'pages'),
|
|
|
|
].map(list => list.map(name => (
|
|
|
|
<Part {...{
|
|
|
|
key:name,
|
|
|
|
name,
|
|
|
|
part: patternProps.parts[name],
|
|
|
|
layout,
|
|
|
|
app,
|
|
|
|
gist,
|
feat(lab): First stab at custom layout
This adds a React component to handle custom layouts.
This React component is a long way from perfect, but it's a start.
There are two 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 updat 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.
So we need to make this translation and that adds complexity.
3) Part-level transforms
It's not just that the DOM coordinates and SVG coordinate system is different, each
part also has it's own transforms applied and because of this behaves as if they have
their own coordinate system.
In other words, a point (0,0) in the part is not the top-left corner of the page.
In the best-case scenario, it's the top-left corner of the part. But even this is
often not the case as parts will have transforms applied to them.
4) Flip along X or Y axis
Parts can be flipped along the X or Y axis to facilitate a custom layout.
This is handled in a transform, so the part's coordinate's don't actually change. They
are flipped late into the rendering process (by the browser displaying the SVG).
Handling this adds yet more mental overhead
5) Bounding box
While moving and rotating parts around is one thing. Recalculating the bounding box
(think auto-cropping the pattern) gets kinda complicated because of the reasons
outlined above.
We are currently handling a lot in the frontend code. It might be more elegant to move
some of this to core. For example, core expects the custom layout to set the widht and height
rather than figuring it out on its own as it does for auto-generated layouts.
Known issues
- Rotating gets a little weird sometimes as the part rotates around it's center in the
SVG coordinate system, but the mouse uses it's own coordinates as the center point that's
used to calculate the angle of the rotation
- Moving parts into the negative space (minus X or Y coordinated) does not extend the bounding box.
- Rotation gets weird when a part is mirrored
- The bounding box update when a part is rotated is not entirely accurate
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.
2022-03-12 18:53:47 +01:00
|
|
|
updateLayout,
|
2022-03-06 18:54:30 +01:00
|
|
|
}}/>
|
|
|
|
)))}
|
2022-02-20 18:46:21 +01:00
|
|
|
</g>
|
|
|
|
</Svg>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Draft
|
|
|
|
|