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

View file

@ -2,6 +2,7 @@ import Command from "./command"
import history from "../history" import history from "../history"
import { Data, TransformCorner, TransformEdge } from "types" import { Data, TransformCorner, TransformEdge } from "types"
import { getShapeUtils } from "lib/shapes" import { getShapeUtils } from "lib/shapes"
import { current } from "immer"
import { TransformSingleSnapshot } from "state/sessions/transform-single-session" import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
export default function transformSingleCommand( export default function transformSingleCommand(
@ -9,38 +10,54 @@ export default function transformSingleCommand(
before: TransformSingleSnapshot, before: TransformSingleSnapshot,
after: TransformSingleSnapshot, after: TransformSingleSnapshot,
scaleX: number, scaleX: number,
scaleY: number scaleY: number,
isCreating: boolean
) { ) {
const shape =
current(data).document.pages[after.currentPageId].shapes[after.id]
history.execute( history.execute(
data, data,
new Command({ new Command({
name: "transform_single_shape", name: "transform_single_shape",
category: "canvas", category: "canvas",
manualSelection: true,
do(data) { do(data) {
const { id, currentPageId, type, initialShape, initialShapeBounds } = const { id, currentPageId, type, initialShape, initialShapeBounds } =
after after
const shape = data.document.pages[currentPageId].shapes[id] data.selectedIds.clear()
data.selectedIds.add(id)
getShapeUtils(shape).transformSingle(shape, initialShapeBounds, { if (isCreating) {
type, data.document.pages[currentPageId].shapes[id] = shape
initialShape, } else {
scaleX, getShapeUtils(shape).transformSingle(shape, initialShapeBounds, {
scaleY, type,
}) initialShape,
scaleX,
scaleY,
})
}
}, },
undo(data) { undo(data) {
const { id, currentPageId, type, initialShape, initialShapeBounds } = const { id, currentPageId, type, initialShapeBounds } = before
before
const shape = data.document.pages[currentPageId].shapes[id] data.selectedIds.clear()
getShapeUtils(shape).transform(shape, initialShapeBounds, { if (isCreating) {
type, delete data.document.pages[currentPageId].shapes[id]
initialShape: after.initialShape, } else {
scaleX: 1, const shape = data.document.pages[currentPageId].shapes[id]
scaleY: 1, 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( export default function translateCommand(
data: Data, data: Data,
before: TranslateSnapshot, before: TranslateSnapshot,
after: TranslateSnapshot after: TranslateSnapshot,
isCloning: boolean
) { ) {
history.execute( history.execute(
data, data,
new Command({ new Command({
name: "translate_shapes", name: isCloning ? "clone_shapes" : "translate_shapes",
category: "canvas", category: "canvas",
do(data) { manualSelection: true,
const { shapes } = data.document.pages[after.currentPageId] do(data, initial) {
if (initial) return
for (let { id, point } of after.shapes) { const { shapes } = data.document.pages[after.currentPageId]
shapes[id].point = point 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) { undo(data) {
const { shapes } = data.document.pages[before.currentPageId] const { shapes } = data.document.pages[before.currentPageId]
const { initialShapes, clones } = before
for (let { id, point } of before.shapes) { data.selectedIds.clear()
shapes[id].point = point
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 } return { ...data }
} }
pop() {
if (this.stack.length > 0) {
this.stack.pop()
this.pointer--
}
}
get disabled() { get disabled() {
return !this._enabled return !this._enabled
} }

View file

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

View file

@ -12,22 +12,24 @@ import {
} from "utils/utils" } from "utils/utils"
export default class TransformSingleSession extends BaseSession { export default class TransformSingleSession extends BaseSession {
delta = [0, 0] transformType: TransformEdge | TransformCorner
origin: number[]
scaleX = 1 scaleX = 1
scaleY = 1 scaleY = 1
transformType: TransformEdge | TransformCorner
snapshot: TransformSingleSnapshot snapshot: TransformSingleSnapshot
origin: number[] isCreating: boolean
constructor( constructor(
data: Data, data: Data,
transformType: TransformCorner | TransformEdge, transformType: TransformCorner | TransformEdge,
point: number[] point: number[],
isCreating = false
) { ) {
super(data) super(data)
this.origin = point this.origin = point
this.transformType = transformType this.transformType = transformType
this.snapshot = getTransformSingleSnapshot(data, transformType) this.snapshot = getTransformSingleSnapshot(data, transformType)
this.isCreating = isCreating
} }
update(data: Data, point: number[]) { update(data: Data, point: number[]) {
@ -78,7 +80,8 @@ export default class TransformSingleSession extends BaseSession {
this.snapshot, this.snapshot,
getTransformSingleSnapshot(data, this.transformType), getTransformSingleSnapshot(data, this.transformType),
this.scaleX, 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 BaseSession from "./base-session"
import commands from "state/commands" import commands from "state/commands"
import { current } from "immer" import { current } from "immer"
import { v4 as uuid } from "uuid"
export default class TranslateSession extends BaseSession { export default class TranslateSession extends BaseSession {
delta = [0, 0] delta = [0, 0]
origin: number[] origin: number[]
snapshot: TranslateSnapshot snapshot: TranslateSnapshot
isCloning = false
constructor(data: Data, point: number[]) { constructor(data: Data, point: number[]) {
super(data) super(data)
@ -15,31 +17,62 @@ export default class TranslateSession extends BaseSession {
this.snapshot = getTranslateSnapshot(data) this.snapshot = getTranslateSnapshot(data)
} }
update(data: Data, point: number[]) { update(data: Data, point: number[], isCloning: boolean) {
const { currentPageId, shapes } = this.snapshot const { currentPageId, clones, initialShapes } = this.snapshot
const { document } = data const { document } = data
const { shapes } = document.pages[this.snapshot.currentPageId]
const delta = vec.vec(this.origin, point) const delta = vec.vec(this.origin, point)
for (let shape of shapes) { if (isCloning && !this.isCloning) {
document.pages[currentPageId].shapes[shape.id].point = vec.add( // Enter cloning state, create clones at shape points and move shapes
shape.point, // back to initial point.
delta 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) { cancel(data: Data) {
const { document } = data const { document } = data
const { initialShapes, clones, currentPageId } = this.snapshot
for (let shape of this.snapshot.shapes) { const { shapes } = document.pages[currentPageId]
document.pages[this.snapshot.currentPageId].shapes[shape.id].point =
shape.point for (let id in initialShapes) {
shapes[id].point = initialShapes[id].point
delete shapes[clones[id].id]
} }
} }
complete(data: Data) { 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, currentPageId,
} = current(data) } = 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 { return {
currentPageId, currentPageId,
shapes: Array.from(data.selectedIds.values()) initialShapes: Object.fromEntries(shapes.map((shape) => [shape.id, shape])),
.map((id) => shapes[id]) clones: Object.fromEntries(
.map(({ id, point }) => ({ id, point })), shapes.map((shape) => [shape.id, { ...shape, id: uuid() }])
),
} }
} }

View file

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