Adds context menu, "move to page"
This commit is contained in:
parent
c48537121f
commit
f2d3231315
17 changed files with 533 additions and 47 deletions
|
@ -51,7 +51,7 @@ export default function Bounds() {
|
||||||
|
|
||||||
if (isSingleHandles) return null
|
if (isSingleHandles) return null
|
||||||
|
|
||||||
const size = (isMobile().any ? 10 : 8) / zoom // Touch target size
|
const size = (isMobile() ? 10 : 8) / zoom // Touch target size
|
||||||
const center = getBoundsCenter(bounds)
|
const center = getBoundsCenter(bounds)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,15 +5,18 @@ import styled from 'styles'
|
||||||
import { deepCompareArrays, getPage } from 'utils/utils'
|
import { deepCompareArrays, getPage } from 'utils/utils'
|
||||||
|
|
||||||
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
|
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
|
||||||
if (e.buttons !== 1) return
|
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.currentTarget.setPointerCapture(e.pointerId)
|
e.currentTarget.setPointerCapture(e.pointerId)
|
||||||
state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
|
|
||||||
|
if (e.button === 0) {
|
||||||
|
state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
|
||||||
|
} else if (e.button === 2) {
|
||||||
|
state.send('RIGHT_POINTED', inputs.pointerDown(e, 'bounds'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
|
function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
|
||||||
if (e.buttons !== 1) return
|
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||||
|
@ -36,7 +39,7 @@ export default function BoundsBg() {
|
||||||
if (selectedIds.length === 1) {
|
if (selectedIds.length === 1) {
|
||||||
const { shapes } = getPage(s.data)
|
const { shapes } = getPage(s.data)
|
||||||
const selected = Array.from(s.values.selectedIds.values())[0]
|
const selected = Array.from(s.values.selectedIds.values())[0]
|
||||||
return shapes[selected].rotation
|
return shapes[selected]?.rotation
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,12 @@ export default function Handles() {
|
||||||
s.isInAny('notPointing', 'pinching', 'translatingHandles')
|
s.isInAny('notPointing', 'pinching', 'translatingHandles')
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!shape.handles || !isSelecting) return null
|
if (!shape || !shape.handles || !isSelecting) return null
|
||||||
|
|
||||||
const center = getShapeUtils(shape).getCenter(shape)
|
const center = getShapeUtils(shape).getCenter(shape)
|
||||||
|
|
||||||
|
console.log(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g transform={`rotate(${shape.rotation * (180 / Math.PI)},${center})`}>
|
<g transform={`rotate(${shape.rotation * (180 / Math.PI)},${center})`}>
|
||||||
{Object.values(shape.handles).map((handle) => (
|
{Object.values(shape.handles).map((handle) => (
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import inputs from 'state/inputs'
|
import React, { useRef } from 'react'
|
||||||
import React, { useCallback, useRef } from 'react'
|
|
||||||
import useZoomEvents from 'hooks/useZoomEvents'
|
import useZoomEvents from 'hooks/useZoomEvents'
|
||||||
import useCamera from 'hooks/useCamera'
|
import useCamera from 'hooks/useCamera'
|
||||||
import Defs from './defs'
|
import Defs from './defs'
|
||||||
|
@ -12,6 +11,7 @@ import BoundsBg from './bounds/bounds-bg'
|
||||||
import Selected from './selected'
|
import Selected from './selected'
|
||||||
import Handles from './bounds/handles'
|
import Handles from './bounds/handles'
|
||||||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||||
|
import ContextMenu from 'components/context-menu'
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const rCanvas = useRef<SVGSVGElement>(null)
|
const rCanvas = useRef<SVGSVGElement>(null)
|
||||||
|
@ -26,19 +26,21 @@ export default function Canvas() {
|
||||||
const isReady = useSelector((s) => s.isIn('ready'))
|
const isReady = useSelector((s) => s.isIn('ready'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainSVG ref={rCanvas} {...events}>
|
<ContextMenu>
|
||||||
<Defs />
|
<MainSVG ref={rCanvas} {...events}>
|
||||||
{isReady && (
|
<Defs />
|
||||||
<g ref={rGroup}>
|
{isReady && (
|
||||||
<BoundsBg />
|
<g ref={rGroup}>
|
||||||
<Page />
|
<BoundsBg />
|
||||||
<Selected />
|
<Page />
|
||||||
<Bounds />
|
<Selected />
|
||||||
<Handles />
|
<Bounds />
|
||||||
<Brush />
|
<Handles />
|
||||||
</g>
|
<Brush />
|
||||||
)}
|
</g>
|
||||||
</MainSVG>
|
)}
|
||||||
|
</MainSVG>
|
||||||
|
</ContextMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { ShapeStyles, ShapeType } from 'types'
|
||||||
import useShapeEvents from 'hooks/useShapeEvents'
|
import useShapeEvents from 'hooks/useShapeEvents'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
import { getShapeStyle } from 'lib/shape-styles'
|
import { getShapeStyle } from 'lib/shape-styles'
|
||||||
|
import ContextMenu from 'components/context-menu'
|
||||||
|
|
||||||
interface ShapeProps {
|
interface ShapeProps {
|
||||||
id: string
|
id: string
|
||||||
|
@ -51,6 +52,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
{...events}
|
{...events}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!shape.isHidden && <RealShape isGroup={isGroup} id={id} style={style} />}
|
{!shape.isHidden && <RealShape isGroup={isGroup} id={id} style={style} />}
|
||||||
{isGroup &&
|
{isGroup &&
|
||||||
shape.children.map((shapeId) => (
|
shape.children.map((shapeId) => (
|
||||||
|
|
297
components/context-menu.tsx
Normal file
297
components/context-menu.tsx
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
import * as _ContextMenu from '@radix-ui/react-context-menu'
|
||||||
|
import * as _Dropdown from '@radix-ui/react-dropdown-menu'
|
||||||
|
import styled from 'styles'
|
||||||
|
import { RowButton } from './shared'
|
||||||
|
import {
|
||||||
|
commandKey,
|
||||||
|
deepCompareArrays,
|
||||||
|
getSelectedShapes,
|
||||||
|
isMobile,
|
||||||
|
} from 'utils/utils'
|
||||||
|
import state, { useSelector } from 'state'
|
||||||
|
import { MoveType, ShapeType } from 'types'
|
||||||
|
import React, { useRef } from 'react'
|
||||||
|
|
||||||
|
export default function ContextMenu({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const selectedShapes = useSelector(
|
||||||
|
(s) => getSelectedShapes(s.data),
|
||||||
|
deepCompareArrays
|
||||||
|
)
|
||||||
|
|
||||||
|
const rContent = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const hasGroupSelectd = selectedShapes.some((s) => s.type === ShapeType.Group)
|
||||||
|
const hasMultipleSelected = selectedShapes.length > 1
|
||||||
|
|
||||||
|
return (
|
||||||
|
<_ContextMenu.Root>
|
||||||
|
<_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
|
||||||
|
<StyledContent ref={rContent} isMobile={isMobile()}>
|
||||||
|
{selectedShapes.length ? (
|
||||||
|
<>
|
||||||
|
{/* <Button onSelect={() => state.send('COPIED')}>
|
||||||
|
<span>Copy</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>C</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
<Button onSelect={() => state.send('CUT')}>
|
||||||
|
<span>Cut</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>X</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
*/}
|
||||||
|
<Button onSelect={() => state.send('DUPLICATED')}>
|
||||||
|
<span>Duplicate</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>D</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
<StyledDivider />
|
||||||
|
<Button
|
||||||
|
onSelect={() =>
|
||||||
|
state.send('MOVED', {
|
||||||
|
type: MoveType.ToFront,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>Move To Front</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>⇧</span>
|
||||||
|
<span>]</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onSelect={() =>
|
||||||
|
state.send('MOVED', {
|
||||||
|
type: MoveType.Forward,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>Move Forward</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>]</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onSelect={() =>
|
||||||
|
state.send('MOVED', {
|
||||||
|
type: MoveType.Backward,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>Move Backward</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>[</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onSelect={() =>
|
||||||
|
state.send('MOVED', {
|
||||||
|
type: MoveType.ToBack,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>Move to Back</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>⇧</span>
|
||||||
|
<span>[</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
{hasGroupSelectd ||
|
||||||
|
(hasMultipleSelected && (
|
||||||
|
<>
|
||||||
|
<StyledDivider />
|
||||||
|
{hasGroupSelectd && (
|
||||||
|
<Button onSelect={() => state.send('UNGROUPED')}>
|
||||||
|
<span>Ungroup</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>⇧</span>
|
||||||
|
<span>G</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasMultipleSelected && (
|
||||||
|
<Button onSelect={() => state.send('GROUPED')}>
|
||||||
|
<span>Group</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>G</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
<StyledDivider />
|
||||||
|
|
||||||
|
{/* <Button onSelect={() => state.send('MOVED_TO_PAGE')}> */}
|
||||||
|
<_ContextMenu.Item>
|
||||||
|
<MoveToPageDropDown>Move to Page</MoveToPageDropDown>
|
||||||
|
</_ContextMenu.Item>
|
||||||
|
{/* </Button> */}
|
||||||
|
<Button onSelect={() => state.send('DELETED')}>
|
||||||
|
<span>Delete</span>
|
||||||
|
<kbd>
|
||||||
|
<span>⌫</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button onSelect={() => state.send('UNDO')}>
|
||||||
|
<span>Undo</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>Z</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
<Button onSelect={() => state.send('REDO')}>
|
||||||
|
<span>Redo</span>
|
||||||
|
<kbd>
|
||||||
|
<span>{commandKey()}</span>
|
||||||
|
<span>⇧</span>
|
||||||
|
<span>Z</span>
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</StyledContent>
|
||||||
|
</_ContextMenu.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledContent = styled(_ContextMenu.Content, {
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: '$panel',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
userSelect: 'none',
|
||||||
|
zIndex: 200,
|
||||||
|
padding: 2,
|
||||||
|
border: '1px solid $panel',
|
||||||
|
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||||
|
minWidth: 128,
|
||||||
|
|
||||||
|
'& kbd': {
|
||||||
|
marginLeft: '32px',
|
||||||
|
fontSize: '$1',
|
||||||
|
fontFamily: '$ui',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& kbd > span': {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '12px',
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isMobile: {
|
||||||
|
true: {
|
||||||
|
'& kbd': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledDivider = styled(_ContextMenu.Separator, {
|
||||||
|
backgroundColor: '$hover',
|
||||||
|
height: 1,
|
||||||
|
margin: '2px -2px',
|
||||||
|
})
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
onSelect: () => void
|
||||||
|
disabled?: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<_ContextMenu.Item
|
||||||
|
as={RowButton}
|
||||||
|
disabled={disabled}
|
||||||
|
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||||
|
onSelect={onSelect}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</_ContextMenu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoveToPageDropDown({ children }: { children: React.ReactNode }) {
|
||||||
|
const documentPages = useSelector((s) => s.data.document.pages)
|
||||||
|
const currentPageId = useSelector((s) => s.data.currentPageId)
|
||||||
|
|
||||||
|
if (!documentPages[currentPageId]) return null
|
||||||
|
|
||||||
|
const sorted = Object.values(documentPages)
|
||||||
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
.filter((a) => a.id !== currentPageId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<_Dropdown.Root>
|
||||||
|
<_Dropdown.Trigger
|
||||||
|
as={RowButton}
|
||||||
|
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</_Dropdown.Trigger>
|
||||||
|
<StyledDialogContent side="right" sideOffset={8}>
|
||||||
|
{sorted.map(({ id, name }) => (
|
||||||
|
<_Dropdown.Item
|
||||||
|
as={RowButton}
|
||||||
|
key={id}
|
||||||
|
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||||
|
disabled={id === currentPageId}
|
||||||
|
onSelect={() => state.send('MOVED_TO_PAGE', { id })}
|
||||||
|
>
|
||||||
|
<span>{name}</span>
|
||||||
|
</_Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</StyledDialogContent>
|
||||||
|
</_Dropdown.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledDialogContent = styled(_Dropdown.Content, {
|
||||||
|
// position: 'fixed',
|
||||||
|
// top: '50%',
|
||||||
|
// left: '50%',
|
||||||
|
// transform: 'translate(-50%, -50%)',
|
||||||
|
// minWidth: 200,
|
||||||
|
// maxWidth: 'fit-content',
|
||||||
|
// maxHeight: '85vh',
|
||||||
|
// marginTop: '-5vh',
|
||||||
|
minWidth: 128,
|
||||||
|
backgroundColor: '$panel',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
userSelect: 'none',
|
||||||
|
zIndex: 200,
|
||||||
|
padding: 2,
|
||||||
|
border: '1px solid $panel',
|
||||||
|
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||||
|
|
||||||
|
'&:focus': {
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
|
})
|
|
@ -9,6 +9,7 @@ import StylePanel from './style-panel/style-panel'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import PagePanel from './page-panel/page-panel'
|
import PagePanel from './page-panel/page-panel'
|
||||||
|
import ContextMenu from './context-menu'
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
useKeyboardEvents()
|
useKeyboardEvents()
|
||||||
|
@ -25,11 +26,6 @@ export default function Editor() {
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<StylePanel />
|
<StylePanel />
|
||||||
<Canvas />
|
<Canvas />
|
||||||
|
|
||||||
{/* <LeftPanels>
|
|
||||||
<CodePanel />
|
|
||||||
{hasControls && <ControlsPanel />}
|
|
||||||
</LeftPanels> */}
|
|
||||||
<ToolsPanel />
|
<ToolsPanel />
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -105,6 +105,10 @@ export const RowButton = styled('button', {
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'& :disabled': {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
bp: {
|
bp: {
|
||||||
mobile: {},
|
mobile: {},
|
||||||
|
|
|
@ -9,8 +9,14 @@ export default function useCanvasEvents(
|
||||||
) {
|
) {
|
||||||
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
|
|
||||||
rCanvas.current.setPointerCapture(e.pointerId)
|
rCanvas.current.setPointerCapture(e.pointerId)
|
||||||
state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
|
|
||||||
|
if (e.button === 0) {
|
||||||
|
state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
|
||||||
|
} else if (e.button === 2) {
|
||||||
|
state.send('RIGHT_POINTED', inputs.pointerDown(e, 'canvas'))
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||||
|
|
|
@ -14,10 +14,15 @@ export default function useShapeEvents(
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
rGroup.current.setPointerCapture(e.pointerId)
|
rGroup.current.setPointerCapture(e.pointerId)
|
||||||
|
|
||||||
if (inputs.isDoubleClick()) {
|
if (e.button === 0) {
|
||||||
state.send('DOUBLE_POINTED_SHAPE', inputs.pointerDown(e, id))
|
if (inputs.isDoubleClick()) {
|
||||||
|
state.send('DOUBLE_POINTED_SHAPE', inputs.pointerDown(e, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
|
||||||
|
} else {
|
||||||
|
state.send('RIGHT_POINTED', inputs.pointerDown(e, id))
|
||||||
}
|
}
|
||||||
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
|
|
||||||
},
|
},
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,14 +2,17 @@ import align from './align'
|
||||||
import arrow from './arrow'
|
import arrow from './arrow'
|
||||||
import changePage from './change-page'
|
import changePage from './change-page'
|
||||||
import createPage from './create-page'
|
import createPage from './create-page'
|
||||||
import deleteSelected from './delete-selected'
|
|
||||||
import deletePage from './delete-page'
|
import deletePage from './delete-page'
|
||||||
|
import deleteSelected from './delete-selected'
|
||||||
import direct from './direct'
|
import direct from './direct'
|
||||||
import distribute from './distribute'
|
import distribute from './distribute'
|
||||||
import draw from './draw'
|
import draw from './draw'
|
||||||
import duplicate from './duplicate'
|
import duplicate from './duplicate'
|
||||||
import generate from './generate'
|
import generate from './generate'
|
||||||
|
import group from './group'
|
||||||
|
import handle from './handle'
|
||||||
import move from './move'
|
import move from './move'
|
||||||
|
import moveToPage from './move-to-page'
|
||||||
import nudge from './nudge'
|
import nudge from './nudge'
|
||||||
import rotate from './rotate'
|
import rotate from './rotate'
|
||||||
import rotateCcw from './rotate-ccw'
|
import rotateCcw from './rotate-ccw'
|
||||||
|
@ -19,8 +22,6 @@ import toggle from './toggle'
|
||||||
import transform from './transform'
|
import transform from './transform'
|
||||||
import transformSingle from './transform-single'
|
import transformSingle from './transform-single'
|
||||||
import translate from './translate'
|
import translate from './translate'
|
||||||
import handle from './handle'
|
|
||||||
import group from './group'
|
|
||||||
import ungroup from './ungroup'
|
import ungroup from './ungroup'
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
|
@ -35,7 +36,10 @@ const commands = {
|
||||||
draw,
|
draw,
|
||||||
duplicate,
|
duplicate,
|
||||||
generate,
|
generate,
|
||||||
|
group,
|
||||||
|
handle,
|
||||||
move,
|
move,
|
||||||
|
moveToPage,
|
||||||
nudge,
|
nudge,
|
||||||
rotate,
|
rotate,
|
||||||
rotateCcw,
|
rotateCcw,
|
||||||
|
@ -45,8 +49,6 @@ const commands = {
|
||||||
transform,
|
transform,
|
||||||
transformSingle,
|
transformSingle,
|
||||||
translate,
|
translate,
|
||||||
handle,
|
|
||||||
group,
|
|
||||||
ungroup,
|
ungroup,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
107
state/commands/move-to-page.ts
Normal file
107
state/commands/move-to-page.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import Command from './command'
|
||||||
|
import history from '../history'
|
||||||
|
import { Data } from 'types'
|
||||||
|
import {
|
||||||
|
getDocumentBranch,
|
||||||
|
getPage,
|
||||||
|
getPageState,
|
||||||
|
getSelectedIds,
|
||||||
|
getSelectedShapes,
|
||||||
|
getTopParentId,
|
||||||
|
setToArray,
|
||||||
|
uniqueArray,
|
||||||
|
} from 'utils/utils'
|
||||||
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
import * as vec from 'utils/vec'
|
||||||
|
import storage from 'state/storage'
|
||||||
|
|
||||||
|
export default function nudgeCommand(data: Data, toPageId: string) {
|
||||||
|
const { currentPageId: fromPageId } = data
|
||||||
|
const selectedIds = setToArray(getSelectedIds(data))
|
||||||
|
|
||||||
|
const selectedParents = uniqueArray(
|
||||||
|
...selectedIds.map((id) => getTopParentId(data, id))
|
||||||
|
)
|
||||||
|
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name: 'set_direction',
|
||||||
|
category: 'canvas',
|
||||||
|
manualSelection: true,
|
||||||
|
do(data) {
|
||||||
|
// The page we're moving the shapes from
|
||||||
|
const fromPage = getPage(data, fromPageId)
|
||||||
|
|
||||||
|
// Get all of the selected shapes and their descendents
|
||||||
|
const shapesToMove = selectedParents.flatMap((id) =>
|
||||||
|
getDocumentBranch(data, id).map((id) => fromPage.shapes[id])
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete the shapes from the "from" page
|
||||||
|
shapesToMove.forEach((shape) => delete fromPage.shapes[shape.id])
|
||||||
|
|
||||||
|
// Clear the current page state's selected ids
|
||||||
|
getPageState(data, fromPageId).selectedIds.clear()
|
||||||
|
|
||||||
|
// Save the "from" page
|
||||||
|
storage.savePage(data, fromPageId)
|
||||||
|
|
||||||
|
// Load the "to" page
|
||||||
|
storage.loadPage(data, toPageId)
|
||||||
|
|
||||||
|
// The page we're moving the shapes to
|
||||||
|
const toPage = getPage(data, toPageId)
|
||||||
|
|
||||||
|
// Add all of the selected shapes to the "from" page. Any shapes that
|
||||||
|
// were children of the "from" page should become children of the "to"
|
||||||
|
// page. Grouped shapes should keep their same parent.
|
||||||
|
|
||||||
|
// What about shapes that were children of a group that we haven't moved?
|
||||||
|
shapesToMove.forEach((shape) => {
|
||||||
|
toPage.shapes[shape.id] = shape
|
||||||
|
if (shape.parentId === fromPageId) {
|
||||||
|
getShapeUtils(shape).setProperty(shape, 'parentId', toPageId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('from', getPage(data, fromPageId))
|
||||||
|
console.log('to', getPage(data, toPageId))
|
||||||
|
|
||||||
|
// Select the selected ids on the new page
|
||||||
|
getPageState(data, toPageId).selectedIds = new Set(selectedIds)
|
||||||
|
|
||||||
|
// Move to the new page
|
||||||
|
data.currentPageId = toPageId
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
const toPage = getPage(data, fromPageId)
|
||||||
|
|
||||||
|
const shapesToMove = selectedParents.flatMap((id) =>
|
||||||
|
getDocumentBranch(data, id).map((id) => toPage.shapes[id])
|
||||||
|
)
|
||||||
|
|
||||||
|
shapesToMove.forEach((shape) => delete toPage.shapes[shape.id])
|
||||||
|
|
||||||
|
getPageState(data, toPageId).selectedIds.clear()
|
||||||
|
|
||||||
|
storage.savePage(data, toPageId)
|
||||||
|
|
||||||
|
storage.loadPage(data, fromPageId)
|
||||||
|
|
||||||
|
const fromPage = getPage(data, toPageId)
|
||||||
|
|
||||||
|
shapesToMove.forEach((shape) => {
|
||||||
|
fromPage.shapes[shape.id] = shape
|
||||||
|
if (shape.parentId === toPageId) {
|
||||||
|
getShapeUtils(shape).setProperty(shape, 'parentId', fromPageId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
getPageState(data, fromPageId).selectedIds = new Set(selectedIds)
|
||||||
|
|
||||||
|
data.currentPageId = fromPageId
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import {
|
||||||
getPage,
|
getPage,
|
||||||
getPageState,
|
getPageState,
|
||||||
getShapes,
|
getShapes,
|
||||||
|
getTopParentId,
|
||||||
setSelectedIds,
|
setSelectedIds,
|
||||||
setToArray,
|
setToArray,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
|
@ -77,7 +78,7 @@ export function getBrushSnapshot(data: Data) {
|
||||||
const { selectedIds } = getPageState(cData)
|
const { selectedIds } = getPageState(cData)
|
||||||
|
|
||||||
const shapesToTest = getShapes(cData)
|
const shapesToTest = getShapes(cData)
|
||||||
.filter((shape) => shape.type !== ShapeType.Group)
|
.filter((shape) => shape.type !== ShapeType.Group && !shape.isHidden)
|
||||||
.filter(
|
.filter(
|
||||||
(shape) => !(selectedIds.has(shape.id) || selectedIds.has(shape.parentId))
|
(shape) => !(selectedIds.has(shape.id) || selectedIds.has(shape.parentId))
|
||||||
)
|
)
|
||||||
|
@ -100,11 +101,3 @@ export function getBrushSnapshot(data: Data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BrushSnapshot = ReturnType<typeof getBrushSnapshot>
|
export type BrushSnapshot = ReturnType<typeof getBrushSnapshot>
|
||||||
|
|
||||||
function getTopParentId(data: Data, id: string): string {
|
|
||||||
const shape = getPage(data).shapes[id]
|
|
||||||
return shape.parentId === data.currentPageId ||
|
|
||||||
shape.parentId === data.currentParentId
|
|
||||||
? id
|
|
||||||
: getTopParentId(data, shape.parentId)
|
|
||||||
}
|
|
||||||
|
|
|
@ -188,6 +188,10 @@ const state = createState({
|
||||||
CHANGED_CODE_CONTROL: 'updateControls',
|
CHANGED_CODE_CONTROL: 'updateControls',
|
||||||
GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
|
GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
|
||||||
TOGGLED_TOOL_LOCK: 'toggleToolLock',
|
TOGGLED_TOOL_LOCK: 'toggleToolLock',
|
||||||
|
MOVED_TO_PAGE: {
|
||||||
|
if: 'hasSelection',
|
||||||
|
do: ['moveSelectionToPage', 'zoomCameraToSelectionActual'],
|
||||||
|
},
|
||||||
MOVED: { if: 'hasSelection', do: 'moveSelection' },
|
MOVED: { if: 'hasSelection', do: 'moveSelection' },
|
||||||
DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
|
DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
|
||||||
ROTATED_CCW: { if: 'hasSelection', do: 'rotateSelectionCcw' },
|
ROTATED_CCW: { if: 'hasSelection', do: 'rotateSelectionCcw' },
|
||||||
|
@ -268,6 +272,25 @@ const state = createState({
|
||||||
to: 'pointingBounds',
|
to: 'pointingBounds',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
RIGHT_POINTED: [
|
||||||
|
{
|
||||||
|
if: 'isPointingCanvas',
|
||||||
|
do: 'clearSelectedIds',
|
||||||
|
else: {
|
||||||
|
if: 'isPointingShape',
|
||||||
|
then: [
|
||||||
|
'setPointedId',
|
||||||
|
{
|
||||||
|
unless: 'isPointedShapeSelected',
|
||||||
|
do: [
|
||||||
|
'clearSelectedIds',
|
||||||
|
'pushPointedIdToSelectedIds',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pointingBounds: {
|
pointingBounds: {
|
||||||
|
@ -756,15 +779,28 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
conditions: {
|
conditions: {
|
||||||
|
isPointingCanvas(data, payload: PointerInfo) {
|
||||||
|
return payload.target === 'canvas'
|
||||||
|
},
|
||||||
isPointingBounds(data, payload: PointerInfo) {
|
isPointingBounds(data, payload: PointerInfo) {
|
||||||
return getSelectedIds(data).size > 0 && payload.target === 'bounds'
|
return getSelectedIds(data).size > 0 && payload.target === 'bounds'
|
||||||
},
|
},
|
||||||
|
isPointingShape(data, payload: PointerInfo) {
|
||||||
|
return (
|
||||||
|
payload.target &&
|
||||||
|
payload.target !== 'canvas' &&
|
||||||
|
payload.target !== 'bounds'
|
||||||
|
)
|
||||||
|
},
|
||||||
isReadOnly(data) {
|
isReadOnly(data) {
|
||||||
return data.isReadOnly
|
return data.isReadOnly
|
||||||
},
|
},
|
||||||
distanceImpliesDrag(data, payload: PointerInfo) {
|
distanceImpliesDrag(data, payload: PointerInfo) {
|
||||||
return vec.dist2(payload.origin, payload.point) > 8
|
return vec.dist2(payload.origin, payload.point) > 8
|
||||||
},
|
},
|
||||||
|
hasPointedTarget(data, payload: PointerInfo) {
|
||||||
|
return payload.target !== undefined
|
||||||
|
},
|
||||||
isPointedShapeSelected(data) {
|
isPointedShapeSelected(data) {
|
||||||
return getSelectedIds(data).has(data.pointedId)
|
return getSelectedIds(data).has(data.pointedId)
|
||||||
},
|
},
|
||||||
|
@ -1121,6 +1157,9 @@ const state = createState({
|
||||||
moveSelection(data, payload: { type: MoveType }) {
|
moveSelection(data, payload: { type: MoveType }) {
|
||||||
commands.move(data, payload.type)
|
commands.move(data, payload.type)
|
||||||
},
|
},
|
||||||
|
moveSelectionToPage(data, payload: { id: string }) {
|
||||||
|
commands.moveToPage(data, payload.id)
|
||||||
|
},
|
||||||
alignSelection(data, payload: { type: AlignType }) {
|
alignSelection(data, payload: { type: AlignType }) {
|
||||||
commands.align(data, payload.type)
|
commands.align(data, payload.type)
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,8 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
hint: 'rgba(216, 226, 249, 1.000)',
|
hint: 'rgba(216, 226, 249, 1.000)',
|
||||||
selected: 'rgba(66, 133, 244, 1.000)',
|
selected: 'rgba(66, 133, 244, 1.000)',
|
||||||
bounds: 'rgba(65, 132, 244, 1.000)',
|
bounds: 'rgba(65, 132, 244, 1.000)',
|
||||||
boundsBg: 'rgba(65, 132, 244, 0.050)',
|
boundsBg: 'rgba(65, 132, 244, 0.05)',
|
||||||
|
overlay: 'rgba(0, 0, 0, 0.15)',
|
||||||
border: '#aaa',
|
border: '#aaa',
|
||||||
canvas: '#fafafa',
|
canvas: '#fafafa',
|
||||||
panel: '#fefefe',
|
panel: '#fefefe',
|
||||||
|
|
12
todo.md
12
todo.md
|
@ -24,4 +24,14 @@
|
||||||
## Pages
|
## Pages
|
||||||
|
|
||||||
- [x] Make selection part of page state
|
- [x] Make selection part of page state
|
||||||
- [ ] Allow only one page to be in the document at a time
|
- [x] Allow only one page to be in the document at a time
|
||||||
|
|
||||||
|
## Context Menu
|
||||||
|
|
||||||
|
- [x] Create context Menu
|
||||||
|
- [ ] Wire up events
|
||||||
|
|
||||||
|
## Move to Page
|
||||||
|
|
||||||
|
- [ ] Move to Page Command
|
||||||
|
- [ ] Dialog
|
||||||
|
|
|
@ -1398,7 +1398,7 @@ export function getSelectedBounds(data: Data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMobile() {
|
export function isMobile() {
|
||||||
return _isMobile()
|
return _isMobile().any
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRotatedBounds(shape: Shape) {
|
export function getRotatedBounds(shape: Shape) {
|
||||||
|
@ -1661,6 +1661,7 @@ export function getParentRotation(
|
||||||
|
|
||||||
export function getDocumentBranch(data: Data, id: string): string[] {
|
export function getDocumentBranch(data: Data, id: string): string[] {
|
||||||
const shape = getPage(data).shapes[id]
|
const shape = getPage(data).shapes[id]
|
||||||
|
|
||||||
if (shape.type !== ShapeType.Group) return [id]
|
if (shape.type !== ShapeType.Group) return [id]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -1741,3 +1742,19 @@ export function pointsBetween(a: number[], b: number[], steps = 6) {
|
||||||
export function shuffleArr<T>(arr: T[], offset: number): T[] {
|
export function shuffleArr<T>(arr: T[], offset: number): T[] {
|
||||||
return arr.map((_, i) => arr[(i + offset) % arr.length])
|
return arr.map((_, i) => arr[(i + offset) % arr.length])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function commandKey() {
|
||||||
|
return isDarwin() ? '⌘' : 'Ctrl'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTopParentId(data: Data, id: string): string {
|
||||||
|
const shape = getPage(data).shapes[id]
|
||||||
|
return shape.parentId === data.currentPageId ||
|
||||||
|
shape.parentId === data.currentParentId
|
||||||
|
? id
|
||||||
|
: getTopParentId(data, shape.parentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uniqueArray<T extends string | number | Symbol>(...items: T[]) {
|
||||||
|
return Array.from(new Set(items).values())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue