You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

transform-session.ts 4.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { Data, Edge, Corner } from "types"
  2. import * as vec from "utils/vec"
  3. import BaseSession from "./base-session"
  4. import commands from "state/commands"
  5. import { current } from "immer"
  6. import { getShapeUtils } from "lib/shape-utils"
  7. import {
  8. getBoundsCenter,
  9. getBoundsFromPoints,
  10. getCommonBounds,
  11. getPage,
  12. getRelativeTransformedBoundingBox,
  13. getShapes,
  14. getTransformedBoundingBox,
  15. } from "utils/utils"
  16. export default class TransformSession extends BaseSession {
  17. scaleX = 1
  18. scaleY = 1
  19. transformType: Edge | Corner
  20. origin: number[]
  21. snapshot: TransformSnapshot
  22. constructor(data: Data, transformType: Corner | Edge, point: number[]) {
  23. super(data)
  24. this.origin = point
  25. this.transformType = transformType
  26. this.snapshot = getTransformSnapshot(data, transformType)
  27. }
  28. update(data: Data, point: number[], isAspectRatioLocked = false) {
  29. const { transformType } = this
  30. const { selectedIds, shapeBounds, initialBounds } = this.snapshot
  31. const { shapes } = getPage(data)
  32. const newBoundingBox = getTransformedBoundingBox(
  33. initialBounds,
  34. transformType,
  35. vec.vec(this.origin, point),
  36. data.boundsRotation,
  37. isAspectRatioLocked
  38. )
  39. this.scaleX = newBoundingBox.scaleX
  40. this.scaleY = newBoundingBox.scaleY
  41. // Now work backward to calculate a new bounding box for each of the shapes.
  42. selectedIds.forEach((id) => {
  43. const { initialShape, initialShapeBounds, transformOrigin } =
  44. shapeBounds[id]
  45. const newShapeBounds = getRelativeTransformedBoundingBox(
  46. newBoundingBox,
  47. initialBounds,
  48. initialShapeBounds,
  49. this.scaleX < 0,
  50. this.scaleY < 0
  51. )
  52. const shape = shapes[id]
  53. getShapeUtils(shape).transform(shape, newShapeBounds, {
  54. type: this.transformType,
  55. initialShape,
  56. scaleX: this.scaleX,
  57. scaleY: this.scaleY,
  58. transformOrigin,
  59. })
  60. })
  61. }
  62. cancel(data: Data) {
  63. const { currentPageId, selectedIds, shapeBounds } = this.snapshot
  64. const page = getPage(data, currentPageId)
  65. selectedIds.forEach((id) => {
  66. const shape = page.shapes[id]
  67. const { initialShape, initialShapeBounds, transformOrigin } =
  68. shapeBounds[id]
  69. getShapeUtils(shape).transform(shape, initialShapeBounds, {
  70. type: this.transformType,
  71. initialShape,
  72. scaleX: 1,
  73. scaleY: 1,
  74. transformOrigin,
  75. })
  76. })
  77. }
  78. complete(data: Data) {
  79. commands.transform(
  80. data,
  81. this.snapshot,
  82. getTransformSnapshot(data, this.transformType),
  83. this.scaleX,
  84. this.scaleY
  85. )
  86. }
  87. }
  88. export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
  89. const {
  90. document: { pages },
  91. selectedIds,
  92. currentPageId,
  93. } = current(data)
  94. const pageShapes = pages[currentPageId].shapes
  95. // A mapping of selected shapes and their bounds
  96. const shapesBounds = Object.fromEntries(
  97. Array.from(selectedIds.values()).map((id) => {
  98. const shape = pageShapes[id]
  99. return [shape.id, getShapeUtils(shape).getBounds(shape)]
  100. })
  101. )
  102. const boundsArr = Object.values(shapesBounds)
  103. // The common (exterior) bounds of the selected shapes
  104. const bounds = getCommonBounds(...boundsArr)
  105. const initialInnerBounds = getBoundsFromPoints(boundsArr.map(getBoundsCenter))
  106. // Return a mapping of shapes to bounds together with the relative
  107. // positions of the shape's bounds within the common bounds shape.
  108. return {
  109. type: transformType,
  110. currentPageId,
  111. selectedIds: new Set(selectedIds),
  112. initialBounds: bounds,
  113. shapeBounds: Object.fromEntries(
  114. Array.from(selectedIds.values()).map((id) => {
  115. const shape = pageShapes[id]
  116. const initialShapeBounds = shapesBounds[id]
  117. const ic = getBoundsCenter(initialShapeBounds)
  118. let ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width
  119. let iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height
  120. return [
  121. id,
  122. {
  123. initialShape: shape,
  124. initialShapeBounds,
  125. transformOrigin: [ix, iy],
  126. },
  127. ]
  128. })
  129. ),
  130. }
  131. }
  132. export type TransformSnapshot = ReturnType<typeof getTransformSnapshot>
  133. // const transformOrigins = {
  134. // [Edge.Top]: [0.5, 1],
  135. // [Edge.Right]: [0, 0.5],
  136. // [Edge.Bottom]: [0.5, 0],
  137. // [Edge.Left]: [1, 0.5],
  138. // [Corner.TopLeft]: [1, 1],
  139. // [Corner.TopRight]: [0, 1],
  140. // [Corner.BottomLeft]: [1, 0],
  141. // [Corner.BottomRight]: [0, 0],
  142. // }
  143. // const origin = transformOrigins[this.transformType]