123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- /* eslint-disable @typescript-eslint/no-explicit-any */
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- import {
- EMPTY_OBJECT,
- isNonNullable,
- TLAsset,
- TLBinding,
- TLBounds,
- TLCursor,
- TLTheme,
- } from '@tldraw/core'
- import { observer } from 'mobx-react-lite'
- import * as React from 'react'
- import { NOOP } from '../../constants'
- import {
- useApp,
- useCanvasEvents,
- useCursor,
- useGestureEvents,
- usePreventNavigation,
- useRendererContext,
- useResizeObserver,
- useRestoreCamera,
- useStylesheet,
- useZoom,
- } from '../../hooks'
- import { useKeyboardEvents } from '../../hooks/useKeyboardEvents'
- import type { TLReactShape } from '../../lib'
- import { Container } from '../Container'
- import { ContextBarContainer } from '../ContextBarContainer'
- import { HTMLLayer } from '../HTMLLayer'
- import { Indicator } from '../Indicator'
- import { QuickLinksContainer } from '../QuickLinksContainer'
- import { BacklinksCountContainer } from '../BacklinksCountContainer'
- import { SelectionDetailContainer } from '../SelectionDetailContainer'
- import { Shape } from '../Shape'
- import { SVGContainer } from '../SVGContainer'
- import { DirectionIndicator } from '../ui'
- export interface TLCanvasProps<S extends TLReactShape> {
- id: string
- className: string
- bindings: TLBinding[]
- brush: TLBounds
- shapes: S[]
- assets: Record<string, TLAsset>
- theme: TLTheme
- hoveredShape: S
- hoveredGroup: S
- editingShape: S
- bindingShapes: S[]
- selectionDirectionHint: number[]
- selectionBounds: TLBounds
- selectedShapes: S[]
- erasingShapes: S[]
- gridSize: number
- cursor: TLCursor
- cursorRotation: number
- selectionRotation: number
- onEditingEnd: () => void
- showGrid: boolean
- showSelection: boolean
- showHandles: boolean
- showResizeHandles: boolean
- showRotateHandles: boolean
- showContextBar: boolean
- showSelectionDetail: boolean
- showSelectionRotation: boolean
- children: React.ReactNode
- }
- export const Canvas = observer(function Renderer<S extends TLReactShape>({
- id,
- className,
- brush,
- shapes,
- assets,
- bindingShapes,
- editingShape,
- hoveredShape,
- hoveredGroup,
- selectionBounds,
- selectedShapes,
- erasingShapes,
- selectionDirectionHint,
- cursor = TLCursor.Default,
- cursorRotation = 0,
- selectionRotation = 0,
- showSelection = true,
- showHandles = true,
- showSelectionRotation = false,
- showResizeHandles = true,
- showRotateHandles = true,
- showSelectionDetail = true,
- showContextBar = true,
- showGrid = true,
- gridSize = 8,
- onEditingEnd = NOOP,
- theme = EMPTY_OBJECT,
- children,
- }: Partial<TLCanvasProps<S>>) {
- const rContainer = React.useRef<HTMLDivElement>(null)
- const { viewport, components, meta } = useRendererContext()
- const app = useApp()
- const onBoundsChange = React.useCallback((bounds: TLBounds) => {
- app.inputs.updateContainerOffset([bounds.minX, bounds.minY])
- }, [])
- useStylesheet(theme, id)
- usePreventNavigation(rContainer)
- useResizeObserver(rContainer, viewport, onBoundsChange)
- useGestureEvents(rContainer)
- useRestoreCamera()
- useCursor(rContainer, cursor, cursorRotation)
- useZoom(rContainer)
- useKeyboardEvents(rContainer)
- const events = useCanvasEvents()
- const onlySelectedShape = selectedShapes?.length === 1 && selectedShapes[0]
- const onlySelectedShapeWithHandles =
- onlySelectedShape && 'handles' in onlySelectedShape.props ? selectedShapes?.[0] : undefined
- const selectedShapesSet = React.useMemo(() => new Set(selectedShapes || []), [selectedShapes])
- const erasingShapesSet = React.useMemo(() => new Set(erasingShapes || []), [erasingShapes])
- const singleSelectedShape = selectedShapes?.length === 1 ? selectedShapes[0] : undefined
- const hoveredShapes: S[] = [...new Set([hoveredGroup, hoveredShape])].filter(isNonNullable)
- return (
- <div ref={rContainer} className={`tl-container ${className ?? ''}`}>
- <div tabIndex={-1} className="tl-absolute tl-canvas" {...events}>
- {showGrid && components.Grid && <components.Grid size={gridSize} />}
- <HTMLLayer>
- {components.SelectionBackground && selectedShapes && selectionBounds && showSelection && (
- <Container
- data-type="SelectionBackground"
- bounds={selectionBounds}
- zIndex={2}
- data-html2canvas-ignore="true"
- >
- <components.SelectionBackground
- shapes={selectedShapes}
- bounds={selectionBounds}
- showResizeHandles={showResizeHandles}
- showRotateHandles={showRotateHandles}
- />
- </Container>
- )}
- {shapes &&
- shapes.map((shape, i) => (
- <Shape
- key={'shape_' + shape.id}
- shape={shape}
- asset={assets && shape.props.assetId ? assets[shape.props.assetId] : undefined}
- isEditing={shape === editingShape}
- isHovered={shape === hoveredShape}
- isBinding={bindingShapes?.includes(shape)}
- isSelected={selectedShapesSet.has(shape)}
- isErasing={erasingShapesSet.has(shape)}
- meta={meta}
- zIndex={1000 + i}
- onEditingEnd={onEditingEnd}
- />
- ))}
- {!app.isIn('select.pinching') &&
- selectedShapes?.map(shape => (
- <Indicator
- key={'selected_indicator_' + shape.id}
- shape={shape}
- isEditing={shape === editingShape}
- isHovered={false}
- isBinding={false}
- isSelected={true}
- />
- ))}
- {hoveredShapes.map(
- s => s !== editingShape && <Indicator key={'hovered_indicator_' + s.id} shape={s} />
- )}
- {singleSelectedShape && components.BacklinksCount && (
- <BacklinksCountContainer
- hidden={false}
- bounds={singleSelectedShape.bounds}
- shape={singleSelectedShape}
- />
- )}
- {hoveredShape && hoveredShape !== singleSelectedShape && components.QuickLinks && (
- <QuickLinksContainer hidden={false} bounds={hoveredShape.bounds} shape={hoveredShape} />
- )}
- {brush && components.Brush && <components.Brush bounds={brush} />}
- {selectedShapes && selectionBounds && (
- <>
- {showSelection && components.SelectionForeground && (
- <Container
- data-type="SelectionForeground"
- data-html2canvas-ignore="true"
- bounds={selectionBounds}
- zIndex={editingShape && selectedShapes.includes(editingShape) ? 1002 : 10002}
- >
- <components.SelectionForeground
- shapes={selectedShapes}
- bounds={selectionBounds}
- showResizeHandles={showResizeHandles}
- showRotateHandles={showRotateHandles}
- />
- </Container>
- )}
- {showHandles && onlySelectedShapeWithHandles && components.Handle && (
- <Container
- data-type="onlySelectedShapeWithHandles"
- data-html2canvas-ignore="true"
- bounds={selectionBounds}
- zIndex={10003}
- >
- <SVGContainer>
- {Object.entries(onlySelectedShapeWithHandles.props.handles ?? {}).map(
- ([id, handle]) =>
- React.createElement(components.Handle!, {
- key: `${handle.id}_handle_${handle.id}`,
- shape: onlySelectedShapeWithHandles,
- handle,
- id,
- })
- )}
- </SVGContainer>
- </Container>
- )}
- {selectedShapes && components.SelectionDetail && (
- <SelectionDetailContainer
- key={'detail' + selectedShapes.map(shape => shape.id).join('')}
- data-html2canvas-ignore="true"
- shapes={selectedShapes}
- bounds={selectionBounds}
- detail={showSelectionRotation ? 'rotation' : 'size'}
- hidden={!showSelectionDetail}
- rotation={selectionRotation}
- />
- )}
- </>
- )}
- </HTMLLayer>
- {selectionDirectionHint && selectionBounds && selectedShapes && (
- <DirectionIndicator
- direction={selectionDirectionHint}
- bounds={selectionBounds}
- shapes={selectedShapes}
- />
- )}
- <div id="tl-dev-tools-canvas-anchor" data-html2canvas-ignore="true" />
- </div>
- <HTMLLayer>
- {selectedShapes && selectionBounds && (
- <>
- {selectedShapes && components.ContextBar && (
- <ContextBarContainer
- key={'context' + selectedShapes.map(shape => shape.id).join('')}
- shapes={selectedShapes}
- hidden={!showContextBar}
- bounds={singleSelectedShape ? singleSelectedShape.bounds : selectionBounds}
- rotation={singleSelectedShape ? singleSelectedShape.props.rotation : 0}
- />
- )}
- </>
- )}
- </HTMLLayer>
- {children}
- </div>
- )
- })
|