import type { PPTElement } from '../types/slides'; import { type ClientCoords } from './prosemirror/caret'; /** Temporary: paint every pointer hit target so the hit engine can be verified. */ export declare const DEBUG_HIT_AREAS = false; /** Minimum visual-pixel size so thin lines remain hittable. */ export declare const MIN_HIT_PX = 8; /** * Drag-ring sizing in visual pixels, based on the box's effective side length. * * Inner eats the edit surface; outer is extra grab outside the box. * Short/zoomed-out boxes shrink inner toward 0 so the whole interior stays editable. */ export declare const MIN_INNER_DRAG_PX = 1; export declare const MAX_INNER_DRAG_PX = 4; export declare const MIN_OUTER_DRAG_PX = 3; export declare const MAX_OUTER_DRAG_PX = 5; export declare const MIN_EDIT_INTERIOR_PX = 10; export declare const RING_SIZE_MIN = 20; export declare const RING_SIZE_MAX = 56; /** Inner/outer drag thickness for one axis (visual width or height). */ export declare function dragRingMetrics(visualSize: number): { innerPx: number; outerPx: number; }; /** Inward drag thickness on the constraining side (0 when the box is too short to spare any). */ export declare function borderHitPx(width: number, height: number): number; export declare const HIT_RING_SIDES: readonly ['top', 'bottom', 'left', 'right']; export type HitRingSide = (typeof HIT_RING_SIDES)[number]; /** Matches `.resize-handler` (10px box, centered on the edge with -5px margin). */ export declare const RESIZE_HANDLE_SIZE_PX = 10; export declare const RESIZE_HANDLE_RADIUS_PX: number; export declare const RESIZE_HANDLE_DIRECTIONS: readonly ['left-top', 'top', 'right-top', 'left', 'right', 'left-bottom', 'bottom', 'right-bottom']; export type ResizeHandleDirection = (typeof RESIZE_HANDLE_DIRECTIONS)[number]; export interface ResizeHandleCenter { x: number; y: number; direction: ResizeHandleDirection; } /** Box-local centers of the 8 resize squares (origin = top-left). */ export declare function resizeHandleCenters(width: number, height: number, directions?: readonly ResizeHandleDirection[]): ResizeHandleCenter[]; /** Handles actually rendered for this element (none for lines). */ export declare function resizeHandleDirectionsFor(element: PPTElement): readonly ResizeHandleDirection[]; export interface HitRingLayout { inset: string; sides: Record>; vertical: { innerPx: number; outerPx: number; }; horizontal: { innerPx: number; outerPx: number; }; } export interface HitRingLayoutOptions { /** * Cut these handle squares out of the move strips. * Resize must beat move: the ring must not claim those pixels. */ clearResizeHandles?: readonly ResizeHandleDirection[]; } /** * Per-side drag strips. Top/bottom follow visual height; left/right follow width. * Outer grab does not consume the edit interior. * When `clearResizeHandles` is set, handle squares are punched out so move cannot steal resize. */ export declare function hitRingLayout(width: number, height: number, options?: HitRingLayoutOptions): HitRingLayout; export interface VisualHitRect { frameClip?: { x: number; y: number; }[]; id: string; left: number; top: number; width: number; height: number; rotate: number; zIndex: number; /** * Empty placeholder slot (dashed prompt, no authored content). It is a * background affordance: any element carrying visible content that * overlaps it wins the pointer regardless of z-order — users click the * text they SEE, not the dashed frame behind it. */ yieldToContent?: boolean; } /** True when the element paints authored content the user can click "on". */ export declare function elementHasClickableContent(element: PPTElement): boolean; /** * Map a slide element to a hit rectangle in `.viewport-wrapper` visual pixels. * * Hit-testing must never go through the CSS-scaled unsized `.viewport`. * That path fragments (glyph boxes, SVG fills) and offsets under zoom. */ export declare function elementVisualHitRect(element: PPTElement, canvasScale: number, zIndex: number): VisualHitRect; export declare function pointInVisualHitRect(x: number, y: number, rect: VisualHitRect): boolean; /** Topmost rect at a wrapper-local point (highest zIndex wins). RBush AABB, then pointInVisualHitRect refine. */ export declare function hitTestVisualRects(rects: VisualHitRect[], x: number, y: number): VisualHitRect | null; export declare const TEXT_EDITABLE_TYPES: Set; /** * Clicking the interior opens the text editor. Drag from the border only. * Includes text boxes and shapes/tables that already contain text. */ export declare function clicksToEditText(element: PPTElement): boolean; /** Interior stays interactive (text caret, or media playback). Drag from the border. */ export declare function hasInteractiveSurface(element: PPTElement): boolean; export declare function canEditElementText(type: PPTElement['type']): boolean; /** * Geometry / type / ring-vs-body identity for one element. * Ignores HTML content except the empty↔text flip that changes hasInteractiveSurface. */ export declare function hitLayerElementUnchanged(prev: PPTElement, next: PPTElement): boolean; /** True when element order and per-element hit identity are unchanged. */ export declare function hitLayerElementsUnchanged(prev: PPTElement[], next: PPTElement[]): boolean; /** Inputs that invalidate HitLayer `hitRects` / `occluderRects` memos. */ export type HitLayerMemoInput = { elementList: PPTElement[]; canvasScale: number; hiddenElementIdList: string[]; activeElementIdList: string[]; editingElementId: string | null; clipingImageElementId: string; disabled: boolean; }; export type VisualHitPlanInput = { elementList: PPTElement[]; canvasScale: number; hiddenElementIdList: Iterable; activeElementIdList: Iterable; editingElementId: string | null; clipingImageElementId: string; }; export type VisualHitPlan = { hitRects: VisualHitRect[]; occluderRects: VisualHitRect[]; /** Rects of elements painting authored content — they beat empty placeholder slots. */ contentRects: VisualHitRect[]; }; /** * Selected / editing / clipping boxes leave HitLayer so Operate or the live * editor can own the pointer. Those boxes must still punch holes in every * *lower* hit rect — otherwise the first click works, then hover/click/drag * fall through to the card underneath. Empty placeholder slots additionally * yield to ANY overlapping content rect regardless of z-order. */ export declare function collectVisualHitPlan(input: VisualHitPlanInput): VisualHitPlan; export declare function pointInAnyVisualHitRect(x: number, y: number, rects: VisualHitRect[]): boolean; /** Occluders that sit on top of `rect` (same stack level included). Empty placeholder slots yield to every occluder. */ export declare function occludersAboveRect(rect: VisualHitRect, occluders: VisualHitRect[]): VisualHitRect[]; /** * Skip rebuilding hit rects when only HTML content changed, * or the lists are reference-equal. */ export declare function hitLayerSkipRebuild(prev: HitLayerMemoInput, next: HitLayerMemoInput): boolean; /** Canvas-space corners of a (possibly rotated) hit rect. */ export declare function visualHitCorners(rect: VisualHitRect): { x: number; y: number; }[]; export declare function visualHitRectsOverlap(a: VisualHitRect, b: VisualHitRect): boolean; export declare function visualHitIntersectsBox(rect: VisualHitRect, box: { left: number; top: number; width: number; height: number; }): boolean; /** Elements whose visual hit rect intersects a wrapper-space marquee. */ export declare function elementIdsIntersectingSelection(elements: PPTElement[], box: { left: number; top: number; width: number; height: number; }, canvasScale: number, hiddenIds?: Iterable): string[]; export declare function visualHitAabb(rect: VisualHitRect): { left: number; top: number; right: number; bottom: number; }; /** * Clip a hit rect so a higher selected/editing box can keep the pointer. * HitLayer sits above the viewport; without this hole, lower cards steal * hover/click/drag after the top element leaves the hit layer. */ export declare function hitRectClipPath(rect: VisualHitRect, occluders: VisualHitRect[]): string | undefined; /** True when the wrapper-local point sits on a resize handle square. */ export declare function isPointOnResizeHandle(x: number, y: number, rect: VisualHitRect, directions?: readonly ResizeHandleDirection[]): boolean; /** * True when the wrapper-local point sits on the move ring (inner + outer grab). * Same geometry as {@link hitRingLayout} / `.operate-drag-border` / `.hit-border`. */ export declare function isPointOnVisualBorder(x: number, y: number, rect: VisualHitRect, options?: { clearResizeHandles?: readonly ResizeHandleDirection[]; }): boolean; export type OperateHitTarget = 'resize' | 'move' | 'edit' | null; /** * Selected-element hit kind. Resize handles beat the move ring; the move ring * beats the edit interior. Used as the source of truth for operate-layer stacking. */ export declare function hitTestOperateTarget(x: number, y: number, rect: VisualHitRect, options: { interactive: boolean; handles?: readonly ResizeHandleDirection[]; }): OperateHitTarget; export declare function retryPendingCaret(elementId: string): void; export declare function focusElementEditor(elementId: string, coords?: ClientCoords): void; export type { ClientCoords };