Canvas.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  3. import {
  4. EMPTY_OBJECT,
  5. isNonNullable,
  6. TLAsset,
  7. TLBinding,
  8. TLBounds,
  9. TLCursor,
  10. TLTheme,
  11. } from '@tldraw/core'
  12. import { observer } from 'mobx-react-lite'
  13. import * as React from 'react'
  14. import { NOOP } from '../../constants'
  15. import {
  16. useApp,
  17. useCanvasEvents,
  18. useCursor,
  19. useGestureEvents,
  20. usePreventNavigation,
  21. useRendererContext,
  22. useResizeObserver,
  23. useRestoreCamera,
  24. useStylesheet,
  25. useZoom,
  26. } from '../../hooks'
  27. import { useKeyboardEvents } from '../../hooks/useKeyboardEvents'
  28. import type { TLReactShape } from '../../lib'
  29. import { Container } from '../Container'
  30. import { ContextBarContainer } from '../ContextBarContainer'
  31. import { HTMLLayer } from '../HTMLLayer'
  32. import { Indicator } from '../Indicator'
  33. import { QuickLinksContainer } from '../QuickLinksContainer'
  34. import { BacklinksCountContainer } from '../BacklinksCountContainer'
  35. import { SelectionDetailContainer } from '../SelectionDetailContainer'
  36. import { Shape } from '../Shape'
  37. import { SVGContainer } from '../SVGContainer'
  38. import { DirectionIndicator } from '../ui'
  39. export interface TLCanvasProps<S extends TLReactShape> {
  40. id: string
  41. className: string
  42. bindings: TLBinding[]
  43. brush: TLBounds
  44. shapes: S[]
  45. assets: Record<string, TLAsset>
  46. theme: TLTheme
  47. hoveredShape: S
  48. hoveredGroup: S
  49. editingShape: S
  50. bindingShapes: S[]
  51. selectionDirectionHint: number[]
  52. selectionBounds: TLBounds
  53. selectedShapes: S[]
  54. erasingShapes: S[]
  55. gridSize: number
  56. cursor: TLCursor
  57. cursorRotation: number
  58. selectionRotation: number
  59. onEditingEnd: () => void
  60. showGrid: boolean
  61. showSelection: boolean
  62. showHandles: boolean
  63. showResizeHandles: boolean
  64. showRotateHandles: boolean
  65. showContextBar: boolean
  66. showSelectionDetail: boolean
  67. showSelectionRotation: boolean
  68. children: React.ReactNode
  69. }
  70. export const Canvas = observer(function Renderer<S extends TLReactShape>({
  71. id,
  72. className,
  73. brush,
  74. shapes,
  75. assets,
  76. bindingShapes,
  77. editingShape,
  78. hoveredShape,
  79. hoveredGroup,
  80. selectionBounds,
  81. selectedShapes,
  82. erasingShapes,
  83. selectionDirectionHint,
  84. cursor = TLCursor.Default,
  85. cursorRotation = 0,
  86. selectionRotation = 0,
  87. showSelection = true,
  88. showHandles = true,
  89. showSelectionRotation = false,
  90. showResizeHandles = true,
  91. showRotateHandles = true,
  92. showSelectionDetail = true,
  93. showContextBar = true,
  94. showGrid = true,
  95. gridSize = 8,
  96. onEditingEnd = NOOP,
  97. theme = EMPTY_OBJECT,
  98. children,
  99. }: Partial<TLCanvasProps<S>>) {
  100. const rContainer = React.useRef<HTMLDivElement>(null)
  101. const { viewport, components, meta } = useRendererContext()
  102. const app = useApp()
  103. const onBoundsChange = React.useCallback((bounds: TLBounds) => {
  104. app.inputs.updateContainerOffset([bounds.minX, bounds.minY])
  105. }, [])
  106. useStylesheet(theme, id)
  107. usePreventNavigation(rContainer)
  108. useResizeObserver(rContainer, viewport, onBoundsChange)
  109. useGestureEvents(rContainer)
  110. useRestoreCamera()
  111. useCursor(rContainer, cursor, cursorRotation)
  112. useZoom(rContainer)
  113. useKeyboardEvents(rContainer)
  114. const events = useCanvasEvents()
  115. const onlySelectedShape = selectedShapes?.length === 1 && selectedShapes[0]
  116. const onlySelectedShapeWithHandles =
  117. onlySelectedShape && 'handles' in onlySelectedShape.props ? selectedShapes?.[0] : undefined
  118. const selectedShapesSet = React.useMemo(() => new Set(selectedShapes || []), [selectedShapes])
  119. const erasingShapesSet = React.useMemo(() => new Set(erasingShapes || []), [erasingShapes])
  120. const singleSelectedShape = selectedShapes?.length === 1 ? selectedShapes[0] : undefined
  121. const hoveredShapes: S[] = [...new Set([hoveredGroup, hoveredShape])].filter(isNonNullable)
  122. return (
  123. <div ref={rContainer} className={`tl-container ${className ?? ''}`}>
  124. <div tabIndex={-1} className="tl-absolute tl-canvas" {...events}>
  125. {showGrid && components.Grid && <components.Grid size={gridSize} />}
  126. <HTMLLayer>
  127. {components.SelectionBackground && selectedShapes && selectionBounds && showSelection && (
  128. <Container
  129. data-type="SelectionBackground"
  130. bounds={selectionBounds}
  131. zIndex={2}
  132. data-html2canvas-ignore="true"
  133. >
  134. <components.SelectionBackground
  135. shapes={selectedShapes}
  136. bounds={selectionBounds}
  137. showResizeHandles={showResizeHandles}
  138. showRotateHandles={showRotateHandles}
  139. />
  140. </Container>
  141. )}
  142. {shapes &&
  143. shapes.map((shape, i) => (
  144. <Shape
  145. key={'shape_' + shape.id}
  146. shape={shape}
  147. asset={assets && shape.props.assetId ? assets[shape.props.assetId] : undefined}
  148. isEditing={shape === editingShape}
  149. isHovered={shape === hoveredShape}
  150. isBinding={bindingShapes?.includes(shape)}
  151. isSelected={selectedShapesSet.has(shape)}
  152. isErasing={erasingShapesSet.has(shape)}
  153. meta={meta}
  154. zIndex={1000 + i}
  155. onEditingEnd={onEditingEnd}
  156. />
  157. ))}
  158. {!app.isIn('select.pinching') &&
  159. selectedShapes?.map(shape => (
  160. <Indicator
  161. key={'selected_indicator_' + shape.id}
  162. shape={shape}
  163. isEditing={shape === editingShape}
  164. isHovered={false}
  165. isBinding={false}
  166. isSelected={true}
  167. />
  168. ))}
  169. {hoveredShapes.map(
  170. s => s !== editingShape && <Indicator key={'hovered_indicator_' + s.id} shape={s} />
  171. )}
  172. {singleSelectedShape && components.BacklinksCount && (
  173. <BacklinksCountContainer
  174. hidden={false}
  175. bounds={singleSelectedShape.bounds}
  176. shape={singleSelectedShape}
  177. />
  178. )}
  179. {hoveredShape && hoveredShape !== singleSelectedShape && components.QuickLinks && (
  180. <QuickLinksContainer hidden={false} bounds={hoveredShape.bounds} shape={hoveredShape} />
  181. )}
  182. {brush && components.Brush && <components.Brush bounds={brush} />}
  183. {selectedShapes && selectionBounds && (
  184. <>
  185. {showSelection && components.SelectionForeground && (
  186. <Container
  187. data-type="SelectionForeground"
  188. data-html2canvas-ignore="true"
  189. bounds={selectionBounds}
  190. zIndex={editingShape && selectedShapes.includes(editingShape) ? 1002 : 10002}
  191. >
  192. <components.SelectionForeground
  193. shapes={selectedShapes}
  194. bounds={selectionBounds}
  195. showResizeHandles={showResizeHandles}
  196. showRotateHandles={showRotateHandles}
  197. />
  198. </Container>
  199. )}
  200. {showHandles && onlySelectedShapeWithHandles && components.Handle && (
  201. <Container
  202. data-type="onlySelectedShapeWithHandles"
  203. data-html2canvas-ignore="true"
  204. bounds={selectionBounds}
  205. zIndex={10003}
  206. >
  207. <SVGContainer>
  208. {Object.entries(onlySelectedShapeWithHandles.props.handles ?? {}).map(
  209. ([id, handle]) =>
  210. React.createElement(components.Handle!, {
  211. key: `${handle.id}_handle_${handle.id}`,
  212. shape: onlySelectedShapeWithHandles,
  213. handle,
  214. id,
  215. })
  216. )}
  217. </SVGContainer>
  218. </Container>
  219. )}
  220. {selectedShapes && components.SelectionDetail && (
  221. <SelectionDetailContainer
  222. key={'detail' + selectedShapes.map(shape => shape.id).join('')}
  223. data-html2canvas-ignore="true"
  224. shapes={selectedShapes}
  225. bounds={selectionBounds}
  226. detail={showSelectionRotation ? 'rotation' : 'size'}
  227. hidden={!showSelectionDetail}
  228. rotation={selectionRotation}
  229. />
  230. )}
  231. </>
  232. )}
  233. </HTMLLayer>
  234. {selectionDirectionHint && selectionBounds && selectedShapes && (
  235. <DirectionIndicator
  236. direction={selectionDirectionHint}
  237. bounds={selectionBounds}
  238. shapes={selectedShapes}
  239. />
  240. )}
  241. <div id="tl-dev-tools-canvas-anchor" data-html2canvas-ignore="true" />
  242. </div>
  243. <HTMLLayer>
  244. {selectedShapes && selectionBounds && (
  245. <>
  246. {selectedShapes && components.ContextBar && (
  247. <ContextBarContainer
  248. key={'context' + selectedShapes.map(shape => shape.id).join('')}
  249. shapes={selectedShapes}
  250. hidden={!showContextBar}
  251. bounds={singleSelectedShape ? singleSelectedShape.bounds : selectionBounds}
  252. rotation={singleSelectedShape ? singleSelectedShape.props.rotation : 0}
  253. />
  254. )}
  255. </>
  256. )}
  257. </HTMLLayer>
  258. {children}
  259. </div>
  260. )
  261. })