kopia lustrzana https://github.com/Tldraw/Tldraw
Improves undo/redo, fixes pinching and multitouch
rodzic
bc6f5cf5b7
commit
76a4ccdfcb
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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]]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}),
|
||||
|
|
124
state/state.ts
124
state/state.ts
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Ładowanie…
Reference in New Issue