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

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