import { react, track, useLayoutReaction, 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 { COARSE_HANDLE_RADIUS, HANDLE_RADIUS } from '../../constants' 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 { Mat } from '../../primitives/Mat' import { Vec } from '../../primitives/Vec' import { toDomPrecision } from '../../primitives/utils' import { debugFlags } from '../../utils/debug-flags' import { GeometryDebuggingView } from '../GeometryDebuggingView' import { LiveCollaborators } from '../LiveCollaborators' import { Shape } from '../Shape' /** @public */ export type TLCanvasComponentProps = { className?: string } /** @public */ export function DefaultCanvas({ className }: TLCanvasComponentProps) { const editor = useEditor() const { Background, SvgDefs } = useEditorComponents() const rCanvas = React.useRef(null) const rHtmlLayer = React.useRef(null) const rHtmlLayer2 = React.useRef(null) useScreenBounds(rCanvas) useDocumentEvents() useCoarsePointer() useGestureEvents(rCanvas) useFixSafariDoubleTapZoomPencilEvents(rCanvas) useLayoutReaction('position layers', () => { const htmlElm = rHtmlLayer.current if (!htmlElm) return const htmlElm2 = rHtmlLayer2.current if (!htmlElm2) return const { x, y, z } = editor.getCamera() // 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) }) 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 (
{shapeSvgDefs} {Cursor && } {SvgDefs && } {Background && }
{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.getCamera(), [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.getZoomLevel(), [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 SnapIndicatorWrapper() { const editor = useEditor() const lines = useValue('snapLines', () => editor.snaps.getIndicators(), [editor]) const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor]) const { SnapIndicator } = useEditorComponents() if (!(SnapIndicator && lines.length > 0)) return null return ( <> {lines.map((line) => ( ))} ) } function HandlesWrapper() { const editor = useEditor() const { Handles } = useEditorComponents() const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor]) const isCoarse = useValue('coarse pointer', () => editor.getInstanceState().isCoarsePointer, [ editor, ]) const isReadonly = useValue('isChangingStyle', () => editor.getInstanceState().isReadonly, [ editor, ]) const isChangingStyle = useValue( 'isChangingStyle', () => editor.getInstanceState().isChangingStyle, [editor] ) const onlySelectedShape = useValue('onlySelectedShape', () => editor.getOnlySelectedShape(), [ editor, ]) const transform = useValue( 'transform', () => { if (!onlySelectedShape) return null return editor.getShapePageTransform(onlySelectedShape) }, [editor, onlySelectedShape] ) const handles = useValue( 'handles', () => { if (!onlySelectedShape) return null const handles = editor.getShapeHandles(onlySelectedShape) if (!handles) return null const minDistBetweenVirtualHandlesAndRegularHandles = ((isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoomLevel) * 2 return ( handles .filter( (handle) => // if the handle isn't a virtual handle, we'll display it handle.type !== 'virtual' || // but for virtual handles, we'll only display them if they're far enough away from vertex handles !handles.some( (h) => // skip the handle we're checking against h !== handle && // only check against vertex handles h.type === 'vertex' && // and check that their distance isn't below the minimum distance Vec.Dist(handle, h) < minDistBetweenVirtualHandlesAndRegularHandles ) ) // We want vertex handles in front of all other handles .sort((a) => (a.type === 'vertex' ? 1 : -1)) ) }, [editor, onlySelectedShape, zoomLevel, isCoarse] ) if (!Handles || !onlySelectedShape || isChangingStyle || isReadonly || !handles || !transform) { return null } return ( {handles.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.getRenderingShapes(), [editor]) return ( <> {renderingShapes.map((result) => ( ))} ) } function ShapesToDisplay() { const editor = useEditor() const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor]) return ( <> {renderingShapes.map((result) => ( ))} ) } function SelectedIdIndicators() { const editor = useEditor() const selectedShapeIds = useValue('selectedShapeIds', () => editor.getSelectedShapeIds(), [ 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] ) const { ShapeIndicator } = useEditorComponents() if (!ShapeIndicator) return null 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.getCurrentPageState().hoveredShapeId, [ editor, ]) if (isCoarsePointer || !isHoveringCanvas || !hoveredShapeId || !HoveredShapeIndicator) return null return } const HintedShapeIndicator = track(function HintedShapeIndicator() { const editor = useEditor() const { ShapeIndicator } = useEditorComponents() const ids = dedupe(editor.getHintingShapeIds()) if (!ids.length) return null if (!ShapeIndicator) 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.getCurrentPageId() 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, background: editor.getInstanceState().exportBackground, }) 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 SelectionForegroundWrapper() { const editor = useEditor() const selectionRotation = useValue('selection rotation', () => editor.getSelectionRotation(), [ editor, ]) const selectionBounds = useValue( 'selection bounds', () => editor.getSelectionRotatedPageBounds(), [editor] ) const { SelectionForeground } = useEditorComponents() if (!selectionBounds || !SelectionForeground) return null return } function SelectionBackgroundWrapper() { const editor = useEditor() const selectionRotation = useValue('selection rotation', () => editor.getSelectionRotation(), [ editor, ]) const selectionBounds = useValue( 'selection bounds', () => editor.getSelectionRotatedPageBounds(), [editor] ) const { SelectionBackground } = useEditorComponents() if (!selectionBounds || !SelectionBackground) return null return } function OnTheCanvasWrapper() { const { OnTheCanvas } = useEditorComponents() if (!OnTheCanvas) return null return } function InFrontOfTheCanvasWrapper() { const { InFrontOfTheCanvas } = useEditorComponents() if (!InFrontOfTheCanvas) return null return }