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 state, { useSelector } from "state"
|
||||||
|
import inputs from "state/inputs"
|
||||||
import styled from "styles"
|
import styled from "styles"
|
||||||
|
|
||||||
export default function BoundsBg() {
|
export default function BoundsBg() {
|
||||||
|
const rBounds = useRef<SVGRectElement>(null)
|
||||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||||
|
|
||||||
if (!bounds) return null
|
if (!bounds) return null
|
||||||
|
@ -10,19 +13,15 @@ export default function BoundsBg() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBoundsBg
|
<StyledBoundsBg
|
||||||
|
ref={rBounds}
|
||||||
x={minX}
|
x={minX}
|
||||||
y={minY}
|
y={minY}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
if (e.buttons !== 1) return
|
if (e.buttons !== 1) return
|
||||||
state.send("POINTED_BOUNDS", {
|
rBounds.current.setPointerCapture(e.pointerId)
|
||||||
shiftKey: e.shiftKey,
|
state.send("POINTED_BOUNDS", inputs.pointerDown(e))
|
||||||
optionKey: e.altKey,
|
|
||||||
metaKey: e.metaKey || e.ctrlKey,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
buttons: e.buttons,
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import state, { useSelector } from "state"
|
import state, { useSelector } from "state"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import styled from "styles"
|
import styled from "styles"
|
||||||
|
import inputs from "state/inputs"
|
||||||
|
|
||||||
export default function Bounds() {
|
export default function Bounds() {
|
||||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||||
|
@ -23,38 +24,42 @@ export default function Bounds() {
|
||||||
height={height}
|
height={height}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
/>
|
/>
|
||||||
<Corner
|
{width * zoom > 8 && (
|
||||||
x={minX}
|
<>
|
||||||
y={minY}
|
<Corner
|
||||||
corner={0}
|
x={minX}
|
||||||
width={cp}
|
y={minY}
|
||||||
height={cp}
|
corner={0}
|
||||||
cursor="nwse-resize"
|
width={cp}
|
||||||
/>
|
height={cp}
|
||||||
<Corner
|
cursor="nwse-resize"
|
||||||
x={maxX}
|
/>
|
||||||
y={minY}
|
<Corner
|
||||||
corner={1}
|
x={maxX}
|
||||||
width={cp}
|
y={minY}
|
||||||
height={cp}
|
corner={1}
|
||||||
cursor="nesw-resize"
|
width={cp}
|
||||||
/>
|
height={cp}
|
||||||
<Corner
|
cursor="nesw-resize"
|
||||||
x={maxX}
|
/>
|
||||||
y={maxY}
|
<Corner
|
||||||
corner={2}
|
x={maxX}
|
||||||
width={cp}
|
y={maxY}
|
||||||
height={cp}
|
corner={2}
|
||||||
cursor="nwse-resize"
|
width={cp}
|
||||||
/>
|
height={cp}
|
||||||
<Corner
|
cursor="nwse-resize"
|
||||||
x={minX}
|
/>
|
||||||
y={maxY}
|
<Corner
|
||||||
corner={3}
|
x={minX}
|
||||||
width={cp}
|
y={maxY}
|
||||||
height={cp}
|
corner={3}
|
||||||
cursor="nesw-resize"
|
width={cp}
|
||||||
/>
|
height={cp}
|
||||||
|
cursor="nesw-resize"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<EdgeHorizontal
|
<EdgeHorizontal
|
||||||
x={minX + p}
|
x={minX + p}
|
||||||
y={minY}
|
y={minY}
|
||||||
|
@ -65,11 +70,7 @@ export default function Bounds() {
|
||||||
if (e.buttons !== 1) return
|
if (e.buttons !== 1) return
|
||||||
state.send("POINTED_BOUNDS_EDGE", {
|
state.send("POINTED_BOUNDS_EDGE", {
|
||||||
edge: 0,
|
edge: 0,
|
||||||
shiftKey: e.shiftKey,
|
...inputs.pointerDown(e),
|
||||||
optionKey: e.altKey,
|
|
||||||
metaKey: e.metaKey,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
buttons: e.buttons,
|
|
||||||
})
|
})
|
||||||
document.body.style.cursor = "ns-resize"
|
document.body.style.cursor = "ns-resize"
|
||||||
}}
|
}}
|
||||||
|
@ -84,11 +85,7 @@ export default function Bounds() {
|
||||||
if (e.buttons !== 1) return
|
if (e.buttons !== 1) return
|
||||||
state.send("POINTED_BOUNDS_EDGE", {
|
state.send("POINTED_BOUNDS_EDGE", {
|
||||||
edge: 1,
|
edge: 1,
|
||||||
shiftKey: e.shiftKey,
|
...inputs.pointerDown(e),
|
||||||
optionKey: e.altKey,
|
|
||||||
metaKey: e.metaKey,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
buttons: e.buttons,
|
|
||||||
})
|
})
|
||||||
document.body.style.cursor = "ew-resize"
|
document.body.style.cursor = "ew-resize"
|
||||||
}}
|
}}
|
||||||
|
@ -103,11 +100,7 @@ export default function Bounds() {
|
||||||
if (e.buttons !== 1) return
|
if (e.buttons !== 1) return
|
||||||
state.send("POINTED_BOUNDS_EDGE", {
|
state.send("POINTED_BOUNDS_EDGE", {
|
||||||
edge: 2,
|
edge: 2,
|
||||||
shiftKey: e.shiftKey,
|
...inputs.pointerDown(e),
|
||||||
optionKey: e.altKey,
|
|
||||||
metaKey: e.metaKey,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
buttons: e.buttons,
|
|
||||||
})
|
})
|
||||||
document.body.style.cursor = "ns-resize"
|
document.body.style.cursor = "ns-resize"
|
||||||
}}
|
}}
|
||||||
|
@ -122,11 +115,7 @@ export default function Bounds() {
|
||||||
if (e.buttons !== 1) return
|
if (e.buttons !== 1) return
|
||||||
state.send("POINTED_BOUNDS_EDGE", {
|
state.send("POINTED_BOUNDS_EDGE", {
|
||||||
edge: 3,
|
edge: 3,
|
||||||
shiftKey: e.shiftKey,
|
...inputs.pointerDown(e),
|
||||||
optionKey: e.altKey,
|
|
||||||
metaKey: e.metaKey,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
buttons: e.buttons,
|
|
||||||
})
|
})
|
||||||
document.body.style.cursor = "ew-resize"
|
document.body.style.cursor = "ew-resize"
|
||||||
}}
|
}}
|
||||||
|
@ -168,11 +157,7 @@ function Corner({
|
||||||
if (e.buttons !== 1) return
|
if (e.buttons !== 1) return
|
||||||
state.send("POINTED_ROTATE_CORNER", {
|
state.send("POINTED_ROTATE_CORNER", {
|
||||||
corner,
|
corner,
|
||||||
shiftKey: e.shiftKey,
|
...inputs.pointerDown(e),
|
||||||
optionKey: e.altKey,
|
|
||||||
metaKey: e.metaKey,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
buttons: e.buttons,
|
|
||||||
})
|
})
|
||||||
document.body.style.cursor = "grabbing"
|
document.body.style.cursor = "grabbing"
|
||||||
}}
|
}}
|
||||||
|
@ -190,18 +175,13 @@ function Corner({
|
||||||
if (e.buttons !== 1) return
|
if (e.buttons !== 1) return
|
||||||
state.send("POINTED_BOUNDS_CORNER", {
|
state.send("POINTED_BOUNDS_CORNER", {
|
||||||
corner,
|
corner,
|
||||||
shiftKey: e.shiftKey,
|
...inputs.pointerDown(e),
|
||||||
optionKey: e.altKey,
|
|
||||||
metaKey: e.metaKey,
|
|
||||||
ctrlKey: e.ctrlKey,
|
|
||||||
buttons: e.buttons,
|
|
||||||
})
|
})
|
||||||
document.body.style.cursor = "nesw-resize"
|
document.body.style.cursor = "nesw-resize"
|
||||||
}}
|
}}
|
||||||
onPanEnd={restoreCursor}
|
onPanEnd={restoreCursor}
|
||||||
onTap={restoreCursor}
|
onTap={restoreCursor}
|
||||||
style={{ cursor }}
|
style={{ cursor }}
|
||||||
className="strokewidth-ui stroke-bounds fill-corner"
|
|
||||||
/>
|
/>
|
||||||
</g>
|
</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"
|
document.body.style.cursor = "default"
|
||||||
state.send("STOPPED_POINTING")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledEdge = styled(motion.rect, {
|
const StyledEdge = styled(motion.rect, {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Brush from "./brush"
|
||||||
import state from "state"
|
import state from "state"
|
||||||
import Bounds from "./bounds"
|
import Bounds from "./bounds"
|
||||||
import BoundsBg from "./bounds-bg"
|
import BoundsBg from "./bounds-bg"
|
||||||
|
import inputs from "state/inputs"
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const rCanvas = useRef<SVGSVGElement>(null)
|
const rCanvas = useRef<SVGSVGElement>(null)
|
||||||
|
@ -18,16 +19,16 @@ export default function Canvas() {
|
||||||
|
|
||||||
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||||
rCanvas.current.setPointerCapture(e.pointerId)
|
rCanvas.current.setPointerCapture(e.pointerId)
|
||||||
state.send("POINTED_CANVAS", getPointerEventInfo(e))
|
state.send("POINTED_CANVAS", inputs.pointerDown(e))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
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) => {
|
const handlePointerUp = useCallback((e: React.PointerEvent) => {
|
||||||
rCanvas.current.releasePointerCapture(e.pointerId)
|
rCanvas.current.releasePointerCapture(e.pointerId)
|
||||||
state.send("STOPPED_POINTING", getPointerEventInfo(e))
|
state.send("STOPPED_POINTING", { id: "canvas", ...inputs.pointerUp(e) })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useCallback, useRef, memo } from "react"
|
import React, { useCallback, useRef, memo } from "react"
|
||||||
import state, { useSelector } from "state"
|
import state, { useSelector } from "state"
|
||||||
import { getPointerEventInfo } from "utils/utils"
|
import { getPointerEventInfo } from "utils/utils"
|
||||||
|
import inputs from "state/inputs"
|
||||||
import shapes from "lib/shapes"
|
import shapes from "lib/shapes"
|
||||||
import styled from "styles"
|
import styled from "styles"
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ function Shape({ id }: { id: string }) {
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
rGroup.current.setPointerCapture(e.pointerId)
|
rGroup.current.setPointerCapture(e.pointerId)
|
||||||
state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) })
|
state.send("POINTED_SHAPE", { id, ...inputs.pointerDown(e) })
|
||||||
},
|
},
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
@ -27,20 +28,18 @@ function Shape({ id }: { id: string }) {
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
rGroup.current.releasePointerCapture(e.pointerId)
|
rGroup.current.releasePointerCapture(e.pointerId)
|
||||||
state.send("STOPPED_POINTING_SHAPE", { id, ...getPointerEventInfo(e) })
|
state.send("STOPPED_POINTING", { id, ...inputs.pointerUp(e) })
|
||||||
},
|
},
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerEnter = useCallback(
|
const handlePointerEnter = useCallback(
|
||||||
(e: React.PointerEvent) =>
|
(e: React.PointerEvent) => state.send("HOVERED_SHAPE", { id }),
|
||||||
state.send("HOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
|
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerLeave = useCallback(
|
const handlePointerLeave = useCallback(
|
||||||
(e: React.PointerEvent) =>
|
(e: React.PointerEvent) => state.send("UNHOVERED_SHAPE", { id }),
|
||||||
state.send("UNHOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
|
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,8 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
translate(shape) {
|
translate(shape, delta) {
|
||||||
|
shape.point = vec.add(shape.point, delta)
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,8 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
translate(shape) {
|
translate(shape, delta) {
|
||||||
|
shape.point = vec.add(shape.point, delta)
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,8 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
translate(shape) {
|
translate(shape, delta) {
|
||||||
|
shape.point = vec.add(shape.point, delta)
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,8 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
translate(shape) {
|
translate(shape, delta) {
|
||||||
|
shape.point = vec.add(shape.point, delta)
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -4,30 +4,14 @@ import { Data } from "types"
|
||||||
|
|
||||||
export type CommandFn<T> = (data: T, initial?: boolean) => void
|
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"
|
* 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.
|
* method to reverse its changes. The apps history is a series of commands.
|
||||||
*/
|
*/
|
||||||
export class BaseCommand<T extends any> {
|
export class BaseCommand<T extends any> {
|
||||||
timestamp = Date.now()
|
timestamp = Date.now()
|
||||||
|
name: string
|
||||||
|
category: string
|
||||||
private undoFn: CommandFn<T>
|
private undoFn: CommandFn<T>
|
||||||
private doFn: CommandFn<T>
|
private doFn: CommandFn<T>
|
||||||
protected restoreBeforeSelectionState: (data: T) => void
|
protected restoreBeforeSelectionState: (data: T) => void
|
||||||
|
@ -36,11 +20,14 @@ export class BaseCommand<T extends any> {
|
||||||
protected manualSelection: boolean
|
protected manualSelection: boolean
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
type: CommandType
|
|
||||||
do: CommandFn<T>
|
do: CommandFn<T>
|
||||||
undo: CommandFn<T>
|
undo: CommandFn<T>
|
||||||
|
name: string
|
||||||
|
category: string
|
||||||
manualSelection?: boolean
|
manualSelection?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
this.name = options.name
|
||||||
|
this.category = options.category
|
||||||
this.doFn = options.do
|
this.doFn = options.do
|
||||||
this.undoFn = options.undo
|
this.undoFn = options.undo
|
||||||
this.manualSelection = options.manualSelection || false
|
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
|
* to mutate the state's data. Actions do not effect the "active states" in
|
||||||
* the app.
|
* the app.
|
||||||
*/
|
*/
|
||||||
export class Command extends BaseCommand<Data> {
|
export default class Command extends BaseCommand<Data> {
|
||||||
saveSelectionState = (data: 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 { Data } from "types"
|
||||||
import { BaseCommand } from "./command"
|
import { BaseCommand } from "./command"
|
||||||
|
|
||||||
class BaseHistory<T> {
|
// A singleton to manage history changes.
|
||||||
|
|
||||||
|
class History<T> {
|
||||||
private stack: BaseCommand<T>[] = []
|
private stack: BaseCommand<T>[] = []
|
||||||
private pointer = -1
|
private pointer = -1
|
||||||
private maxLength = 100
|
private maxLength = 100
|
||||||
|
@ -44,7 +46,7 @@ class BaseHistory<T> {
|
||||||
if (typeof window === "undefined") return
|
if (typeof window === "undefined") return
|
||||||
if (typeof localStorage === "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 = () => {
|
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 {
|
export default class BaseSession {
|
||||||
constructor(data: Data) {}
|
constructor(data: Data) {}
|
||||||
|
|
||||||
update = (data: Data, ...args: unknown[]) => {
|
update(data: Data, ...args: unknown[]) {
|
||||||
// Update the state
|
// Update the state
|
||||||
}
|
}
|
||||||
|
|
||||||
complete = (data: Data, ...args: unknown[]) => {
|
complete(data: Data, ...args: unknown[]) {
|
||||||
// Create a command
|
// Create a command
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel = (data: Data) => {
|
cancel(data: Data) {
|
||||||
// Clean up the change
|
// Clean up the change
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import BaseSession from "./brush-session"
|
import BaseSession from "./base-session"
|
||||||
import BrushSession from "./brush-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 { createSelectorHook, createState } from "@state-designer/react"
|
||||||
import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
|
import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
|
||||||
import * as vec from "utils/vec"
|
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 { defaultDocument } from "./data"
|
||||||
import Shapes from "lib/shapes"
|
import Shapes from "lib/shapes"
|
||||||
import * as Sessions from "./sessions"
|
import * as Sessions from "./sessions"
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
|
isReadOnly: false,
|
||||||
camera: {
|
camera: {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
|
@ -31,36 +32,84 @@ const state = createState({
|
||||||
initial: "selecting",
|
initial: "selecting",
|
||||||
states: {
|
states: {
|
||||||
selecting: {
|
selecting: {
|
||||||
on: {
|
initial: "notPointing",
|
||||||
POINTED_CANVAS: { to: "brushSelecting" },
|
states: {
|
||||||
POINTED_SHAPE: [
|
notPointing: {
|
||||||
"setPointedId",
|
on: {
|
||||||
{
|
POINTED_CANVAS: { to: "brushSelecting" },
|
||||||
if: "isPressingShiftKey",
|
POINTED_BOUNDS: { to: "pointingBounds" },
|
||||||
then: {
|
POINTED_SHAPE: [
|
||||||
if: "isPointedShapeSelected",
|
"setPointedId",
|
||||||
do: "pullPointedIdFromSelectedIds",
|
{
|
||||||
else: "pushPointedIdToSelectedIds",
|
if: "isPressingShiftKey",
|
||||||
},
|
then: {
|
||||||
else: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
if: "isPointedShapeSelected",
|
||||||
|
do: "pullPointedIdFromSelectedIds",
|
||||||
|
else: {
|
||||||
|
do: "pushPointedIdToSelectedIds",
|
||||||
|
to: "pointingBounds",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
else: [
|
||||||
|
{
|
||||||
|
unless: "isPointedShapeSelected",
|
||||||
|
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: "pointingBounds",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
pointingBounds: {
|
||||||
},
|
on: {
|
||||||
brushSelecting: {
|
STOPPED_POINTING: [
|
||||||
onEnter: [
|
{
|
||||||
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
|
unless: "isPressingShiftKey",
|
||||||
"startBrushSession",
|
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
|
||||||
],
|
},
|
||||||
on: {
|
{ to: "notPointing" },
|
||||||
MOVED_POINTER: "updateBrushSession",
|
],
|
||||||
PANNED_CAMERA: "updateBrushSession",
|
MOVED_POINTER: {
|
||||||
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
unless: "isReadOnly",
|
||||||
CANCELLED: { do: "cancelSession", to: "selecting" },
|
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: {
|
conditions: {
|
||||||
|
isReadOnly(data) {
|
||||||
|
return data.isReadOnly
|
||||||
|
},
|
||||||
|
distanceImpliesDrag(data, payload: PointerInfo) {
|
||||||
|
return vec.dist2(payload.origin, payload.point) > 16
|
||||||
|
},
|
||||||
isPointedShapeSelected(data) {
|
isPointedShapeSelected(data) {
|
||||||
return data.selectedIds.has(data.pointedId)
|
return data.selectedIds.has(data.pointedId)
|
||||||
},
|
},
|
||||||
|
@ -77,6 +126,7 @@ const state = createState({
|
||||||
session.complete(data)
|
session.complete(data)
|
||||||
session = undefined
|
session = undefined
|
||||||
},
|
},
|
||||||
|
// Brushing
|
||||||
startBrushSession(data, payload: { point: number[] }) {
|
startBrushSession(data, payload: { point: number[] }) {
|
||||||
session = new Sessions.BrushSession(
|
session = new Sessions.BrushSession(
|
||||||
data,
|
data,
|
||||||
|
@ -86,6 +136,17 @@ const state = createState({
|
||||||
updateBrushSession(data, payload: { point: number[] }) {
|
updateBrushSession(data, payload: { point: number[] }) {
|
||||||
session.update(data, screenToWorld(payload.point, data))
|
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
|
// Selection
|
||||||
setPointedId(data, payload: { id: string }) {
|
setPointedId(data, payload: { id: string }) {
|
||||||
data.pointedId = payload.id
|
data.pointedId = payload.id
|
||||||
|
|
13
types.ts
13
types.ts
|
@ -1,6 +1,7 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
|
isReadOnly: boolean
|
||||||
camera: {
|
camera: {
|
||||||
point: number[]
|
point: number[]
|
||||||
zoom: number
|
zoom: number
|
||||||
|
@ -124,8 +125,18 @@ export type BaseLibShape<K extends ShapeType> = {
|
||||||
getBounds(shape: Shapes[K]): Bounds
|
getBounds(shape: Shapes[K]): Bounds
|
||||||
hitTest(shape: Shapes[K], test: number[]): boolean
|
hitTest(shape: Shapes[K], test: number[]): boolean
|
||||||
rotate(shape: Shapes[K]): Shapes[K]
|
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]
|
scale(shape: Shapes[K], scale: number): Shapes[K]
|
||||||
stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K]
|
stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K]
|
||||||
render(shape: Shapes[K]): JSX.Element
|
render(shape: Shapes[K]): JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PointerInfo {
|
||||||
|
pointerId: number
|
||||||
|
origin: number[]
|
||||||
|
point: number[]
|
||||||
|
shiftKey: boolean
|
||||||
|
ctrlKey: boolean
|
||||||
|
metaKey: boolean
|
||||||
|
altKey: boolean
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue