您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

index.tsx 8.7KB

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