/** * Generic "try candidates in order, fall back with error" label placement. * * Node, Connector, and Region all run the same loop: * 1. build a list of candidate slots up front — each carries the * pixel rect it would draw at, plus a human-readable description * ("top-right", "segment 2 / above", …) * 2. accept the first slot that collides with nothing * 3. otherwise force a fallback (explicit or last candidate) and flag * `error: true` * * The placer records every attempt with the specific obstacles it hit * (see CollisionHit). The pipeline turns those into PlacementDiagnostic * records that agents / humans read to debug a tight layout. */ import type { LabelRect, LineSeg, CanvasBounds, Circle, CollisionHit } from './collision.js'; export interface CollisionContext { placedLabels: LabelRect[]; connLines: LineSeg[]; bounds?: CanvasBounds; /** Node icon discs the label must not overlap. Callers that want a * node to sit on top of its *own* icon should filter it out first. */ iconCircles?: Circle[]; } /** * A candidate slot with its pre-built rect and a short description. * Descriptions end up in PlacementAttempt.slot so an agent reading * the diagnostic can tell which positions were tried. */ export interface SlotCandidate { slot: Slot; rect: LabelRect; description: string; } /** One position that was tested. Always present in the result — the * accepted slot is the last one. */ export interface AttemptRecord { description: string; rect: LabelRect; hits: CollisionHit[]; accepted: boolean; } export interface PlacementResult { rect: LabelRect; slot: Slot; /** True when placement fell back to the blocked fallback slot. */ error: boolean; /** Every candidate the placer examined, in order tried. The final * entry has `accepted: true` and represents the chosen slot (whether * clean or forced-with-collisions). */ attempts: AttemptRecord[]; } /** * What to do when NO candidate fits cleanly AND no explicit `fallback` * was supplied: * - 'last-candidate' (default): use the last candidate in the list * (preserves the historical behavior). * - 'smallest-collision': walk every attempt and pick the candidate * with the fewest collision hits. Ties broken by earliest index — * so tier-1 candidates win over tier-2 / tier-3 expanded positions * when they're tied. Useful when the candidate list itself * encodes a preference order (e.g. node labels try closer leader * offsets first, then stretch out). */ export type NoFitStrategy = 'last-candidate' | 'smallest-collision'; export interface PlaceLabelOptions { noFitStrategy?: NoFitStrategy; } /** * Walk `candidates` in order and return the first one whose rect does * not collide with the context. If none fit: * - when `fallback` is supplied, use it (flagged `error: true`). * - otherwise dispatch on `options.noFitStrategy` — default * 'last-candidate' reproduces the historical last-wins behavior, * 'smallest-collision' picks the candidate with the fewest hits. * * Returns null only when `candidates` is empty AND no `fallback` is * supplied — i.e. the caller has nothing to place at all. */ export declare function placeLabel(candidates: readonly SlotCandidate[], context: CollisionContext, fallback?: SlotCandidate, options?: PlaceLabelOptions): PlacementResult | null; //# sourceMappingURL=label-placer.d.ts.map