/** * Layout geometry: a PURE function from expression trees to positioned, * id-keyed boxes and glyphs. No DOM, no font measurement โ€” static metric * tables only. The UI package projects this onto SVG; hit testing is a * geometry query answered here. * * Design rules (the display decisions the AST deliberately does not encode): * - Sum(a, Neg(b)) renders as binary subtraction "a โˆ’ b". The minus sign is * owned by the NEG node (so dragging the signed term grabs sign and all); * the Sum only contributes "+" separators and spacing. * - Negative literals (canonically Neg(Integer)) render as "โˆ’3". * - Parentheses are owned by the PARENT that requires them: a Sum factor in * a Product, a Sum under Neg, compound Pow bases, Neg factors. * - Products juxtapose ("3x"); a "ยท" appears only where juxtaposition would * glue digits to digits. * - Fraction numerator/denominator lists render as centered rows above and * below the bar; empty lists render an implicit "1". * - Pow exponents are raised and scaled by SUP_SCALE. * * Geometry invariants (property-tested): * - every tree node has exactly one box; child boxes nest inside parents; * - sibling boxes never overlap; * - glyphs stay inside their owner's box; * - a subtree's internal geometry is context-independent up to translation * and uniform scale (what makes id-keyed animation work); * - hitTest returns the deepest box containing a point. */ import type { Node, NodeId } from "./expr.js"; export declare const METRICS: { readonly ASCENT: 0.78; readonly DESCENT: 0.22; readonly AXIS: 0.26; readonly DIGIT_W: 0.54; readonly LETTER_W: 0.54; readonly SIGN_W: 0.58; readonly DOT_W: 0.3; readonly PAREN_W: 0.34; readonly OP_PAD: 0.18; readonly EQ_PAD: 0.26; readonly MUL_GAP: 0.07; readonly NEG_GAP: 0.05; readonly EXP_GAP: 0.04; readonly SUP_SCALE: 0.7; readonly SUP_RAISE: 0.42; readonly FRAC_GAP: 0.12; readonly FRAC_PAD: 0.08; readonly BAR_TH: 0.06; readonly RADICAL_W: 0.5; readonly RAD_GAP: 0.1; }; export interface LayoutRect { readonly x: number; readonly y: number; readonly width: number; readonly height: number; } export interface LayoutBox { readonly nodeId: NodeId; readonly kind: Node["kind"]; readonly rect: LayoutRect; /** Absolute y of this node's baseline. */ readonly baseline: number; /** Font scale relative to the root (1 = base size). */ readonly scale: number; /** Tree depth from the layout root (root = 0); hitTest picks the max. */ readonly depth: number; } export type PlacedGlyph = { readonly kind: "char"; readonly char: string; readonly x: number; /** Absolute y of the glyph baseline. */ readonly baseline: number; readonly scale: number; readonly width: number; readonly ascent: number; readonly descent: number; readonly owner: NodeId; } | { readonly kind: "bar"; readonly x: number; /** Absolute y of the bar's vertical center. */ readonly y: number; readonly width: number; readonly thickness: number; readonly owner: NodeId; }; export interface Layout { readonly rootId: NodeId; readonly width: number; readonly height: number; readonly boxes: ReadonlyMap; readonly glyphs: readonly PlacedGlyph[]; } export declare function layoutNode(root: Node): Layout; /** * The deepest node whose box contains the point, or undefined. Sibling boxes * are disjoint (property-tested), so "deepest containing" is unambiguous: the * containing boxes always form a single ancestor chain. */ export declare function hitTest(layout: Layout, x: number, y: number): NodeId | undefined; /** Convenience: the center of a node's box (a natural anchor for gestures). */ export declare function boxCenter(layout: Layout, id: NodeId): { x: number; y: number; }; //# sourceMappingURL=layout.d.ts.map