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) =>
|
||||
state.whenIn({
|
||||
selecting: "select",
|
||||
creatingDot: "dot",
|
||||
creatingCircle: "circle",
|
||||
creatingEllipse: "ellipse",
|
||||
creatingRay: "ray",
|
||||
creatingLine: "line",
|
||||
creatingPolyline: "polyline",
|
||||
creatingRectangle: "rectangle",
|
||||
dot: "dot",
|
||||
circle: "circle",
|
||||
ellipse: "ellipse",
|
||||
ray: "ray",
|
||||
line: "line",
|
||||
polyline: "polyline",
|
||||
rectangle: "rectangle",
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -87,10 +87,6 @@ const circle = createShape<CircleShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
stretch(shape, scaleX, scaleY) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds, { anchor }) {
|
||||
// Set the new corner or position depending on the anchor
|
||||
switch (anchor) {
|
||||
|
|
|
@ -66,16 +66,12 @@ const dot = createShape<DotShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
|
|
|
@ -93,10 +93,6 @@ const ellipse = createShape<EllipseShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds) {
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
shape.radiusX = bounds.width / 2
|
||||
|
|
|
@ -67,9 +67,6 @@ export interface ShapeUtility<K extends Shape> {
|
|||
// Apply a scale to a shape.
|
||||
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(this: ShapeUtility<K>, shape: K): JSX.Element
|
||||
|
||||
|
|
|
@ -84,10 +84,6 @@ const line = createShape<LineShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds) {
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
|
||||
|
|
|
@ -98,10 +98,6 @@ const polyline = createShape<PolylineShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(
|
||||
shape,
|
||||
bounds,
|
||||
|
|
|
@ -17,9 +17,12 @@ const ray = createShape<RayShape>({
|
|||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
direction: [0, 0],
|
||||
direction: [0, 1],
|
||||
rotation: 0,
|
||||
style: {},
|
||||
style: {
|
||||
stroke: "#000",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
@ -83,11 +86,9 @@ const ray = createShape<RayShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds) {
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
|
||||
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 generateShapes from "./generate-shapes"
|
||||
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
|
||||
|
|
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 TranslateSession from "./translate-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",
|
||||
},
|
||||
SELECTED_SELECT_TOOL: { to: "selecting" },
|
||||
SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "creatingDot" },
|
||||
SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "creatingCircle" },
|
||||
SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "creatingEllipse" },
|
||||
SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "creatingRay" },
|
||||
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "creatingLine" },
|
||||
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "creatingPolyline" },
|
||||
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "creatingRectangle" },
|
||||
SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "dot" },
|
||||
SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "circle" },
|
||||
SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "ellipse" },
|
||||
SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "ray" },
|
||||
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
|
||||
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
|
||||
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
|
||||
},
|
||||
initial: "selecting",
|
||||
states: {
|
||||
|
@ -154,18 +154,18 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
},
|
||||
creatingDot: {
|
||||
dot: {
|
||||
initial: "creating",
|
||||
states: {
|
||||
creating: {
|
||||
on: {
|
||||
POINTED_CANVAS: {
|
||||
do: "createDot",
|
||||
to: "creatingDot.positioning",
|
||||
to: "dot.editing",
|
||||
},
|
||||
},
|
||||
},
|
||||
positioning: {
|
||||
editing: {
|
||||
onEnter: "startTranslateSession",
|
||||
on: {
|
||||
MOVED_POINTER: "updateTranslateSession",
|
||||
|
@ -179,12 +179,36 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
},
|
||||
creatingCircle: {},
|
||||
creatingEllipse: {},
|
||||
creatingRay: {},
|
||||
creatingLine: {},
|
||||
creatingPolyline: {},
|
||||
creatingRectangle: {},
|
||||
circle: {},
|
||||
ellipse: {},
|
||||
ray: {
|
||||
initial: "creating",
|
||||
states: {
|
||||
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: {
|
||||
isPointingBounds(data, payload: PointerInfo) {
|
||||
|
@ -216,41 +240,31 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
// Shapes
|
||||
/* --------------------- Shapes --------------------- */
|
||||
|
||||
// Dot
|
||||
createDot(data, payload: PointerInfo) {
|
||||
const shape = shapeUtilityMap[ShapeType.Dot].create({
|
||||
point: screenToWorld(payload.point, data),
|
||||
})
|
||||
|
||||
commands.createShape(data, shape)
|
||||
data.selectedIds.add(shape.id)
|
||||
},
|
||||
|
||||
// History
|
||||
enableHistory() {
|
||||
history.enable()
|
||||
},
|
||||
disableHistory() {
|
||||
history.disable()
|
||||
},
|
||||
undo(data) {
|
||||
history.undo(data)
|
||||
},
|
||||
redo(data) {
|
||||
history.redo(data)
|
||||
// Ray
|
||||
createRay(data, payload: PointerInfo) {
|
||||
const shape = shapeUtilityMap[ShapeType.Ray].create({
|
||||
point: screenToWorld(payload.point, data),
|
||||
})
|
||||
|
||||
commands.createShape(data, shape)
|
||||
data.selectedIds.add(shape.id)
|
||||
},
|
||||
|
||||
// Code
|
||||
setGeneratedShapes(data, payload: { shapes: Shape[] }) {
|
||||
commands.generateShapes(data, data.currentPageId, payload.shapes)
|
||||
},
|
||||
increaseCodeFontSize(data) {
|
||||
data.settings.fontSize++
|
||||
},
|
||||
decreaseCodeFontSize(data) {
|
||||
data.settings.fontSize--
|
||||
},
|
||||
/* -------------------- Sessions -------------------- */
|
||||
|
||||
// Sessions
|
||||
// Shared
|
||||
cancelSession(data) {
|
||||
session.cancel(data)
|
||||
session = undefined
|
||||
|
@ -297,20 +311,19 @@ const state = createState({
|
|||
session.update(data, screenToWorld(payload.point, data))
|
||||
},
|
||||
|
||||
// Selection
|
||||
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
|
||||
// Direction
|
||||
startDirectionSession(data, payload: PointerInfo) {
|
||||
session = new Sessions.DirectionSession(
|
||||
data,
|
||||
screenToWorld(payload.point, data)
|
||||
)
|
||||
},
|
||||
updateDirectionSession(data, payload: PointerInfo) {
|
||||
session.update(data, screenToWorld(payload.point, data))
|
||||
},
|
||||
|
||||
/* -------------------- Selection ------------------- */
|
||||
|
||||
setHoveredId(data, payload: PointerInfo) {
|
||||
data.hoveredId = payload.target
|
||||
},
|
||||
|
@ -357,6 +370,46 @@ const state = createState({
|
|||
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: {
|
||||
selectedIds(data) {
|
||||
|
|
Loading…
Reference in a new issue