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

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