Improves performance for certain actions
This commit is contained in:
parent
8623958e74
commit
704e92faa4
15 changed files with 846 additions and 530 deletions
|
@ -11,52 +11,26 @@ import Bounds from './bounds/bounding-box'
|
||||||
import BoundsBg from './bounds/bounds-bg'
|
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 { isMobile, throttle } from 'utils/utils'
|
import { isMobile, screenToWorld, throttle } from 'utils/utils'
|
||||||
|
import session from 'state/session'
|
||||||
|
import { PointerInfo } from 'types'
|
||||||
|
import { fastDrawUpdate } from 'state/hacks'
|
||||||
|
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const rCanvas = useRef<SVGSVGElement>(null)
|
const rCanvas = useRef<SVGSVGElement>(null)
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
|
||||||
useCamera(rGroup)
|
useCamera(rGroup)
|
||||||
|
|
||||||
useZoomEvents()
|
useZoomEvents()
|
||||||
|
|
||||||
|
const events = useCanvasEvents(rCanvas)
|
||||||
|
|
||||||
const isReady = useSelector((s) => s.isIn('ready'))
|
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')
|
|
||||||
} else {
|
|
||||||
if (isMobile()) {
|
|
||||||
state.send('TOUCHED_CANVAS')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
|
||||||
throttledPointerMove(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) })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainSVG
|
<MainSVG ref={rCanvas} {...events}>
|
||||||
ref={rCanvas}
|
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
onPointerMove={handlePointerMove}
|
|
||||||
onPointerUp={handlePointerUp}
|
|
||||||
onTouchStart={handleTouchStart}
|
|
||||||
>
|
|
||||||
<Defs />
|
<Defs />
|
||||||
{isReady && (
|
{isReady && (
|
||||||
<g ref={rGroup}>
|
<g ref={rGroup}>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { useSelector } from 'state'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { GroupShape } from 'types'
|
import state, { useSelector } from 'state'
|
||||||
import { deepCompareArrays, getPage } from 'utils/utils'
|
import { Bounds, GroupShape, PageState } from 'types'
|
||||||
|
import { boundsContain } from 'utils/bounds'
|
||||||
|
import { deepCompareArrays, getPage, screenToWorld } from 'utils/utils'
|
||||||
import Shape from './shape'
|
import Shape from './shape'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -11,12 +13,42 @@ here; and still cheaper than any other pattern I've found.
|
||||||
|
|
||||||
const noOffset = [0, 0]
|
const noOffset = [0, 0]
|
||||||
|
|
||||||
|
const viewportCache = new WeakMap<PageState, Bounds>()
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const currentPageShapeIds = useSelector(({ data }) => {
|
const currentPageShapeIds = useSelector((s) => {
|
||||||
return Object.values(getPage(data).shapes)
|
const page = getPage(s.data)
|
||||||
.filter((shape) => shape.parentId === data.currentPageId)
|
const pageState = s.data.pageStates[page.id]
|
||||||
|
|
||||||
|
// if (!viewportCache.has(pageState)) {
|
||||||
|
// const [minX, minY] = screenToWorld([0, 0], s.data)
|
||||||
|
// const [maxX, maxY] = screenToWorld(
|
||||||
|
// [window.innerWidth, window.innerHeight],
|
||||||
|
// s.data
|
||||||
|
// )
|
||||||
|
// viewportCache.set(pageState, {
|
||||||
|
// minX,
|
||||||
|
// minY,
|
||||||
|
// maxX,
|
||||||
|
// maxY,
|
||||||
|
// height: maxX - minX,
|
||||||
|
// width: maxY - minY,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const viewport = viewportCache.get(pageState)
|
||||||
|
|
||||||
|
return (
|
||||||
|
Object.values(page.shapes)
|
||||||
|
.filter((shape) => shape.parentId === page.id)
|
||||||
|
// .filter((shape) => {
|
||||||
|
// const shapeBounds = getShapeUtils(shape).getBounds(shape)
|
||||||
|
// console.log(shapeBounds, viewport)
|
||||||
|
// return boundsContain(viewport, shapeBounds)
|
||||||
|
// })
|
||||||
.sort((a, b) => a.childIndex - b.childIndex)
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
.map((shape) => shape.id)
|
.map((shape) => shape.id)
|
||||||
|
)
|
||||||
}, deepCompareArrays)
|
}, deepCompareArrays)
|
||||||
|
|
||||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default function Selected() {
|
||||||
return Array.from(data.selectedIds.values())
|
return Array.from(data.selectedIds.values())
|
||||||
}, deepCompareArrays)
|
}, deepCompareArrays)
|
||||||
|
|
||||||
const isSelecting = useSelector((s) => s.isInAny('notPointing', 'pinching'))
|
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||||
|
|
||||||
if (!isSelecting) return null
|
if (!isSelecting) return null
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
{...events}
|
{...events}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!shape.isHidden && <ReadShape 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) => (
|
||||||
<Shape
|
<Shape
|
||||||
|
@ -69,7 +69,7 @@ interface RealShapeProps {
|
||||||
style: Partial<React.SVGProps<SVGUseElement>>
|
style: Partial<React.SVGProps<SVGUseElement>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReadShape = memo(function RealShape({
|
const RealShape = memo(function RealShape({
|
||||||
isGroup,
|
isGroup,
|
||||||
id,
|
id,
|
||||||
style,
|
style,
|
||||||
|
@ -157,10 +157,10 @@ function Label({ children }: { children: React.ReactNode }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { HoverIndicator }
|
|
||||||
|
|
||||||
export default memo(Shape)
|
|
||||||
|
|
||||||
function pp(n: number[]) {
|
function pp(n: number[]) {
|
||||||
return '[' + n.map((v) => v.toFixed(1)).join(', ') + ']'
|
return '[' + n.map((v) => v.toFixed(1)).join(', ') + ']'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { HoverIndicator }
|
||||||
|
|
||||||
|
export default memo(Shape)
|
||||||
|
|
53
hooks/useCanvasEvents.ts
Normal file
53
hooks/useCanvasEvents.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { MutableRefObject, useCallback } from 'react'
|
||||||
|
import state from 'state'
|
||||||
|
import { fastBrushSelect, fastDrawUpdate } from 'state/hacks'
|
||||||
|
import inputs from 'state/inputs'
|
||||||
|
import { isMobile } from 'utils/utils'
|
||||||
|
|
||||||
|
export default function useCanvasEvents(
|
||||||
|
rCanvas: MutableRefObject<SVGGElement>
|
||||||
|
) {
|
||||||
|
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 (isMobile()) {
|
||||||
|
if (e.touches.length === 2) {
|
||||||
|
state.send('TOUCH_UNDO')
|
||||||
|
} else state.send('TOUCHED_CANVAS')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
||||||
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
|
|
||||||
|
if (state.isIn('draw.editing')) {
|
||||||
|
fastDrawUpdate(inputs.pointerMove(e))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isIn('brushSelecting')) {
|
||||||
|
const info = inputs.pointerMove(e)
|
||||||
|
fastBrushSelect(info.point)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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) })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
onPointerDown: handlePointerDown,
|
||||||
|
onTouchStart: handleTouchStart,
|
||||||
|
onPointerMove: handlePointerMove,
|
||||||
|
onPointerUp: handlePointerUp,
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,7 @@ export default function useShapeEvents(
|
||||||
const handlePointerMove = useCallback(
|
const handlePointerMove = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
state.send('MOVED_OVER_GROUP', inputs.pointerEnter(e, id))
|
state.send('MOVED_OVER_GROUP', inputs.pointerEnter(e, id))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,6 +3,12 @@ import state from 'state'
|
||||||
import inputs from 'state/inputs'
|
import inputs from 'state/inputs'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
import { useGesture } from 'react-use-gesture'
|
import { useGesture } from 'react-use-gesture'
|
||||||
|
import {
|
||||||
|
fastBrushSelect,
|
||||||
|
fastPanUpdate,
|
||||||
|
fastPinchCamera,
|
||||||
|
fastZoomUpdate,
|
||||||
|
} from 'state/hacks'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture zoom gestures (pinches, wheels and pans) and send to the state.
|
* Capture zoom gestures (pinches, wheels and pans) and send to the state.
|
||||||
|
@ -17,10 +23,17 @@ export default function useZoomEvents() {
|
||||||
{
|
{
|
||||||
onWheel: ({ event, delta }) => {
|
onWheel: ({ event, delta }) => {
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
state.send('ZOOMED_CAMERA', {
|
const { point } = inputs.wheel(event as WheelEvent)
|
||||||
delta: delta[1],
|
fastZoomUpdate(point, delta[1])
|
||||||
...inputs.wheel(event as WheelEvent),
|
// state.send('ZOOMED_CAMERA', {
|
||||||
})
|
// delta: delta[1],
|
||||||
|
// ...inputs.wheel(event as WheelEvent),
|
||||||
|
// })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isIn('pointing')) {
|
||||||
|
fastPanUpdate(delta)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,12 +58,19 @@ export default function useZoomEvents() {
|
||||||
|
|
||||||
const [distanceDelta, angleDelta] = vec.sub(rPinchDa.current, da)
|
const [distanceDelta, angleDelta] = vec.sub(rPinchDa.current, da)
|
||||||
|
|
||||||
state.send('PINCHED', {
|
fastPinchCamera(
|
||||||
delta: vec.sub(rPinchPoint.current, origin),
|
origin,
|
||||||
point: origin,
|
vec.sub(rPinchPoint.current, origin),
|
||||||
distanceDelta,
|
distanceDelta,
|
||||||
angleDelta,
|
angleDelta
|
||||||
})
|
)
|
||||||
|
|
||||||
|
// state.send('PINCHED', {
|
||||||
|
// delta: vec.sub(rPinchPoint.current, origin),
|
||||||
|
// point: origin,
|
||||||
|
// distanceDelta,
|
||||||
|
// angleDelta,
|
||||||
|
// })
|
||||||
|
|
||||||
rPinchDa.current = da
|
rPinchDa.current = da
|
||||||
rPinchPoint.current = origin
|
rPinchPoint.current = origin
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"@radix-ui/react-icons": "^1.0.3",
|
"@radix-ui/react-icons": "^1.0.3",
|
||||||
"@radix-ui/react-radio-group": "^0.0.16",
|
"@radix-ui/react-radio-group": "^0.0.16",
|
||||||
"@radix-ui/react-tooltip": "^0.0.18",
|
"@radix-ui/react-tooltip": "^0.0.18",
|
||||||
"@state-designer/react": "^1.7.1",
|
"@state-designer/react": "^1.7.3",
|
||||||
"@stitches/react": "^0.1.9",
|
"@stitches/react": "^0.1.9",
|
||||||
"framer-motion": "^4.1.16",
|
"framer-motion": "^4.1.16",
|
||||||
"ismobilejs": "^1.1.1",
|
"ismobilejs": "^1.1.1",
|
||||||
|
|
94
state/hacks.ts
Normal file
94
state/hacks.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { PointerInfo } from 'types'
|
||||||
|
import {
|
||||||
|
getCameraZoom,
|
||||||
|
getCurrentCamera,
|
||||||
|
screenToWorld,
|
||||||
|
setZoomCSS,
|
||||||
|
} from 'utils/utils'
|
||||||
|
import session from './session'
|
||||||
|
import state from './state'
|
||||||
|
import * as vec from 'utils/vec'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While a user is drawing with the draw tool, we want to update the shape without
|
||||||
|
* going through the trouble of updating the entire state machine. Speciifcally, we
|
||||||
|
* do not want to push the change through immer. Instead, we'll push the change
|
||||||
|
* directly to the state using `forceData`.
|
||||||
|
* @param info
|
||||||
|
*/
|
||||||
|
export function fastDrawUpdate(info: PointerInfo) {
|
||||||
|
const data = { ...state.data }
|
||||||
|
|
||||||
|
session.current.update(
|
||||||
|
data,
|
||||||
|
screenToWorld(info.point, data),
|
||||||
|
info.pressure,
|
||||||
|
info.shiftKey
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedId = Array.from(data.selectedIds.values())[0]
|
||||||
|
|
||||||
|
const shape = data.document.pages[data.currentPageId].shapes[selectedId]
|
||||||
|
|
||||||
|
data.document.pages[data.currentPageId].shapes[selectedId] = { ...shape }
|
||||||
|
|
||||||
|
state.forceData(Object.freeze(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fastPanUpdate(delta: number[]) {
|
||||||
|
const data = { ...state.data }
|
||||||
|
const camera = getCurrentCamera(data)
|
||||||
|
camera.point = vec.sub(camera.point, vec.div(delta, camera.zoom))
|
||||||
|
|
||||||
|
data.pageStates[data.currentPageId].camera = { ...camera }
|
||||||
|
|
||||||
|
state.forceData(Object.freeze(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fastZoomUpdate(point: number[], delta: number) {
|
||||||
|
const data = { ...state.data }
|
||||||
|
const camera = getCurrentCamera(data)
|
||||||
|
|
||||||
|
const next = camera.zoom - (delta / 100) * camera.zoom
|
||||||
|
|
||||||
|
const p0 = screenToWorld(point, data)
|
||||||
|
camera.zoom = getCameraZoom(next)
|
||||||
|
const p1 = screenToWorld(point, data)
|
||||||
|
camera.point = vec.add(camera.point, vec.sub(p1, p0))
|
||||||
|
|
||||||
|
data.pageStates[data.currentPageId].camera = { ...camera }
|
||||||
|
|
||||||
|
state.forceData(Object.freeze(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fastPinchCamera(
|
||||||
|
point: number[],
|
||||||
|
delta: number[],
|
||||||
|
distanceDelta: number,
|
||||||
|
angleDelta: number
|
||||||
|
) {
|
||||||
|
const data = { ...state.data }
|
||||||
|
const camera = getCurrentCamera(data)
|
||||||
|
|
||||||
|
camera.point = vec.sub(camera.point, vec.div(delta, camera.zoom))
|
||||||
|
|
||||||
|
const next = camera.zoom - (distanceDelta / 300) * camera.zoom
|
||||||
|
|
||||||
|
const p0 = screenToWorld(point, data)
|
||||||
|
camera.zoom = getCameraZoom(next)
|
||||||
|
const p1 = screenToWorld(point, data)
|
||||||
|
camera.point = vec.add(camera.point, vec.sub(p1, p0))
|
||||||
|
|
||||||
|
data.pageStates[data.currentPageId].camera = { ...camera }
|
||||||
|
|
||||||
|
state.forceData(Object.freeze(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fastBrushSelect(point: number[]) {
|
||||||
|
const data = { ...state.data }
|
||||||
|
session.current.update(data, screenToWorld(point, data))
|
||||||
|
|
||||||
|
data.selectedIds = new Set(data.selectedIds)
|
||||||
|
|
||||||
|
state.forceData(Object.freeze(data))
|
||||||
|
}
|
27
state/session.ts
Normal file
27
state/session.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { BaseSession } from './sessions'
|
||||||
|
|
||||||
|
class SessionManager {
|
||||||
|
private _current?: BaseSession
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._current = undefined
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrent(session: BaseSession) {
|
||||||
|
this._current = session
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
get current() {
|
||||||
|
return this._current
|
||||||
|
}
|
||||||
|
|
||||||
|
set current(session: BaseSession) {
|
||||||
|
this._current = session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = new SessionManager()
|
||||||
|
|
||||||
|
export default session
|
|
@ -4,6 +4,7 @@ import BaseSession from './base-session'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { getBoundsFromPoints, getPage, getShapes } from 'utils/utils'
|
import { getBoundsFromPoints, getPage, getShapes } from 'utils/utils'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
|
import state from 'state/state'
|
||||||
|
|
||||||
export default class BrushSession extends BaseSession {
|
export default class BrushSession extends BaseSession {
|
||||||
origin: number[]
|
origin: number[]
|
||||||
|
@ -62,7 +63,7 @@ export function getBrushSnapshot(data: Data) {
|
||||||
return {
|
return {
|
||||||
selectedIds: new Set(data.selectedIds),
|
selectedIds: new Set(data.selectedIds),
|
||||||
shapeHitTests: Object.fromEntries(
|
shapeHitTests: Object.fromEntries(
|
||||||
getShapes(current(data))
|
getShapes(state.data)
|
||||||
.filter((shape) => shape.type !== ShapeType.Group)
|
.filter((shape) => shape.type !== ShapeType.Group)
|
||||||
.map((shape) => {
|
.map((shape) => {
|
||||||
return [
|
return [
|
||||||
|
|
100
state/state.ts
100
state/state.ts
|
@ -25,6 +25,7 @@ import {
|
||||||
rotateBounds,
|
rotateBounds,
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
getDocumentBranch,
|
getDocumentBranch,
|
||||||
|
getCameraZoom,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
|
@ -43,6 +44,7 @@ import {
|
||||||
SizeStyle,
|
SizeStyle,
|
||||||
ColorStyle,
|
ColorStyle,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
|
import session from './session'
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
@ -324,17 +326,22 @@ const state = createState({
|
||||||
],
|
],
|
||||||
on: {
|
on: {
|
||||||
STARTED_PINCHING: { do: 'completeSession', to: 'pinching' },
|
STARTED_PINCHING: { do: 'completeSession', to: 'pinching' },
|
||||||
MOVED_POINTER: 'updateBrushSession',
|
// Currently using hacks.fastBrushSelect
|
||||||
|
// MOVED_POINTER: 'updateBrushSession',
|
||||||
PANNED_CAMERA: 'updateBrushSession',
|
PANNED_CAMERA: 'updateBrushSession',
|
||||||
STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
|
STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
|
||||||
CANCELLED: { do: 'cancelSession', to: 'selecting' },
|
CANCELLED: { do: 'cancelSession', to: 'selecting' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
pinching: {
|
pinching: {
|
||||||
on: {
|
on: {
|
||||||
PINCHED: { do: 'pinchCamera' },
|
// Pinching uses hacks.fastPinchCamera
|
||||||
|
// PINCHED: { do: 'pinchCamera' },
|
||||||
},
|
},
|
||||||
initial: 'selectPinching',
|
initial: 'selectPinching',
|
||||||
|
onExit: { secretlyDo: 'updateZoomCSS' },
|
||||||
states: {
|
states: {
|
||||||
selectPinching: {
|
selectPinching: {
|
||||||
on: {
|
on: {
|
||||||
|
@ -348,8 +355,6 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
usingTool: {
|
usingTool: {
|
||||||
initial: 'draw',
|
initial: 'draw',
|
||||||
onEnter: 'clearSelectedIds',
|
onEnter: 'clearSelectedIds',
|
||||||
|
@ -385,17 +390,17 @@ const state = createState({
|
||||||
editing: {
|
editing: {
|
||||||
onEnter: 'startDrawSession',
|
onEnter: 'startDrawSession',
|
||||||
on: {
|
on: {
|
||||||
STOPPED_POINTING: {
|
|
||||||
do: 'completeSession',
|
|
||||||
to: 'draw.creating',
|
|
||||||
},
|
|
||||||
CANCELLED: {
|
CANCELLED: {
|
||||||
do: 'breakSession',
|
do: 'breakSession',
|
||||||
to: 'selecting',
|
to: 'selecting',
|
||||||
},
|
},
|
||||||
|
STOPPED_POINTING: {
|
||||||
|
do: 'completeSession',
|
||||||
|
to: 'draw.creating',
|
||||||
|
},
|
||||||
PRESSED_SHIFT: 'keyUpdateDrawSession',
|
PRESSED_SHIFT: 'keyUpdateDrawSession',
|
||||||
RELEASED_SHIFT: 'keyUpdateDrawSession',
|
RELEASED_SHIFT: 'keyUpdateDrawSession',
|
||||||
MOVED_POINTER: 'updateDrawSession',
|
// MOVED_POINTER: 'updateDrawSession',
|
||||||
PANNED_CAMERA: 'updateDrawSession',
|
PANNED_CAMERA: 'updateDrawSession',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -816,53 +821,57 @@ const state = createState({
|
||||||
|
|
||||||
// Shared
|
// Shared
|
||||||
breakSession(data) {
|
breakSession(data) {
|
||||||
session?.cancel(data)
|
session.current?.cancel(data)
|
||||||
session = undefined
|
session.clear()
|
||||||
history.disable()
|
history.disable()
|
||||||
commands.deleteSelected(data)
|
commands.deleteSelected(data)
|
||||||
history.enable()
|
history.enable()
|
||||||
},
|
},
|
||||||
cancelSession(data) {
|
cancelSession(data) {
|
||||||
session?.cancel(data)
|
session.current?.cancel(data)
|
||||||
session = undefined
|
session.clear()
|
||||||
},
|
},
|
||||||
completeSession(data) {
|
completeSession(data) {
|
||||||
session?.complete(data)
|
session.current?.complete(data)
|
||||||
session = undefined
|
session.clear()
|
||||||
},
|
},
|
||||||
|
|
||||||
// Brushing
|
// Brushing
|
||||||
startBrushSession(data, payload: PointerInfo) {
|
startBrushSession(data, payload: PointerInfo) {
|
||||||
session = new Sessions.BrushSession(
|
session.current = new Sessions.BrushSession(
|
||||||
data,
|
data,
|
||||||
screenToWorld(payload.point, data)
|
screenToWorld(payload.point, data)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateBrushSession(data, payload: PointerInfo) {
|
updateBrushSession(data, payload: PointerInfo) {
|
||||||
session.update(data, screenToWorld(payload.point, data))
|
session.current.update(data, screenToWorld(payload.point, data))
|
||||||
},
|
},
|
||||||
|
|
||||||
// Rotating
|
// Rotating
|
||||||
startRotateSession(data, payload: PointerInfo) {
|
startRotateSession(data, payload: PointerInfo) {
|
||||||
session = new Sessions.RotateSession(
|
session.current = new Sessions.RotateSession(
|
||||||
data,
|
data,
|
||||||
screenToWorld(payload.point, data)
|
screenToWorld(payload.point, data)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
keyUpdateRotateSession(data, payload: PointerInfo) {
|
keyUpdateRotateSession(data, payload: PointerInfo) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(inputs.pointer.point, data),
|
screenToWorld(inputs.pointer.point, data),
|
||||||
payload.shiftKey
|
payload.shiftKey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateRotateSession(data, payload: PointerInfo) {
|
updateRotateSession(data, payload: PointerInfo) {
|
||||||
session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
|
session.current.update(
|
||||||
|
data,
|
||||||
|
screenToWorld(payload.point, data),
|
||||||
|
payload.shiftKey
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Dragging / Translating
|
// Dragging / Translating
|
||||||
startTranslateSession(data) {
|
startTranslateSession(data) {
|
||||||
session = new Sessions.TranslateSession(
|
session.current = new Sessions.TranslateSession(
|
||||||
data,
|
data,
|
||||||
screenToWorld(inputs.pointer.origin, data)
|
screenToWorld(inputs.pointer.origin, data)
|
||||||
)
|
)
|
||||||
|
@ -871,7 +880,7 @@ const state = createState({
|
||||||
data,
|
data,
|
||||||
payload: { shiftKey: boolean; altKey: boolean }
|
payload: { shiftKey: boolean; altKey: boolean }
|
||||||
) {
|
) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(inputs.pointer.point, data),
|
screenToWorld(inputs.pointer.point, data),
|
||||||
payload.shiftKey,
|
payload.shiftKey,
|
||||||
|
@ -879,7 +888,7 @@ const state = createState({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateTranslateSession(data, payload: PointerInfo) {
|
updateTranslateSession(data, payload: PointerInfo) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(payload.point, data),
|
screenToWorld(payload.point, data),
|
||||||
payload.shiftKey,
|
payload.shiftKey,
|
||||||
|
@ -892,7 +901,7 @@ const state = createState({
|
||||||
const shapeId = Array.from(data.selectedIds.values())[0]
|
const shapeId = Array.from(data.selectedIds.values())[0]
|
||||||
const handleId = payload.target
|
const handleId = payload.target
|
||||||
|
|
||||||
session = new Sessions.HandleSession(
|
session.current = new Sessions.HandleSession(
|
||||||
data,
|
data,
|
||||||
shapeId,
|
shapeId,
|
||||||
handleId,
|
handleId,
|
||||||
|
@ -903,7 +912,7 @@ const state = createState({
|
||||||
data,
|
data,
|
||||||
payload: { shiftKey: boolean; altKey: boolean }
|
payload: { shiftKey: boolean; altKey: boolean }
|
||||||
) {
|
) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(inputs.pointer.point, data),
|
screenToWorld(inputs.pointer.point, data),
|
||||||
payload.shiftKey,
|
payload.shiftKey,
|
||||||
|
@ -911,7 +920,7 @@ const state = createState({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateHandleSession(data, payload: PointerInfo) {
|
updateHandleSession(data, payload: PointerInfo) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(payload.point, data),
|
screenToWorld(payload.point, data),
|
||||||
payload.shiftKey,
|
payload.shiftKey,
|
||||||
|
@ -925,14 +934,13 @@ const state = createState({
|
||||||
payload: PointerInfo & { target: Corner | Edge }
|
payload: PointerInfo & { target: Corner | Edge }
|
||||||
) {
|
) {
|
||||||
const point = screenToWorld(inputs.pointer.origin, data)
|
const point = screenToWorld(inputs.pointer.origin, data)
|
||||||
session = new Sessions.TransformSession(data, payload.target, point)
|
session.current =
|
||||||
session =
|
|
||||||
data.selectedIds.size === 1
|
data.selectedIds.size === 1
|
||||||
? new Sessions.TransformSingleSession(data, payload.target, point)
|
? new Sessions.TransformSingleSession(data, payload.target, point)
|
||||||
: new Sessions.TransformSession(data, payload.target, point)
|
: new Sessions.TransformSession(data, payload.target, point)
|
||||||
},
|
},
|
||||||
startDrawTransformSession(data, payload: PointerInfo) {
|
startDrawTransformSession(data, payload: PointerInfo) {
|
||||||
session = new Sessions.TransformSingleSession(
|
session.current = new Sessions.TransformSingleSession(
|
||||||
data,
|
data,
|
||||||
Corner.BottomRight,
|
Corner.BottomRight,
|
||||||
screenToWorld(payload.point, data),
|
screenToWorld(payload.point, data),
|
||||||
|
@ -940,7 +948,7 @@ const state = createState({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
keyUpdateTransformSession(data, payload: PointerInfo) {
|
keyUpdateTransformSession(data, payload: PointerInfo) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(inputs.pointer.point, data),
|
screenToWorld(inputs.pointer.point, data),
|
||||||
payload.shiftKey,
|
payload.shiftKey,
|
||||||
|
@ -948,7 +956,7 @@ const state = createState({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateTransformSession(data, payload: PointerInfo) {
|
updateTransformSession(data, payload: PointerInfo) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(payload.point, data),
|
screenToWorld(payload.point, data),
|
||||||
payload.shiftKey,
|
payload.shiftKey,
|
||||||
|
@ -958,19 +966,19 @@ const state = createState({
|
||||||
|
|
||||||
// Direction
|
// Direction
|
||||||
startDirectionSession(data, payload: PointerInfo) {
|
startDirectionSession(data, payload: PointerInfo) {
|
||||||
session = new Sessions.DirectionSession(
|
session.current = new Sessions.DirectionSession(
|
||||||
data,
|
data,
|
||||||
screenToWorld(inputs.pointer.origin, data)
|
screenToWorld(inputs.pointer.origin, data)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateDirectionSession(data, payload: PointerInfo) {
|
updateDirectionSession(data, payload: PointerInfo) {
|
||||||
session.update(data, screenToWorld(payload.point, data))
|
session.current.update(data, screenToWorld(payload.point, data))
|
||||||
},
|
},
|
||||||
|
|
||||||
// Drawing
|
// Drawing
|
||||||
startDrawSession(data, payload: PointerInfo) {
|
startDrawSession(data, payload: PointerInfo) {
|
||||||
const id = Array.from(data.selectedIds.values())[0]
|
const id = Array.from(data.selectedIds.values())[0]
|
||||||
session = new Sessions.DrawSession(
|
session.current = new Sessions.DrawSession(
|
||||||
data,
|
data,
|
||||||
id,
|
id,
|
||||||
screenToWorld(inputs.pointer.origin, data),
|
screenToWorld(inputs.pointer.origin, data),
|
||||||
|
@ -978,7 +986,7 @@ const state = createState({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
keyUpdateDrawSession(data, payload: PointerInfo) {
|
keyUpdateDrawSession(data, payload: PointerInfo) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(inputs.pointer.point, data),
|
screenToWorld(inputs.pointer.point, data),
|
||||||
payload.pressure,
|
payload.pressure,
|
||||||
|
@ -986,7 +994,7 @@ const state = createState({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateDrawSession(data, payload: PointerInfo) {
|
updateDrawSession(data, payload: PointerInfo) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(payload.point, data),
|
screenToWorld(payload.point, data),
|
||||||
payload.pressure,
|
payload.pressure,
|
||||||
|
@ -997,7 +1005,7 @@ const state = createState({
|
||||||
// Arrow
|
// Arrow
|
||||||
startArrowSession(data, payload: PointerInfo) {
|
startArrowSession(data, payload: PointerInfo) {
|
||||||
const id = Array.from(data.selectedIds.values())[0]
|
const id = Array.from(data.selectedIds.values())[0]
|
||||||
session = new Sessions.ArrowSession(
|
session.current = new Sessions.ArrowSession(
|
||||||
data,
|
data,
|
||||||
id,
|
id,
|
||||||
screenToWorld(inputs.pointer.origin, data),
|
screenToWorld(inputs.pointer.origin, data),
|
||||||
|
@ -1005,14 +1013,18 @@ const state = createState({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
keyUpdateArrowSession(data, payload: PointerInfo) {
|
keyUpdateArrowSession(data, payload: PointerInfo) {
|
||||||
session.update(
|
session.current.update(
|
||||||
data,
|
data,
|
||||||
screenToWorld(inputs.pointer.point, data),
|
screenToWorld(inputs.pointer.point, data),
|
||||||
payload.shiftKey
|
payload.shiftKey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateArrowSession(data, payload: PointerInfo) {
|
updateArrowSession(data, payload: PointerInfo) {
|
||||||
session.update(data, screenToWorld(payload.point, data), payload.shiftKey)
|
session.current.update(
|
||||||
|
data,
|
||||||
|
screenToWorld(payload.point, data),
|
||||||
|
payload.shiftKey
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Nudges
|
// Nudges
|
||||||
|
@ -1257,6 +1269,10 @@ const state = createState({
|
||||||
const camera = getCurrentCamera(data)
|
const camera = getCurrentCamera(data)
|
||||||
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
|
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
|
||||||
},
|
},
|
||||||
|
updateZoomCSS(data) {
|
||||||
|
const camera = getCurrentCamera(data)
|
||||||
|
setZoomCSS(camera.zoom)
|
||||||
|
},
|
||||||
pinchCamera(
|
pinchCamera(
|
||||||
data,
|
data,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -1509,16 +1525,10 @@ const state = createState({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let session: Sessions.BaseSession
|
|
||||||
|
|
||||||
export default state
|
export default state
|
||||||
|
|
||||||
export const useSelector = createSelectorHook(state)
|
export const useSelector = createSelectorHook(state)
|
||||||
|
|
||||||
function getCameraZoom(zoom: number) {
|
|
||||||
return clamp(zoom, 0.1, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getParentId(data: Data, id: string) {
|
function getParentId(data: Data, id: string) {
|
||||||
const shape = getPage(data).shapes[id]
|
const shape = getPage(data).shapes[id]
|
||||||
return shape.parentId
|
return shape.parentId
|
||||||
|
|
17
types.ts
17
types.ts
|
@ -33,15 +33,7 @@ export interface Data {
|
||||||
pages: Record<string, Page>
|
pages: Record<string, Page>
|
||||||
code: Record<string, CodeFile>
|
code: Record<string, CodeFile>
|
||||||
}
|
}
|
||||||
pageStates: Record<
|
pageStates: Record<string, PageState>
|
||||||
string,
|
|
||||||
{
|
|
||||||
camera: {
|
|
||||||
point: number[]
|
|
||||||
zoom: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
@ -56,6 +48,13 @@ export interface Page {
|
||||||
shapes: Record<string, Shape>
|
shapes: Record<string, Shape>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageState {
|
||||||
|
camera: {
|
||||||
|
point: number[]
|
||||||
|
zoom: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export enum ShapeType {
|
export enum ShapeType {
|
||||||
Dot = 'dot',
|
Dot = 'dot',
|
||||||
Circle = 'circle',
|
Circle = 'circle',
|
||||||
|
|
|
@ -834,6 +834,10 @@ export function throttle<P extends any[], T extends (...args: P) => any>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCameraZoom(zoom: number) {
|
||||||
|
return clamp(zoom, 0.1, 5)
|
||||||
|
}
|
||||||
|
|
||||||
export function pointInRect(
|
export function pointInRect(
|
||||||
point: number[],
|
point: number[],
|
||||||
minX: number,
|
minX: number,
|
||||||
|
|
Loading…
Reference in a new issue