tldraw/state/state.ts

179 lines
4.6 KiB
TypeScript
Raw Normal View History

2021-05-09 12:03:39 +00:00
import { createSelectorHook, createState } from "@state-designer/react"
2021-05-12 22:08:53 +00:00
import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
2021-05-09 21:22:25 +00:00
import * as vec from "utils/vec"
2021-05-12 22:08:53 +00:00
import { Bounds, Data, Shape, ShapeType } from "types"
2021-05-10 12:16:57 +00:00
import { defaultDocument } from "./data"
2021-05-12 22:08:53 +00:00
import Shapes from "lib/shapes"
2021-05-10 12:16:57 +00:00
import * as Sessions from "./sessions"
2021-05-09 12:03:39 +00:00
2021-05-09 21:22:25 +00:00
const initialData: Data = {
2021-05-09 13:04:42 +00:00
camera: {
point: [0, 0],
zoom: 1,
},
2021-05-10 12:16:57 +00:00
brush: undefined,
pointedId: null,
2021-05-12 11:27:33 +00:00
selectedIds: new Set([]),
2021-05-09 21:22:25 +00:00
currentPageId: "page0",
2021-05-10 12:16:57 +00:00
document: defaultDocument,
2021-05-09 13:04:42 +00:00
}
const state = createState({
data: initialData,
on: {
ZOOMED_CAMERA: {
do: "zoomCamera",
},
PANNED_CAMERA: {
do: "panCamera",
},
},
2021-05-10 12:16:57 +00:00
initial: "selecting",
states: {
selecting: {
on: {
POINTED_CANVAS: { to: "brushSelecting" },
POINTED_SHAPE: [
"setPointedId",
{
if: "isPressingShiftKey",
then: {
if: "isPointedShapeSelected",
do: "pullPointedIdFromSelectedIds",
else: "pushPointedIdToSelectedIds",
},
else: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
},
],
2021-05-10 12:16:57 +00:00
},
},
brushSelecting: {
onEnter: [
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
"startBrushSession",
],
2021-05-10 12:16:57 +00:00
on: {
MOVED_POINTER: "updateBrushSession",
2021-05-10 20:44:17 +00:00
PANNED_CAMERA: "updateBrushSession",
2021-05-10 12:16:57 +00:00
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
},
conditions: {
isPointedShapeSelected(data) {
2021-05-12 11:27:33 +00:00
return data.selectedIds.has(data.pointedId)
},
isPressingShiftKey(data, payload: { shiftKey: boolean }) {
return payload.shiftKey
},
},
2021-05-09 13:04:42 +00:00
actions: {
2021-05-10 12:16:57 +00:00
cancelSession(data) {
session.cancel(data)
session = undefined
},
completeSession(data) {
session.complete(data)
session = undefined
},
2021-05-10 20:44:17 +00:00
startBrushSession(data, payload: { point: number[] }) {
session = new Sessions.BrushSession(
data,
screenToWorld(payload.point, data)
)
2021-05-10 12:16:57 +00:00
},
2021-05-10 20:44:17 +00:00
updateBrushSession(data, payload: { point: number[] }) {
session.update(data, screenToWorld(payload.point, data))
2021-05-10 12:16:57 +00:00
},
// Selection
setPointedId(data, payload: { id: string }) {
data.pointedId = payload.id
},
clearPointedId(data) {
data.pointedId = undefined
},
clearSelectedIds(data) {
2021-05-12 11:27:33 +00:00
data.selectedIds.clear()
},
pullPointedIdFromSelectedIds(data) {
const { selectedIds, pointedId } = data
2021-05-12 11:27:33 +00:00
selectedIds.delete(pointedId)
},
pushPointedIdToSelectedIds(data) {
2021-05-12 11:27:33 +00:00
data.selectedIds.add(data.pointedId)
},
// Camera
2021-05-09 13:04:42 +00:00
zoomCamera(data, payload: { delta: number; point: number[] }) {
const { camera } = data
const p0 = screenToWorld(payload.point, data)
camera.zoom = clamp(
camera.zoom - (payload.delta / 100) * camera.zoom,
0.5,
3
)
const p1 = screenToWorld(payload.point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
document.documentElement.style.setProperty(
"--camera-zoom",
camera.zoom.toString()
)
2021-05-09 13:04:42 +00:00
},
panCamera(data, payload: { delta: number[]; point: number[] }) {
const { camera } = data
data.camera.point = vec.sub(
camera.point,
vec.div(payload.delta, camera.zoom)
)
},
},
values: {
selectedIds(data) {
return new Set(data.selectedIds)
},
2021-05-12 22:08:53 +00:00
selectedBounds(data) {
const {
selectedIds,
currentPageId,
document: { pages },
} = data
return getCommonBounds(
...Array.from(selectedIds.values())
.map((id) => {
const shape = pages[currentPageId].shapes[id]
switch (shape.type) {
case ShapeType.Dot: {
return Shapes[shape.type].getBounds(shape)
}
case ShapeType.Circle: {
return Shapes[shape.type].getBounds(shape)
}
case ShapeType.Line: {
return Shapes[shape.type].getBounds(shape)
}
case ShapeType.Polyline: {
return Shapes[shape.type].getBounds(shape)
}
case ShapeType.Rectangle: {
return Shapes[shape.type].getBounds(shape)
}
default: {
return null
}
}
})
.filter(Boolean)
)
},
},
2021-05-09 13:04:42 +00:00
})
2021-05-09 12:03:39 +00:00
2021-05-10 12:16:57 +00:00
let session: Sessions.BaseSession
2021-05-09 12:03:39 +00:00
export default state
export const useSelector = createSelectorHook(state)