Improves undo/redo, fixes pinching and multitouch

canvas-rendering
Steve Ruiz 2021-05-30 14:14:35 +01:00
rodzic bc6f5cf5b7
commit 76a4ccdfcb
15 zmienionych plików z 366 dodań i 288 usunięć

Wyświetl plik

@ -1,27 +1,29 @@
import { useCallback, useRef } from "react"
import state, { useSelector } from "state"
import inputs from "state/inputs"
import styled from "styles"
import { getPage } from "utils/utils"
import { useCallback, useRef } from 'react'
import state, { useSelector } from 'state'
import inputs from 'state/inputs'
import styled from 'styles'
import { 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)
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
state.send('POINTED_BOUNDS', 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)
state.send("STOPPED_POINTING", inputs.pointerUp(e))
state.send('STOPPED_POINTING', inputs.pointerUp(e))
}
export default function BoundsBg() {
const rBounds = useRef<SVGRectElement>(null)
const bounds = useSelector((state) => state.values.selectedBounds)
const isSelecting = useSelector((s) => s.isIn("selecting"))
const isSelecting = useSelector((s) => s.isIn('selecting'))
const rotation = useSelector((s) => {
if (s.data.selectedIds.size === 1) {
const { shapes } = getPage(s.data)
@ -53,6 +55,6 @@ export default function BoundsBg() {
)
}
const StyledBoundsBg = styled("rect", {
fill: "$boundsBg",
const StyledBoundsBg = styled('rect', {
fill: '$boundsBg',
})

Wyświetl plik

@ -21,15 +21,26 @@ export default function Canvas() {
const isReady = useSelector((s) => s.isIn('ready'))
const handlePointerDown = useCallback((e: React.PointerEvent) => {
if (!inputs.canAccept(e.pointerId)) return
rCanvas.current.setPointerCapture(e.pointerId)
state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
}, [])
const handleTouchStart = useCallback((e: React.TouchEvent) => {
if (e.touches.length === 2) {
state.send('TOUCH_UNDO')
}
}, [])
const handlePointerMove = useCallback((e: React.PointerEvent) => {
state.send('MOVED_POINTER', inputs.pointerMove(e))
if (!inputs.canAccept(e.pointerId)) return
if (inputs.canAccept(e.pointerId)) {
state.send('MOVED_POINTER', inputs.pointerMove(e))
}
}, [])
const handlePointerUp = useCallback((e: React.PointerEvent) => {
if (!inputs.canAccept(e.pointerId)) return
rCanvas.current.releasePointerCapture(e.pointerId)
state.send('STOPPED_POINTING', { id: 'canvas', ...inputs.pointerUp(e) })
}, [])
@ -41,14 +52,15 @@ export default function Canvas() {
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onTouchStart={handleTouchStart}
>
<Defs />
{isReady && (
<g ref={rGroup}>
<BoundsBg />
<Page />
<Bounds />
<Selected />
<Bounds />
<Brush />
</g>
)}

Wyświetl plik

@ -26,12 +26,11 @@ export const IconButton = styled('button', {
'& > svg': {
height: '16px',
width: '16px',
// strokeWidth: '2px',
// stroke: '$text',
},
variants: {
size: {
small: {},
medium: {
height: 44,
width: 44,

Wyświetl plik

@ -1,48 +1,62 @@
import { useStateDesigner } from "@state-designer/react"
import state from "state"
import styled from "styles"
import { useRef } from "react"
import { useStateDesigner } from '@state-designer/react'
import state from 'state'
import styled from 'styles'
import { useRef } from 'react'
export default function StatusBar() {
const local = useStateDesigner(state)
const { count, time } = useRenderCount()
const active = local.active.slice(1).map((s) => s.split("root.")[1])
const active = local.active.slice(1).map((s) => s.split('root.')[1])
const log = local.log[0]
return (
<StatusBarContainer>
<Section>{active.join(" | ")}</Section>
<StatusBarContainer
size={{
'@sm': 'small',
}}
>
<Section>{active.join(' | ')}</Section>
<Section>| {log}</Section>
<Section title="Renders | Time">
{count} | {time.toString().padStart(3, "0")}
</Section>
{/* <Section
title="Renders | Time"
>
{count} | {time.toString().padStart(3, '0')}
</Section> */}
</StatusBarContainer>
)
}
const StatusBarContainer = styled("div", {
position: "absolute",
const StatusBarContainer = styled('div', {
position: 'absolute',
bottom: 0,
left: 0,
width: "100%",
width: '100%',
height: 40,
userSelect: "none",
borderTop: "1px solid black",
gridArea: "status",
display: "grid",
gridTemplateColumns: "auto 1fr auto",
alignItems: "center",
backgroundColor: "white",
userSelect: 'none',
borderTop: '1px solid black',
gridArea: 'status',
display: 'grid',
gridTemplateColumns: 'auto 1fr auto',
alignItems: 'center',
backgroundColor: 'white',
gap: 8,
fontSize: "$1",
padding: "0 16px",
fontSize: '$0',
padding: '0 16px',
zIndex: 200,
variants: {
size: {
small: {
fontSize: '$1',
},
},
},
})
const Section = styled("div", {
whiteSpace: "nowrap",
overflow: "hidden",
const Section = styled('div', {
whiteSpace: 'nowrap',
overflow: 'hidden',
})
function useRenderCount() {

Wyświetl plik

@ -52,101 +52,117 @@ export default function ToolsPanel() {
return (
<OuterContainer>
<Zoom />
<Container>
<IconButton
name="select"
size="large"
onClick={selectSelectTool}
isActive={activeTool === 'select'}
>
<CursorArrowIcon />
</IconButton>
</Container>
<Container>
<IconButton
name={ShapeType.Draw}
size="large"
onClick={selectDrawTool}
isActive={activeTool === ShapeType.Draw}
>
<Pencil1Icon />
</IconButton>
<IconButton
name={ShapeType.Rectangle}
size="large"
onClick={selectRectangleTool}
isActive={activeTool === ShapeType.Rectangle}
>
<SquareIcon />
</IconButton>
<IconButton
name={ShapeType.Circle}
size="large"
onClick={selectCircleTool}
isActive={activeTool === ShapeType.Circle}
>
<CircleIcon />
</IconButton>
<IconButton
name={ShapeType.Ellipse}
size="large"
onClick={selectEllipseTool}
isActive={activeTool === ShapeType.Ellipse}
>
<CircleIcon transform="rotate(-45) scale(1, .8)" />
</IconButton>
<IconButton
name={ShapeType.Line}
size="large"
onClick={selectLineTool}
isActive={activeTool === ShapeType.Line}
>
<DividerHorizontalIcon transform="rotate(-45)" />
</IconButton>
<IconButton
name={ShapeType.Ray}
size="large"
onClick={selectRayTool}
isActive={activeTool === ShapeType.Ray}
>
<SewingPinIcon transform="rotate(-135)" />
</IconButton>
<IconButton
name={ShapeType.Dot}
size="large"
onClick={selectDotTool}
isActive={activeTool === ShapeType.Dot}
>
<DotIcon />
</IconButton>
</Container>
<Container>
<IconButton size="medium" onClick={selectToolLock}>
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
{isPenLocked && (
<IconButton size="medium" onClick={selectToolLock}>
<Pencil2Icon />
<Flex>
<Container>
<IconButton
name="select"
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectSelectTool}
isActive={activeTool === 'select'}
>
<CursorArrowIcon />
</IconButton>
)}
</Container>
</Container>
<Container>
<IconButton
name={ShapeType.Draw}
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectDrawTool}
isActive={activeTool === ShapeType.Draw}
>
<Pencil1Icon />
</IconButton>
<IconButton
name={ShapeType.Rectangle}
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectRectangleTool}
isActive={activeTool === ShapeType.Rectangle}
>
<SquareIcon />
</IconButton>
<IconButton
name={ShapeType.Circle}
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectCircleTool}
isActive={activeTool === ShapeType.Circle}
>
<CircleIcon />
</IconButton>
<IconButton
name={ShapeType.Ellipse}
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectEllipseTool}
isActive={activeTool === ShapeType.Ellipse}
>
<CircleIcon transform="rotate(-45) scale(1, .8)" />
</IconButton>
<IconButton
name={ShapeType.Line}
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectLineTool}
isActive={activeTool === ShapeType.Line}
>
<DividerHorizontalIcon transform="rotate(-45)" />
</IconButton>
<IconButton
name={ShapeType.Ray}
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectRayTool}
isActive={activeTool === ShapeType.Ray}
>
<SewingPinIcon transform="rotate(-135)" />
</IconButton>
<IconButton
name={ShapeType.Dot}
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectDotTool}
isActive={activeTool === ShapeType.Dot}
>
<DotIcon />
</IconButton>
</Container>
<Container>
<IconButton
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectToolLock}
>
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
{isPenLocked && (
<IconButton
size={{ '@sm': 'small', '@md': 'large' }}
onClick={selectToolLock}
>
<Pencil2Icon />
</IconButton>
)}
</Container>
</Flex>
<UndoRedo />
</OuterContainer>
)
}
const Spacer = styled('div', { flexGrow: 2 })
const OuterContainer = styled('div', {
position: 'relative',
gridArea: 'tools',
position: 'fixed',
bottom: 40,
left: 0,
right: 0,
padding: '0 8px 12px 8px',
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 16,
zIndex: 200,
})
const Flex = styled('div', {
display: 'flex',
'& > *:nth-child(n+2)': {
marginLeft: 16,
},
})
const Container = styled('div', {
@ -157,8 +173,6 @@ const Container = styled('div', {
border: '1px solid $border',
pointerEvents: 'all',
userSelect: 'none',
zIndex: 200,
boxShadow: '0px 2px 25px rgba(0,0,0,.16)',
height: '100%',
display: 'flex',
padding: 4,

Wyświetl plik

@ -9,7 +9,7 @@ const clear = () => state.send('CLEARED_PAGE')
export default function UndoRedo() {
return (
<Container>
<Container size={{ '@sm': 'small' }}>
<IconButton onClick={undo}>
<RotateCcw />
</IconButton>
@ -25,7 +25,7 @@ export default function UndoRedo() {
const Container = styled('div', {
position: 'absolute',
bottom: 12,
bottom: 64,
right: 12,
backgroundColor: '$panel',
borderRadius: '4px',
@ -43,4 +43,12 @@ const Container = styled('div', {
height: 13,
width: 13,
},
variants: {
size: {
small: {
bottom: 12,
},
},
},
})

Wyświetl plik

@ -10,7 +10,7 @@ const zoomToActual = () => state.send('ZOOMED_TO_ACTUAL')
export default function Zoom() {
return (
<Container>
<Container size={{ '@sm': 'small' }}>
<IconButton onClick={zoomOut}>
<ZoomOutIcon />
</IconButton>
@ -33,8 +33,8 @@ function ZoomCounter() {
const Container = styled('div', {
position: 'absolute',
bottom: 12,
left: 12,
bottom: 64,
backgroundColor: '$panel',
borderRadius: '4px',
overflow: 'hidden',
@ -50,6 +50,14 @@ const Container = styled('div', {
'& svg': {
strokeWidth: 0,
},
variants: {
size: {
small: {
bottom: 12,
},
},
},
})
const ZoomButton = styled(IconButton, {

Wyświetl plik

@ -1,18 +1,19 @@
import { useCallback, useRef } from "react"
import inputs from "state/inputs"
import { Edge, Corner } from "types"
import { useCallback, useRef } from 'react'
import inputs from 'state/inputs'
import { Edge, Corner } from 'types'
import state from "../state"
import state from '../state'
export default function useBoundsHandleEvents(
handle: Edge | Corner | "rotate"
handle: Edge | Corner | 'rotate'
) {
const onPointerDown = useCallback(
(e) => {
if (e.buttons !== 1) return
if (!inputs.canAccept(e.pointerId)) return
e.stopPropagation()
e.currentTarget.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS_HANDLE", inputs.pointerDown(e, handle))
state.send('POINTED_BOUNDS_HANDLE', inputs.pointerDown(e, handle))
},
[handle]
)
@ -20,18 +21,20 @@ export default function useBoundsHandleEvents(
const onPointerMove = useCallback(
(e) => {
if (e.buttons !== 1) return
if (!inputs.canAccept(e.pointerId)) return
e.stopPropagation()
state.send("MOVED_POINTER", inputs.pointerMove(e))
state.send('MOVED_POINTER', inputs.pointerMove(e))
},
[handle]
)
const onPointerUp = useCallback((e) => {
if (e.buttons !== 1) return
if (!inputs.canAccept(e.pointerId)) return
e.stopPropagation()
e.currentTarget.releasePointerCapture(e.pointerId)
e.currentTarget.replaceWith(e.currentTarget)
state.send("STOPPED_POINTING", inputs.pointerUp(e))
state.send('STOPPED_POINTING', inputs.pointerUp(e))
}, [])
return { onPointerDown, onPointerMove, onPointerUp }

Wyświetl plik

@ -8,7 +8,8 @@ export default function useShapeEvents(
) {
const handlePointerDown = useCallback(
(e: React.PointerEvent) => {
e.stopPropagation()
if (!inputs.canAccept(e.pointerId)) return
// e.stopPropagation()
rGroup.current.setPointerCapture(e.pointerId)
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
},
@ -17,7 +18,8 @@ export default function useShapeEvents(
const handlePointerUp = useCallback(
(e: React.PointerEvent) => {
e.stopPropagation()
if (!inputs.canAccept(e.pointerId)) return
// e.stopPropagation()
rGroup.current.releasePointerCapture(e.pointerId)
state.send('STOPPED_POINTING', inputs.pointerUp(e))
},
@ -26,6 +28,7 @@ export default function useShapeEvents(
const handlePointerEnter = useCallback(
(e: React.PointerEvent) => {
if (!inputs.canAccept(e.pointerId)) return
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
},
[id]
@ -33,13 +36,17 @@ export default function useShapeEvents(
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
if (!inputs.canAccept(e.pointerId)) return
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
},
[id]
)
const handlePointerLeave = useCallback(
() => state.send('UNHOVERED_SHAPE', { target: id }),
(e: React.PointerEvent) => {
if (!inputs.canAccept(e.pointerId)) return
state.send('UNHOVERED_SHAPE', { target: id })
},
[id]
)

Wyświetl plik

@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react'
import state from 'state'
import inputs from 'state/inputs'
import * as vec from 'utils/vec'
import { usePinch } from 'react-use-gesture'
import { useGesture } from 'react-use-gesture'
/**
* Capture zoom gestures (pinches, wheels and pans) and send to the state.
@ -12,91 +12,57 @@ import { usePinch } from 'react-use-gesture'
export default function useZoomEvents(
ref: React.MutableRefObject<SVGSVGElement>
) {
const rTouchDist = useRef(0)
useEffect(() => {
const element = ref.current
if (!element) return
function handleWheel(e: WheelEvent) {
e.preventDefault()
e.stopPropagation()
if (e.ctrlKey) {
state.send('ZOOMED_CAMERA', {
delta: e.deltaY,
...inputs.wheel(e),
})
return
}
state.send('PANNED_CAMERA', {
delta: [e.deltaX, e.deltaY],
...inputs.wheel(e),
})
}
function handleTouchMove(e: TouchEvent) {
e.preventDefault()
e.stopPropagation()
if (e.touches.length === 2) {
const { clientX: x0, clientY: y0 } = e.touches[0]
const { clientX: x1, clientY: y1 } = e.touches[1]
const dist = vec.dist([x0, y0], [x1, y1])
const point = vec.med([x0, y0], [x1, y1])
state.send('WHEELED', {
delta: dist - rTouchDist.current,
point,
})
rTouchDist.current = dist
}
}
element.addEventListener('wheel', handleWheel, { passive: false })
element.addEventListener('touchstart', handleTouchMove, { passive: false })
element.addEventListener('touchmove', handleTouchMove, { passive: false })
return () => {
element.removeEventListener('wheel', handleWheel)
element.removeEventListener('touchstart', handleTouchMove)
element.removeEventListener('touchmove', handleTouchMove)
}
}, [ref])
const rPinchDa = useRef<number[] | undefined>(undefined)
const rPinchPoint = useRef<number[] | undefined>(undefined)
const bind = usePinch(({ pinching, da, origin }) => {
if (!pinching) {
state.send('STOPPED_PINCHING')
rPinchDa.current = undefined
rPinchPoint.current = undefined
return
const bind = useGesture(
{
onWheel: ({ event, delta }) => {
if (event.ctrlKey) {
state.send('ZOOMED_CAMERA', {
delta: delta[1],
...inputs.wheel(event as WheelEvent),
})
return
}
state.send('PANNED_CAMERA', {
delta,
...inputs.wheel(event as WheelEvent),
})
},
onPinch: ({ pinching, da, origin }) => {
if (!pinching) {
state.send('STOPPED_PINCHING')
rPinchDa.current = undefined
rPinchPoint.current = undefined
return
}
if (rPinchPoint.current === undefined) {
state.send('STARTED_PINCHING')
rPinchDa.current = da
rPinchPoint.current = origin
}
const [distanceDelta, angleDelta] = vec.sub(rPinchDa.current, da)
state.send('PINCHED', {
delta: vec.sub(rPinchPoint.current, origin),
point: origin,
distanceDelta,
angleDelta,
})
rPinchDa.current = da
rPinchPoint.current = origin
},
},
{
domTarget: document.body,
eventOptions: { passive: false },
}
if (rPinchPoint.current === undefined) {
state.send('STARTED_PINCHING')
rPinchDa.current = da
rPinchPoint.current = origin
}
const [distanceDelta, angleDelta] = vec.sub(rPinchDa.current, da)
state.send('PINCHED', {
delta: vec.sub(rPinchPoint.current, origin),
point: origin,
distanceDelta,
angleDelta,
})
rPinchDa.current = da
rPinchPoint.current = origin
})
)
return { ...bind() }
}

Wyświetl plik

@ -1,6 +1,6 @@
import { Data } from "types"
import { BaseCommand } from "./commands/command"
import state from "./state"
import { Data } from 'types'
import { BaseCommand } from './commands/command'
import state from './state'
// A singleton to manage history changes.
@ -11,10 +11,11 @@ class BaseHistory<T> {
private _enabled = true
execute = (data: T, command: BaseCommand<T>) => {
command.redo(data, true)
if (this.disabled) return
this.stack = this.stack.slice(0, this.pointer + 1)
this.stack.push(command)
command.redo(data, true)
this.pointer++
if (this.stack.length > this.maxLength) {
@ -26,26 +27,26 @@ class BaseHistory<T> {
}
undo = (data: T) => {
if (this.disabled) return
if (this.pointer === -1) return
const command = this.stack[this.pointer]
command.undo(data)
if (this.disabled) return
this.pointer--
this.save(data)
}
redo = (data: T) => {
if (this.disabled) return
if (this.pointer === this.stack.length - 1) return
const command = this.stack[this.pointer + 1]
command.redo(data, false)
if (this.disabled) return
this.pointer++
this.save(data)
}
load(data: T, id = "code_slate_0.0.1") {
if (typeof window === "undefined") return
if (typeof localStorage === "undefined") return
load(data: T, id = 'code_slate_0.0.1') {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
const savedData = localStorage.getItem(id)
@ -54,9 +55,9 @@ class BaseHistory<T> {
}
}
save = (data: T, id = "code_slate_0.0.1") => {
if (typeof window === "undefined") return
if (typeof localStorage === "undefined") return
save = (data: T, id = 'code_slate_0.0.1') => {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
localStorage.setItem(id, JSON.stringify(this.prepareDataForSave(data)))
}
@ -110,14 +111,14 @@ class History extends BaseHistory<Data> {
restoredData.selectedIds = new Set(restoredData.selectedIds)
// Also restore camera position, which is saved separately in this app
const cameraInfo = localStorage.getItem("code_slate_camera")
const cameraInfo = localStorage.getItem('code_slate_camera')
if (cameraInfo !== null) {
Object.assign(restoredData.camera, JSON.parse(cameraInfo))
// And update the CSS property
document.documentElement.style.setProperty(
"--camera-zoom",
'--camera-zoom',
restoredData.camera.zoom.toString()
)
}

Wyświetl plik

@ -1,7 +1,8 @@
import { PointerInfo } from "types"
import { isDarwin } from "utils/utils"
import { PointerInfo } from 'types'
import { isDarwin } from 'utils/utils'
class Inputs {
activePointerId?: number
points: Record<string, PointerInfo> = {}
pointerDown(e: PointerEvent | React.PointerEvent, target: string) {
@ -19,6 +20,7 @@ class Inputs {
}
this.points[e.pointerId] = info
this.activePointerId = e.pointerId
return info
}
@ -78,6 +80,7 @@ class Inputs {
}
delete this.points[e.pointerId]
delete this.activePointerId
return info
}
@ -87,6 +90,12 @@ class Inputs {
return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
}
canAccept(pointerId: PointerEvent['pointerId']) {
return (
this.activePointerId === undefined || this.activePointerId === pointerId
)
}
get pointer() {
return this.points[Object.keys(this.points)[0]]
}

Wyświetl plik

@ -45,6 +45,11 @@ export default class RotateSession extends BaseSession {
for (let { id, center, offset, rotation } of initialShapes) {
const shape = page.shapes[id]
// const rotationOffset = vec.sub(
// getBoundsCenter(getShapeBounds(shape)),
// getBoundsCenter(getRotatedBounds(shape))
// )
const nextRotation = isLocked
? clampToRotationToSegments(rotation + rot, 24)
: rotation + rot
@ -100,11 +105,17 @@ export function getRotateSnapshot(data: Data) {
const center = getBoundsCenter(bounds)
const offset = vec.sub(center, shape.point)
const rotationOffset = vec.sub(
center,
getBoundsCenter(getRotatedBounds(shape))
)
return {
id: shape.id,
point: shape.point,
rotation: shape.rotation,
offset,
rotationOffset,
center,
}
}),

Wyświetl plik

@ -69,47 +69,7 @@ const initialData: Data = {
const state = createState({
data: initialData,
on: {
ZOOMED_CAMERA: {
do: 'zoomCamera',
},
PANNED_CAMERA: {
do: 'panCamera',
},
ZOOMED_TO_ACTUAL: {
if: 'hasSelection',
do: 'zoomCameraToSelectionActual',
else: 'zoomCameraToActual',
},
ZOOMED_TO_SELECTION: {
if: 'hasSelection',
do: 'zoomCameraToSelection',
},
ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
ZOOMED_IN: 'zoomIn',
ZOOMED_OUT: 'zoomOut',
RESET_CAMERA: 'resetCamera',
TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
TOGGLED_SHAPE_ASPECT_LOCK: {
if: 'hasSelection',
do: 'aspectLockSelection',
},
SELECTED_SELECT_TOOL: { to: 'selecting' },
SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
NUDGED: { do: 'nudgeSelection' },
USED_PEN_DEVICE: 'enablePenLock',
DISABLED_PEN_LOCK: 'disablePenLock',
UNMOUNTED: [{ unless: 'isReadOnly', do: 'forceSave' }, { to: 'loading' }],
},
initial: 'loading',
states: {
@ -131,10 +91,48 @@ const state = createState({
else: ['zoomCameraToFit', 'zoomCameraToActual'],
},
on: {
UNMOUNTED: [
{ unless: 'isReadOnly', do: 'forceSave' },
{ to: 'loading' },
],
ZOOMED_CAMERA: {
do: 'zoomCamera',
},
PANNED_CAMERA: {
do: 'panCamera',
},
ZOOMED_TO_ACTUAL: {
if: 'hasSelection',
do: 'zoomCameraToSelectionActual',
else: 'zoomCameraToActual',
},
ZOOMED_TO_SELECTION: {
if: 'hasSelection',
do: 'zoomCameraToSelection',
},
ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
ZOOMED_IN: 'zoomIn',
ZOOMED_OUT: 'zoomOut',
RESET_CAMERA: 'resetCamera',
TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
TOGGLED_SHAPE_ASPECT_LOCK: {
if: 'hasSelection',
do: 'aspectLockSelection',
},
SELECTED_SELECT_TOOL: { to: 'selecting' },
SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
NUDGED: { do: 'nudgeSelection' },
USED_PEN_DEVICE: 'enablePenLock',
DISABLED_PEN_LOCK: 'disablePenLock',
CLEARED_PAGE: ['selectAll', 'deleteSelection'],
},
initial: 'selecting',
states: {
@ -143,10 +141,8 @@ const state = createState({
SAVED: 'forceSave',
UNDO: 'undo',
REDO: 'redo',
CLEARED_PAGE: ['selectAll', 'deleteSelection'],
SAVED_CODE: 'saveCode',
DELETED: 'deleteSelection',
STARTED_PINCHING: { to: 'pinching' },
INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize',
CHANGED_CODE_CONTROL: 'updateControls',
@ -164,6 +160,7 @@ const state = createState({
notPointing: {
on: {
CANCELLED: 'clearSelectedIds',
STARTED_PINCHING: { to: 'pinching' },
POINTED_CANVAS: { to: 'brushSelecting' },
POINTED_BOUNDS: { to: 'pointingBounds' },
POINTED_BOUNDS_HANDLE: {
@ -269,7 +266,7 @@ const state = createState({
'startBrushSession',
],
on: {
STARTED_PINCHING: { to: 'pinching' },
STARTED_PINCHING: { do: 'completeSession', to: 'pinching' },
MOVED_POINTER: 'updateBrushSession',
PANNED_CAMERA: 'updateBrushSession',
STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
@ -280,14 +277,30 @@ const state = createState({
},
pinching: {
on: {
STOPPED_PINCHING: { to: 'selecting' },
PINCHED: { do: 'pinchCamera' },
},
initial: 'selectPinching',
states: {
selectPinching: {
on: {
STOPPED_PINCHING: { to: 'selecting' },
},
},
toolPinching: {
on: {
STOPPED_PINCHING: { to: 'usingTool.previous' },
},
},
},
},
usingTool: {
initial: 'draw',
onEnter: 'clearSelectedIds',
on: {
STARTED_PINCHING: {
do: 'breakSession',
to: 'pinching.toolPinching',
},
TOGGLED_TOOL_LOCK: 'toggleToolLock',
},
states: {
@ -319,7 +332,7 @@ const state = createState({
to: 'draw.creating',
},
CANCELLED: {
do: ['cancelSession', 'deleteSelection'],
do: 'breakSession',
to: 'selecting',
},
MOVED_POINTER: 'updateDrawSession',
@ -359,7 +372,7 @@ const state = createState({
},
],
CANCELLED: {
do: ['cancelSession', 'deleteSelection'],
do: 'breakSession',
to: 'selecting',
},
},
@ -545,7 +558,7 @@ const state = createState({
},
],
CANCELLED: {
do: ['cancelSession', 'deleteSelection'],
do: 'breakSession',
to: 'selecting',
},
},
@ -662,6 +675,13 @@ const state = createState({
/* -------------------- Sessions -------------------- */
// Shared
breakSession(data) {
session?.cancel(data)
session = undefined
history.disable()
commands.deleteSelected(data)
history.enable()
},
cancelSession(data) {
session?.cancel(data)
session = undefined

Wyświetl plik

@ -42,6 +42,10 @@ const { styled, global, css, theme, getCssString } = createCss({
zIndices: {},
transitions: {},
},
media: {
sm: '(min-width: 640px)',
md: '(min-width: 768px)',
},
utils: {
zDash: () => (value: number) => {
return {