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 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import {
  2. Data,
  3. TransformEdge,
  4. TransformCorner,
  5. Bounds,
  6. BoundsSnapshot,
  7. } from "types"
  8. import * as vec from "utils/vec"
  9. import BaseSession from "./base-session"
  10. import commands from "state/commands"
  11. import { current } from "immer"
  12. import { getShapeUtils } from "lib/shapes"
  13. import { getCommonBounds, getTransformAnchor } from "utils/utils"
  14. export default class TransformSession extends BaseSession {
  15. delta = [0, 0]
  16. isFlippedX = false
  17. isFlippedY = false
  18. transformType: TransformEdge | TransformCorner
  19. origin: number[]
  20. snapshot: TransformSnapshot
  21. corners: {
  22. a: number[]
  23. b: number[]
  24. }
  25. constructor(
  26. data: Data,
  27. transformType: TransformCorner | TransformEdge,
  28. point: number[]
  29. ) {
  30. super(data)
  31. this.origin = point
  32. this.transformType = transformType
  33. this.snapshot = getTransformSnapshot(data, transformType)
  34. const { minX, minY, maxX, maxY } = this.snapshot.initialBounds
  35. this.corners = {
  36. a: [minX, minY],
  37. b: [maxX, maxY],
  38. }
  39. }
  40. update(data: Data, point: number[]) {
  41. const { shapeBounds, initialBounds, currentPageId, selectedIds } =
  42. this.snapshot
  43. const { shapes } = data.document.pages[currentPageId]
  44. const delta = vec.vec(this.origin, point)
  45. const {
  46. corners: { a, b },
  47. transformType,
  48. } = this
  49. // Edge Transform
  50. switch (transformType) {
  51. case TransformEdge.Top: {
  52. a[1] = initialBounds.minY + delta[1]
  53. break
  54. }
  55. case TransformEdge.Right: {
  56. b[0] = initialBounds.maxX + delta[0]
  57. break
  58. }
  59. case TransformEdge.Bottom: {
  60. b[1] = initialBounds.maxY + delta[1]
  61. break
  62. }
  63. case TransformEdge.Left: {
  64. a[0] = initialBounds.minX + delta[0]
  65. break
  66. }
  67. case TransformCorner.TopLeft: {
  68. a[0] = initialBounds.minX + delta[0]
  69. a[1] = initialBounds.minY + delta[1]
  70. break
  71. }
  72. case TransformCorner.TopRight: {
  73. a[1] = initialBounds.minY + delta[1]
  74. b[0] = initialBounds.maxX + delta[0]
  75. break
  76. }
  77. case TransformCorner.BottomRight: {
  78. b[0] = initialBounds.maxX + delta[0]
  79. b[1] = initialBounds.maxY + delta[1]
  80. break
  81. }
  82. case TransformCorner.BottomLeft: {
  83. a[0] = initialBounds.minX + delta[0]
  84. b[1] = initialBounds.maxY + delta[1]
  85. break
  86. }
  87. }
  88. // Calculate new common (externior) bounding box
  89. const newBounds = {
  90. minX: Math.min(a[0], b[0]),
  91. minY: Math.min(a[1], b[1]),
  92. maxX: Math.max(a[0], b[0]),
  93. maxY: Math.max(a[1], b[1]),
  94. width: Math.abs(b[0] - a[0]),
  95. height: Math.abs(b[1] - a[1]),
  96. }
  97. this.isFlippedX = b[0] < a[0]
  98. this.isFlippedY = b[1] < a[1]
  99. // Now work backward to calculate a new bounding box for each of the shapes.
  100. selectedIds.forEach((id) => {
  101. const { initialShape, initialShapeBounds } = shapeBounds[id]
  102. const { nx, nmx, nw, ny, nmy, nh } = initialShapeBounds
  103. const shape = shapes[id]
  104. const minX =
  105. newBounds.minX + (this.isFlippedX ? nmx : nx) * newBounds.width
  106. const minY =
  107. newBounds.minY + (this.isFlippedY ? nmy : ny) * newBounds.height
  108. const width = nw * newBounds.width
  109. const height = nh * newBounds.height
  110. const newShapeBounds = {
  111. minX,
  112. minY,
  113. maxX: minX + width,
  114. maxY: minY + height,
  115. width,
  116. height,
  117. isFlippedX: this.isFlippedX,
  118. isFlippedY: this.isFlippedY,
  119. }
  120. // Pass the new data to the shape's transform utility for mutation.
  121. // Most shapes should be able to transform using only the bounding box,
  122. // however some shapes (e.g. those with internal points) will need more
  123. // data here too.
  124. getShapeUtils(shape).transform(shape, newShapeBounds, {
  125. type: this.transformType,
  126. initialShape,
  127. initialShapeBounds,
  128. initialBounds,
  129. isFlippedX: this.isFlippedX,
  130. isFlippedY: this.isFlippedY,
  131. anchor: getTransformAnchor(
  132. this.transformType,
  133. this.isFlippedX,
  134. this.isFlippedY
  135. ),
  136. })
  137. })
  138. }
  139. cancel(data: Data) {
  140. const { shapeBounds, initialBounds, currentPageId, selectedIds } =
  141. this.snapshot
  142. const { shapes } = data.document.pages[currentPageId]
  143. selectedIds.forEach((id) => {
  144. const shape = shapes.shapes[id]
  145. const { initialShape, initialShapeBounds } = shapeBounds[id]
  146. getShapeUtils(shape).transform(shape, initialShapeBounds, {
  147. type: this.transformType,
  148. initialShape,
  149. initialShapeBounds,
  150. initialBounds,
  151. isFlippedX: false,
  152. isFlippedY: false,
  153. anchor: getTransformAnchor(this.transformType, false, false),
  154. })
  155. })
  156. }
  157. complete(data: Data) {
  158. commands.transform(
  159. data,
  160. this.snapshot,
  161. getTransformSnapshot(data, this.transformType),
  162. getTransformAnchor(this.transformType, false, false)
  163. )
  164. }
  165. }
  166. export function getTransformSnapshot(
  167. data: Data,
  168. transformType: TransformEdge | TransformCorner
  169. ) {
  170. const {
  171. document: { pages },
  172. selectedIds,
  173. currentPageId,
  174. } = current(data)
  175. const pageShapes = pages[currentPageId].shapes
  176. // A mapping of selected shapes and their bounds
  177. const shapesBounds = Object.fromEntries(
  178. Array.from(selectedIds.values()).map((id) => {
  179. const shape = pageShapes[id]
  180. return [shape.id, getShapeUtils(shape).getBounds(shape)]
  181. })
  182. )
  183. // The common (exterior) bounds of the selected shapes
  184. const bounds = getCommonBounds(...Object.values(shapesBounds))
  185. // Return a mapping of shapes to bounds together with the relative
  186. // positions of the shape's bounds within the common bounds shape.
  187. return {
  188. currentPageId,
  189. type: transformType,
  190. initialBounds: bounds,
  191. selectedIds: new Set(selectedIds),
  192. shapeBounds: Object.fromEntries(
  193. Array.from(selectedIds.values()).map((id) => {
  194. const { minX, minY, width, height } = shapesBounds[id]
  195. return [
  196. id,
  197. {
  198. initialShape: pageShapes[id],
  199. initialShapeBounds: {
  200. ...shapesBounds[id],
  201. nx: (minX - bounds.minX) / bounds.width,
  202. ny: (minY - bounds.minY) / bounds.height,
  203. nmx: 1 - (minX + width - bounds.minX) / bounds.width,
  204. nmy: 1 - (minY + height - bounds.minY) / bounds.height,
  205. nw: width / bounds.width,
  206. nh: height / bounds.height,
  207. },
  208. },
  209. ]
  210. })
  211. ),
  212. }
  213. }
  214. export type TransformSnapshot = ReturnType<typeof getTransformSnapshot>