Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

index.tsx 7.3KB

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