Adds cloning, fixes some undo bugs

This commit is contained in:
Steve Ruiz 2021-05-19 22:24:41 +01:00
parent 0c205d1377
commit e8b13103ac
8 changed files with 184 additions and 64 deletions

View file

@ -15,6 +15,10 @@ export default function useKeyboardEvents() {
}
}
if (e.altKey) {
state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
}
if (e.key === "Backspace" && !(metaKey(e) || e.shiftKey || e.altKey)) {
state.send("DELETED", getKeyboardEventInfo(e))
}
@ -66,6 +70,10 @@ export default function useKeyboardEvents() {
state.send("CANCELLED")
}
if (e.altKey) {
state.send("RELEASED_ALT_KEY")
}
state.send("RELEASED_KEY", getKeyboardEventInfo(e))
}

View file

@ -2,6 +2,7 @@ import Command from "./command"
import history from "../history"
import { Data, TransformCorner, TransformEdge } from "types"
import { getShapeUtils } from "lib/shapes"
import { current } from "immer"
import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
export default function transformSingleCommand(
@ -9,38 +10,54 @@ export default function transformSingleCommand(
before: TransformSingleSnapshot,
after: TransformSingleSnapshot,
scaleX: number,
scaleY: number
scaleY: number,
isCreating: boolean
) {
const shape =
current(data).document.pages[after.currentPageId].shapes[after.id]
history.execute(
data,
new Command({
name: "transform_single_shape",
category: "canvas",
manualSelection: true,
do(data) {
const { id, currentPageId, type, initialShape, initialShapeBounds } =
after
const shape = data.document.pages[currentPageId].shapes[id]
data.selectedIds.clear()
data.selectedIds.add(id)
getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
type,
initialShape,
scaleX,
scaleY,
})
if (isCreating) {
data.document.pages[currentPageId].shapes[id] = shape
} else {
getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
type,
initialShape,
scaleX,
scaleY,
})
}
},
undo(data) {
const { id, currentPageId, type, initialShape, initialShapeBounds } =
before
const { id, currentPageId, type, initialShapeBounds } = before
const shape = data.document.pages[currentPageId].shapes[id]
data.selectedIds.clear()
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape: after.initialShape,
scaleX: 1,
scaleY: 1,
})
if (isCreating) {
delete data.document.pages[currentPageId].shapes[id]
} else {
const shape = data.document.pages[currentPageId].shapes[id]
data.selectedIds.add(id)
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape: after.initialShape,
scaleX: 1,
scaleY: 1,
})
}
},
})
)

View file

@ -6,25 +6,46 @@ import { Data } from "types"
export default function translateCommand(
data: Data,
before: TranslateSnapshot,
after: TranslateSnapshot
after: TranslateSnapshot,
isCloning: boolean
) {
history.execute(
data,
new Command({
name: "translate_shapes",
name: isCloning ? "clone_shapes" : "translate_shapes",
category: "canvas",
do(data) {
const { shapes } = data.document.pages[after.currentPageId]
manualSelection: true,
do(data, initial) {
if (initial) return
for (let { id, point } of after.shapes) {
shapes[id].point = point
const { shapes } = data.document.pages[after.currentPageId]
const { initialShapes, clones } = after
data.selectedIds.clear()
for (let id in initialShapes) {
if (isCloning) {
shapes[id] = initialShapes[id]
} else {
shapes[id].point = initialShapes[id].point
}
data.selectedIds.add(id)
}
},
undo(data) {
const { shapes } = data.document.pages[before.currentPageId]
const { initialShapes, clones } = before
for (let { id, point } of before.shapes) {
shapes[id].point = point
data.selectedIds.clear()
for (let id in initialShapes) {
shapes[id].point = initialShapes[id].point
data.selectedIds.add(id)
if (isCloning) {
const clone = clones[id]
delete shapes[clone.id]
}
}
},
})

View file

@ -77,6 +77,13 @@ class BaseHistory<T> {
return { ...data }
}
pop() {
if (this.stack.length > 0) {
this.stack.pop()
this.pointer--
}
}
get disabled() {
return !this._enabled
}

View file

@ -86,6 +86,10 @@ class Inputs {
const { shiftKey, ctrlKey, metaKey, altKey } = e
return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
}
get pointer() {
return this.points[Object.keys(this.points)[0]]
}
}
export default new Inputs()

View file

@ -12,22 +12,24 @@ import {
} from "utils/utils"
export default class TransformSingleSession extends BaseSession {
delta = [0, 0]
transformType: TransformEdge | TransformCorner
origin: number[]
scaleX = 1
scaleY = 1
transformType: TransformEdge | TransformCorner
snapshot: TransformSingleSnapshot
origin: number[]
isCreating: boolean
constructor(
data: Data,
transformType: TransformCorner | TransformEdge,
point: number[]
point: number[],
isCreating = false
) {
super(data)
this.origin = point
this.transformType = transformType
this.snapshot = getTransformSingleSnapshot(data, transformType)
this.isCreating = isCreating
}
update(data: Data, point: number[]) {
@ -78,7 +80,8 @@ export default class TransformSingleSession extends BaseSession {
this.snapshot,
getTransformSingleSnapshot(data, this.transformType),
this.scaleX,
this.scaleY
this.scaleY,
this.isCreating
)
}
}

View file

@ -3,11 +3,13 @@ import * as vec from "utils/vec"
import BaseSession from "./base-session"
import commands from "state/commands"
import { current } from "immer"
import { v4 as uuid } from "uuid"
export default class TranslateSession extends BaseSession {
delta = [0, 0]
origin: number[]
snapshot: TranslateSnapshot
isCloning = false
constructor(data: Data, point: number[]) {
super(data)
@ -15,31 +17,62 @@ export default class TranslateSession extends BaseSession {
this.snapshot = getTranslateSnapshot(data)
}
update(data: Data, point: number[]) {
const { currentPageId, shapes } = this.snapshot
update(data: Data, point: number[], isCloning: boolean) {
const { currentPageId, clones, initialShapes } = this.snapshot
const { document } = data
const { shapes } = document.pages[this.snapshot.currentPageId]
const delta = vec.vec(this.origin, point)
for (let shape of shapes) {
document.pages[currentPageId].shapes[shape.id].point = vec.add(
shape.point,
delta
)
if (isCloning && !this.isCloning) {
// Enter cloning state, create clones at shape points and move shapes
// back to initial point.
this.isCloning = true
data.selectedIds.clear()
for (let id in clones) {
const clone = clones[id]
data.selectedIds.add(clone.id)
shapes[id].point = initialShapes[id].point
data.document.pages[currentPageId].shapes[clone.id] = clone
}
} else if (!isCloning && this.isCloning) {
// Exit cloning state, delete up clones and move shapes to clone points
this.isCloning = false
data.selectedIds.clear()
for (let id in clones) {
const clone = clones[id]
data.selectedIds.add(id)
delete data.document.pages[currentPageId].shapes[clone.id]
}
}
// Calculate the new points and update data
for (let id in initialShapes) {
const point = vec.add(initialShapes[id].point, delta)
const targetId = this.isCloning ? clones[id].id : id
document.pages[currentPageId].shapes[targetId].point = point
}
}
cancel(data: Data) {
const { document } = data
const { initialShapes, clones, currentPageId } = this.snapshot
for (let shape of this.snapshot.shapes) {
document.pages[this.snapshot.currentPageId].shapes[shape.id].point =
shape.point
const { shapes } = document.pages[currentPageId]
for (let id in initialShapes) {
shapes[id].point = initialShapes[id].point
delete shapes[clones[id].id]
}
}
complete(data: Data) {
commands.translate(data, this.snapshot, getTranslateSnapshot(data))
commands.translate(
data,
this.snapshot,
getTranslateSnapshot(data),
this.isCloning
)
}
}
@ -49,13 +82,19 @@ export function getTranslateSnapshot(data: Data) {
currentPageId,
} = current(data)
const { shapes } = pages[currentPageId]
const shapes = Array.from(data.selectedIds.values()).map(
(id) => pages[currentPageId].shapes[id]
)
// Clones and shapes are keyed under the same id, though the clone itself
// has a different id.
return {
currentPageId,
shapes: Array.from(data.selectedIds.values())
.map((id) => shapes[id])
.map(({ id, point }) => ({ id, point })),
initialShapes: Object.fromEntries(shapes.map((shape) => [shape.id, shape])),
clones: Object.fromEntries(
shapes.map((shape) => [shape.id, { ...shape, id: uuid() }])
),
}
}

View file

@ -10,6 +10,7 @@ import {
TransformEdge,
CodeControl,
} from "types"
import inputs from "./inputs"
import { defaultDocument } from "./data"
import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
import history from "state/history"
@ -174,6 +175,8 @@ const state = createState({
on: {
MOVED_POINTER: "updateTranslateSession",
PANNED_CAMERA: "updateTranslateSession",
PRESSED_ALT_KEY: "updateCloningTranslateSession",
RELEASED_ALT_KEY: "updateCloningTranslateSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
@ -261,6 +264,7 @@ const state = createState({
states: {
creating: {
on: {
CANCELLED: { to: "selecting" },
POINTED_CANVAS: {
to: "ellipse.editing",
},
@ -284,6 +288,7 @@ const state = createState({
states: {
creating: {
on: {
CANCELLED: { to: "selecting" },
POINTED_CANVAS: {
to: "rectangle.editing",
},
@ -307,6 +312,7 @@ const state = createState({
states: {
creating: {
on: {
CANCELLED: { to: "selecting" },
POINTED_CANVAS: {
do: "createRay",
to: "ray.editing",
@ -315,11 +321,8 @@ const state = createState({
},
editing: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
STOPPED_POINTING: { to: "selecting" },
CANCELLED: { to: "selecting" },
MOVED_POINTER: {
if: "distanceImpliesDrag",
to: "drawingShape.direction",
@ -333,6 +336,7 @@ const state = createState({
states: {
creating: {
on: {
CANCELLED: { to: "selecting" },
POINTED_CANVAS: {
do: "createLine",
to: "line.editing",
@ -341,11 +345,8 @@ const state = createState({
},
editing: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
STOPPED_POINTING: { to: "selecting" },
CANCELLED: { to: "selecting" },
MOVED_POINTER: {
if: "distanceImpliesDrag",
to: "drawingShape.direction",
@ -359,7 +360,10 @@ const state = createState({
},
drawingShape: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
STOPPED_POINTING: {
do: "completeSession",
to: "selecting",
},
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
@ -422,7 +426,8 @@ const state = createState({
point: screenToWorld(payload.point, data),
})
commands.createShape(data, shape)
data.document.pages[data.currentPageId].shapes[shape.id] = shape
data.selectedIds.clear()
data.selectedIds.add(shape.id)
},
@ -432,7 +437,8 @@ const state = createState({
point: screenToWorld(payload.point, data),
})
commands.createShape(data, shape)
data.document.pages[data.currentPageId].shapes[shape.id] = shape
data.selectedIds.clear()
data.selectedIds.add(shape.id)
},
@ -443,7 +449,8 @@ const state = createState({
direction: [0, 1],
})
commands.createShape(data, shape)
data.document.pages[data.currentPageId].shapes[shape.id] = shape
data.selectedIds.clear()
data.selectedIds.add(shape.id)
},
@ -453,7 +460,8 @@ const state = createState({
radius: 1,
})
commands.createShape(data, shape)
data.document.pages[data.currentPageId].shapes[shape.id] = shape
data.selectedIds.clear()
data.selectedIds.add(shape.id)
},
@ -464,7 +472,8 @@ const state = createState({
radiusY: 1,
})
commands.createShape(data, shape)
data.document.pages[data.currentPageId].shapes[shape.id] = shape
data.selectedIds.clear()
data.selectedIds.add(shape.id)
},
@ -474,7 +483,8 @@ const state = createState({
size: [1, 1],
})
commands.createShape(data, shape)
data.document.pages[data.currentPageId].shapes[shape.id] = shape
data.selectedIds.clear()
data.selectedIds.add(shape.id)
},
/* -------------------- Sessions -------------------- */
@ -518,8 +528,15 @@ const state = createState({
screenToWorld(payload.point, data)
)
},
updateCloningTranslateSession(data, payload: { altKey: boolean }) {
session.update(
data,
screenToWorld(inputs.pointer.point, data),
payload.altKey
)
},
updateTranslateSession(data, payload: PointerInfo) {
session.update(data, screenToWorld(payload.point, data))
session.update(data, screenToWorld(payload.point, data), payload.altKey)
},
// Dragging / Translating
@ -544,7 +561,8 @@ const state = createState({
session = new Sessions.TransformSingleSession(
data,
TransformCorner.BottomRight,
screenToWorld(payload.point, data)
screenToWorld(payload.point, data),
true
)
},
updateTransformSession(data, payload: PointerInfo) {
@ -642,6 +660,9 @@ const state = createState({
/* ---------------------- History ---------------------- */
// History
popHistory() {
history.pop()
},
forceSave(data) {
history.save(data)
},