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.

index.tsx 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import {
  2. Bounds,
  3. Shape,
  4. ShapeType,
  5. Corner,
  6. Edge,
  7. ShapeStyles,
  8. ShapeBinding,
  9. Mutable,
  10. } from 'types'
  11. import { v4 as uuid } from 'uuid'
  12. import circle from './circle'
  13. import dot from './dot'
  14. import polyline from './polyline'
  15. import rectangle from './rectangle'
  16. import ellipse from './ellipse'
  17. import line from './line'
  18. import ray from './ray'
  19. import draw from './draw'
  20. import arrow from './arrow'
  21. import {
  22. getBoundsCenter,
  23. getBoundsFromPoints,
  24. getRotatedCorners,
  25. } from 'utils/utils'
  26. import {
  27. boundsCollidePolygon,
  28. boundsContainPolygon,
  29. pointInBounds,
  30. } from 'utils/bounds'
  31. /*
  32. Shape Utiliies
  33. A shape utility is an object containing utility methods for each type of shape
  34. in the application. While shapes may be very different, each shape must support
  35. a common set of utility methods, such as hit tests or translations, that
  36. Operations throughout the app will call these utility methods
  37. when performing tests (such as hit tests) or mutations, such as translations.
  38. */
  39. export interface ShapeUtility<K extends Shape> {
  40. // A cache for the computed bounds of this kind of shape.
  41. boundsCache: WeakMap<K, Bounds>
  42. // Whether to show transform controls when this shape is selected.
  43. canTransform: boolean
  44. // Whether the shape's aspect ratio can change
  45. canChangeAspectRatio: boolean
  46. // Whether the shape's style can be filled
  47. canStyleFill: boolean
  48. // Create a new shape.
  49. create(props: Partial<K>): K
  50. applyStyles(
  51. this: ShapeUtility<K>,
  52. shape: Mutable<K>,
  53. style: Partial<ShapeStyles>
  54. ): ShapeUtility<K>
  55. // Transform to fit a new bounding box when more than one shape is selected.
  56. transform(
  57. this: ShapeUtility<K>,
  58. shape: Mutable<K>,
  59. bounds: Bounds,
  60. info: {
  61. type: Edge | Corner
  62. initialShape: K
  63. scaleX: number
  64. scaleY: number
  65. transformOrigin: number[]
  66. }
  67. ): ShapeUtility<K>
  68. // Transform a single shape to fit a new bounding box.
  69. transformSingle(
  70. this: ShapeUtility<K>,
  71. shape: Mutable<K>,
  72. bounds: Bounds,
  73. info: {
  74. type: Edge | Corner
  75. initialShape: K
  76. scaleX: number
  77. scaleY: number
  78. transformOrigin: number[]
  79. }
  80. ): ShapeUtility<K>
  81. setProperty<P extends keyof K>(
  82. this: ShapeUtility<K>,
  83. shape: Mutable<K>,
  84. prop: P,
  85. value: K[P]
  86. ): ShapeUtility<K>
  87. // Respond when a user moves one of the shape's bound elements.
  88. onBindingMove?(
  89. this: ShapeUtility<K>,
  90. shape: Mutable<K>,
  91. bindings: Record<string, ShapeBinding>
  92. ): ShapeUtility<K>
  93. // Respond when a user moves one of the shape's handles.
  94. onHandleMove?(
  95. this: ShapeUtility<K>,
  96. shape: Mutable<K>,
  97. handle: Partial<K['handles']>
  98. ): ShapeUtility<K>
  99. // Render a shape to JSX.
  100. render(this: ShapeUtility<K>, shape: K): JSX.Element
  101. // Get the bounds of the a shape.
  102. getBounds(this: ShapeUtility<K>, shape: K): Bounds
  103. // Get the routated bounds of the a shape.
  104. getRotatedBounds(this: ShapeUtility<K>, shape: K): Bounds
  105. // Get the center of the shape
  106. getCenter(this: ShapeUtility<K>, shape: K): number[]
  107. // Test whether a point lies within a shape.
  108. hitTest(this: ShapeUtility<K>, shape: K, test: number[]): boolean
  109. // Test whether bounds collide with or contain a shape.
  110. hitTestBounds(this: ShapeUtility<K>, shape: K, bounds: Bounds): boolean
  111. }
  112. // A mapping of shape types to shape utilities.
  113. const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
  114. [ShapeType.Circle]: circle,
  115. [ShapeType.Dot]: dot,
  116. [ShapeType.Polyline]: polyline,
  117. [ShapeType.Rectangle]: rectangle,
  118. [ShapeType.Ellipse]: ellipse,
  119. [ShapeType.Line]: line,
  120. [ShapeType.Ray]: ray,
  121. [ShapeType.Draw]: draw,
  122. [ShapeType.Arrow]: arrow,
  123. }
  124. /**
  125. * A helper to retrieve a shape utility based on a shape object.
  126. * @param shape
  127. * @returns
  128. */
  129. export function getShapeUtils<T extends Shape>(shape: T): ShapeUtility<T> {
  130. return shapeUtilityMap[shape.type] as ShapeUtility<T>
  131. }
  132. function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
  133. return {
  134. boundsCache: new WeakMap(),
  135. canTransform: true,
  136. canChangeAspectRatio: true,
  137. canStyleFill: true,
  138. create(props) {
  139. return {
  140. id: uuid(),
  141. isGenerated: false,
  142. point: [0, 0],
  143. name: 'Shape',
  144. parentId: 'page0',
  145. childIndex: 0,
  146. rotation: 0,
  147. isAspectRatioLocked: false,
  148. isLocked: false,
  149. isHidden: false,
  150. ...props,
  151. } as T
  152. },
  153. render(shape) {
  154. return <circle id={shape.id} />
  155. },
  156. transform(shape, bounds) {
  157. shape.point = [bounds.minX, bounds.minY]
  158. return this
  159. },
  160. transformSingle(shape, bounds, info) {
  161. return this.transform(shape, bounds, info)
  162. },
  163. onBindingMove() {
  164. return this
  165. },
  166. onHandleMove() {
  167. return this
  168. },
  169. getBounds(shape) {
  170. const [x, y] = shape.point
  171. return {
  172. minX: x,
  173. minY: y,
  174. maxX: x + 1,
  175. maxY: y + 1,
  176. width: 1,
  177. height: 1,
  178. }
  179. },
  180. getRotatedBounds(shape) {
  181. return getBoundsFromPoints(
  182. getRotatedCorners(this.getBounds(shape), shape.rotation)
  183. )
  184. },
  185. getCenter(shape) {
  186. return getBoundsCenter(this.getBounds(shape))
  187. },
  188. hitTest(shape, point) {
  189. return pointInBounds(point, this.getBounds(shape))
  190. },
  191. hitTestBounds(shape, brushBounds) {
  192. const rotatedCorners = getRotatedCorners(
  193. this.getBounds(shape),
  194. shape.rotation
  195. )
  196. return (
  197. boundsContainPolygon(brushBounds, rotatedCorners) ||
  198. boundsCollidePolygon(brushBounds, rotatedCorners)
  199. )
  200. },
  201. setProperty(shape, prop, value) {
  202. shape[prop] = value
  203. return this
  204. },
  205. applyStyles(shape, style) {
  206. Object.assign(shape.style, style)
  207. return this
  208. },
  209. }
  210. }
  211. /**
  212. * A factory of shape utilities, with typing enforced.
  213. * @param shape
  214. * @returns
  215. */
  216. export function registerShapeUtils<K extends Shape>(
  217. shapeUtil: Partial<ShapeUtility<K>>
  218. ): ShapeUtility<K> {
  219. return Object.freeze({ ...getDefaultShapeUtil<K>(), ...shapeUtil })
  220. }
  221. export function createShape<T extends Shape>(
  222. type: T['type'],
  223. props: Partial<T>
  224. ) {
  225. return shapeUtilityMap[type].create(props) as T
  226. }
  227. export default shapeUtilityMap