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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import {
  2. Bounds,
  3. Shape,
  4. ShapeType,
  5. Corner,
  6. Edge,
  7. ShapeStyles,
  8. ShapeBinding,
  9. Mutable,
  10. ShapeByType,
  11. } from 'types'
  12. import * as vec from 'utils/vec'
  13. import {
  14. getBoundsCenter,
  15. getBoundsFromPoints,
  16. getRotatedCorners,
  17. } from 'utils/utils'
  18. import {
  19. boundsCollidePolygon,
  20. boundsContainPolygon,
  21. pointInBounds,
  22. } from 'utils/bounds'
  23. import { v4 as uuid } from 'uuid'
  24. import circle from './circle'
  25. import dot from './dot'
  26. import polyline from './polyline'
  27. import rectangle from './rectangle'
  28. import ellipse from './ellipse'
  29. import line from './line'
  30. import ray from './ray'
  31. import draw from './draw'
  32. import arrow from './arrow'
  33. import group from './group'
  34. import text from './text'
  35. /*
  36. Shape Utiliies
  37. A shape utility is an object containing utility methods for each type of shape
  38. in the application. While shapes may be very different, each shape must support
  39. a common set of utility methods, such as hit tests or translations, that
  40. Operations throughout the app will call these utility methods
  41. when performing tests (such as hit tests) or mutations, such as translations.
  42. */
  43. export interface ShapeUtility<K extends Shape> {
  44. // A cache for the computed bounds of this kind of shape.
  45. boundsCache: WeakMap<K, Bounds>
  46. // Whether to show transform controls when this shape is selected.
  47. canTransform: boolean
  48. // Whether the shape's aspect ratio can change.
  49. canChangeAspectRatio: boolean
  50. // Whether the shape's style can be filled.
  51. canStyleFill: boolean
  52. // Whether the shape may be edited in an editing mode
  53. canEdit: boolean
  54. // Whether the shape is a foreign object.
  55. isForeignObject: boolean
  56. // Whether the shape can contain other shapes.
  57. isParent: boolean
  58. // Whether the shape is only shown when on hovered.
  59. isShy: boolean
  60. // Create a new shape.
  61. create(props: Partial<K>): K
  62. // Update a shape's styles
  63. applyStyles(
  64. this: ShapeUtility<K>,
  65. shape: Mutable<K>,
  66. style: Partial<ShapeStyles>
  67. ): ShapeUtility<K>
  68. translateBy(
  69. this: ShapeUtility<K>,
  70. shape: Mutable<K>,
  71. point: number[]
  72. ): ShapeUtility<K>
  73. translateTo(
  74. this: ShapeUtility<K>,
  75. shape: Mutable<K>,
  76. point: number[]
  77. ): ShapeUtility<K>
  78. rotateBy(
  79. this: ShapeUtility<K>,
  80. shape: Mutable<K>,
  81. rotation: number
  82. ): ShapeUtility<K>
  83. rotateTo(
  84. this: ShapeUtility<K>,
  85. shape: Mutable<K>,
  86. rotation: number,
  87. delta: number
  88. ): ShapeUtility<K>
  89. // Transform to fit a new bounding box when more than one shape is selected.
  90. transform(
  91. this: ShapeUtility<K>,
  92. shape: Mutable<K>,
  93. bounds: Bounds,
  94. info: {
  95. type: Edge | Corner
  96. initialShape: K
  97. scaleX: number
  98. scaleY: number
  99. transformOrigin: number[]
  100. }
  101. ): ShapeUtility<K>
  102. // Transform a single shape to fit a new bounding box.
  103. transformSingle(
  104. this: ShapeUtility<K>,
  105. shape: Mutable<K>,
  106. bounds: Bounds,
  107. info: {
  108. type: Edge | Corner
  109. initialShape: K
  110. scaleX: number
  111. scaleY: number
  112. transformOrigin: number[]
  113. }
  114. ): ShapeUtility<K>
  115. setProperty<P extends keyof K>(
  116. this: ShapeUtility<K>,
  117. shape: Mutable<K>,
  118. prop: P,
  119. value: K[P]
  120. ): ShapeUtility<K>
  121. // Respond when any child of this shape changes.
  122. onChildrenChange(
  123. this: ShapeUtility<K>,
  124. shape: Mutable<K>,
  125. children: Shape[]
  126. ): ShapeUtility<K>
  127. // Respond when a user moves one of the shape's bound elements.
  128. onBindingChange(
  129. this: ShapeUtility<K>,
  130. shape: Mutable<K>,
  131. bindings: Record<string, ShapeBinding>
  132. ): ShapeUtility<K>
  133. // Respond when a user moves one of the shape's handles.
  134. onHandleChange(
  135. this: ShapeUtility<K>,
  136. shape: Mutable<K>,
  137. handle: Partial<K['handles']>
  138. ): ShapeUtility<K>
  139. // Respond when a user double clicks the shape's bounds.
  140. onBoundsReset(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
  141. // Respond when a user double clicks the center of the shape.
  142. onDoubleFocus(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
  143. // Clean up changes when a session ends.
  144. onSessionComplete(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
  145. // Render a shape to JSX.
  146. render(
  147. this: ShapeUtility<K>,
  148. shape: K,
  149. info: { isEditing: boolean }
  150. ): JSX.Element
  151. // Get the bounds of the a shape.
  152. getBounds(this: ShapeUtility<K>, shape: K): Bounds
  153. // Get the routated bounds of the a shape.
  154. getRotatedBounds(this: ShapeUtility<K>, shape: K): Bounds
  155. // Get the center of the shape
  156. getCenter(this: ShapeUtility<K>, shape: K): number[]
  157. // Test whether a point lies within a shape.
  158. hitTest(this: ShapeUtility<K>, shape: K, test: number[]): boolean
  159. // Test whether bounds collide with or contain a shape.
  160. hitTestBounds(this: ShapeUtility<K>, shape: K, bounds: Bounds): boolean
  161. getShouldDelete(this: ShapeUtility<K>, shape: K): boolean
  162. }
  163. // A mapping of shape types to shape utilities.
  164. const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
  165. [ShapeType.Circle]: circle,
  166. [ShapeType.Dot]: dot,
  167. [ShapeType.Polyline]: polyline,
  168. [ShapeType.Rectangle]: rectangle,
  169. [ShapeType.Ellipse]: ellipse,
  170. [ShapeType.Line]: line,
  171. [ShapeType.Ray]: ray,
  172. [ShapeType.Draw]: draw,
  173. [ShapeType.Arrow]: arrow,
  174. [ShapeType.Text]: text,
  175. [ShapeType.Group]: group,
  176. }
  177. /**
  178. * A helper to retrieve a shape utility based on a shape object.
  179. * @param shape
  180. * @returns
  181. */
  182. export function getShapeUtils<T extends Shape>(shape: T): ShapeUtility<T> {
  183. return shapeUtilityMap[shape?.type] as ShapeUtility<T>
  184. }
  185. function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
  186. return {
  187. boundsCache: new WeakMap(),
  188. canTransform: true,
  189. canChangeAspectRatio: true,
  190. canStyleFill: true,
  191. canEdit: false,
  192. isShy: false,
  193. isParent: false,
  194. isForeignObject: false,
  195. create(props) {
  196. return {
  197. id: uuid(),
  198. isGenerated: false,
  199. point: [0, 0],
  200. name: 'Shape',
  201. parentId: 'page1',
  202. childIndex: 0,
  203. rotation: 0,
  204. isAspectRatioLocked: false,
  205. isLocked: false,
  206. isHidden: false,
  207. ...props,
  208. } as T
  209. },
  210. render(shape) {
  211. return <circle id={shape.id} />
  212. },
  213. translateBy(shape, delta) {
  214. shape.point = vec.round(vec.add(shape.point, delta))
  215. return this
  216. },
  217. translateTo(shape, point) {
  218. shape.point = vec.round(point)
  219. return this
  220. },
  221. rotateTo(shape, rotation) {
  222. shape.rotation = rotation
  223. return this
  224. },
  225. rotateBy(shape, rotation) {
  226. shape.rotation += rotation
  227. return this
  228. },
  229. transform(shape, bounds) {
  230. shape.point = [bounds.minX, bounds.minY]
  231. return this
  232. },
  233. transformSingle(shape, bounds, info) {
  234. return this.transform(shape, bounds, info)
  235. },
  236. onChildrenChange() {
  237. return this
  238. },
  239. onBindingChange() {
  240. return this
  241. },
  242. onHandleChange() {
  243. return this
  244. },
  245. onDoubleFocus() {
  246. return this
  247. },
  248. onBoundsReset() {
  249. return this
  250. },
  251. onSessionComplete() {
  252. return this
  253. },
  254. getBounds(shape) {
  255. const [x, y] = shape.point
  256. return {
  257. minX: x,
  258. minY: y,
  259. maxX: x + 1,
  260. maxY: y + 1,
  261. width: 1,
  262. height: 1,
  263. }
  264. },
  265. getRotatedBounds(shape) {
  266. return getBoundsFromPoints(
  267. getRotatedCorners(this.getBounds(shape), shape.rotation)
  268. )
  269. },
  270. getCenter(shape) {
  271. return getBoundsCenter(this.getBounds(shape))
  272. },
  273. hitTest(shape, point) {
  274. return pointInBounds(point, this.getBounds(shape))
  275. },
  276. hitTestBounds(shape, brushBounds) {
  277. const rotatedCorners = getRotatedCorners(
  278. this.getBounds(shape),
  279. shape.rotation
  280. )
  281. return (
  282. boundsContainPolygon(brushBounds, rotatedCorners) ||
  283. boundsCollidePolygon(brushBounds, rotatedCorners)
  284. )
  285. },
  286. setProperty(shape, prop, value) {
  287. shape[prop] = value
  288. return this
  289. },
  290. applyStyles(shape, style) {
  291. Object.assign(shape.style, style)
  292. return this
  293. },
  294. getShouldDelete(shape) {
  295. return false
  296. },
  297. }
  298. }
  299. /**
  300. * A factory of shape utilities, with typing enforced.
  301. * @param shape
  302. * @returns
  303. */
  304. export function registerShapeUtils<K extends Shape>(
  305. shapeUtil: Partial<ShapeUtility<K>>
  306. ): ShapeUtility<K> {
  307. return Object.freeze({ ...getDefaultShapeUtil<K>(), ...shapeUtil })
  308. }
  309. export function createShape<T extends ShapeType>(
  310. type: T,
  311. props: Partial<ShapeByType<T>>
  312. ): ShapeByType<T> {
  313. return shapeUtilityMap[type].create(props) as ShapeByType<T>
  314. }
  315. export default shapeUtilityMap