import { RotateCorner, toDomPrecision } from '@tldraw/primitives' import classNames from 'classnames' import { useRef } from 'react' import { track } from 'signia-react' import { useApp } from '../hooks/useApp' import { getCursor } from '../hooks/useCursor' import { useSelectionEvents } from '../hooks/useSelectionEvents' import { useTransform } from '../hooks/useTransform' import { CropHandles } from './CropHandles' const IS_FIREFOX = typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('firefox') > -1 export const SelectionFg = track(function SelectionFg() { const app = useApp() const rSvg = useRef(null) const isReadonlyMode = app.isReadOnly const topEvents = useSelectionEvents('top') const rightEvents = useSelectionEvents('right') const bottomEvents = useSelectionEvents('bottom') const leftEvents = useSelectionEvents('left') const topLeftEvents = useSelectionEvents('top_left') const topRightEvents = useSelectionEvents('top_right') const bottomRightEvents = useSelectionEvents('bottom_right') const bottomLeftEvents = useSelectionEvents('bottom_left') const isDefaultCursor = !app.isMenuOpen && app.cursor.type === 'default' const isCoarsePointer = app.isCoarsePointer let bounds = app.selectionBounds const shapes = app.selectedShapes const onlyShape = shapes.length === 1 ? shapes[0] : null // if all shapes have an expandBy for the selection outline, we can expand by the l const expandOutlineBy = onlyShape ? app.getShapeUtil(onlyShape).expandSelectionOutlinePx(onlyShape) : 0 useTransform(rSvg, bounds?.x, bounds?.y, 1, app.selectionRotation, { x: -expandOutlineBy, y: -expandOutlineBy, }) if (!bounds) return null bounds = bounds.clone().expandBy(expandOutlineBy) const zoom = app.zoomLevel const rotation = app.selectionRotation const isChangingStyles = app.isChangingStyle const width = Math.max(1, bounds.width) const height = Math.max(1, bounds.height) const size = 8 / zoom const isTinyX = width < size * 2 const isTinyY = height < size * 2 const isSmallX = width < size * 4 const isSmallY = height < size * 4 const isSmallCropX = width < size * 5 const isSmallCropY = height < size * 5 const mobileHandleMultiplier = isCoarsePointer ? 1.75 : 1 const targetSize = (6 / zoom) * mobileHandleMultiplier const targetSizeX = (isSmallX ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75) const targetSizeY = (isSmallY ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75) const showSelectionBounds = (onlyShape ? !app.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) && !isChangingStyles let shouldDisplayBox = (showSelectionBounds && app.isInAny( 'select.idle', 'select.brushing', 'select.scribble_brushing', 'select.pointing_canvas', 'select.pointing_selection', 'select.pointing_shape', 'select.crop.idle', 'select.crop.pointing_crop', 'select.pointing_resize_handle', 'select.pointing_crop_handle', 'select.editing_shape' )) || (showSelectionBounds && app.isIn('select.resizing') && onlyShape && shapes[0].type === 'text') if (IS_FIREFOX && shouldDisplayBox) { if (app.onlySelectedShape?.type === 'embed') { shouldDisplayBox = false } } const showCropHandles = app.isInAny('select.pointing_crop_handle', 'select.crop.idle', 'select.crop.pointing_crop') && !isChangingStyles && !isReadonlyMode const shouldDisplayControls = app.isInAny( 'select.idle', 'select.pointing_selection', 'select.pointing_shape', 'select.crop.idle' ) && !isChangingStyles && !isReadonlyMode const showCornerRotateHandles = !isCoarsePointer && !(isTinyX || isTinyY) && (shouldDisplayControls || showCropHandles) && (onlyShape ? !app.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) const showMobileRotateHandle = isCoarsePointer && (!isSmallX || !isSmallY) && (shouldDisplayControls || showCropHandles) && (onlyShape ? !app.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) const showResizeHandles = shouldDisplayControls && (onlyShape ? app.getShapeUtil(onlyShape).canResize(onlyShape) && !app.getShapeUtil(onlyShape).hideResizeHandles(onlyShape) : true) && !showCropHandles const hideAlternateCornerHandles = isTinyX || isTinyY const showOnlyOneHandle = isTinyX && isTinyY const hideAlternateCropHandles = isSmallCropX || isSmallCropY const showHandles = showResizeHandles || showCropHandles const hideRotateCornerHandles = !showCornerRotateHandles const hideMobileRotateHandle = !shouldDisplayControls || !showMobileRotateHandle const hideTopLeftCorner = !shouldDisplayControls || !showHandles const hideTopRightCorner = !shouldDisplayControls || !showHandles || hideAlternateCornerHandles const hideBottomLeftCorner = !shouldDisplayControls || !showHandles || hideAlternateCornerHandles const hideBottomRightCorner = !shouldDisplayControls || !showHandles || (showOnlyOneHandle && !showCropHandles) let hideEdgeTargetsDueToCoarsePointer = isCoarsePointer if ( hideEdgeTargetsDueToCoarsePointer && shapes.every((shape) => app.getShapeUtil(shape).isAspectRatioLocked(shape)) ) { hideEdgeTargetsDueToCoarsePointer = false } // If we're showing crop handles, then show the edges too. // If we're showing resize handles, then show the edges only // if we're not hiding them for some other reason let hideEdgeTargets = true if (showCropHandles) { hideEdgeTargets = hideAlternateCropHandles } else if (showResizeHandles) { hideEdgeTargets = hideAlternateCornerHandles || showOnlyOneHandle || hideEdgeTargetsDueToCoarsePointer } const textHandleHeight = Math.min(24 / zoom, height - targetSizeY * 3) const showTextResizeHandles = shouldDisplayControls && isCoarsePointer && onlyShape?.type === 'text' && textHandleHeight * zoom >= 4 return ( {shouldDisplayBox && ( )} {' '} {/* Targets */} {/* Corner Targets */} {/* Resize Handles */} {showResizeHandles && ( <> )} {showTextResizeHandles && ( <> )} {/* Crop Handles */} {showCropHandles && ( )} ) }) export const RotateCornerHandle = function RotateCornerHandle({ cx, cy, targetSize, corner, cursor, isHidden, 'data-testid': testId, }: { cx: number cy: number targetSize: number corner: RotateCorner cursor?: string isHidden: boolean 'data-testid'?: string }) { const events = useSelectionEvents(corner) return ( ) } const SQUARE_ROOT_PI = Math.sqrt(Math.PI) export const MobileRotateHandle = function RotateHandle({ cx, cy, size, isHidden, 'data-testid': testId, }: { cx: number cy: number size: number isHidden: boolean 'data-testid'?: string }) { const events = useSelectionEvents('mobile_rotate') return ( ) }