tldraw/state/sessions/transform-session.ts

254 lines
6.9 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[]) {
2021-05-16 06:29:48 +00:00
const { shapeBounds, initialBounds, currentPageId, selectedIds } =
this.snapshot
2021-05-14 21:05:21 +00:00
const { shapes } = data.document.pages[currentPageId]
2021-05-14 12:44:23 +00:00
2021-05-16 06:29:48 +00:00
const delta = vec.vec(this.origin, 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
/*
Edge transform
Corners a and b are the original top-left and bottom-right corners of the
bounding box. Depending on what the user is dragging, change one or both
points. To keep things smooth, calculate based by adding the delta (the
vector between the current point and its original point) to the original
bounding box values.
*/
2021-05-14 12:44:23 +00:00
switch (transformType) {
case TransformEdge.Top: {
2021-05-16 06:29:48 +00:00
a[1] = initialBounds.minY + delta[1]
2021-05-14 12:44:23 +00:00
break
}
case TransformEdge.Right: {
2021-05-16 06:29:48 +00:00
b[0] = initialBounds.maxX + delta[0]
2021-05-14 12:44:23 +00:00
break
}
case TransformEdge.Bottom: {
2021-05-16 06:29:48 +00:00
b[1] = initialBounds.maxY + delta[1]
2021-05-14 12:44:23 +00:00
break
}
case TransformEdge.Left: {
2021-05-16 06:29:48 +00:00
a[0] = initialBounds.minX + delta[0]
2021-05-14 12:44:23 +00:00
break
}
case TransformCorner.TopLeft: {
2021-05-16 06:29:48 +00:00
a[0] = initialBounds.minX + delta[0]
a[1] = initialBounds.minY + delta[1]
2021-05-14 12:44:23 +00:00
break
}
case TransformCorner.TopRight: {
2021-05-16 06:29:48 +00:00
a[1] = initialBounds.minY + delta[1]
b[0] = initialBounds.maxX + delta[0]
2021-05-14 12:44:23 +00:00
break
}
case TransformCorner.BottomRight: {
2021-05-16 06:29:48 +00:00
b[0] = initialBounds.maxX + delta[0]
b[1] = initialBounds.maxY + delta[1]
2021-05-14 12:44:23 +00:00
break
}
case TransformCorner.BottomLeft: {
2021-05-16 06:29:48 +00:00
a[0] = initialBounds.minX + delta[0]
b[1] = initialBounds.maxY + delta[1]
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-16 06:29:48 +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) => {
2021-05-17 21:27:18 +00:00
const shape = shapes[id]
2021-05-14 21:05:21 +00:00
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>