Canvas.tsx 7.8 KB

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