import { react, track, useQuickReactor, useValue } from '@tldraw/state' import { TLHandle, TLShapeId } from '@tldraw/tlschema' import { dedupe, modulate, objectMapValues } from '@tldraw/utils' import classNames from 'classnames' import React from 'react' import { useCanvasEvents } from '../hooks/useCanvasEvents' import { useCoarsePointer } from '../hooks/useCoarsePointer' import { useDocumentEvents } from '../hooks/useDocumentEvents' import { useEditor } from '../hooks/useEditor' import { useEditorComponents } from '../hooks/useEditorComponents' import { useFixSafariDoubleTapZoomPencilEvents } from '../hooks/useFixSafariDoubleTapZoomPencilEvents' import { useGestureEvents } from '../hooks/useGestureEvents' import { useHandleEvents } from '../hooks/useHandleEvents' import { useScreenBounds } from '../hooks/useScreenBounds' import { Matrix2d } from '../primitives/Matrix2d' import { toDomPrecision } from '../primitives/utils' import { debugFlags } from '../utils/debug-flags' import { GeometryDebuggingView } from './GeometryDebuggingView' import { LiveCollaborators } from './LiveCollaborators' import { Shape } from './Shape' import { ShapeIndicator } from './ShapeIndicator' /** @public */ export function Canvas({ className }: { className?: string }) { const editor = useEditor() const { Background, SvgDefs } = useEditorComponents() const rCanvas = React.useRef(null) const rHtmlLayer = React.useRef(null) const rHtmlLayer2 = React.useRef(null) useScreenBounds() useDocumentEvents() useCoarsePointer() useGestureEvents(rCanvas) useFixSafariDoubleTapZoomPencilEvents(rCanvas) useQuickReactor( 'position layers', () => { const htmlElm = rHtmlLayer.current if (!htmlElm) return const htmlElm2 = rHtmlLayer2.current if (!htmlElm2) return const { x, y, z } = editor.camera // Because the html container has a width/height of 1px, we // need to create a small offset when zoomed to ensure that // the html container and svg container are lined up exactly. const offset = z >= 1 ? modulate(z, [1, 8], [0.125, 0.5], true) : modulate(z, [0.1, 1], [-2, 0.125], true) const transform = `scale(${toDomPrecision(z)}) translate(${toDomPrecision( x + offset )}px,${toDomPrecision(y + offset)}px)` htmlElm.style.setProperty('transform', transform) htmlElm2.style.setProperty('transform', transform) }, [editor] ) const events = useCanvasEvents() const shapeSvgDefs = useValue( 'shapeSvgDefs', () => { const shapeSvgDefsByKey = new Map() for (const util of objectMapValues(editor.shapeUtils)) { if (!util) return const defs = util.getCanvasSvgDefs() for (const { key, component: Component } of defs) { if (shapeSvgDefsByKey.has(key)) continue shapeSvgDefsByKey.set(key, ) } } return [...shapeSvgDefsByKey.values()] }, [editor] ) const hideShapes = useValue('debug_shapes', () => debugFlags.hideShapes.get(), [debugFlags]) const debugSvg = useValue('debug_svg', () => debugFlags.debugSvg.get(), [debugFlags]) const debugGeometry = useValue('debug_geometry', () => debugFlags.debugGeometry.get(), [ debugFlags, ]) return (
{Background && } {shapeSvgDefs} {Cursor && } {SvgDefs && }
{hideShapes ? null : debugSvg ? : }
{debugGeometry ? : null}
) } function GridWrapper() { const editor = useEditor() const gridSize = useValue('gridSize', () => editor.getDocumentSettings().gridSize, [editor]) const { x, y, z } = useValue('camera', () => editor.camera, [editor]) const isGridMode = useValue('isGridMode', () => editor.getInstanceState().isGridMode, [editor]) const { Grid } = useEditorComponents() if (!(Grid && isGridMode)) return null return } function ScribbleWrapper() { const editor = useEditor() const scribbles = useValue('scribbles', () => editor.getInstanceState().scribbles, [editor]) const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor]) const { Scribble } = useEditorComponents() if (!(Scribble && scribbles.length)) return null return ( <> {scribbles.map((scribble) => ( ))} ) } function BrushWrapper() { const editor = useEditor() const brush = useValue('brush', () => editor.getInstanceState().brush, [editor]) const { Brush } = useEditorComponents() if (!(Brush && brush)) return null return } function ZoomBrushWrapper() { const editor = useEditor() const zoomBrush = useValue('zoomBrush', () => editor.getInstanceState().zoomBrush, [editor]) const { ZoomBrush } = useEditorComponents() if (!(ZoomBrush && zoomBrush)) return null return } function SnapLinesWrapper() { const editor = useEditor() const lines = useValue('snapLines', () => editor.snaps.lines, [editor]) const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor]) const { SnapLine } = useEditorComponents() if (!(SnapLine && lines.length > 0)) return null return ( <> {lines.map((line) => ( ))} ) } const MIN_HANDLE_DISTANCE = 48 function HandlesWrapper() { const editor = useEditor() const { Handles } = useEditorComponents() const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor]) const isCoarse = useValue('coarse pointer', () => editor.getInstanceState().isCoarsePointer, [ editor, ]) const onlySelectedShape = useValue('onlySelectedShape', () => editor.onlySelectedShape, [editor]) const isChangingStyle = useValue( 'isChangingStyle', () => editor.getInstanceState().isChangingStyle, [editor] ) const isReadonly = useValue('isChangingStyle', () => editor.getInstanceState().isReadonly, [ editor, ]) const handles = useValue( 'handles', () => (editor.onlySelectedShape ? editor.getShapeHandles(editor.onlySelectedShape) : undefined), [editor] ) const transform = useValue( 'transform', () => editor.onlySelectedShape ? editor.getShapePageTransform(editor.onlySelectedShape) : undefined, [editor] ) if (!Handles || !onlySelectedShape || isChangingStyle || isReadonly) return null if (!handles) return null if (!transform) return null // Don't display a temporary handle if the distance between it and its neighbors is too small. const handlesToDisplay: TLHandle[] = [] for (let i = 0, handle = handles[i]; i < handles.length; i++, handle = handles[i]) { if (handle.type !== 'vertex') { const prev = handles[i - 1] const next = handles[i + 1] if (prev && next) { if (Math.hypot(prev.y - next.y, prev.x - next.x) < MIN_HANDLE_DISTANCE / zoomLevel) { continue } } } handlesToDisplay.push(handle) } handlesToDisplay.sort((a) => (a.type === 'vertex' ? 1 : -1)) return ( {handlesToDisplay.map((handle) => { return ( ) })} ) } function HandleWrapper({ shapeId, handle, zoom, isCoarse, }: { shapeId: TLShapeId handle: TLHandle zoom: number isCoarse: boolean }) { const events = useHandleEvents(shapeId, handle.id) const { Handle } = useEditorComponents() if (!Handle) return null return ( ) } function ShapesWithSVGs() { const editor = useEditor() const renderingShapes = useValue('rendering shapes', () => editor.renderingShapes, [editor]) return ( <> {renderingShapes.map((result) => ( ))} ) } function ShapesToDisplay() { const editor = useEditor() const renderingShapes = useValue('rendering shapes', () => editor.renderingShapes, [editor]) return ( <> {renderingShapes.map((result) => ( ))} ) } function SelectedIdIndicators() { const editor = useEditor() const selectedShapeIds = useValue( 'selectedShapeIds', () => editor.currentPageState.selectedShapeIds, [editor] ) const shouldDisplay = useValue( 'should display selected ids', () => { // todo: move to tldraw selected ids wrapper return ( editor.isInAny( 'select.idle', 'select.brushing', 'select.scribble_brushing', 'select.editing_shape', 'select.pointing_shape', 'select.pointing_selection', 'select.pointing_handle' ) && !editor.getInstanceState().isChangingStyle ) }, [editor] ) if (!shouldDisplay) return null return ( <> {selectedShapeIds.map((id) => ( ))} ) } const HoveredShapeIndicator = function HoveredShapeIndicator() { const editor = useEditor() const { HoveredShapeIndicator } = useEditorComponents() const isCoarsePointer = useValue( 'coarse pointer', () => editor.getInstanceState().isCoarsePointer, [editor] ) const isHoveringCanvas = useValue( 'hovering canvas', () => editor.getInstanceState().isHoveringCanvas, [editor] ) const hoveredShapeId = useValue('hovered id', () => editor.currentPageState.hoveredShapeId, [ editor, ]) if (isCoarsePointer || !isHoveringCanvas || !hoveredShapeId || !HoveredShapeIndicator) return null return } const HintedShapeIndicator = track(function HintedShapeIndicator() { const editor = useEditor() const ids = dedupe(editor.hintingShapeIds) if (!ids.length) return null return ( <> {ids.map((id) => ( ))} ) }) function Cursor() { return ( ) } function CollaboratorHint() { return } function ArrowheadDot() { return ( ) } function ArrowheadCross() { return ( ) } const DebugSvgCopy = track(function DupSvg({ id }: { id: TLShapeId }) { const editor = useEditor() const shape = editor.getShape(id) const [html, setHtml] = React.useState('') const isInRoot = shape?.parentId === editor.currentPageId React.useEffect(() => { if (!isInRoot) return let latest = null const unsubscribe = react('shape to svg', async () => { const renderId = Math.random() latest = renderId const bb = editor.getShapePageBounds(id) const el = await editor.getSvg([id], { padding: 0 }) if (el && bb && latest === renderId) { el.style.setProperty('overflow', 'visible') el.setAttribute('preserveAspectRatio', 'xMidYMin slice') el.style.setProperty('transform', `translate(${bb.x}px, ${bb.y + bb.h + 12}px)`) el.style.setProperty('border', '1px solid black') setHtml(el?.outerHTML) } }) return () => { latest = null unsubscribe() } }, [editor, id, isInRoot]) if (!isInRoot) return null return (
) }) function UiLogger() { const uiLog = useValue('debugging ui log', () => debugFlags.logMessages.get(), [debugFlags]) if (!uiLog.length) return null return (
{uiLog.map((message, messageIndex) => { const text = typeof message === 'string' ? message : JSON.stringify(message) return (
{text}
) })}
) } export function SelectionForegroundWrapper() { const editor = useEditor() const selectionRotation = useValue('selection rotation', () => editor.selectionRotation, [editor]) const selectionBounds = useValue('selection bounds', () => editor.selectionRotatedPageBounds, [ editor, ]) const { SelectionForeground } = useEditorComponents() if (!selectionBounds || !SelectionForeground) return null return } export function SelectionBackgroundWrapper() { const editor = useEditor() const selectionRotation = useValue('selection rotation', () => editor.selectionRotation, [editor]) const selectionBounds = useValue('selection bounds', () => editor.selectionRotatedPageBounds, [ editor, ]) const { SelectionBackground } = useEditorComponents() if (!selectionBounds || !SelectionBackground) return null return } export function OnTheCanvasWrapper() { const { OnTheCanvas } = useEditorComponents() if (!OnTheCanvas) return null return } export function InFrontOfTheCanvasWrapper() { const { InFrontOfTheCanvas } = useEditorComponents() if (!InFrontOfTheCanvas) return null return }