Canvas.tsx 8.8 KB

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