/** * Callout Module — add callout annotations with an editable label and a * draggable tail pointer (like markerjs CalloutMarker). * * Architecture (all separate Fabric objects, no Group): * 1. tailImage — filled triangle rendered via off-screen canvas * 2. bgRect — colored rounded rectangle behind the text * 3. border — dashed selection border around bgRect * 4. label — fabric.IText so the user can click (desktop) or * double-tap (mobile) to edit inline * 5. anchor — small draggable circle at the tail tip * * Moving the rect drags border + label along and redraws the tail. * Moving the anchor redraws only the tail. * Text constraints (max 40 chars, word-wrap at ~15 chars) are enforced * when the user finishes editing. * The box cannot be resized smaller than the label's natural size + minimum padding. */ import { fabric } from 'fabric'; export interface CalloutOptions { text?: string; color?: string; textColor?: string; fontSize?: number; left?: number; top?: number; anchorLeft?: number; anchorTop?: number; /** Maximum characters allowed in a callout label (default 40) */ maxChars?: number; /** Character position around which to insert a line-break (default 15) */ lineBreakAt?: number; } export declare class CalloutModule { private canvas; private isActive; private pendingAdd; private calloutColor; private calloutTextColor; private fontSize; private callouts; private calloutCounter; /** Max characters allowed in a callout label */ private calloutMaxChars; /** Character position around which to insert a line-break */ private calloutLineBreakAt; /** Initial text used for newly-placed callouts */ private defaultText; private boundsProvider; constructor(canvas: fabric.Canvas); activate(): void; deactivate(): void; setColor(color: string): void; setTextColor(color: string): void; setFontSize(size: number): void; /** * Update the defaults used for newly-placed callouts. Existing * callouts on the canvas are not modified. All keys are optional; * omitted keys keep their current value. */ setDefaults(defaults: { text?: string; maxChars?: number; lineBreakAt?: number; }): void; getIsActive(): boolean; setPlacementBoundsProvider(provider: (() => { left: number; top: number; right: number; bottom: number; } | null) | null): void; private getBounds; private clampPointToBounds; private isPointInsideBounds; private clampCalloutIntoBounds; /** * Move mode intentionally freezes all annotation objects; when the user * switches back to callout mode, previously placed callouts must be * re-enabled so they can be selected and edited again. */ private restoreExistingCalloutInteractivity; /** * Rebuild internal callout handles from existing canvas objects. * Needed after loadFromJSON (undo/redo) because runtime references, * listeners and non-serialized interaction locks are not preserved. */ rehydrateFromCanvas(): void; /** Returns the number of callouts currently on the canvas */ getCalloutCount(): number; /** * Test whether any callout visually intersects a circle at (x, y) with * the given radius (in canvas coordinates). The box, border, label and * anchor are hit-tested by inflated bounding rect; the tail is * hit-tested per-pixel against its off-screen bitmap so the * full-canvas-sized tail image doesn't produce false positives on * every click. Returns the calloutId of the first hit, or null. */ getCalloutIdAtPoint(x: number, y: number, radius: number): number | null; /** * Per-pixel alpha test against the tail's off-screen bitmap around * (x, y) within the eraser radius. Returns true if any non-transparent * pixel is found in the sampled region. */ private tailHitTest; /** * Remove all fabric objects belonging to a callout (tail, border, box, * label, anchor) and drop the internal handle. Returns true if a * callout with that id was found and removed. */ removeCalloutById(id: number): boolean; /** * Delete the currently selected callout (if any). * Removes all 5 fabric objects belonging to that callout. * Returns true if something was deleted. */ deleteSelected(): boolean; /** * Hide all callout borders and anchors (call before export). */ hideAllControls(): void; /** * Refresh every callout's tail bitmap — needed after operations * (like crop) that change the underlying canvas dimensions, since the * tail is rendered onto an off-screen canvas sized to the main canvas. */ refreshAllTails(): void; /** * Show borders and anchors only for currently-selected callouts. * Call after export to restore interactive state. */ showAllControls(): void; addCallout(opts?: CalloutOptions): void; /** * Re-apply interaction constraints for each callout part. */ private enforceCalloutPartLocks; /** * Attach callout interaction listeners. */ private wireHandleEvents; /** * Truncate text to a maximum length, adding ellipsis if truncated. */ private constrainText; /** * Insert a line-break (\n) near `breakAt` at the closest word boundary * so `fabric.IText` renders a compact two-line label. */ private wrapText; /** Constrain + word-wrap a callout label in one step */ private formatCalloutText; /** Focus the label IText and enter inline editing mode */ private enterLabelEditing; /** Called when the user finishes editing — enforce constraints, resize, re-lock label */ private onLabelEditingExited; /** Prevent the box from being resized smaller than the label's natural size + minimum padding */ private clampBoxSize; /** Keep border + label in sync with bgRect position/size */ private syncBoxParts; /** After text edit, resize bgRect + border to fit the new label */ private resizeBoxToFitLabel; /** * Redraw the tail triangle from the rect edge to the anchor point. * Uses ray-rect intersection so the tail exits from the correct edge * regardless of where the anchor is (below, above, left, right). * The base is pulled slightly inside the box to eliminate the visual gap. */ private redrawTail; /** * Find where a ray from (ox,oy) in direction (dx,dy) exits the rect. */ private rayRectIntersection; /** Canvas click handler — place a new callout */ private handleCanvasClick; }