tldraw/state/sessions/transform-session.ts

252 lines
6.2 KiB
TypeScript
Raw Normal View History

2021-05-14 21:05:21 +00:00
import {
Data,
TransformEdge,
TransformCorner,
Bounds,
BoundsSnapshot,
} from "types"
2021-05-14 12:44:23 +00:00
import * as vec from "utils/vec"
import BaseSession from "./base-session"
import commands from "state/commands"
import { current } from "immer"
import { getShapeUtils } from "lib/shapes"
2021-05-15 13:02:13 +00:00
import { getCommonBounds, getTransformAnchor } from "utils/utils"
2021-05-14 12:44:23 +00:00
export default class TransformSession extends BaseSession {
delta = [0, 0]
2021-05-15 13:02:13 +00:00
isFlippedX = false
isFlippedY = false
2021-05-14 12:44:23 +00:00
transformType: TransformEdge | TransformCorner
origin: number[]
snapshot: TransformSnapshot
corners: {
a: number[]
b: number[]
}
constructor(
data: Data,
2021-05-15 13:02:13 +00:00
transformType: TransformCorner | TransformEdge,
2021-05-14 12:44:23 +00:00
point: number[]
) {
super(data)
this.origin = point
2021-05-15 13:02:13 +00:00
this.transformType = transformType
this.snapshot = getTransformSnapshot(data, transformType)
2021-05-14 12:44:23 +00:00
const { minX, minY, maxX, maxY } = this.snapshot.initialBounds
this.corners = {
a: [minX, minY],
b: [maxX, maxY],
}
}
update(data: Data, point: number[]) {
const {
2021-05-14 21:05:21 +00:00
shapeBounds,
initialBounds,
currentPageId,
selectedIds,
} = this.snapshot
const { shapes } = data.document.pages[currentPageId]
2021-05-14 12:44:23 +00:00
let [x, y] = point
2021-05-14 21:05:21 +00:00
const {
corners: { a, b },
transformType,
} = this
2021-05-14 12:44:23 +00:00
// Edge Transform
switch (transformType) {
case TransformEdge.Top: {
2021-05-14 21:05:21 +00:00
a[1] = y
2021-05-14 12:44:23 +00:00
break
}
case TransformEdge.Right: {
2021-05-14 21:05:21 +00:00
b[0] = x
2021-05-14 12:44:23 +00:00
break
}
case TransformEdge.Bottom: {
2021-05-14 21:05:21 +00:00
b[1] = y
2021-05-14 12:44:23 +00:00
break
}
case TransformEdge.Left: {
2021-05-14 21:05:21 +00:00
a[0] = x
2021-05-14 12:44:23 +00:00
break
}
case TransformCorner.TopLeft: {
2021-05-14 21:05:21 +00:00
a[1] = y
a[0] = x
2021-05-14 12:44:23 +00:00
break
}
case TransformCorner.TopRight: {
2021-05-14 21:05:21 +00:00
b[0] = x
a[1] = y
2021-05-14 12:44:23 +00:00
break
}
case TransformCorner.BottomRight: {
2021-05-14 21:05:21 +00:00
b[1] = y
b[0] = x
2021-05-14 12:44:23 +00:00
break
}
case TransformCorner.BottomLeft: {
2021-05-14 21:05:21 +00:00
a[0] = x
b[1] = y
2021-05-14 12:44:23 +00:00
break
}
}
2021-05-14 21:05:21 +00:00
// Calculate new common (externior) bounding box
2021-05-14 12:44:23 +00:00
const newBounds = {
2021-05-14 21:05:21 +00:00
minX: Math.min(a[0], b[0]),
minY: Math.min(a[1], b[1]),
maxX: Math.max(a[0], b[0]),
maxY: Math.max(a[1], b[1]),
width: Math.abs(b[0] - a[0]),
height: Math.abs(b[1] - a[1]),
2021-05-14 12:44:23 +00:00
}
2021-05-15 13:02:13 +00:00
this.isFlippedX = b[0] < a[0]
this.isFlippedY = b[1] < a[1]
2021-05-14 12:44:23 +00:00
2021-05-14 21:05:21 +00:00
// Now work backward to calculate a new bounding box for each of the shapes.
2021-05-14 12:44:23 +00:00
selectedIds.forEach((id) => {
2021-05-14 21:05:21 +00:00
const { initialShape, initialShapeBounds } = shapeBounds[id]
const { nx, nmx, nw, ny, nmy, nh } = initialShapeBounds
const shape = shapes[id]
2021-05-14 12:44:23 +00:00
2021-05-15 13:02:13 +00:00
const minX =
newBounds.minX + (this.isFlippedX ? nmx : nx) * newBounds.width
const minY =
newBounds.minY + (this.isFlippedY ? nmy : ny) * newBounds.height
2021-05-14 12:44:23 +00:00
const width = nw * newBounds.width
const height = nh * newBounds.height
2021-05-14 21:05:21 +00:00
const newShapeBounds = {
2021-05-14 12:44:23 +00:00
minX,
minY,
maxX: minX + width,
maxY: minY + height,
width,
height,
2021-05-15 13:02:13 +00:00
isFlippedX: this.isFlippedX,
isFlippedY: this.isFlippedY,
2021-05-14 21:05:21 +00:00
}
2021-05-14 12:44:23 +00:00
2021-05-14 21:05:21 +00:00
// Pass the new data to the shape's transform utility for mutation.
// Most shapes should be able to transform using only the bounding box,
// however some shapes (e.g. those with internal points) will need more
// data here too.
2021-05-15 13:02:13 +00:00
getShapeUtils(shape).transform(shape, newShapeBounds, {
type: this.transformType,
2021-05-14 21:05:21 +00:00
initialShape,
initialShapeBounds,
2021-05-15 13:02:13 +00:00
initialBounds,
isFlippedX: this.isFlippedX,
isFlippedY: this.isFlippedY,
anchor: getTransformAnchor(
this.transformType,
this.isFlippedX,
this.isFlippedY
),
})
2021-05-14 21:05:21 +00:00
})
2021-05-14 12:44:23 +00:00
}
cancel(data: Data) {
2021-05-14 21:05:21 +00:00
const {
shapeBounds,
initialBounds,
currentPageId,
selectedIds,
} = this.snapshot
2021-05-14 12:44:23 +00:00
2021-05-14 21:05:21 +00:00
const { shapes } = data.document.pages[currentPageId]
selectedIds.forEach((id) => {
const shape = shapes.shapes[id]
const { initialShape, initialShapeBounds } = shapeBounds[id]
2021-05-15 13:02:13 +00:00
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type: this.transformType,
2021-05-14 21:05:21 +00:00
initialShape,
initialShapeBounds,
2021-05-15 13:02:13 +00:00
initialBounds,
isFlippedX: false,
isFlippedY: false,
anchor: getTransformAnchor(this.transformType, false, false),
})
2021-05-14 21:05:21 +00:00
})
2021-05-14 12:44:23 +00:00
}
complete(data: Data) {
2021-05-15 13:02:13 +00:00
commands.transform(
data,
this.snapshot,
getTransformSnapshot(data, this.transformType),
getTransformAnchor(this.transformType, false, false)
)
2021-05-14 12:44:23 +00:00
}
}
2021-05-15 13:02:13 +00:00
export function getTransformSnapshot(
data: Data,
transformType: TransformEdge | TransformCorner
) {
2021-05-14 12:44:23 +00:00
const {
document: { pages },
selectedIds,
currentPageId,
} = current(data)
2021-05-14 21:05:21 +00:00
const pageShapes = pages[currentPageId].shapes
2021-05-14 12:44:23 +00:00
// A mapping of selected shapes and their bounds
const shapesBounds = Object.fromEntries(
Array.from(selectedIds.values()).map((id) => {
2021-05-14 21:05:21 +00:00
const shape = pageShapes[id]
2021-05-14 12:44:23 +00:00
return [shape.id, getShapeUtils(shape).getBounds(shape)]
})
)
// The common (exterior) bounds of the selected shapes
2021-05-14 21:05:21 +00:00
const bounds = getCommonBounds(...Object.values(shapesBounds))
2021-05-14 12:44:23 +00:00
// Return a mapping of shapes to bounds together with the relative
// positions of the shape's bounds within the common bounds shape.
return {
currentPageId,
2021-05-15 13:02:13 +00:00
type: transformType,
2021-05-14 12:44:23 +00:00
initialBounds: bounds,
selectedIds: new Set(selectedIds),
shapeBounds: Object.fromEntries(
Array.from(selectedIds.values()).map((id) => {
const { minX, minY, width, height } = shapesBounds[id]
return [
id,
{
2021-05-14 21:05:21 +00:00
initialShape: pageShapes[id],
initialShapeBounds: {
...shapesBounds[id],
nx: (minX - bounds.minX) / bounds.width,
ny: (minY - bounds.minY) / bounds.height,
nmx: 1 - (minX + width - bounds.minX) / bounds.width,
nmy: 1 - (minY + height - bounds.minY) / bounds.height,
nw: width / bounds.width,
nh: height / bounds.height,
},
2021-05-14 12:44:23 +00:00
},
]
})
),
}
}
export type TransformSnapshot = ReturnType<typeof getTransformSnapshot>