import { HTMLElementBase } from "../env"; import { type RuntimeContext } from "../ctx"; import { type LightingIR } from "../scene-lighting"; import { type ClipBoxIR } from "../clip-box"; import { type TerrainIR } from "../terrain"; import { type WidgetSlot } from "../widget-layout"; import { type SnapshotOptions } from "../snapshot"; import { type ValidationEntry } from "../validation"; import type { LayerIR } from "../ir"; import type { Selection } from "../selection"; import type { MapViewport } from "../basemap"; interface OverlayHost { updatePosition(viewport: MapViewport | undefined, selection: Selection | null, pickType?: "hover" | "click"): void; /** * Screen rect for collision-dim (spec: "Overlap avoidance") — non-null * only when the overlay is VISIBLE and participates in dimming. Returns * null when hidden OR when it's transient runtime chrome (the internal * hover tooltip / trace / draw preview): those glide constantly and must * not flicker widgets. The map reads this off its registered host set * rather than re-querying the DOM. */ getVisibleRect(): DOMRect | null; } export declare class OmMapElement extends HTMLElementBase { private core; private observer; private history; private keydownHandler?; private keyupHandler?; private dblclickHandler?; private contextmenuHandler?; private mount; private reconcilePending; private readonly dataOwner; private dataReleasePending; /** Decoded property tables per pick-features layer — see RuntimeCoreCallbacks.onFeatureTable. */ private featureTables; private layerIRs; private selection; /** * The pointer event behind the most recent pick — kept SEPARATELY from * `selection` because empty picks store `selection = null`, losing their * type, yet `selection-type`-scoped overlays must tell a click on empty * space (dismisses a click popup) from a hover over empty space (inert). */ private lastPickType; private widgets; private overlays; private overlayFlushPending; private errorPanel; private runtimeErrors; /** Dedup guard for the approximate-BIM-georeferencing warning — same shape as applyLicenseGates' own `warned` set, keyed per layer id. */ private warnedApproximatePlacement; /** Dedup guard for the no-terrain-for-elevation error — same shape as `warnedApproximatePlacement`. */ private erroredNoTerrainForElevation; private readyFired; private firstSettleDrawDone; /** Custom-state holder for the `om-collapsed` height-floor marker — see applyHeightFloor. */ private internals; private resolveReady; readonly ready: Promise; private readonly viewSettle; private firstReconcileDone; private rendererLoadPending; private loadDispatched; connectedCallback(): void; disconnectedCallback(): void; /** Library-owned stacking layer: one absolutely-positioned flex container per managed slot, created lazily. Rendering plumbing, never authored state. */ private widgetLayer; private readonly slotContainers; private readonly mandatedChromeHosts; /** Slots that host mandated chrome (badge/attribution) — set at host creation, so the collision-dim exemption is a slot-level flag, not a per-flush DOM scan. */ private readonly slotsWithMandatedChrome; private widgetResizeObserver; private widgetsFolded; private foldPassPending; private inFoldPass; private readonly foldMapId; private readonly foldedWidgetSlots; private readonly foldDrawers; /** * Is this node layout plumbing (the widget layer, slot containers, or * cluster wrappers)? Their childList churn is rendering, not manifest * edits. om-widget elements themselves are NOT plumbing — their attribute * edits stay manifest-visible. */ private isWidgetLayoutPlumbing; /** * Places an into its managed slot container (creating layer/ * container on demand), or back into the light DOM for the manual tier. * The reparent is RENDERING PLUMBING: history-suppressed (undo must never * replay it) and filtered out of manifest-change detection. The widget's * authored state — its `position` attribute — never changes here. */ /** Per-widget comment anchors marking the AUTHORED light-DOM position — a managed→manual flip restores the widget where the author wrote it, not at the end. */ private readonly slotAnchors; /** The widget layer div — lazy, shared by slot containers AND the layout tokens (both lanes put tokens on the layer, never on an authored element). */ private ensureWidgetLayer; private ensureSlotContainer; /** * Provider attribution and the free-license badge are immutable members of * managed slots. Their stable wrappers stay front-end-owned while renderer * adapters mount/unmount the actual controls inside. */ private ensureMandatedChromeHost; /** True while an internal reparent (slotting/manual-flip) is moving a widget — its transient disconnect must NOT be read as an author removal. */ private inSlotReparent; /** Wrap an internal reparent: suppress history AND flag it as a move so unregisterWidgetInternal keeps the fold/anchor bookkeeping. */ private reparent; slotWidgetInternal(el: HTMLElement, slot: WidgetSlot): void; /** Called by when its live `fold` attribute changes. */ refreshWidgetFoldInternal(): void; private startWidgetFoldObserver; private foldBreakpointRaw?; private foldBreakpointCache; /** Cached breakpoint resolution — re-probes only when the token string actually changes, not on every ResizeObserver tick. */ private foldBreakpointPx; private measureWidgetFold; private updateWidgetFoldForWidth; /** Lift the bottom-end row clear of the bottom drawer toggle while folded. Applied here AND in the fold pass so a lazily-created bottom-end container still gets it. */ private syncFoldBottomOffset; private scheduleFoldPass; private shouldFoldWidget; private ensureFoldDrawer; private setFoldDrawerOpen; private focusFirstIn; private restoreFoldedWidget; private runFoldPass; private clusterPassPending; /** Re-entry guard: the pass's own reparenting fires connect callbacks that call back into scheduleClusterPass — self-inflicted, never re-schedule. */ private inClusterPass; /** Microtask-coalesced: one pass per placement burst (boot slots N widgets). */ private scheduleClusterPass; /** * Re-derives cluster wrappers in every slot container: runs of ≥2 * adjacent compact widgets (in VISUAL order — `order` attr, then DOM) * merge into one `data-om-cluster` wrapper div carrying the group's * radius/shadow/divider look; the widgets' own shells flatten via the * `clustered` class they toggle at render. Unwrap-then-rewrap per pass: * wrappers are cheap plumbing divs, and idempotence beats bookkeeping. * Members hidden by hide-all are excluded — a hidden widget can't cluster, * so a group never leaves a ghost wrapper pill around invisible members. * The re-entry guard is inline (the pass's own reparenting fires connect * callbacks that call back into scheduleClusterPass — self-inflicted). */ private runClusterPass; /** * `widgets-hidden` — visibility:hidden + pointer-events:none on every * widget EXCEPT attribution and the toggle itself (attribution never * hides — license compliance). Never removal: an open listbox or * mid-scrub slider survives. Inline styles are history-excluded plumbing. */ private applyWidgetsHidden; private applyHiddenToWidget; /** Custom properties applied from widget-style — tracked so an edit reverts removed pairs to their defaults (CSSStyleDeclaration iteration of custom props is not portable). */ private appliedWidgetStyleProps; /** * `widget-style` sugar → custom properties on the WIDGET LAYER, never the * authored element (matching the React lane): inline style on an * authored element would collide with author styles and leak into any * outerHTML serialization. Unparseable/out-of-range pairs are skipped * live; validation carries the loud version. Author page-CSS tokens on * `om-map { --om-widget-*: … }` still cascade INTO the layer; the sugar * (being inline on the layer) wins over page CSS, matching attr > CSS. */ private applyWidgetStyle; registerWidgetInternal(el: Element, watch: string[], notify: (ctx: RuntimeContext) => void): void; unregisterWidgetInternal(el: Element): void; registerOverlayInternal(overlay: OverlayHost): void; unregisterOverlayInternal(overlay: OverlayHost): void; /** * rAF-batched (spec: "Overlay Renderer" — the callback only stores the * latest state and schedules one requestAnimationFrame; every overlay's * `viewport.project()` + DOM write happens in that one flush, coalescing * multiple viewport/selection changes per frame into one DOM pass). */ private scheduleOverlayFlush; /** * Dynamic overlap avoidance (spec: "Widget Layout Manager / Overlap * avoidance") — a slot container whose rect intersects an OPEN * `` popup is dimmed (`--om-widget-opacity-dimmed`, default * 0.35) rather than repositioned. Own overlays only (issue Q1: foreign * absolutely-positioned DOM is unknowable). `widgets-dim="off"` opts out. * Real-geometry only — verified behaviorally in the layout audit, never * modeled (headless has no rects; this no-ops there). */ private dimSlotsAgainstOverlays; /** widgets-toggle can sit in any author-chosen slot, so still scan for it (attribution/badge are covered by the slot flag). */ private slotHasNeverHidesWidget; emit(event: string, payload?: Record): void; /** Recenters (and optionally rezooms) the map — instant, not an animated fly. */ flyTo(coords: [number, number], zoom?: number, opts?: { duration?: number; curve?: boolean; }): void; setLayerVisible(id: string, visible: boolean): void; /** A snapshot of the current layer IRs — the same shape `ctx.layers` is built from. */ getLayers(): readonly LayerIR[]; private notifyWidgets; private buildCurrentCtx; /** Picked object, normalized to flat — GeoJSON properties lifted; columnar rows materialized from columns at the pick index. */ private resolvePickedObject; /** * The single selection path — deck.gl's real picks (via RuntimeCore's * onSelectionChange callback) and synthetic harness picks * (injectPickInternal) both land here, so flattening, columnar row * materialization, widget notification, overlay flush, and behavior * dispatch are same-path by construction. * * Flattening happens here (not in toSelection()) — this is the one place * with access to the picked layer's shape, needed for GeoJSON `{{field}}` * template interpolation against ctx.selection.object (spec: "Because the * picked object is already normalized to flat..."). Columnar layers pick * with no object at all (deck.gl's non-iterable data path only reports the * index) — materialized here, the one place with both the index and the * layer's columns. */ private handleSelectionChange; /** * The single drag-pick path — deck.gl's continuous drag picks (via * RuntimeCore's onDragPick callback) and synthetic harness drags * (injectDragPickInternal) both land here. Deliberately does NOT touch * ctx.selection (spec: a widget watching `selection` shouldn't re-render * on every drag-move) — it only dispatches `on="drag"` behaviors. */ private handleDragPick; /** * Synthetic pick entries for the test harness (spec: "Consumer Testing * Surface") — internal-suffix API like registerWidgetInternal, consumed * by mountForTest's pick()/clearSelection(), not part of the public * authoring surface. */ injectPickInternal(selection: Selection | null, pickType?: "hover" | "click"): void; injectDragPickInternal(selection: Selection): void; /** Every click/hover map coordinate (spec: "Manual Drawing"): drives the * internal draw controller AND fires the public `om-map-point` event, so * consumer capture tools the built-in draw widget doesn't cover * (rectangle/circle AOIs) can subscribe. The two consumers are * DECOUPLED: the event dispatches first (dispatchEvent isolates a * throwing listener), then the draw controller runs — so neither a * throwing draw session nor a throwing listener can starve the other. * Fires at pointer rate on hover (no debounce — vertex capture needs * every point); heavy listeners should throttle their own work. */ private handleMapPoint; /** * XY snapping's snap-tip (spec: issue #34 Part A — "a snap tip shows * which agent fired on which layer, otherwise users can't tell why a * vertex jumped"). A SEPARATE runtime overlay from the shared * `show-tooltip` singleton (`actions.ts`'s `getOrCreateTooltipOverlay`): * that one is `anchor-from="selection"` (tied to `ctx.selection`) and * shared across every `show-tooltip` behavior a page authors (this * page's own z-tooltip, for one) — reusing it here would fight over the * same element on every hover. This one anchors at a plain, static * `anchor="[lng,lat]"` instead, since a snap position has nothing to do * with whatever `ctx.selection` currently holds. */ private ensureSnapTipOverlay; private handleSnapPoint; /** Harness map-point injection (spec: "Consumer Testing Surface") — the * same path a real deck click/hover coordinate takes, so the om-map-point * event and custom capture tools are testable without a GPU. */ /** * The map's "done drawing" promise (issue #37): resolves once every live * 3D tileset has refined for the CURRENT view and one further animation * frame has been painted — the await a pull-model frame renderer (or a * consumer Playwright test) needs before screenshotting. Resolves * `{settled:false}` with a console warning on timeout rather than * rejecting (a late frame is ordinary pop-in, not an error). v1 gates 3D * tilesets only — basemap raster tiles are not yet awaited. */ whenSettled(opts?: { timeout?: number; }): Promise<{ settled: boolean; }>; /** Test surface: is the determinism switch active on the core (issue #37 — must be true BEFORE the first render when data-om-recording is authored; the basemap adapter reads it in its constructor). */ isDeterministicInternal(): boolean; injectMapPointInternal(coordinate: [number, number] | null, kind?: "click" | "hover", pointerType?: string): void; /** Harness setView (spec: "Consumer Testing Surface") — the one path that reaches pitch/bearing. */ setViewInternal(partial: { longitude?: number; latitude?: number; zoom?: number; pitch?: number; bearing?: number; }): void; /** Cancels all channel-driven effects (trace/pulse), clears their per-frame patches, and removes runtime temp trace layers — story scrub restore. */ clearEffectsInternal(): void; /** A layer's current per-frame channel patch (tests/e2e assert trace progress through this). */ getAnimatedPropsInternal(layerId: string): Record | undefined; /** Current camera state (story initial-state capture) — internal-suffix API. */ getViewStateInternal(): { longitude: number; latitude: number; zoom: number; pitch: number; bearing: number; } | undefined; /** Live Tileset3D per currently-present 3D-tiles layer — the paced-flyby driver's isLoaded() gate (same surface the warm-tiles action reads). */ getLiveTilesetsInternal(): Map; /** Rendered viewport size — the paced driver's fly-to arc math needs real dims (undefined until the renderer has one). */ getViewportSizeInternal(): { width: number; height: number; } | undefined; /** Resolved scene-lighting IR (headless test inspection) — null = deck default lights. */ getLightingInternal(): LightingIR | null; /** Resolved terrain IR (headless test inspection) — null = no surface. */ getTerrainInternal(): TerrainIR | null; /** Resolved clip-box IR (headless test inspection) — null = no active box. */ getClipBoxInternal(): ClipBoxIR | null; /** * Canvas-only scene snapshot (spec: "Snapshot API") — basemap + deck * composited at device pixels. DOM widgets/overlays/badge/attribution are * NOT captured: consumers exporting imagery must render provider credits * themselves. Await `ready` first; headless maps reject. */ snapshot(opts?: SnapshotOptions): Promise; /** terrain* attributes → IR, key-substituted like basemap presets; resolution problems log once per apply. */ private resolveTerrainAttr; /** Projects a lng/lat through the current viewport (harness pixel derivation) — undefined before the viewport resolves. */ projectInternal(lngLat: [number, number]): [number, number] | undefined; /** Renderer finished async init — defer to the first reconcile if it hasn't run yet (see the field comment). */ private handleRendererLoad; private dispatchRendererLoad; /** Fires `om-map-ready` / resolves `.ready` once the first reconcile ran, the renderer is up, and no declared data URL is still loading. */ private checkReady; /** * Per-element half of the base-layout guardrail: mark the host `om-collapsed` * so the layered `min-height` floor applies, but ONLY while it genuinely * measures zero. Gating the floor this way is what lets an explicit author * height win outright — a map with any height never carries the state, so the * floor rule never matches it and a 300px map stays 300px. * * Clear-then-re-measure makes it idempotent and self-correcting: the connect- * time call runs mid-parse, where a parent sized by later siblings still * measures zero, and the post-ready call drops a floor that turned out to be * unnecessary. * * A custom state rather than an attribute or inline style, deliberately: the * host's own MutationObserver treats attribute writes as manifest edits (a * reconcile), history.ts records them as undo steps, and either would * serialize into a saved manifest. A custom state touches none of that. */ private applyHeightFloor; /** * First-shot guardrail: warn (once) if the map has no visible size once it is * ready. The base-layout default covers a bare ``, but a constrained * parent (a 0-height flex/grid cell, `height:100%` under an unsized ancestor) * can still collapse it — a silent blank map otherwise. * Skipped headless: jsdom/happy-dom report 0×0 by design, and `mountForTest` * sets the `headless` attribute, so this never fires in the test harness. */ private checkVisibleSize; private pickPayload; /** * Dispatches to every `` (live-queried, like * the toggle-layer action's own `` lookups — behaviors are * static config, not something worth maintaining a registry for). Every * attribute besides `on`/`layer`/`action` becomes an extra fixed payload * key (kebab->camel), merged with the pick-derived payload — e.g. * `target`/`anchor-offset` for show-overlay, `template` for show-tooltip. */ private dispatchToBehaviors; /** * Microtask-coalesced reconcile (spec: "MutationObserver burst batching"). * Any number of calls within one synchronous block collapse into a single * reconcile() — an LLM streaming several attribute edits in one script * shouldn't trigger a reconcile per edit. */ private scheduleReconcile; private reconcile; /** * Re-runs on every reconcile when the `validate` attribute is present — * live editing feedback, not just the `OmMap.validate(htmlString)` * pre-flight (which calls `validateManifest` directly, standalone). * Runtime errors (the error boundary) are folded in too, so the agent * sees one consistent stream regardless of origin. `validate="silent"` * still runs validation and fires the event, just skips the on-page panel * (spec: "for teams building their own error UI"). */ /** * Reports a runtime-discovered validation entry — deck.gl crashes and * license-limit violations already flow through this (via `onRuntimeError` * below); this is also the entry point for facts a WIDGET discovers * directly (no round trip through RuntimeCore) since it already holds an * `` reference — see `ifc-loader`'s approximate-georeferencing * warning. Same access-pattern precedent as `registerWidgetInternal` * above: real and callable, not part of the documented public API. */ reportRuntimeErrorInternal(entry: ValidationEntry): void; private publishValidation; } declare global { interface HTMLElementTagNameMap { "om-map": OmMapElement; } interface HTMLElementEventMap { /** Camera settled (trailing-debounced) — detail is the full camera state. */ "om-view-changed": CustomEvent<{ longitude: number; latitude: number; zoom: number; pitch: number; bearing: number; /** "user" if any change in the settled burst came from a canvas gesture; "programmatic" for pure API/action/story moves. */ origin: "user" | "programmatic"; }>; /** Every click/hover's map coordinate (null when the pointer is off any * geometry deck can unproject) — the consumer hook for custom capture * tools beyond the built-in draw widget. */ "om-map-point": CustomEvent<{ coordinate: [number, number] | null; kind: "click" | "hover"; }>; /** A Tile3DLayer finished loading its root tileset — detail carries the * authored layer id and the live deck `Tileset3D` for tools (e.g. region * export) that need the real tileset, not the IR. */ "om-tileset-load": CustomEvent<{ layerId: string; tileset: unknown; }>; /** A Route layer finished resolving its route (direct geometry or a * RoutingProvider round-trip) — detail carries the authored layer id and * the normalized route (geometry/distanceMeters/durationSec/legs/bounds), * so a page can read a provider-resolved route without re-fetching it. */ "om-route-resolved": CustomEvent<{ layerId: string; route: { geometry?: { type: "LineString"; coordinates: [number, number][]; }; distanceMeters?: number; durationSec?: number; legs?: { distanceMeters: number; durationSec: number; }[]; bounds?: [[number, number], [number, number]]; }; }>; } } export {};