Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

transform-session.ts 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. /*
  51. Edge transform
  52. Corners a and b are the original top-left and bottom-right corners of the
  53. bounding box. Depending on what the user is dragging, change one or both
  54. points. To keep things smooth, calculate based by adding the delta (the
  55. vector between the current point and its original point) to the original
  56. bounding box values.
  57. */
  58. switch (transformType) {
  59. case TransformEdge.Top: {
  60. a[1] = initialBounds.minY + delta[1]
  61. break
  62. }
  63. case TransformEdge.Right: {
  64. b[0] = initialBounds.maxX + delta[0]
  65. break
  66. }
  67. case TransformEdge.Bottom: {
  68. b[1] = initialBounds.maxY + delta[1]
  69. break
  70. }
  71. case TransformEdge.Left: {
  72. a[0] = initialBounds.minX + delta[0]
  73. break
  74. }
  75. case TransformCorner.TopLeft: {
  76. a[0] = initialBounds.minX + delta[0]
  77. a[1] = initialBounds.minY + delta[1]
  78. break
  79. }
  80. case TransformCorner.TopRight: {
  81. a[1] = initialBounds.minY + delta[1]
  82. b[0] = initialBounds.maxX + delta[0]
  83. break
  84. }
  85. case TransformCorner.BottomRight: {
  86. b[0] = initialBounds.maxX + delta[0]
  87. b[1] = initialBounds.maxY + delta[1]
  88. break
  89. }
  90. case TransformCorner.BottomLeft: {
  91. a[0] = initialBounds.minX + delta[0]
  92. b[1] = initialBounds.maxY + delta[1]
  93. break
  94. }
  95. }
  96. // Calculate new common (externior) bounding box
  97. const newBounds = {
  98. minX: Math.min(a[0], b[0]),
  99. minY: Math.min(a[1], b[1]),
  100. maxX: Math.max(a[0], b[0]),
  101. maxY: Math.max(a[1], b[1]),
  102. width: Math.abs(b[0] - a[0]),
  103. height: Math.abs(b[1] - a[1]),
  104. }
  105. this.isFlippedX = b[0] < a[0]
  106. this.isFlippedY = b[1] < a[1]
  107. // Now work backward to calculate a new bounding box for each of the shapes.
  108. selectedIds.forEach((id) => {
  109. const { initialShape, initialShapeBounds } = shapeBounds[id]
  110. const { nx, nmx, nw, ny, nmy, nh } = initialShapeBounds
  111. const shape = shapes[id]
  112. const minX =
  113. newBounds.minX + (this.isFlippedX ? nmx : nx) * newBounds.width
  114. const minY =
  115. newBounds.minY + (this.isFlippedY ? nmy : ny) * newBounds.height
  116. const width = nw * newBounds.width
  117. const height = nh * newBounds.height
  118. const newShapeBounds = {
  119. minX,
  120. minY,
  121. maxX: minX + width,
  122. maxY: minY + height,
  123. width,
  124. height,
  125. isFlippedX: this.isFlippedX,
  126. isFlippedY: this.isFlippedY,
  127. }
  128. // Pass the new data to the shape's transform utility for mutation.
  129. // Most shapes should be able to transform using only the bounding box,
  130. // however some shapes (e.g. those with internal points) will need more
  131. // data here too.
  132. getShapeUtils(shape).transform(shape, newShapeBounds, {
  133. type: this.transformType,
  134. initialShape,
  135. initialShapeBounds,
  136. initialBounds,
  137. isFlippedX: this.isFlippedX,
  138. isFlippedY: this.isFlippedY,
  139. anchor: getTransformAnchor(
  140. this.transformType,
  141. this.isFlippedX,
  142. this.isFlippedY
  143. ),
  144. })
  145. })
  146. }
  147. cancel(data: Data) {
  148. const { shapeBounds, initialBounds, currentPageId, selectedIds } =
  149. this.snapshot
  150. const { shapes } = data.document.pages[currentPageId]
  151. selectedIds.forEach((id) => {
  152. const shape = shapes.shapes[id]
  153. const { initialShape, initialShapeBounds } = shapeBounds[id]
  154. getShapeUtils(shape).transform(shape, initialShapeBounds, {
  155. type: this.transformType,
  156. initialShape,
  157. initialShapeBounds,
  158. initialBounds,
  159. isFlippedX: false,
  160. isFlippedY: false,
  161. anchor: getTransformAnchor(this.transformType, false, false),
  162. })
  163. })
  164. }
  165. complete(data: Data) {
  166. commands.transform(
  167. data,
  168. this.snapshot,
  169. getTransformSnapshot(data, this.transformType),
  170. getTransformAnchor(this.transformType, false, false)
  171. )
  172. }
  173. }
  174. export function getTransformSnapshot(
  175. data: Data,
  176. transformType: TransformEdge | TransformCorner
  177. ) {
  178. const {
  179. document: { pages },
  180. selectedIds,
  181. currentPageId,
  182. } = current(data)
  183. const pageShapes = pages[currentPageId].shapes
  184. // A mapping of selected shapes and their bounds
  185. const shapesBounds = Object.fromEntries(
  186. Array.from(selectedIds.values()).map((id) => {
  187. const shape = pageShapes[id]
  188. return [shape.id, getShapeUtils(shape).getBounds(shape)]
  189. })
  190. )
  191. // The common (exterior) bounds of the selected shapes
  192. const bounds = getCommonBounds(...Object.values(shapesBounds))
  193. // Return a mapping of shapes to bounds together with the relative
  194. // positions of the shape's bounds within the common bounds shape.
  195. return {
  196. currentPageId,
  197. type: transformType,
  198. initialBounds: bounds,
  199. selectedIds: new Set(selectedIds),
  200. shapeBounds: Object.fromEntries(
  201. Array.from(selectedIds.values()).map((id) => {
  202. const { minX, minY, width, height } = shapesBounds[id]
  203. return [
  204. id,
  205. {
  206. initialShape: pageShapes[id],
  207. initialShapeBounds: {
  208. ...shapesBounds[id],
  209. nx: (minX - bounds.minX) / bounds.width,
  210. ny: (minY - bounds.minY) / bounds.height,
  211. nmx: 1 - (minX + width - bounds.minX) / bounds.width,
  212. nmy: 1 - (minY + height - bounds.minY) / bounds.height,
  213. nw: width / bounds.width,
  214. nh: height / bounds.height,
  215. },
  216. },
  217. ]
  218. })
  219. ),
  220. }
  221. }
  222. export type TransformSnapshot = ReturnType<typeof getTransformSnapshot>