Canvas.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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. useRendererContext,
  9. useApp,
  10. useStylesheet,
  11. usePreventNavigation,
  12. useResizeObserver,
  13. useGestureEvents,
  14. useCursor,
  15. useZoom,
  16. useCanvasEvents,
  17. } from '../../hooks'
  18. import { useKeyboardEvents } from '../../hooks/useKeyboardEvents'
  19. import type { TLReactShape } from '../../lib'
  20. import { Container } from '../Container'
  21. import { ContextBarContainer } from '../ContextBarContainer'
  22. import { HTMLLayer } from '../HTMLLayer'
  23. import { Indicator } from '../Indicator'
  24. import { SelectionDetailContainer } from '../SelectionDetailContainer'
  25. import { Shape } from '../Shape'
  26. import { SVGContainer } from '../SVGContainer'
  27. import { DirectionIndicator } from '../ui'
  28. export interface TLCanvasProps<S extends TLReactShape> {
  29. id: string
  30. className: string
  31. bindings: TLBinding[]
  32. brush: TLBounds
  33. shapes: S[]
  34. assets: Record<string, TLAsset>
  35. theme: TLTheme
  36. hoveredShape: S
  37. editingShape: S
  38. bindingShapes: S[]
  39. selectionDirectionHint: number[]
  40. selectionBounds: TLBounds
  41. selectedShapes: S[]
  42. erasingShapes: S[]
  43. gridSize: number
  44. cursor: TLCursor
  45. cursorRotation: number
  46. selectionRotation: number
  47. onEditingEnd: () => void
  48. showGrid: boolean
  49. showSelection: boolean
  50. showHandles: boolean
  51. showResizeHandles: boolean
  52. showRotateHandles: boolean
  53. showContextBar: boolean
  54. showSelectionDetail: boolean
  55. showSelectionRotation: boolean
  56. children: React.ReactNode
  57. }
  58. export const Canvas = observer(function Renderer<S extends TLReactShape>({
  59. id,
  60. className,
  61. brush,
  62. shapes,
  63. assets,
  64. bindingShapes,
  65. editingShape,
  66. hoveredShape,
  67. selectionBounds,
  68. selectedShapes,
  69. erasingShapes,
  70. selectionDirectionHint,
  71. cursor = TLCursor.Default,
  72. cursorRotation = 0,
  73. selectionRotation = 0,
  74. showSelection = true,
  75. showHandles = true,
  76. showSelectionRotation = false,
  77. showResizeHandles = true,
  78. showRotateHandles = true,
  79. showSelectionDetail = true,
  80. showContextBar = true,
  81. showGrid = true,
  82. gridSize = 8,
  83. onEditingEnd = NOOP,
  84. theme = EMPTY_OBJECT,
  85. children,
  86. }: Partial<TLCanvasProps<S>>) {
  87. const rContainer = React.useRef<HTMLDivElement>(null)
  88. const { viewport, components, meta } = useRendererContext()
  89. const app = useApp()
  90. const onBoundsChange = React.useCallback((bounds: TLBounds) => {
  91. app.inputs.updateContainerOffset([bounds.minX, bounds.minY])
  92. }, [])
  93. useStylesheet(theme, id)
  94. usePreventNavigation(rContainer)
  95. useResizeObserver(rContainer, viewport, onBoundsChange)
  96. useGestureEvents(rContainer)
  97. useCursor(rContainer, cursor, cursorRotation)
  98. useZoom(rContainer)
  99. useKeyboardEvents(rContainer)
  100. const events = useCanvasEvents()
  101. const onlySelectedShape = selectedShapes?.length === 1 && selectedShapes[0]
  102. const onlySelectedShapeWithHandles =
  103. onlySelectedShape && 'handles' in onlySelectedShape.props ? selectedShapes?.[0] : undefined
  104. const selectedShapesSet = React.useMemo(() => new Set(selectedShapes || []), [selectedShapes])
  105. const erasingShapesSet = React.useMemo(() => new Set(erasingShapes || []), [erasingShapes])
  106. return (
  107. <div ref={rContainer} className={`tl-container ${className ?? ''}`}>
  108. <div tabIndex={-1} className="tl-absolute tl-canvas" {...events}>
  109. {showGrid && components.Grid && <components.Grid size={gridSize} />}
  110. <HTMLLayer>
  111. {components.SelectionBackground && selectedShapes && selectionBounds && showSelection && (
  112. <Container data-type="SelectionBackground" bounds={selectionBounds} zIndex={2}>
  113. <components.SelectionBackground
  114. shapes={selectedShapes}
  115. bounds={selectionBounds}
  116. showResizeHandles={showResizeHandles}
  117. showRotateHandles={showRotateHandles}
  118. />
  119. </Container>
  120. )}
  121. {shapes &&
  122. shapes.map((shape, i) => (
  123. <Shape
  124. key={'shape_' + shape.id}
  125. shape={shape}
  126. asset={assets && shape.props.assetId ? assets[shape.props.assetId] : undefined}
  127. isEditing={shape === editingShape}
  128. isHovered={shape === hoveredShape}
  129. isBinding={bindingShapes?.includes(shape)}
  130. isSelected={selectedShapesSet.has(shape)}
  131. isErasing={erasingShapesSet.has(shape)}
  132. meta={meta}
  133. zIndex={1000 + i}
  134. onEditingEnd={onEditingEnd}
  135. />
  136. ))}
  137. {!app.isIn('select.pinching') &&
  138. selectedShapes?.map(shape => (
  139. <Indicator
  140. key={'selected_indicator_' + shape.id}
  141. shape={shape}
  142. isEditing={shape === editingShape}
  143. isHovered={false}
  144. isBinding={false}
  145. isSelected={true}
  146. />
  147. ))}
  148. {hoveredShape && app.isInAny('creating') && (
  149. <Indicator key={'hovered_indicator_' + hoveredShape.id} shape={hoveredShape} />
  150. )}
  151. {brush && components.Brush && <components.Brush bounds={brush} />}
  152. {selectedShapes && selectionBounds && (
  153. <>
  154. {showSelection && components.SelectionForeground && (
  155. <Container
  156. data-type="SelectionForeground"
  157. bounds={selectionBounds}
  158. zIndex={editingShape && selectedShapes.includes(editingShape) ? 1002 : 10002}
  159. >
  160. <components.SelectionForeground
  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. })