Adds direction session
This commit is contained in:
parent
a4643edd62
commit
8a650a99d6
13 changed files with 245 additions and 93 deletions
|
@ -6,13 +6,13 @@ export default function Toolbar() {
|
||||||
const activeTool = useSelector((state) =>
|
const activeTool = useSelector((state) =>
|
||||||
state.whenIn({
|
state.whenIn({
|
||||||
selecting: "select",
|
selecting: "select",
|
||||||
creatingDot: "dot",
|
dot: "dot",
|
||||||
creatingCircle: "circle",
|
circle: "circle",
|
||||||
creatingEllipse: "ellipse",
|
ellipse: "ellipse",
|
||||||
creatingRay: "ray",
|
ray: "ray",
|
||||||
creatingLine: "line",
|
line: "line",
|
||||||
creatingPolyline: "polyline",
|
polyline: "polyline",
|
||||||
creatingRectangle: "rectangle",
|
rectangle: "rectangle",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -87,10 +87,6 @@ const circle = createShape<CircleShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
stretch(shape, scaleX, scaleY) {
|
|
||||||
return shape
|
|
||||||
},
|
|
||||||
|
|
||||||
transform(shape, bounds, { anchor }) {
|
transform(shape, bounds, { anchor }) {
|
||||||
// Set the new corner or position depending on the anchor
|
// Set the new corner or position depending on the anchor
|
||||||
switch (anchor) {
|
switch (anchor) {
|
||||||
|
|
|
@ -66,16 +66,12 @@ const dot = createShape<DotShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
translate(shape, delta) {
|
|
||||||
shape.point = vec.add(shape.point, delta)
|
|
||||||
return shape
|
|
||||||
},
|
|
||||||
|
|
||||||
scale(shape, scale: number) {
|
scale(shape, scale: number) {
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
stretch(shape, scaleX: number, scaleY: number) {
|
translate(shape, delta) {
|
||||||
|
shape.point = vec.add(shape.point, delta)
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -93,10 +93,6 @@ const ellipse = createShape<EllipseShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
stretch(shape, scaleX: number, scaleY: number) {
|
|
||||||
return shape
|
|
||||||
},
|
|
||||||
|
|
||||||
transform(shape, bounds) {
|
transform(shape, bounds) {
|
||||||
shape.point = [bounds.minX, bounds.minY]
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
shape.radiusX = bounds.width / 2
|
shape.radiusX = bounds.width / 2
|
||||||
|
|
|
@ -67,9 +67,6 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
// Apply a scale to a shape.
|
// Apply a scale to a shape.
|
||||||
scale(this: ShapeUtility<K>, shape: K, scale: number): K
|
scale(this: ShapeUtility<K>, shape: K, scale: number): K
|
||||||
|
|
||||||
// Apply a stretch to a shape.
|
|
||||||
stretch(this: ShapeUtility<K>, shape: K, scaleX: number, scaleY: number): K
|
|
||||||
|
|
||||||
// Render a shape to JSX.
|
// Render a shape to JSX.
|
||||||
render(this: ShapeUtility<K>, shape: K): JSX.Element
|
render(this: ShapeUtility<K>, shape: K): JSX.Element
|
||||||
|
|
||||||
|
|
|
@ -84,10 +84,6 @@ const line = createShape<LineShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
stretch(shape, scaleX: number, scaleY: number) {
|
|
||||||
return shape
|
|
||||||
},
|
|
||||||
|
|
||||||
transform(shape, bounds) {
|
transform(shape, bounds) {
|
||||||
shape.point = [bounds.minX, bounds.minY]
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
|
||||||
|
|
|
@ -98,10 +98,6 @@ const polyline = createShape<PolylineShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
stretch(shape, scaleX: number, scaleY: number) {
|
|
||||||
return shape
|
|
||||||
},
|
|
||||||
|
|
||||||
transform(
|
transform(
|
||||||
shape,
|
shape,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
|
@ -17,9 +17,12 @@ const ray = createShape<RayShape>({
|
||||||
parentId: "page0",
|
parentId: "page0",
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
direction: [0, 0],
|
direction: [0, 1],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {},
|
style: {
|
||||||
|
stroke: "#000",
|
||||||
|
strokeWidth: 1,
|
||||||
|
},
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -83,11 +86,9 @@ const ray = createShape<RayShape>({
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
stretch(shape, scaleX: number, scaleY: number) {
|
|
||||||
return shape
|
|
||||||
},
|
|
||||||
|
|
||||||
transform(shape, bounds) {
|
transform(shape, bounds) {
|
||||||
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
36
state/commands/direction.ts
Normal file
36
state/commands/direction.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Command from "./command"
|
||||||
|
import history from "../history"
|
||||||
|
import { DirectionSnapshot } from "state/sessions/direction-session"
|
||||||
|
import { Data, LineShape, RayShape } from "types"
|
||||||
|
|
||||||
|
export default function translateCommand(
|
||||||
|
data: Data,
|
||||||
|
before: DirectionSnapshot,
|
||||||
|
after: DirectionSnapshot
|
||||||
|
) {
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name: "set_direction",
|
||||||
|
category: "canvas",
|
||||||
|
do(data) {
|
||||||
|
const { shapes } = data.document.pages[after.currentPageId]
|
||||||
|
|
||||||
|
for (let { id, direction } of after.shapes) {
|
||||||
|
const shape = shapes[id] as RayShape | LineShape
|
||||||
|
|
||||||
|
shape.direction = direction
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
const { shapes } = data.document.pages[before.currentPageId]
|
||||||
|
|
||||||
|
for (let { id, direction } of after.shapes) {
|
||||||
|
const shape = shapes[id] as RayShape | LineShape
|
||||||
|
|
||||||
|
shape.direction = direction
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,7 +2,14 @@ import translate from "./translate"
|
||||||
import transform from "./transform"
|
import transform from "./transform"
|
||||||
import generateShapes from "./generate-shapes"
|
import generateShapes from "./generate-shapes"
|
||||||
import createShape from "./create-shape"
|
import createShape from "./create-shape"
|
||||||
|
import direction from "./direction"
|
||||||
|
|
||||||
const commands = { translate, transform, generateShapes, createShape }
|
const commands = {
|
||||||
|
translate,
|
||||||
|
transform,
|
||||||
|
generateShapes,
|
||||||
|
createShape,
|
||||||
|
direction,
|
||||||
|
}
|
||||||
|
|
||||||
export default commands
|
export default commands
|
||||||
|
|
71
state/sessions/direction-session.ts
Normal file
71
state/sessions/direction-session.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { Data, LineShape, RayShape } 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 DirectionSession extends BaseSession {
|
||||||
|
delta = [0, 0]
|
||||||
|
origin: number[]
|
||||||
|
snapshot: DirectionSnapshot
|
||||||
|
|
||||||
|
constructor(data: Data, point: number[]) {
|
||||||
|
super(data)
|
||||||
|
this.origin = point
|
||||||
|
this.snapshot = getDirectionSnapshot(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data: Data, point: number[]) {
|
||||||
|
const { currentPageId, shapes } = this.snapshot
|
||||||
|
const { document } = data
|
||||||
|
|
||||||
|
for (let { id } of shapes) {
|
||||||
|
const shape = document.pages[currentPageId].shapes[id] as
|
||||||
|
| RayShape
|
||||||
|
| LineShape
|
||||||
|
|
||||||
|
shape.direction = vec.uni(vec.vec(shape.point, point))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(data: Data) {
|
||||||
|
const { document } = data
|
||||||
|
|
||||||
|
for (let { id, direction } of this.snapshot.shapes) {
|
||||||
|
const shape = document.pages[this.snapshot.currentPageId].shapes[id] as
|
||||||
|
| RayShape
|
||||||
|
| LineShape
|
||||||
|
|
||||||
|
shape.direction = direction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(data: Data) {
|
||||||
|
commands.direction(data, this.snapshot, getDirectionSnapshot(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDirectionSnapshot(data: Data) {
|
||||||
|
const {
|
||||||
|
document: { pages },
|
||||||
|
currentPageId,
|
||||||
|
} = current(data)
|
||||||
|
|
||||||
|
const { shapes } = pages[currentPageId]
|
||||||
|
|
||||||
|
let snapshapes: { id: string; direction: number[] }[] = []
|
||||||
|
|
||||||
|
data.selectedIds.forEach((id) => {
|
||||||
|
const shape = shapes[id]
|
||||||
|
if ("direction" in shape) {
|
||||||
|
snapshapes.push({ id: shape.id, direction: shape.direction })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPageId,
|
||||||
|
shapes: snapshapes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DirectionSnapshot = ReturnType<typeof getDirectionSnapshot>
|
|
@ -2,5 +2,12 @@ import BaseSession from "./base-session"
|
||||||
import BrushSession from "./brush-session"
|
import BrushSession from "./brush-session"
|
||||||
import TranslateSession from "./translate-session"
|
import TranslateSession from "./translate-session"
|
||||||
import TransformSession from "./transform-session"
|
import TransformSession from "./transform-session"
|
||||||
|
import DirectionSession from "./direction-session"
|
||||||
|
|
||||||
export { BrushSession, BaseSession, TranslateSession, TransformSession }
|
export {
|
||||||
|
BrushSession,
|
||||||
|
BaseSession,
|
||||||
|
TranslateSession,
|
||||||
|
TransformSession,
|
||||||
|
DirectionSession,
|
||||||
|
}
|
||||||
|
|
159
state/state.ts
159
state/state.ts
|
@ -44,13 +44,13 @@ const state = createState({
|
||||||
do: "panCamera",
|
do: "panCamera",
|
||||||
},
|
},
|
||||||
SELECTED_SELECT_TOOL: { to: "selecting" },
|
SELECTED_SELECT_TOOL: { to: "selecting" },
|
||||||
SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "creatingDot" },
|
SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "dot" },
|
||||||
SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "creatingCircle" },
|
SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "circle" },
|
||||||
SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "creatingEllipse" },
|
SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "ellipse" },
|
||||||
SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "creatingRay" },
|
SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "ray" },
|
||||||
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "creatingLine" },
|
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
|
||||||
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "creatingPolyline" },
|
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
|
||||||
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "creatingRectangle" },
|
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
|
||||||
},
|
},
|
||||||
initial: "selecting",
|
initial: "selecting",
|
||||||
states: {
|
states: {
|
||||||
|
@ -154,18 +154,18 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
creatingDot: {
|
dot: {
|
||||||
initial: "creating",
|
initial: "creating",
|
||||||
states: {
|
states: {
|
||||||
creating: {
|
creating: {
|
||||||
on: {
|
on: {
|
||||||
POINTED_CANVAS: {
|
POINTED_CANVAS: {
|
||||||
do: "createDot",
|
do: "createDot",
|
||||||
to: "creatingDot.positioning",
|
to: "dot.editing",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
positioning: {
|
editing: {
|
||||||
onEnter: "startTranslateSession",
|
onEnter: "startTranslateSession",
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: "updateTranslateSession",
|
MOVED_POINTER: "updateTranslateSession",
|
||||||
|
@ -179,12 +179,36 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
creatingCircle: {},
|
circle: {},
|
||||||
creatingEllipse: {},
|
ellipse: {},
|
||||||
creatingRay: {},
|
ray: {
|
||||||
creatingLine: {},
|
initial: "creating",
|
||||||
creatingPolyline: {},
|
states: {
|
||||||
creatingRectangle: {},
|
creating: {
|
||||||
|
on: {
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
do: "createRay",
|
||||||
|
to: "ray.editing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
onEnter: "startDirectionSession",
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: "updateDirectionSession",
|
||||||
|
PANNED_CAMERA: "updateDirectionSession",
|
||||||
|
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
|
||||||
|
CANCELLED: {
|
||||||
|
do: ["cancelSession", "deleteSelectedIds"],
|
||||||
|
to: "selecting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
line: {},
|
||||||
|
polyline: {},
|
||||||
|
rectangle: {},
|
||||||
},
|
},
|
||||||
conditions: {
|
conditions: {
|
||||||
isPointingBounds(data, payload: PointerInfo) {
|
isPointingBounds(data, payload: PointerInfo) {
|
||||||
|
@ -216,41 +240,31 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// Shapes
|
/* --------------------- Shapes --------------------- */
|
||||||
|
|
||||||
|
// Dot
|
||||||
createDot(data, payload: PointerInfo) {
|
createDot(data, payload: PointerInfo) {
|
||||||
const shape = shapeUtilityMap[ShapeType.Dot].create({
|
const shape = shapeUtilityMap[ShapeType.Dot].create({
|
||||||
point: screenToWorld(payload.point, data),
|
point: screenToWorld(payload.point, data),
|
||||||
})
|
})
|
||||||
|
|
||||||
commands.createShape(data, shape)
|
commands.createShape(data, shape)
|
||||||
|
data.selectedIds.add(shape.id)
|
||||||
},
|
},
|
||||||
|
|
||||||
// History
|
// Ray
|
||||||
enableHistory() {
|
createRay(data, payload: PointerInfo) {
|
||||||
history.enable()
|
const shape = shapeUtilityMap[ShapeType.Ray].create({
|
||||||
},
|
point: screenToWorld(payload.point, data),
|
||||||
disableHistory() {
|
})
|
||||||
history.disable()
|
|
||||||
},
|
commands.createShape(data, shape)
|
||||||
undo(data) {
|
data.selectedIds.add(shape.id)
|
||||||
history.undo(data)
|
|
||||||
},
|
|
||||||
redo(data) {
|
|
||||||
history.redo(data)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Code
|
/* -------------------- Sessions -------------------- */
|
||||||
setGeneratedShapes(data, payload: { shapes: Shape[] }) {
|
|
||||||
commands.generateShapes(data, data.currentPageId, payload.shapes)
|
|
||||||
},
|
|
||||||
increaseCodeFontSize(data) {
|
|
||||||
data.settings.fontSize++
|
|
||||||
},
|
|
||||||
decreaseCodeFontSize(data) {
|
|
||||||
data.settings.fontSize--
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sessions
|
// Shared
|
||||||
cancelSession(data) {
|
cancelSession(data) {
|
||||||
session.cancel(data)
|
session.cancel(data)
|
||||||
session = undefined
|
session = undefined
|
||||||
|
@ -297,20 +311,19 @@ const state = createState({
|
||||||
session.update(data, screenToWorld(payload.point, data))
|
session.update(data, screenToWorld(payload.point, data))
|
||||||
},
|
},
|
||||||
|
|
||||||
// Selection
|
// Direction
|
||||||
deleteSelectedIds(data) {
|
startDirectionSession(data, payload: PointerInfo) {
|
||||||
const { document, currentPageId } = data
|
session = new Sessions.DirectionSession(
|
||||||
const shapes = document.pages[currentPageId].shapes
|
data,
|
||||||
|
screenToWorld(payload.point, data)
|
||||||
data.selectedIds.forEach((id) => {
|
)
|
||||||
delete shapes[id]
|
|
||||||
// TODO: recursively delete children
|
|
||||||
})
|
|
||||||
|
|
||||||
data.selectedIds.clear()
|
|
||||||
data.hoveredId = undefined
|
|
||||||
data.pointedId = undefined
|
|
||||||
},
|
},
|
||||||
|
updateDirectionSession(data, payload: PointerInfo) {
|
||||||
|
session.update(data, screenToWorld(payload.point, data))
|
||||||
|
},
|
||||||
|
|
||||||
|
/* -------------------- Selection ------------------- */
|
||||||
|
|
||||||
setHoveredId(data, payload: PointerInfo) {
|
setHoveredId(data, payload: PointerInfo) {
|
||||||
data.hoveredId = payload.target
|
data.hoveredId = payload.target
|
||||||
},
|
},
|
||||||
|
@ -357,6 +370,46 @@ const state = createState({
|
||||||
vec.div(payload.delta, camera.zoom)
|
vec.div(payload.delta, camera.zoom)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
deleteSelectedIds(data) {
|
||||||
|
const { document, currentPageId } = data
|
||||||
|
const shapes = document.pages[currentPageId].shapes
|
||||||
|
|
||||||
|
data.selectedIds.forEach((id) => {
|
||||||
|
delete shapes[id]
|
||||||
|
// TODO: recursively delete children
|
||||||
|
})
|
||||||
|
|
||||||
|
data.selectedIds.clear()
|
||||||
|
data.hoveredId = undefined
|
||||||
|
data.pointedId = undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------------------- Misc ---------------------- */
|
||||||
|
|
||||||
|
// History
|
||||||
|
enableHistory() {
|
||||||
|
history.enable()
|
||||||
|
},
|
||||||
|
disableHistory() {
|
||||||
|
history.disable()
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
history.undo(data)
|
||||||
|
},
|
||||||
|
redo(data) {
|
||||||
|
history.redo(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Code
|
||||||
|
setGeneratedShapes(data, payload: { shapes: Shape[] }) {
|
||||||
|
commands.generateShapes(data, data.currentPageId, payload.shapes)
|
||||||
|
},
|
||||||
|
increaseCodeFontSize(data) {
|
||||||
|
data.settings.fontSize++
|
||||||
|
},
|
||||||
|
decreaseCodeFontSize(data) {
|
||||||
|
data.settings.fontSize--
|
||||||
|
},
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
selectedIds(data) {
|
selectedIds(data) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue