movable pattern for print layout
This commit is contained in:
parent
0e329e195d
commit
b879e23018
10 changed files with 591 additions and 79 deletions
|
@ -1,3 +1,7 @@
|
||||||
import React from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
export const Group = (props) => <g {...props}>{props.children}</g>
|
export const Group = forwardRef((props, ref) => (
|
||||||
|
<g {...props} ref={ref}>
|
||||||
|
{props.children}
|
||||||
|
</g>
|
||||||
|
))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react'
|
import { forwardRef } from 'react'
|
||||||
// Components that can be swizzled
|
// Components that can be swizzled
|
||||||
import { Svg as DefaultSvg } from './svg.mjs'
|
import { Svg as DefaultSvg } from './svg.mjs'
|
||||||
import { Defs as DefaultDefs } from './defs.mjs'
|
import { Defs as DefaultDefs } from './defs.mjs'
|
||||||
|
@ -10,6 +10,7 @@ import { Snippet as DefaultSnippet } from './snippet.mjs'
|
||||||
import { Path as DefaultPath } from './path.mjs'
|
import { Path as DefaultPath } from './path.mjs'
|
||||||
import { Grid as DefaultGrid } from './grid.mjs'
|
import { Grid as DefaultGrid } from './grid.mjs'
|
||||||
import { Text as DefaultText, TextOnPath as DefaultTextOnPath } from './text.mjs'
|
import { Text as DefaultText, TextOnPath as DefaultTextOnPath } from './text.mjs'
|
||||||
|
import { Circle as DefaultCircle } from './circle.mjs'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Allow people to swizzle these components
|
* Allow people to swizzle these components
|
||||||
|
@ -26,55 +27,60 @@ const defaultComponents = {
|
||||||
Grid: DefaultGrid,
|
Grid: DefaultGrid,
|
||||||
Text: DefaultText,
|
Text: DefaultText,
|
||||||
TextOnPath: DefaultTextOnPath,
|
TextOnPath: DefaultTextOnPath,
|
||||||
|
Circle: DefaultCircle,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Pattern = ({
|
export const Pattern = forwardRef(
|
||||||
renderProps = false,
|
(
|
||||||
t = (string) => string,
|
{
|
||||||
components = {},
|
renderProps = false,
|
||||||
children = false,
|
t = (string) => string,
|
||||||
className = 'freesewing pattern',
|
components = {},
|
||||||
ref = false,
|
children = false,
|
||||||
}) => {
|
className = 'freesewing pattern',
|
||||||
if (!renderProps) return null
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
if (!renderProps) return null
|
||||||
|
|
||||||
// Merge default and swizzled components
|
// Merge default and swizzled components
|
||||||
components = {
|
components = {
|
||||||
...defaultComponents,
|
...defaultComponents,
|
||||||
...components,
|
...components,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Svg, Defs, Stack, Group } = components
|
||||||
|
|
||||||
|
const optionalProps = {}
|
||||||
|
if (className) optionalProps.className = className
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Svg
|
||||||
|
viewBox={`0 0 ${renderProps.width} ${renderProps.height}`}
|
||||||
|
embed={renderProps.settings.embed}
|
||||||
|
{...renderProps}
|
||||||
|
{...optionalProps}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Defs {...renderProps} />
|
||||||
|
<style>{`:root { --pattern-scale: ${renderProps.settings.scale || 1}} ${
|
||||||
|
renderProps.svg.style
|
||||||
|
}`}</style>
|
||||||
|
<Group>
|
||||||
|
{children
|
||||||
|
? children
|
||||||
|
: Object.keys(renderProps.stacks).map((stackName) => (
|
||||||
|
<Stack
|
||||||
|
key={stackName}
|
||||||
|
stackName={stackName}
|
||||||
|
stack={renderProps.stacks[stackName]}
|
||||||
|
settings={renderProps.settings}
|
||||||
|
components={components}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
const { Svg, Defs, Stack, Group } = components
|
|
||||||
|
|
||||||
const optionalProps = {}
|
|
||||||
if (ref) optionalProps.ref = ref
|
|
||||||
if (className) optionalProps.className = className
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Svg
|
|
||||||
viewBox={`0 0 ${renderProps.width} ${renderProps.height}`}
|
|
||||||
embed={renderProps.settings.embed}
|
|
||||||
{...renderProps}
|
|
||||||
{...optionalProps}
|
|
||||||
>
|
|
||||||
<Defs {...renderProps} />
|
|
||||||
<style>{`:root { --pattern-scale: ${renderProps.settings.scale || 1}} ${
|
|
||||||
renderProps.svg.style
|
|
||||||
}`}</style>
|
|
||||||
<Group>
|
|
||||||
{children
|
|
||||||
? children
|
|
||||||
: Object.keys(renderProps.stacks).map((stackName) => (
|
|
||||||
<Stack
|
|
||||||
key={stackName}
|
|
||||||
stackName={stackName}
|
|
||||||
stack={renderProps.stacks[stackName]}
|
|
||||||
settings={renderProps.settings}
|
|
||||||
components={components}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Group>
|
|
||||||
</Svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export const getProps = (obj) => {
|
export const getProps = (obj) => {
|
||||||
/** I can't believe it but there seems to be no method on NPM todo this */
|
/** I can't believe it but there seems to be no method on NPM todo this */
|
||||||
const cssKey = (key) => {
|
const cssKey = (key) => {
|
||||||
|
@ -38,6 +36,16 @@ export const getProps = (obj) => {
|
||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const dx = (pointA, pointB) => pointB.x - pointA.x
|
||||||
|
export const dy = (pointA, pointB) => pointB.y - pointA.y
|
||||||
|
export const rad2deg = (radians) => radians * 57.29577951308232
|
||||||
|
export 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)
|
||||||
|
}
|
||||||
|
|
||||||
export const withinPartBounds = (point, part) =>
|
export const withinPartBounds = (point, part) =>
|
||||||
point.x >= part.topLeft.x &&
|
point.x >= part.topLeft.x &&
|
||||||
point.x <= part.bottomRight.x &&
|
point.x <= part.bottomRight.x &&
|
||||||
|
|
|
@ -84,8 +84,9 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
|
||||||
viewContent = <SaveView {...viewProps} from={from} />
|
viewContent = <SaveView {...viewProps} from={from} />
|
||||||
break
|
break
|
||||||
default: {
|
default: {
|
||||||
|
const layout = ui.layouts?.[view] || settings.layout || true
|
||||||
// Generate the pattern here so we can pass it down to both the view and the options menu
|
// Generate the pattern here so we can pass it down to both the view and the options menu
|
||||||
const pattern = settings.measurements !== undefined && new Design(settings)
|
const pattern = settings.measurements !== undefined && new Design({ layout, ...settings })
|
||||||
|
|
||||||
// Return early if the pattern is not initialized yet
|
// Return early if the pattern is not initialized yet
|
||||||
if (typeof pattern.getConfig !== 'function') return null
|
if (typeof pattern.getConfig !== 'function') return null
|
||||||
|
@ -107,7 +108,7 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const View = views[view]
|
const View = views[view]
|
||||||
viewContent = <View {...viewProps} {...{ pattern, patternConfig }} />
|
viewContent = <View {...{ ...viewProps, pattern, patternConfig }} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ export const PanZoomPattern = forwardRef((props, ref) => {
|
||||||
>
|
>
|
||||||
<TransformComponent>
|
<TransformComponent>
|
||||||
<div style={{ width: size.width + 'px' }} className="max-h-screen">
|
<div style={{ width: size.width + 'px' }} className="max-h-screen">
|
||||||
<Pattern {...{ t, ref, components, renderProps }} />
|
<Pattern {...{ t, components, renderProps }} ref={ref} />
|
||||||
</div>
|
</div>
|
||||||
</TransformComponent>
|
</TransformComponent>
|
||||||
</TransformWrapper>
|
</TransformWrapper>
|
||||||
|
|
113
sites/shared/components/workbench/pattern/movable/index.mjs
Normal file
113
sites/shared/components/workbench/pattern/movable/index.mjs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import { Stack } from './stack.mjs'
|
||||||
|
// import { SvgWrapper } from '../../pattern/svg.mjs'
|
||||||
|
// import { PartInner } from '../../pattern/part.mjs'
|
||||||
|
import { PanZoomPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
|
||||||
|
import { MovableStack } from './stack.mjs'
|
||||||
|
import get from 'lodash.get'
|
||||||
|
|
||||||
|
export const MovablePattern = ({
|
||||||
|
design,
|
||||||
|
pattern,
|
||||||
|
renderProps,
|
||||||
|
patternConfig,
|
||||||
|
settings,
|
||||||
|
ui,
|
||||||
|
update,
|
||||||
|
bgProps = {},
|
||||||
|
fitImmovable = false,
|
||||||
|
immovable = [],
|
||||||
|
layoutPath,
|
||||||
|
}) => {
|
||||||
|
const svgRef = useRef(null)
|
||||||
|
if (!renderProps) return null
|
||||||
|
|
||||||
|
// keep a fresh copy of the layout because we might manipulate it without saving to the gist
|
||||||
|
let layout =
|
||||||
|
renderProps.settings[0].layout === true
|
||||||
|
? {
|
||||||
|
...renderProps.autoLayout,
|
||||||
|
width: renderProps.width,
|
||||||
|
height: renderProps.height,
|
||||||
|
}
|
||||||
|
: renderProps.settings[0].layout
|
||||||
|
|
||||||
|
// 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 renderProps.stacks) {
|
||||||
|
if (immovable.includes(pname) && !fitImmovable) 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) {
|
||||||
|
update.ui(layoutPath, newLayout)
|
||||||
|
} 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
|
||||||
|
|
||||||
|
const sortedStacks = {}
|
||||||
|
Object.keys(renderProps.stacks)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const hasA = immovable.includes(a)
|
||||||
|
const hasB = immovable.includes(b)
|
||||||
|
if (hasA && !hasB) return -1
|
||||||
|
if (!hasA && hasB) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
.forEach((s) => (sortedStacks[s] = renderProps.stacks[s]))
|
||||||
|
|
||||||
|
const sortedRenderProps = { ...renderProps, stacks: sortedStacks }
|
||||||
|
|
||||||
|
const Stack = ({ stackName, stack, settings, components, t }) => (
|
||||||
|
<MovableStack
|
||||||
|
{...{
|
||||||
|
stackName,
|
||||||
|
stack,
|
||||||
|
settings,
|
||||||
|
components,
|
||||||
|
t,
|
||||||
|
movable: !immovable.includes(stackName),
|
||||||
|
layout: layout.stacks[stackName],
|
||||||
|
updateLayout,
|
||||||
|
showButtons: get(ui, 'showMovableButtons', true),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanZoomPattern
|
||||||
|
{...{
|
||||||
|
renderProps: sortedRenderProps,
|
||||||
|
components: { Stack },
|
||||||
|
}}
|
||||||
|
ref={svgRef}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
264
sites/shared/components/workbench/pattern/movable/stack.mjs
Normal file
264
sites/shared/components/workbench/pattern/movable/stack.mjs
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
/*
|
||||||
|
* 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 { Stack } from 'pkgs/react-components/src/pattern/stack.mjs'
|
||||||
|
import { getProps, angle } from 'pkgs/react-components/src/pattern/utils.mjs'
|
||||||
|
import { drag } from 'd3-drag'
|
||||||
|
import { select } from 'd3-selection'
|
||||||
|
import { Buttons } from './transform-buttons.mjs'
|
||||||
|
import get from 'lodash.get'
|
||||||
|
|
||||||
|
export const MovableStack = ({
|
||||||
|
stackName,
|
||||||
|
stack,
|
||||||
|
settings,
|
||||||
|
components,
|
||||||
|
t,
|
||||||
|
movable = true,
|
||||||
|
layout,
|
||||||
|
updateLayout,
|
||||||
|
showButtons,
|
||||||
|
}) => {
|
||||||
|
const stackExists = !movable || typeof layout?.move?.x !== 'undefined'
|
||||||
|
|
||||||
|
// Use a ref for direct DOM manipulation
|
||||||
|
const stackRef = 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 && movable) {
|
||||||
|
updateStacklayout(false)
|
||||||
|
}
|
||||||
|
}, [stackRef, layout])
|
||||||
|
|
||||||
|
// Initialize drag handler
|
||||||
|
useEffect(() => {
|
||||||
|
// don't drag the pages
|
||||||
|
if (!movable || !stackExists) return
|
||||||
|
handleDrag(select(stackRef.current))
|
||||||
|
}, [rotate, stackRef, layout])
|
||||||
|
|
||||||
|
// // Don't just assume this makes sense
|
||||||
|
if (!stackExists) return null
|
||||||
|
|
||||||
|
if (!movable) return <Stack {...{ stackName, stack, settings, components, t }} />
|
||||||
|
|
||||||
|
// 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 = layout.move.x
|
||||||
|
let translateY = layout.move.y
|
||||||
|
let stackRotation = layout.rotate || 0
|
||||||
|
let rotation = stackRotation
|
||||||
|
let flipX = !!layout.flipX
|
||||||
|
let flipY = !!layout.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) updateStacklayout()
|
||||||
|
|
||||||
|
didDrag = false
|
||||||
|
})
|
||||||
|
|
||||||
|
/** reset the part's transforms */
|
||||||
|
const resetPart = () => {
|
||||||
|
rotation = 0
|
||||||
|
flipX = 0
|
||||||
|
flipY = 0
|
||||||
|
updateStacklayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** toggle between dragging and rotating */
|
||||||
|
const toggleDragRotate = () => {
|
||||||
|
// only respond if the part should be able to drag/rotate
|
||||||
|
if (!stackRef.current || !movable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setRotate(!rotate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** update the layout either locally or in the gist */
|
||||||
|
const updateStacklayout = (history = true) => {
|
||||||
|
/** don't mess with what we don't lay out */
|
||||||
|
if (!stackRef.current || !movable) 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
|
||||||
|
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
|
||||||
|
updateStacklayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** method to rotate 90 degrees */
|
||||||
|
const rotate90 = (direction = 1) => {
|
||||||
|
if (flipX) direction *= -1
|
||||||
|
if (flipY) direction *= -1
|
||||||
|
|
||||||
|
rotation += 90 * direction
|
||||||
|
|
||||||
|
updateStacklayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Group, Part } = components
|
||||||
|
return (
|
||||||
|
<Group id={`stack-${stackName}`} {...getProps(stack)} ref={stackRef}>
|
||||||
|
<Group id={`stack-inner-${stackName}`} ref={innerRef}>
|
||||||
|
{[...stack.parts].map((part, key) => (
|
||||||
|
<Part {...{ settings, components, t, part, stackName, key }} />
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
{movable && (
|
||||||
|
<>
|
||||||
|
<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}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,26 +2,19 @@ import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
// import { PrintLayoutSettings } from './settings.mjs'
|
// import { PrintLayoutSettings } from './settings.mjs'
|
||||||
// import { Draft } from '../draft/index.mjs'
|
// import { Draft } from '../draft/index.mjs'
|
||||||
import { pagesPlugin } from 'shared/components/workbench/layout/plugin-layout-part.mjs'
|
import { pagesPlugin } from 'shared/plugins/plugin-layout-part.mjs'
|
||||||
// import {
|
// import {
|
||||||
// handleExport,
|
// handleExport,
|
||||||
// defaultPdfSettings,
|
// defaultPdfSettings,
|
||||||
// } from 'shared/components/workbench/exporting/export-handler.mjs'
|
// } from 'shared/components/workbench/exporting/export-handler.mjs'
|
||||||
// import { Popout } from 'shared/components/popout.mjs'
|
// import { Popout } from 'shared/components/popout.mjs'
|
||||||
import get from 'lodash.get'
|
import get from 'lodash.get'
|
||||||
import { PanZoomPattern as ShowPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
|
import { MovablePattern } from 'shared/components/workbench/pattern/movable/index.mjs'
|
||||||
import { PrintMenu, ns as menuNs } from './menu.mjs'
|
import { PrintMenu, ns as menuNs } from './menu.mjs'
|
||||||
|
import { defaultPrintSettings } from './config.mjs'
|
||||||
|
|
||||||
const viewNs = ['print']
|
const viewNs = ['print']
|
||||||
export const ns = [...viewNs, ...menuNs]
|
export const ns = [...viewNs]
|
||||||
|
|
||||||
const defaultPdfSettings = {
|
|
||||||
size: 'a4',
|
|
||||||
orientation: 'portrait',
|
|
||||||
margin: 10,
|
|
||||||
coverPage: true,
|
|
||||||
cutlist: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrintView = ({
|
export const PrintView = ({
|
||||||
design,
|
design,
|
||||||
|
@ -35,27 +28,23 @@ export const PrintView = ({
|
||||||
account,
|
account,
|
||||||
DynamicDocs,
|
DynamicDocs,
|
||||||
}) => {
|
}) => {
|
||||||
// disable xray
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (props.gist?._state?.xray?.enabled) props.updateGist(['_state', 'xray', 'enabled'], false)
|
|
||||||
// })
|
|
||||||
|
|
||||||
const { t } = useTranslation(viewNs)
|
const { t } = useTranslation(viewNs)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
|
|
||||||
|
const defaultSettings = defaultPrintSettings(settings.units)
|
||||||
// add the pages plugin to the draft
|
// add the pages plugin to the draft
|
||||||
const layoutSettings = {
|
const layoutSettings = {
|
||||||
...defaultPdfSettings,
|
...defaultSettings,
|
||||||
...get(ui, ['layout', 'print', 'page']),
|
...get(ui, ['print', 'page']),
|
||||||
}
|
}
|
||||||
|
|
||||||
pattern.use(pagesPlugin(layoutSettings))
|
pattern.use(pagesPlugin(layoutSettings))
|
||||||
|
|
||||||
let patternProps
|
let renderProps
|
||||||
try {
|
try {
|
||||||
// draft the pattern
|
// draft the pattern
|
||||||
pattern.draft()
|
pattern.draft()
|
||||||
patternProps = pattern.getRenderProps()
|
renderProps = pattern.getRenderProps()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
|
@ -80,7 +69,9 @@ export const PrintView = ({
|
||||||
<h2 className="capitalize">{t('layoutThing', { thing: name }) + ': ' + t('forPrinting')}</h2>
|
<h2 className="capitalize">{t('layoutThing', { thing: name }) + ': ' + t('forPrinting')}</h2>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="w-2/3 shrink-0 grow lg:p-4 sticky top-0">
|
<div className="w-2/3 shrink-0 grow lg:p-4 sticky top-0">
|
||||||
<ShowPattern renderProps={patternProps} />
|
<MovablePattern
|
||||||
|
{...{ renderProps, update, immovable: ['pages'], layoutPath: ['layouts', 'print'] }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-screen overflow-scroll">
|
<div className="w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-screen overflow-scroll">
|
||||||
<PrintMenu
|
<PrintMenu
|
||||||
|
|
|
@ -206,7 +206,9 @@ const basePlugin = ({
|
||||||
const { points, Point, paths, Path, part, macro, store } = shorthand
|
const { points, Point, paths, Path, part, macro, store } = shorthand
|
||||||
let count = 0
|
let count = 0
|
||||||
let withContent = {}
|
let withContent = {}
|
||||||
part.topLeft = so.layout.topLeft || { x: 0, y: 0 }
|
part.topLeft = so.layout.topLeft
|
||||||
|
? new Point(so.layout.topLeft.x, so.layout.topLeft.y)
|
||||||
|
: new Point(0, 0)
|
||||||
|
|
||||||
// get the layout from the pattern
|
// get the layout from the pattern
|
||||||
const { layout } = so
|
const { layout } = so
|
||||||
|
@ -265,7 +267,7 @@ const basePlugin = ({
|
||||||
const br = points[`${pageName}-br`]
|
const br = points[`${pageName}-br`]
|
||||||
part.width = br.x
|
part.width = br.x
|
||||||
part.height = br.y
|
part.height = br.y
|
||||||
part.bottomRight = { x: br.x, y: br.y }
|
part.bottomRight = new Point(br.x, br.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!printStyle) {
|
if (!printStyle) {
|
Loading…
Add table
Add a link
Reference in a new issue