Adds dragging / translation
This commit is contained in:
parent
7ec9457ac2
commit
8c81823b20
18 changed files with 340 additions and 141 deletions
|
@ -1,7 +1,10 @@
|
|||
import { useRef } from "react"
|
||||
import state, { useSelector } from "state"
|
||||
import inputs from "state/inputs"
|
||||
import styled from "styles"
|
||||
|
||||
export default function BoundsBg() {
|
||||
const rBounds = useRef<SVGRectElement>(null)
|
||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||
|
||||
if (!bounds) return null
|
||||
|
@ -10,19 +13,15 @@ export default function BoundsBg() {
|
|||
|
||||
return (
|
||||
<StyledBoundsBg
|
||||
ref={rBounds}
|
||||
x={minX}
|
||||
y={minY}
|
||||
width={width}
|
||||
height={height}
|
||||
onPointerDown={(e) => {
|
||||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS", {
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey || e.ctrlKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
})
|
||||
rBounds.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_BOUNDS", inputs.pointerDown(e))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import state, { useSelector } from "state"
|
||||
import { motion } from "framer-motion"
|
||||
import styled from "styles"
|
||||
import inputs from "state/inputs"
|
||||
|
||||
export default function Bounds() {
|
||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||
|
@ -23,38 +24,42 @@ export default function Bounds() {
|
|||
height={height}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<Corner
|
||||
x={minX}
|
||||
y={minY}
|
||||
corner={0}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nwse-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={maxX}
|
||||
y={minY}
|
||||
corner={1}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nesw-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={maxX}
|
||||
y={maxY}
|
||||
corner={2}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nwse-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={minX}
|
||||
y={maxY}
|
||||
corner={3}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nesw-resize"
|
||||
/>
|
||||
{width * zoom > 8 && (
|
||||
<>
|
||||
<Corner
|
||||
x={minX}
|
||||
y={minY}
|
||||
corner={0}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nwse-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={maxX}
|
||||
y={minY}
|
||||
corner={1}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nesw-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={maxX}
|
||||
y={maxY}
|
||||
corner={2}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nwse-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={minX}
|
||||
y={maxY}
|
||||
corner={3}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nesw-resize"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<EdgeHorizontal
|
||||
x={minX + p}
|
||||
y={minY}
|
||||
|
@ -65,11 +70,7 @@ export default function Bounds() {
|
|||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_EDGE", {
|
||||
edge: 0,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
...inputs.pointerDown(e),
|
||||
})
|
||||
document.body.style.cursor = "ns-resize"
|
||||
}}
|
||||
|
@ -84,11 +85,7 @@ export default function Bounds() {
|
|||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_EDGE", {
|
||||
edge: 1,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
...inputs.pointerDown(e),
|
||||
})
|
||||
document.body.style.cursor = "ew-resize"
|
||||
}}
|
||||
|
@ -103,11 +100,7 @@ export default function Bounds() {
|
|||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_EDGE", {
|
||||
edge: 2,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
...inputs.pointerDown(e),
|
||||
})
|
||||
document.body.style.cursor = "ns-resize"
|
||||
}}
|
||||
|
@ -122,11 +115,7 @@ export default function Bounds() {
|
|||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_EDGE", {
|
||||
edge: 3,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
...inputs.pointerDown(e),
|
||||
})
|
||||
document.body.style.cursor = "ew-resize"
|
||||
}}
|
||||
|
@ -168,11 +157,7 @@ function Corner({
|
|||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_ROTATE_CORNER", {
|
||||
corner,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
...inputs.pointerDown(e),
|
||||
})
|
||||
document.body.style.cursor = "grabbing"
|
||||
}}
|
||||
|
@ -190,18 +175,13 @@ function Corner({
|
|||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_CORNER", {
|
||||
corner,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
...inputs.pointerDown(e),
|
||||
})
|
||||
document.body.style.cursor = "nesw-resize"
|
||||
}}
|
||||
onPanEnd={restoreCursor}
|
||||
onTap={restoreCursor}
|
||||
style={{ cursor }}
|
||||
className="strokewidth-ui stroke-bounds fill-corner"
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
|
@ -268,9 +248,9 @@ function EdgeVertical({
|
|||
)
|
||||
}
|
||||
|
||||
function restoreCursor() {
|
||||
function restoreCursor(e: PointerEvent) {
|
||||
state.send("STOPPED_POINTING", { id: "bounds", ...inputs.pointerUp(e) })
|
||||
document.body.style.cursor = "default"
|
||||
state.send("STOPPED_POINTING")
|
||||
}
|
||||
|
||||
const StyledEdge = styled(motion.rect, {
|
||||
|
|
|
@ -8,6 +8,7 @@ import Brush from "./brush"
|
|||
import state from "state"
|
||||
import Bounds from "./bounds"
|
||||
import BoundsBg from "./bounds-bg"
|
||||
import inputs from "state/inputs"
|
||||
|
||||
export default function Canvas() {
|
||||
const rCanvas = useRef<SVGSVGElement>(null)
|
||||
|
@ -18,16 +19,16 @@ export default function Canvas() {
|
|||
|
||||
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
rCanvas.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_CANVAS", getPointerEventInfo(e))
|
||||
state.send("POINTED_CANVAS", inputs.pointerDown(e))
|
||||
}, [])
|
||||
|
||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
||||
state.send("MOVED_POINTER", getPointerEventInfo(e))
|
||||
state.send("MOVED_POINTER", inputs.pointerMove(e))
|
||||
}, [])
|
||||
|
||||
const handlePointerUp = useCallback((e: React.PointerEvent) => {
|
||||
rCanvas.current.releasePointerCapture(e.pointerId)
|
||||
state.send("STOPPED_POINTING", getPointerEventInfo(e))
|
||||
state.send("STOPPED_POINTING", { id: "canvas", ...inputs.pointerUp(e) })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback, useRef, memo } from "react"
|
||||
import state, { useSelector } from "state"
|
||||
import { getPointerEventInfo } from "utils/utils"
|
||||
import inputs from "state/inputs"
|
||||
import shapes from "lib/shapes"
|
||||
import styled from "styles"
|
||||
|
||||
|
@ -18,7 +19,7 @@ function Shape({ id }: { id: string }) {
|
|||
(e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
rGroup.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) })
|
||||
state.send("POINTED_SHAPE", { id, ...inputs.pointerDown(e) })
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
@ -27,20 +28,18 @@ function Shape({ id }: { id: string }) {
|
|||
(e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
rGroup.current.releasePointerCapture(e.pointerId)
|
||||
state.send("STOPPED_POINTING_SHAPE", { id, ...getPointerEventInfo(e) })
|
||||
state.send("STOPPED_POINTING", { id, ...inputs.pointerUp(e) })
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const handlePointerEnter = useCallback(
|
||||
(e: React.PointerEvent) =>
|
||||
state.send("HOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
|
||||
(e: React.PointerEvent) => state.send("HOVERED_SHAPE", { id }),
|
||||
[id]
|
||||
)
|
||||
|
||||
const handlePointerLeave = useCallback(
|
||||
(e: React.PointerEvent) =>
|
||||
state.send("UNHOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
|
||||
(e: React.PointerEvent) => state.send("UNHOVERED_SHAPE", { id }),
|
||||
[id]
|
||||
)
|
||||
|
||||
|
|
|
@ -56,7 +56,8 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
|
|||
return shape
|
||||
},
|
||||
|
||||
translate(shape) {
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
|
|
|
@ -52,7 +52,8 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
|
|||
return shape
|
||||
},
|
||||
|
||||
translate(shape) {
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
|
|
|
@ -61,7 +61,8 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
|||
return shape
|
||||
},
|
||||
|
||||
translate(shape) {
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
|
|
|
@ -54,7 +54,8 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
|||
return shape
|
||||
},
|
||||
|
||||
translate(shape) {
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
|
|
|
@ -4,30 +4,14 @@ import { Data } from "types"
|
|||
|
||||
export type CommandFn<T> = (data: T, initial?: boolean) => void
|
||||
|
||||
export enum CommandType {
|
||||
ChangeBounds,
|
||||
CreateGlob,
|
||||
CreateNode,
|
||||
Delete,
|
||||
Split,
|
||||
Move,
|
||||
MoveAnchor,
|
||||
ReorderGlobs,
|
||||
ReorderNodes,
|
||||
Paste,
|
||||
ToggleCap,
|
||||
ToggleLocked,
|
||||
SetProperty,
|
||||
SetItems,
|
||||
Transform,
|
||||
}
|
||||
|
||||
/**
|
||||
* A command makes changes to some applicate state. Every command has an "undo"
|
||||
* method to reverse its changes. The apps history is a series of commands.
|
||||
*/
|
||||
export class BaseCommand<T extends any> {
|
||||
timestamp = Date.now()
|
||||
name: string
|
||||
category: string
|
||||
private undoFn: CommandFn<T>
|
||||
private doFn: CommandFn<T>
|
||||
protected restoreBeforeSelectionState: (data: T) => void
|
||||
|
@ -36,11 +20,14 @@ export class BaseCommand<T extends any> {
|
|||
protected manualSelection: boolean
|
||||
|
||||
constructor(options: {
|
||||
type: CommandType
|
||||
do: CommandFn<T>
|
||||
undo: CommandFn<T>
|
||||
name: string
|
||||
category: string
|
||||
manualSelection?: boolean
|
||||
}) {
|
||||
this.name = options.name
|
||||
this.category = options.category
|
||||
this.doFn = options.do
|
||||
this.undoFn = options.undo
|
||||
this.manualSelection = options.manualSelection || false
|
||||
|
@ -87,8 +74,11 @@ export class BaseCommand<T extends any> {
|
|||
* to mutate the state's data. Actions do not effect the "active states" in
|
||||
* the app.
|
||||
*/
|
||||
export class Command extends BaseCommand<Data> {
|
||||
export default class Command extends BaseCommand<Data> {
|
||||
saveSelectionState = (data: Data) => {
|
||||
return (data: Data) => {}
|
||||
const selectedIds = new Set(data.selectedIds)
|
||||
return (data: Data) => {
|
||||
data.selectedIds = selectedIds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Data } from "types"
|
||||
import { BaseCommand } from "./command"
|
||||
|
||||
class BaseHistory<T> {
|
||||
// A singleton to manage history changes.
|
||||
|
||||
class History<T> {
|
||||
private stack: BaseCommand<T>[] = []
|
||||
private pointer = -1
|
||||
private maxLength = 100
|
||||
|
@ -44,7 +46,7 @@ class BaseHistory<T> {
|
|||
if (typeof window === "undefined") return
|
||||
if (typeof localStorage === "undefined") return
|
||||
|
||||
localStorage.setItem("glob_aldata_v6", JSON.stringify(data))
|
||||
localStorage.setItem("code_slate_0.0.1", JSON.stringify(data))
|
||||
}
|
||||
|
||||
disable = () => {
|
||||
|
@ -60,4 +62,4 @@ class BaseHistory<T> {
|
|||
}
|
||||
}
|
||||
|
||||
export default new BaseHistory<Data>()
|
||||
export default new History<Data>()
|
||||
|
|
5
state/commands/index.ts
Normal file
5
state/commands/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import translate from "./translate-command"
|
||||
|
||||
const commands = { translate }
|
||||
|
||||
export default commands
|
32
state/commands/translate-command.ts
Normal file
32
state/commands/translate-command.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Command from "./command"
|
||||
import history from "./history"
|
||||
import { TranslateSnapshot } from "state/sessions/translate-session"
|
||||
import { Data } from "types"
|
||||
|
||||
export default function translateCommand(
|
||||
data: Data,
|
||||
before: TranslateSnapshot,
|
||||
after: TranslateSnapshot
|
||||
) {
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: "translate_shapes",
|
||||
category: "canvas",
|
||||
do(data) {
|
||||
const { shapes } = data.document.pages[after.currentPageId]
|
||||
|
||||
for (let { id, point } of after.shapes) {
|
||||
shapes[id].point = point
|
||||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = data.document.pages[before.currentPageId]
|
||||
|
||||
for (let { id, point } of before.shapes) {
|
||||
shapes[id].point = point
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
52
state/inputs.tsx
Normal file
52
state/inputs.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { PointerInfo } from "types"
|
||||
|
||||
class Inputs {
|
||||
points: Record<string, PointerInfo> = {}
|
||||
|
||||
pointerDown(e: PointerEvent | React.PointerEvent) {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
|
||||
this.points[e.pointerId] = {
|
||||
pointerId: e.pointerId,
|
||||
origin: [e.clientX, e.clientY],
|
||||
point: [e.clientX, e.clientY],
|
||||
shiftKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
altKey,
|
||||
}
|
||||
|
||||
return this.points[e.pointerId]
|
||||
}
|
||||
|
||||
pointerMove(e: PointerEvent | React.PointerEvent) {
|
||||
if (this.points[e.pointerId]) {
|
||||
this.points[e.pointerId].point = [e.clientX, e.clientY]
|
||||
return this.points[e.pointerId]
|
||||
}
|
||||
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
|
||||
return {
|
||||
pointerId: e.pointerId,
|
||||
origin: [e.clientX, e.clientY],
|
||||
point: [e.clientX, e.clientY],
|
||||
shiftKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
altKey,
|
||||
}
|
||||
}
|
||||
|
||||
pointerUp(e: PointerEvent | React.PointerEvent) {
|
||||
this.points[e.pointerId].point = [e.clientX, e.clientY]
|
||||
|
||||
const info = this.points[e.pointerId]
|
||||
|
||||
delete this.points[e.pointerId]
|
||||
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inputs()
|
|
@ -3,15 +3,15 @@ import { Data } from "types"
|
|||
export default class BaseSession {
|
||||
constructor(data: Data) {}
|
||||
|
||||
update = (data: Data, ...args: unknown[]) => {
|
||||
update(data: Data, ...args: unknown[]) {
|
||||
// Update the state
|
||||
}
|
||||
|
||||
complete = (data: Data, ...args: unknown[]) => {
|
||||
complete(data: Data, ...args: unknown[]) {
|
||||
// Create a command
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
cancel(data: Data) {
|
||||
// Clean up the change
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import BaseSession from "./brush-session"
|
||||
import BaseSession from "./base-session"
|
||||
import BrushSession from "./brush-session"
|
||||
import TranslateSession from "./translate-session"
|
||||
|
||||
export { BrushSession, BaseSession }
|
||||
export { BrushSession, BaseSession, TranslateSession }
|
||||
|
|
62
state/sessions/translate-session.ts
Normal file
62
state/sessions/translate-session.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { Data } from "types"
|
||||
import * as vec from "utils/vec"
|
||||
import BaseSession from "./base-session"
|
||||
import commands from "state/commands"
|
||||
import { current } from "immer"
|
||||
|
||||
export default class TranslateSession extends BaseSession {
|
||||
delta = [0, 0]
|
||||
origin: number[]
|
||||
snapshot: TranslateSnapshot
|
||||
|
||||
constructor(data: Data, point: number[]) {
|
||||
super(data)
|
||||
this.origin = point
|
||||
this.snapshot = getTranslateSnapshot(data)
|
||||
}
|
||||
|
||||
update(data: Data, point: number[]) {
|
||||
const { currentPageId, shapes } = this.snapshot
|
||||
const { document } = data
|
||||
|
||||
const delta = vec.vec(this.origin, point)
|
||||
|
||||
for (let shape of shapes) {
|
||||
document.pages[currentPageId].shapes[shape.id].point = vec.add(
|
||||
shape.point,
|
||||
delta
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cancel(data: Data) {
|
||||
const { document } = data
|
||||
|
||||
for (let shape of this.snapshot.shapes) {
|
||||
document.pages[this.snapshot.currentPageId].shapes[shape.id].point =
|
||||
shape.point
|
||||
}
|
||||
}
|
||||
|
||||
complete(data: Data) {
|
||||
commands.translate(data, this.snapshot, getTranslateSnapshot(data))
|
||||
}
|
||||
}
|
||||
|
||||
export function getTranslateSnapshot(data: Data) {
|
||||
const {
|
||||
document: { pages },
|
||||
currentPageId,
|
||||
} = current(data)
|
||||
|
||||
const { shapes } = pages[currentPageId]
|
||||
|
||||
return {
|
||||
currentPageId,
|
||||
shapes: Array.from(data.selectedIds.values())
|
||||
.map((id) => shapes[id])
|
||||
.map(({ id, point }) => ({ id, point })),
|
||||
}
|
||||
}
|
||||
|
||||
export type TranslateSnapshot = ReturnType<typeof getTranslateSnapshot>
|
113
state/state.ts
113
state/state.ts
|
@ -1,12 +1,13 @@
|
|||
import { createSelectorHook, createState } from "@state-designer/react"
|
||||
import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import { Bounds, Data, Shape, ShapeType } from "types"
|
||||
import { Bounds, Data, PointerInfo, Shape, ShapeType } from "types"
|
||||
import { defaultDocument } from "./data"
|
||||
import Shapes from "lib/shapes"
|
||||
import * as Sessions from "./sessions"
|
||||
|
||||
const initialData: Data = {
|
||||
isReadOnly: false,
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
|
@ -31,36 +32,84 @@ const state = createState({
|
|||
initial: "selecting",
|
||||
states: {
|
||||
selecting: {
|
||||
on: {
|
||||
POINTED_CANVAS: { to: "brushSelecting" },
|
||||
POINTED_SHAPE: [
|
||||
"setPointedId",
|
||||
{
|
||||
if: "isPressingShiftKey",
|
||||
then: {
|
||||
if: "isPointedShapeSelected",
|
||||
do: "pullPointedIdFromSelectedIds",
|
||||
else: "pushPointedIdToSelectedIds",
|
||||
},
|
||||
else: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
||||
initial: "notPointing",
|
||||
states: {
|
||||
notPointing: {
|
||||
on: {
|
||||
POINTED_CANVAS: { to: "brushSelecting" },
|
||||
POINTED_BOUNDS: { to: "pointingBounds" },
|
||||
POINTED_SHAPE: [
|
||||
"setPointedId",
|
||||
{
|
||||
if: "isPressingShiftKey",
|
||||
then: {
|
||||
if: "isPointedShapeSelected",
|
||||
do: "pullPointedIdFromSelectedIds",
|
||||
else: {
|
||||
do: "pushPointedIdToSelectedIds",
|
||||
to: "pointingBounds",
|
||||
},
|
||||
},
|
||||
else: [
|
||||
{
|
||||
unless: "isPointedShapeSelected",
|
||||
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
||||
},
|
||||
{
|
||||
to: "pointingBounds",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
brushSelecting: {
|
||||
onEnter: [
|
||||
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
|
||||
"startBrushSession",
|
||||
],
|
||||
on: {
|
||||
MOVED_POINTER: "updateBrushSession",
|
||||
PANNED_CAMERA: "updateBrushSession",
|
||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||
CANCELLED: { do: "cancelSession", to: "selecting" },
|
||||
},
|
||||
pointingBounds: {
|
||||
on: {
|
||||
STOPPED_POINTING: [
|
||||
{
|
||||
unless: "isPressingShiftKey",
|
||||
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
||||
},
|
||||
{ to: "notPointing" },
|
||||
],
|
||||
MOVED_POINTER: {
|
||||
unless: "isReadOnly",
|
||||
if: "distanceImpliesDrag",
|
||||
to: "draggingSelection",
|
||||
},
|
||||
},
|
||||
},
|
||||
draggingSelection: {
|
||||
onEnter: "startTranslateSession",
|
||||
on: {
|
||||
MOVED_POINTER: "updateTranslateSession",
|
||||
PANNED_CAMERA: "updateTranslateSession",
|
||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||
CANCELLED: { do: "cancelSession", to: "selecting" },
|
||||
},
|
||||
},
|
||||
brushSelecting: {
|
||||
onEnter: [
|
||||
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
|
||||
"startBrushSession",
|
||||
],
|
||||
on: {
|
||||
MOVED_POINTER: "updateBrushSession",
|
||||
PANNED_CAMERA: "updateBrushSession",
|
||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||
CANCELLED: { do: "cancelSession", to: "selecting" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
conditions: {
|
||||
isReadOnly(data) {
|
||||
return data.isReadOnly
|
||||
},
|
||||
distanceImpliesDrag(data, payload: PointerInfo) {
|
||||
return vec.dist2(payload.origin, payload.point) > 16
|
||||
},
|
||||
isPointedShapeSelected(data) {
|
||||
return data.selectedIds.has(data.pointedId)
|
||||
},
|
||||
|
@ -77,6 +126,7 @@ const state = createState({
|
|||
session.complete(data)
|
||||
session = undefined
|
||||
},
|
||||
// Brushing
|
||||
startBrushSession(data, payload: { point: number[] }) {
|
||||
session = new Sessions.BrushSession(
|
||||
data,
|
||||
|
@ -86,6 +136,17 @@ const state = createState({
|
|||
updateBrushSession(data, payload: { point: number[] }) {
|
||||
session.update(data, screenToWorld(payload.point, data))
|
||||
},
|
||||
// Dragging / Translating
|
||||
startTranslateSession(data, payload: { point: number[] }) {
|
||||
session = new Sessions.TranslateSession(
|
||||
data,
|
||||
screenToWorld(payload.point, data)
|
||||
)
|
||||
},
|
||||
updateTranslateSession(data, payload: { point: number[] }) {
|
||||
session.update(data, screenToWorld(payload.point, data))
|
||||
},
|
||||
|
||||
// Selection
|
||||
setPointedId(data, payload: { id: string }) {
|
||||
data.pointedId = payload.id
|
||||
|
|
13
types.ts
13
types.ts
|
@ -1,6 +1,7 @@
|
|||
import React from "react"
|
||||
|
||||
export interface Data {
|
||||
isReadOnly: boolean
|
||||
camera: {
|
||||
point: number[]
|
||||
zoom: number
|
||||
|
@ -124,8 +125,18 @@ export type BaseLibShape<K extends ShapeType> = {
|
|||
getBounds(shape: Shapes[K]): Bounds
|
||||
hitTest(shape: Shapes[K], test: number[]): boolean
|
||||
rotate(shape: Shapes[K]): Shapes[K]
|
||||
translate(shape: Shapes[K]): Shapes[K]
|
||||
translate(shape: Shapes[K], delta: number[]): Shapes[K]
|
||||
scale(shape: Shapes[K], scale: number): Shapes[K]
|
||||
stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K]
|
||||
render(shape: Shapes[K]): JSX.Element
|
||||
}
|
||||
|
||||
export interface PointerInfo {
|
||||
pointerId: number
|
||||
origin: number[]
|
||||
point: number[]
|
||||
shiftKey: boolean
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
altKey: boolean
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue