Adds cloning, fixes some undo bugs
This commit is contained in:
parent
0c205d1377
commit
e8b13103ac
8 changed files with 184 additions and 64 deletions
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue