import { Data, TransformEdge, TransformCorner, Bounds, BoundsSnapshot, } from "types" 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" import { getCommonBounds, getTransformAnchor } from "utils/utils" export default class TransformSession extends BaseSession { delta = [0, 0] isFlippedX = false isFlippedY = false transformType: TransformEdge | TransformCorner origin: number[] snapshot: TransformSnapshot corners: { a: number[] b: number[] } constructor( data: Data, transformType: TransformCorner | TransformEdge, point: number[] ) { super(data) this.origin = point this.transformType = transformType this.snapshot = getTransformSnapshot(data, transformType) const { minX, minY, maxX, maxY } = this.snapshot.initialBounds this.corners = { a: [minX, minY], b: [maxX, maxY], } } update(data: Data, point: number[]) { const { shapeBounds, initialBounds, currentPageId, selectedIds, } = this.snapshot const { shapes } = data.document.pages[currentPageId] let [x, y] = point const { corners: { a, b }, transformType, } = this // Edge Transform switch (transformType) { case TransformEdge.Top: { a[1] = y break } case TransformEdge.Right: { b[0] = x break } case TransformEdge.Bottom: { b[1] = y break } case TransformEdge.Left: { a[0] = x break } case TransformCorner.TopLeft: { a[1] = y a[0] = x break } case TransformCorner.TopRight: { b[0] = x a[1] = y break } case TransformCorner.BottomRight: { b[1] = y b[0] = x break } case TransformCorner.BottomLeft: { a[0] = x b[1] = y break } } // Calculate new common (externior) bounding box const newBounds = { 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]), } this.isFlippedX = b[0] < a[0] this.isFlippedY = b[1] < a[1] // Now work backward to calculate a new bounding box for each of the shapes. selectedIds.forEach((id) => { const { initialShape, initialShapeBounds } = shapeBounds[id] const { nx, nmx, nw, ny, nmy, nh } = initialShapeBounds const shape = shapes[id] const minX = newBounds.minX + (this.isFlippedX ? nmx : nx) * newBounds.width const minY = newBounds.minY + (this.isFlippedY ? nmy : ny) * newBounds.height const width = nw * newBounds.width const height = nh * newBounds.height const newShapeBounds = { minX, minY, maxX: minX + width, maxY: minY + height, width, height, isFlippedX: this.isFlippedX, isFlippedY: this.isFlippedY, } // 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. getShapeUtils(shape).transform(shape, newShapeBounds, { type: this.transformType, initialShape, initialShapeBounds, initialBounds, isFlippedX: this.isFlippedX, isFlippedY: this.isFlippedY, anchor: getTransformAnchor( this.transformType, this.isFlippedX, this.isFlippedY ), }) }) } cancel(data: Data) { const { shapeBounds, initialBounds, currentPageId, selectedIds, } = this.snapshot const { shapes } = data.document.pages[currentPageId] selectedIds.forEach((id) => { const shape = shapes.shapes[id] const { initialShape, initialShapeBounds } = shapeBounds[id] getShapeUtils(shape).transform(shape, initialShapeBounds, { type: this.transformType, initialShape, initialShapeBounds, initialBounds, isFlippedX: false, isFlippedY: false, anchor: getTransformAnchor(this.transformType, false, false), }) }) } complete(data: Data) { commands.transform( data, this.snapshot, getTransformSnapshot(data, this.transformType), getTransformAnchor(this.transformType, false, false) ) } } export function getTransformSnapshot( data: Data, transformType: TransformEdge | TransformCorner ) { const { document: { pages }, selectedIds, currentPageId, } = current(data) const pageShapes = pages[currentPageId].shapes // A mapping of selected shapes and their bounds const shapesBounds = Object.fromEntries( Array.from(selectedIds.values()).map((id) => { const shape = pageShapes[id] return [shape.id, getShapeUtils(shape).getBounds(shape)] }) ) // The common (exterior) bounds of the selected shapes const bounds = getCommonBounds(...Object.values(shapesBounds)) // Return a mapping of shapes to bounds together with the relative // positions of the shape's bounds within the common bounds shape. return { currentPageId, type: transformType, initialBounds: bounds, selectedIds: new Set(selectedIds), shapeBounds: Object.fromEntries( Array.from(selectedIds.values()).map((id) => { const { minX, minY, width, height } = shapesBounds[id] return [ id, { 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, }, }, ] }) ), } } export type TransformSnapshot = ReturnType