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
|
||||
|
||||
const size = (isMobile().any ? 10 : 8) / zoom // Touch target size
|
||||
const size = (isMobile() ? 10 : 8) / zoom // Touch target size
|
||||
const center = getBoundsCenter(bounds)
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,15 +5,18 @@ import styled from 'styles'
|
|||
import { deepCompareArrays, getPage } from 'utils/utils'
|
||||
|
||||
function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
|
||||
if (e.buttons !== 1) return
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
e.stopPropagation()
|
||||
e.currentTarget.setPointerCapture(e.pointerId)
|
||||
|
||||
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>) {
|
||||
if (e.buttons !== 1) return
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
e.stopPropagation()
|
||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||
|
@ -36,7 +39,7 @@ export default function BoundsBg() {
|
|||
if (selectedIds.length === 1) {
|
||||
const { shapes } = getPage(s.data)
|
||||
const selected = Array.from(s.values.selectedIds.values())[0]
|
||||
return shapes[selected].rotation
|
||||
return shapes[selected]?.rotation
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
|
|
|
@ -21,10 +21,12 @@ export default function Handles() {
|
|||
s.isInAny('notPointing', 'pinching', 'translatingHandles')
|
||||
)
|
||||
|
||||
if (!shape.handles || !isSelecting) return null
|
||||
if (!shape || !shape.handles || !isSelecting) return null
|
||||
|
||||
const center = getShapeUtils(shape).getCenter(shape)
|
||||
|
||||
console.log(shape)
|
||||
|
||||
return (
|
||||
<g transform={`rotate(${shape.rotation * (180 / Math.PI)},${center})`}>
|
||||
{Object.values(shape.handles).map((handle) => (
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import styled from 'styles'
|
||||
import state, { useSelector } from 'state'
|
||||
import inputs from 'state/inputs'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import useZoomEvents from 'hooks/useZoomEvents'
|
||||
import useCamera from 'hooks/useCamera'
|
||||
import Defs from './defs'
|
||||
|
@ -12,6 +11,7 @@ import BoundsBg from './bounds/bounds-bg'
|
|||
import Selected from './selected'
|
||||
import Handles from './bounds/handles'
|
||||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||
import ContextMenu from 'components/context-menu'
|
||||
|
||||
export default function Canvas() {
|
||||
const rCanvas = useRef<SVGSVGElement>(null)
|
||||
|
@ -26,6 +26,7 @@ export default function Canvas() {
|
|||
const isReady = useSelector((s) => s.isIn('ready'))
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<MainSVG ref={rCanvas} {...events}>
|
||||
<Defs />
|
||||
{isReady && (
|
||||
|
@ -39,6 +40,7 @@ export default function Canvas() {
|
|||
</g>
|
||||
)}
|
||||
</MainSVG>
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ShapeStyles, ShapeType } from 'types'
|
|||
import useShapeEvents from 'hooks/useShapeEvents'
|
||||
import * as vec from 'utils/vec'
|
||||
import { getShapeStyle } from 'lib/shape-styles'
|
||||
import ContextMenu from 'components/context-menu'
|
||||
|
||||
interface ShapeProps {
|
||||
id: string
|
||||
|
@ -51,6 +52,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
|||
{...events}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!shape.isHidden && <RealShape isGroup={isGroup} id={id} style={style} />}
|
||||
{isGroup &&
|
||||
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 styled from 'styles'
|
||||
import PagePanel from './page-panel/page-panel'
|
||||
import ContextMenu from './context-menu'
|
||||
|
||||
export default function Editor() {
|
||||
useKeyboardEvents()
|
||||
|
@ -25,11 +26,6 @@ export default function Editor() {
|
|||
<Spacer />
|
||||
<StylePanel />
|
||||
<Canvas />
|
||||
|
||||
{/* <LeftPanels>
|
||||
<CodePanel />
|
||||
{hasControls && <ControlsPanel />}
|
||||
</LeftPanels> */}
|
||||
<ToolsPanel />
|
||||
<StatusBar />
|
||||
</Layout>
|
||||
|
|
|
@ -105,6 +105,10 @@ export const RowButton = styled('button', {
|
|||
zIndex: 1,
|
||||
},
|
||||
|
||||
'& :disabled': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
|
||||
variants: {
|
||||
bp: {
|
||||
mobile: {},
|
||||
|
|
|
@ -9,8 +9,14 @@ export default function useCanvasEvents(
|
|||
) {
|
||||
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
|
||||
rCanvas.current.setPointerCapture(e.pointerId)
|
||||
|
||||
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) => {
|
||||
|
|
|
@ -14,10 +14,15 @@ export default function useShapeEvents(
|
|||
e.stopPropagation()
|
||||
rGroup.current.setPointerCapture(e.pointerId)
|
||||
|
||||
if (e.button === 0) {
|
||||
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))
|
||||
}
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
|
|
@ -2,14 +2,17 @@ import align from './align'
|
|||
import arrow from './arrow'
|
||||
import changePage from './change-page'
|
||||
import createPage from './create-page'
|
||||
import deleteSelected from './delete-selected'
|
||||
import deletePage from './delete-page'
|
||||
import deleteSelected from './delete-selected'
|
||||
import direct from './direct'
|
||||
import distribute from './distribute'
|
||||
import draw from './draw'
|
||||
import duplicate from './duplicate'
|
||||
import generate from './generate'
|
||||
import group from './group'
|
||||
import handle from './handle'
|
||||
import move from './move'
|
||||
import moveToPage from './move-to-page'
|
||||
import nudge from './nudge'
|
||||
import rotate from './rotate'
|
||||
import rotateCcw from './rotate-ccw'
|
||||
|
@ -19,8 +22,6 @@ import toggle from './toggle'
|
|||
import transform from './transform'
|
||||
import transformSingle from './transform-single'
|
||||
import translate from './translate'
|
||||
import handle from './handle'
|
||||
import group from './group'
|
||||
import ungroup from './ungroup'
|
||||
|
||||
const commands = {
|
||||
|
@ -35,7 +36,10 @@ const commands = {
|
|||
draw,
|
||||
duplicate,
|
||||
generate,
|
||||
group,
|
||||
handle,
|
||||
move,
|
||||
moveToPage,
|
||||
nudge,
|
||||
rotate,
|
||||
rotateCcw,
|
||||
|
@ -45,8 +49,6 @@ const commands = {
|
|||
transform,
|
||||
transformSingle,
|
||||
translate,
|
||||
handle,
|
||||
group,
|
||||
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,
|
||||
getPageState,
|
||||
getShapes,
|
||||
getTopParentId,
|
||||
setSelectedIds,
|
||||
setToArray,
|
||||
} from 'utils/utils'
|
||||
|
@ -77,7 +78,7 @@ export function getBrushSnapshot(data: Data) {
|
|||
const { selectedIds } = getPageState(cData)
|
||||
|
||||
const shapesToTest = getShapes(cData)
|
||||
.filter((shape) => shape.type !== ShapeType.Group)
|
||||
.filter((shape) => shape.type !== ShapeType.Group && !shape.isHidden)
|
||||
.filter(
|
||||
(shape) => !(selectedIds.has(shape.id) || selectedIds.has(shape.parentId))
|
||||
)
|
||||
|
@ -100,11 +101,3 @@ export function getBrushSnapshot(data: Data) {
|
|||
}
|
||||
|
||||
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',
|
||||
GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
|
||||
TOGGLED_TOOL_LOCK: 'toggleToolLock',
|
||||
MOVED_TO_PAGE: {
|
||||
if: 'hasSelection',
|
||||
do: ['moveSelectionToPage', 'zoomCameraToSelectionActual'],
|
||||
},
|
||||
MOVED: { if: 'hasSelection', do: 'moveSelection' },
|
||||
DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
|
||||
ROTATED_CCW: { if: 'hasSelection', do: 'rotateSelectionCcw' },
|
||||
|
@ -268,6 +272,25 @@ const state = createState({
|
|||
to: 'pointingBounds',
|
||||
},
|
||||
],
|
||||
RIGHT_POINTED: [
|
||||
{
|
||||
if: 'isPointingCanvas',
|
||||
do: 'clearSelectedIds',
|
||||
else: {
|
||||
if: 'isPointingShape',
|
||||
then: [
|
||||
'setPointedId',
|
||||
{
|
||||
unless: 'isPointedShapeSelected',
|
||||
do: [
|
||||
'clearSelectedIds',
|
||||
'pushPointedIdToSelectedIds',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
pointingBounds: {
|
||||
|
@ -756,15 +779,28 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
conditions: {
|
||||
isPointingCanvas(data, payload: PointerInfo) {
|
||||
return payload.target === 'canvas'
|
||||
},
|
||||
isPointingBounds(data, payload: PointerInfo) {
|
||||
return getSelectedIds(data).size > 0 && payload.target === 'bounds'
|
||||
},
|
||||
isPointingShape(data, payload: PointerInfo) {
|
||||
return (
|
||||
payload.target &&
|
||||
payload.target !== 'canvas' &&
|
||||
payload.target !== 'bounds'
|
||||
)
|
||||
},
|
||||
isReadOnly(data) {
|
||||
return data.isReadOnly
|
||||
},
|
||||
distanceImpliesDrag(data, payload: PointerInfo) {
|
||||
return vec.dist2(payload.origin, payload.point) > 8
|
||||
},
|
||||
hasPointedTarget(data, payload: PointerInfo) {
|
||||
return payload.target !== undefined
|
||||
},
|
||||
isPointedShapeSelected(data) {
|
||||
return getSelectedIds(data).has(data.pointedId)
|
||||
},
|
||||
|
@ -1121,6 +1157,9 @@ const state = createState({
|
|||
moveSelection(data, payload: { type: MoveType }) {
|
||||
commands.move(data, payload.type)
|
||||
},
|
||||
moveSelectionToPage(data, payload: { id: string }) {
|
||||
commands.moveToPage(data, payload.id)
|
||||
},
|
||||
alignSelection(data, payload: { type: AlignType }) {
|
||||
commands.align(data, payload.type)
|
||||
},
|
||||
|
|
|
@ -11,7 +11,8 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
hint: 'rgba(216, 226, 249, 1.000)',
|
||||
selected: 'rgba(66, 133, 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',
|
||||
canvas: '#fafafa',
|
||||
panel: '#fefefe',
|
||||
|
|
12
todo.md
12
todo.md
|
@ -24,4 +24,14 @@
|
|||
## Pages
|
||||
|
||||
- [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() {
|
||||
return _isMobile()
|
||||
return _isMobile().any
|
||||
}
|
||||
|
||||
export function getRotatedBounds(shape: Shape) {
|
||||
|
@ -1661,6 +1661,7 @@ export function getParentRotation(
|
|||
|
||||
export function getDocumentBranch(data: Data, id: string): string[] {
|
||||
const shape = getPage(data).shapes[id]
|
||||
|
||||
if (shape.type !== ShapeType.Group) return [id]
|
||||
|
||||
return [
|
||||
|
@ -1741,3 +1742,19 @@ export function pointsBetween(a: number[], b: number[], steps = 6) {
|
|||
export function shuffleArr<T>(arr: T[], offset: number): T[] {
|
||||
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