import type { PickingInfo } from "@deck.gl/core"; import { type LightingIR } from "./scene-lighting"; import { type TerrainIR } from "./terrain"; import { type ClipBoxIR } from "./clip-box"; import { type SnapAgent, type SnapConfig } from "./snapping"; import type { ViewOrigin } from "./external-store"; import type { LayerIR } from "./ir"; import { type Selection } from "./selection"; import type { ValidationEntry } from "./validation"; import type { MapViewport } from "./basemap"; /** * Composes the terrain + clip-box per-layer patches over a layer's own * authored props (issue #36 — the 0.6.2 regression this replaces). The old * inline merge fabricated an `extensions` key from the two patches ALONE * whenever either patch OBJECT existed — and `applyTerrain` emits an identity * patch (`extraProps: {}`) for every layer while terrain is INACTIVE, so on * every flat map every layer got `extensions: []` spread over its authored * array, silently unmounting PathStyleExtension (dash rendered solid) and * DataFilterExtension (GPU filters stopped visually applying; widget stats * stayed coherent via the CPU predicate, which is what hid it for six * releases). Three rules restore the contract the patch builders' own tests * already state: * * 1. An identity patch (no keys) is NOT a patch — authored props, extensions * included, flow through untouched (return undefined; the constructor's * `...layerProps` already carries them). * 2. A patch that carries extensions already APPENDED them to the authored * array (applyTerrain/applyClipBox both do) — never rebuild from scratch. * 3. Both active: reference-dedup the concatenation — each patch's array * starts with the SAME authored instances, and without the dedup a layer * under terrain + clip box mounted every authored extension twice. * * Exported for direct unit testing (the lerpAngle precedent). */ export declare function mergePatchExtraProps(authoredExtensions: unknown[] | undefined, terrainProps: Record | undefined, clipProps: Record | undefined): Record | undefined; /** RouteLayer's own `onRouteResolved` report (spec: "Routing & Tracking") — a normalized Route plus its bounds. `bounds` drives `follow="fit-route"` here; the WHOLE object forwards to `RuntimeCoreCallbacks.onRouteResolved` (the `om-route-resolved` consumer event), so a page can read a provider-resolved route's geometry/distance/duration without re-fetching it. */ interface RouteResolvedInfo { geometry?: { type: "LineString"; coordinates: [number, number][]; }; distanceMeters?: number; durationSec?: number; legs?: { distanceMeters: number; durationSec: number; }[]; bounds?: [[number, number], [number, number]]; } /** * The tile-level EXT_structural_metadata property-table index, resolved * independently of `FeatureMeshLayer`'s own copy of this same computation. * * `tile.content.featureIdPropertyTableIndex` (which `FeatureMeshLayer` * eventually sets, in its own `renderLayers()`) is NOT readable here: deck.gl's * `Tile3DLayer._onTileLoad` fires the `onTileLoad` prop callback (what feeds * this hook) BEFORE it ever constructs `FeatureMeshLayer` for that tile — * confirmed against `@deck.gl/geo-layers`'s own `_onTileLoad`/`_getSubLayer` * ordering. Reading that field here always saw `undefined`, silently falling * back to property table 0 regardless of which table a tile's feature IDs * actually reference — invisible only because this repo's own IFC converter * never emits more than one table. Recomputing from the tile's raw glTF with * the same pure, already-tested helpers `FeatureMeshLayer` itself uses avoids * depending on that ordering entirely. */ export declare function resolveTilePropertyTableIndex(gltf: unknown, featureIdProperty: string): number; /** Mapbox presets/protocol URLs aren't implemented — kept maplibre-free so validation/runtime can check without loading the chunk. */ export declare function isMapboxBasemap(raw: string): boolean; export interface InitialView { longitude: number; latitude: number; zoom: number; /** Initial camera tilt/rotation — first-class for 3D content (``). */ pitch?: number; bearing?: number; } interface ViewState extends InitialView { pitch: number; bearing: number; } /** Animated-camera options (spec: "Map Stories / Animation primitives"). Omit `duration` (or 0) for an instant move. */ export interface CameraOptions { duration?: number; /** Arc the move (zoom-out-and-in) instead of easing directly — MapLibre `flyTo` / deck `FlyToInterpolator`. */ curve?: boolean; } export { resolveFeatureColor } from "./feature-colors"; /** One layer's accumulated (per-tile) property tables — see RuntimeCore.featureTables. */ export interface FeatureTableEntry { source: string; tiles: Map[]>; rows: Record[]; rowsVersion: number; mappingKey?: string; mapping?: import("./feature-colors").FeatureColorMapping | null; } /** * The accumulator, extracted for direct testing: resets on a SOURCE change * (the loader widget swapping models under one layer id), REPLACES rows for a * re-seen tile key (deck evicts + refetches tiles as new objects — identity * dedup accumulated duplicates), and keeps per-tile row arrays because * feature IDs are tile-local. Returns the (possibly new) entry. */ export declare function accumulateFeatureRows(tables: Map, layerId: string, sourceKey: string, tileKey: string, rows: Record[]): FeatureTableEntry; /** * The subset of deck.gl's MjolnirGestureEvent a gizmo drag-claim needs (spec: * "Cut/fill volume measurement"). `stopPropagation()` sets the underlying * event's `handled` flag, which `Controller.isPointInBounds` checks before * starting a camera pan — calling it from `onGizmoDragStart` when the gizmo * is the picked layer is what lets a LEFT-button drag on the gizmo win over * the map's own drag-to-pan (deck dispatches its root onDragStart before the * controller's own pan handling for the same gesture, so this is in time). */ export interface GizmoDragEvent { stopPropagation(): void; } export interface RuntimeCoreCallbacks { /** * Fired whenever the view state changes (pan/zoom/pitch/bearing) — a * notification, not a value. `origin` (spec: "External-Store Contract") * distinguishes canvas gestures ("user") from camera APIs / actions / * transition frames ("programmatic") — the mapbox `originalEvent` * convention, the echo-loop half of two-way store binding. */ onViewportChange?: (origin: ViewOrigin) => void; /** * `pickType` is the pointer event that produced this pick — REQUIRED even * when `selection` is null (an empty pick), because toSelection discards * empty picks and with them their type. Overlays scoped with * `selection-type` need it: a click on empty space dismisses a * click-anchored popup, a hover over empty space must not. */ onSelectionChange?: (selection: Selection | null, pickType: "hover" | "click") => void; /** Fired once the map (deck.gl standalone, or the basemap) has finished its async init (spec: Behavior Engine "load" event). */ onLoad?: () => void; /** * Continuous drag-pick events (spec: Behavior Engine "drag" event) — kept * separate from `onSelectionChange`/`ctx.selection`, which is scoped to * discrete hover/click picks; a widget watching `selection` shouldn't * re-render on every drag-move. */ onDragPick?: (selection: Selection) => void; /** * Raw deck.gl drag lifecycle (spec: "Cut/fill volume measurement"), * forwarded verbatim alongside `onDragPick` — for a controller that owns a * pickable internal layer (the volume tool's height gizmo) and needs the * real `x`/`y`/`viewport`, not a resolved `Selection`. `onDragPick` stays * the mechanism for declarative `on="drag"` behaviors; this is the * mechanism for a controller driving its own layer via patchAnimatedProps. */ onGizmoDragStart?: (info: PickingInfo, event: GizmoDragEvent) => void; onGizmoDrag?: (info: PickingInfo) => void; onGizmoDragEnd?: (info: PickingInfo) => void; /** * Every click/hover's map coordinate (spec: "Manual Drawing"), fired * ALONGSIDE onSelectionChange but INCLUDING empty-map events — which * `toSelection` discards (no picked object, index < 0), yet sketch capture * needs (a vertex dropped in blank space). Coordinate is null only when * deck reports none (e.g. off-globe). */ onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover", pointerType?: string) => void; /** * XY snapping (spec: issue #34 Part A) — fires ALONGSIDE onMapPoint on * every click/hover, `null` whenever that point ISN'T a snap (no config, * no candidate within tolerance, an opted-out/unsupported layer) — the * snap-tip UI's entire "show only while actually snapped" contract reads * off this being null vs. set, not off onMapPoint's own coordinate. * Carries `position` itself (not just agent/layer) so a consumer never * has to correlate this against a SEPARATE onMapPoint firing in the same * tick to know where to anchor a tip — both already come from the one * `resolveMapPoint` call. */ onSnapPoint?: (result: { position: [number, number]; agent: SnapAgent; layerId: string; elevation?: number; } | null) => void; /** * A `pick-features` layer decoded its EXT_structural_metadata property * table — the whole table, indexed by feature ID. Fires once per layer, on * the first tile that carries one, because every tile in a tileset shares * the same schema. This is what lets a legend enumerate categories without * a hand-built sidecar next to the tileset. */ onFeatureTable?: (layerId: string, rows: Record[]) => void; /** * A tileset-bearing layer (Tile3DLayer) finished loading its root tileset * — the consumer hook for tools that must reach the LIVE deck tileset * (region export, custom traversal) which the IR/metadata surface can't * expose. `tileset` is deck's `Tileset3D` (typed `unknown` here to avoid a * @loaders.gl/tiles type dependency in the core). */ onTilesetLoad?: (layerId: string, tileset: unknown) => void; /** * A `carriesRoute` layer (Route) finished resolving its route — direct * `geometry` or a `RoutingProvider` round-trip alike. The `om-route-resolved` * consumer event's source; `route` carries the normalized geometry/ * distanceMeters/durationSec/legs plus the fitted bounds. */ onRouteResolved?: (layerId: string, route: RouteResolvedInfo) => void; /** * A `carriesGeoreference` layer (BIMLayer) finished loading its source file * and read whatever georeferencing it declares — `hasFullMapConversion` * is true only when the file both resolved a real position * (`IfcMapConversion` into a recognized CRS) AND declared * `OrthogonalHeight`, the two facts absolute elevation needs to mean * anything. Informational for consumers: the library never auto-applies * terrain from it (what-you-write-is-what-you-see) — validation instead * requires the map to author `terrain` explicitly for these layers. * * `approximatePlacement` is a narrower, separate fact: true whenever the * file's position did NOT come from a real `IfcMapConversion` (IfcSite * lat/lon, or no georeference at all) — that source has no rotation data * at all, and is very often an authoring tool's default location rather * than a survey. A plain boolean (not the raw `originSource` string) to * keep this file decoupled from IFC-specific types, same reasoning as * `hasFullMapConversion` already being precomputed rather than passed as * raw parts. The front-end layer surfaces this as a validation warning. */ onGeoreference?: (layerId: string, info: { orthogonalHeight?: number; hasFullMapConversion: boolean; approximatePlacement: boolean; }) => void; /** * Runtime error boundary (spec: "Runtime error boundary") — deck.gl-level * failures (a crashing accessor, an incompatible prop) formatted into the * same structured shape Manifest Validation uses, so the agent sees one * consistent error contract regardless of origin. */ onRuntimeError?: (entry: ValidationEntry) => void; } /** * Stable front-end-owned hosts for legally required map chrome. Keeping the * hosts in the managed slot tree lets framework and HTML front-ends share * the same collision-free layout without giving the renderer ownership of * either front-end's DOM. */ export interface MandatedChromeHosts { badge?: HTMLElement; attribution?: HTMLElement; } /** Headless mode config (spec: "Consumer Testing Surface") — an explicit size, since jsdom layout reports 0×0. */ export interface HeadlessOptions { width: number; height: number; } /** Basemap-preset options (spec: "Basemap presets & switching") — the publishable provider key and the attribution opt-out. */ export interface BasemapRuntimeOptions { /** `basemap-key` — substituted into keyed presets' `{key}` placeholder (falls back to configureBasemap). */ key?: string; /** `attribution="false"` opt-out — default true: the compact attribution control renders whenever a basemap is active. */ attribution?: boolean; } export declare class RuntimeCore { private deck?; private basemap?; /** * Which renderer owns the view. Branching happens on the MODE, not on * `this.basemap` — in basemap mode the adapter arrives asynchronously * (the maplibre chunk lazy-loads), and during that window camera/layer * calls must route to the (not-yet-present) adapter, never fall through * to a standalone Deck that doesn't exist. */ private mode; /** Layers reconciled while the basemap chunk is still loading — applied on adapter arrival. */ private pendingLayers?; /** setDrawCapture's last value — composed with dragPanSuppressed by standaloneControllerOverrides. */ private drawCaptureActive; /** setDragPan's last value, inverted — see standaloneControllerOverrides. */ private dragPanSuppressed; /** Retained descriptors — what the per-frame channel re-applies against. */ private lastIRs; /** layerId → effect-driven plain-prop patches (the per-frame channel). */ private animatedProps; private headless?; /** Animated-props flush coalescing (see patchAnimatedProps): patches since the last rebuild / a microtask flush already queued. */ private animatedDirty; /** * The determinism switch (issue #37; set from om-map's data-om-recording * attribute or MapController.setDeterministic): while on, NOTHING animates * on the wall clock — camera moves land instantly, effect verbs snap to * their end state, GPU transitions are zeroed at parse — so a frame * captured after whenSettled() is a pure function of story time, and * frame N renders byte-identically across processes and orderings. */ private deterministic; /** layerId → live loaders.gl Tileset3D (see tilesetHook in buildLayers). */ private liveTilesets; /** The lastIRs reference getLiveTilesets last pruned against (see its comment). */ private liveTilesetsPrunedFor; private animatedFlushQueued; private destroyed; private viewState; private callbacks; private parent; /** The live `basemap` attribute value — re-read by the lazy adapter chunk so a switch during its load lands the LATEST style. */ private basemapAttr?; private basemapOptions?; /** Bumped on every renderer (re)init — a superseded lazy chunk load must not install its adapter over a newer renderer. */ private rendererGeneration; /** Pre-gate descriptors — what a license settle re-reconciles from (lastIRs holds the GATED set the renderer sees). */ private pregateIRs; /** * Scene lighting (spec: "Scene Lighting") — the retained IR, rebuilt into * a LightingEffect on apply. Survives renderer remounts (initRenderer * reads it, the lastIRs contract) and the lazy basemap-chunk window (the * adapter constructor reads it on arrival). null = deck default lights. */ private lighting; /** * Terrain (spec: "Terrain") — the retained surface IR. Non-null appends * the internal terrain layer and patches drape/offset layers in * buildLayers. `terrainGeneration` bumps on every terrain state change: * extension sets must be BIRTH-stable, so patched layers get * `#t`-based deck ids (fresh instances per flip — see * applyTerrain); an explicit renderer-id map restores authored pick ids. */ private terrain; private terrainGeneration; /** * Clip box (spec: issue #34 §"Cutting / Clipping") — the retained box IR. * Non-null patches every opted-in layer with `ClipBoxExtension` in * buildLayers (see `applyClipBox`'s own doc comment for the default- * applied, `clip="off"`-to-exclude posture). `clipBoxGeneration` bumps on * every active-state flip, same reason and same fix as terrain's own * generation counter: extension sets must be BIRTH-stable (verified * empirically — a box present from the first render works, toggling one * onto an already-mounted DataFilterExtension-carrying layer silently * blanks it), so patched layers get `#c`-based deck ids. */ private clipBox; private clipBoxGeneration; /** XY snapping (spec: issue #34 Part A) — `null` is the free/common case (no pickingRadius change, resolveMapPoint short-circuits to the plain pick). */ private snapConfig; /** See resolveMapPoint's memo comment — the last picked object's derived snap geometry. */ private snapGeometryMemo; /** See pickForSnap's memo comment — the non-terrain layer-id list, keyed by the deck layer array's own identity. */ private snapLayerIdsMemo; /** Spacebar-held suppression (spec: issue #34 Part A) — deliberately leaves `pickingRadius`/`snapConfig` untouched, only gates `resolveMapPoint`'s refinement step, since the underlying pick tolerance isn't what a user means by "hold to disable snapping momentarily." */ private snapSuppressed; /** Per-BIMLayer lonLat/heading/scale, captured off `onGeoreference` (see `BimGeoreferenceInfo`) — `resolveMapPoint`'s only way to convert a picked EdgeRow's raw local vertices back to real `[lng, lat]` for snapping, since that georeference otherwise lives entirely inside BIMLayer's own async load state. */ private bimGeoreference; /** Rendered deck layer id → authored manifest/controller id (terrain uses fresh renderer ids). */ private renderedLayerIds; /** The decoded property table for a pick-features layer, or undefined before its first tile. */ getFeatureTable(layerId: string): Record[] | undefined; /** * Decoded property tables per `pick-features` layer, keyed by layer id. * Held here rather than in the element because LAYER props derive from them * too (`feature-color-by`), not only widgets. `rows` accumulates across * every tile that has loaded so far (see `featureTableHook`) — for a * grid-tiled model this means the table is only as complete as whatever * has actually streamed in, same as any real tiled 3D-Tiles viewer. */ /** * Per-layer accumulated property tables (spec: BIM feature picking). Rows * are kept PER TILE — feature IDs are tile-local, so a flat concatenation * can never be indexed by feature ID (the multi-tile styling bug). `rows` * is the derived aggregate for legends/`ctx.features()`/global domains; * `tiles` is keyed by a STABLE content key so an evicted-and-refetched tile * REPLACES its rows instead of duplicating them, and `source` is the layer's * source URL string (not the per-parse rows-array identity, which reset the * whole accumulator on every reconcile mid-stream). `mapping` memoizes the * derived colour mapping so animation frames reuse one identity — the * style-texture-rebuild-per-rAF fix. */ private featureTables; /** Fallback identity for tiles that expose no content URL — a stable per-object key. */ private tileKeyFallback; private tileKeyCounter; /** * The memoized global colour mapping for a layer — rebuilt only when the * accumulated table grows or a colour prop changes, so `applyLayers` calls * from the per-frame channel (patchAnimatedProps runs one per rAF during * story animations) hand every tile the SAME mapping identity and no GPU * style texture is ever rebuilt for an unrelated animation. */ private featureColorMappingFor; /** * The basemap attribute suppressed while terrain is active — a flat * MapLibre canvas at sea level visibly desyncs from a raised surface * (true coexistence is the interleaved-compositing TODO), so terrain * forces standalone rendering and restores the basemap when it turns off. */ private suppressedBasemapAttr?; /** * Snapshot capture queue (spec: "Snapshot API") — the WebGL context has * no preserveDrawingBuffer, so pixels are only readable synchronously * inside a post-render callback. snapshot() enqueues a capture and forces * a redraw; the permanent onAfterRender (standalone Deck constructor) * drains the queue while the drawing buffer is valid. `cancel` settles the * promise when the renderer goes away before that frame renders (teardown/ * destroy) — a queued capture must never leave its caller hanging. */ private snapshotCaptures; /** (layer, reason) pairs already reported — gate errors fire once per violation, not per reconcile. */ private readonly gateWarned; private badge?; private quotaNotice?; private unsubscribeLicense?; private readonly chromeHosts; constructor(parent: HTMLElement, initialView: InitialView, callbacks?: RuntimeCoreCallbacks, basemapAttr?: string, headless?: HeadlessOptions, basemapOptions?: BasemapRuntimeOptions, chromeHosts?: MandatedChromeHosts); /** `basemap` attribute → concrete style, logging resolution problems (unknown preset / missing key) — the demo-style fallback still renders. */ private resolveBasemap; /** * Constructs the renderer for the requested mode — the constructor's * body, extracted so setBasemap() can remount on a renderer-MODE change * (none ↔ maplibre). Reads/seeds the camera from `this.viewState`. */ private initRenderer; /** * The five pick/drag deck callbacks, shared verbatim by the lazy MapLibre * adapter's deckProps and the standalone `new Deck` — one place to edit the * pick path (they had drifted into two pasted copies). `onLoad`/`onError` * stay per-branch: the basemap path wires load through `basemap.onLoad`. * `toSelection`'s `type` param on drag is a required tag, not meaningful — * the behavior dispatch (om-map.ts) uses its own "drag" event name. */ private pickCallbacks; /** The current lighting as a deck `effects` array — [] restores deck's default lights. */ private buildEffects; /** * Scene lighting (spec: "Scene Lighting") — swap the LightingEffect on * the live renderer. null restores deck's default lights. Headless stores * the IR (inspectable via the map's internal getter) with no renderer to * drive; a change during the lazy basemap-chunk window is picked up by * the adapter constructor (which reads `this.lighting` on arrival). A * sunDate-driven sun resolves az/el HERE, against the current map center * — deterministic per apply, not tracked per frame. */ setLighting(ir: LightingIR | null): void; getLightingInternal(): LightingIR | null; /** * Terrain on/off/source change (spec: "Terrain"). Bumps the generation * (fresh deck ids for extension-carrying layers — birth-stable extension * sets), swaps the renderer mode when needed (terrain suppresses an * active basemap; turning it off restores the suppressed one), and * re-applies layers. Headless stores the IR for inspection. */ setTerrain(ir: TerrainIR | null): void; getTerrainInternal(): TerrainIR | null; /** Clip box on/off/extent change (spec: issue #34) — no basemap-replacement dance like terrain's (a clip box never changes render mode), just a straight re-apply. Bumps the generation on an active-state flip (fresh deck ids — see the `clipBoxGeneration` field doc). */ setClipBox(ir: ClipBoxIR | null): void; getClipBoxInternal(): ClipBoxIR | null; /** * XY snapping on/off + tolerance change (spec: issue #34 Part A). Unlike * clip box/terrain, this never touches the layer list — only deck's * `pickingRadius` (0 by default and never otherwise set in this * codebase; without it a cursor has to land EXACTLY on rendered pixels * to pick anything, which defeats snapping's own point of "near enough * counts") and `resolveMapPoint`'s own read of `this.snapConfig` on the * next pick — no `applyLayers()` needed. */ /** Spacebar held/released (spec: issue #34 Part A) — no-op when snapping isn't configured at all. */ setSnapSuppressed(suppressed: boolean): void; /** * Whether a snap resolver is actually configured — the ONLY condition under * which om-map's window-level keydown listener may `preventDefault()` the * spacebar. Without this gate, every page embedding an `` lost * space-to-scroll and space-to-activate-a-focused-button, snapping * configured or not (`setSnapSuppressed` no-ops in that case, but * `preventDefault` does not). */ hasSnapConfig(): boolean; setSnapConfig(config: SnapConfig | null): void; /** * `coordOf` plus snap refinement — the two-stage resolver (see * src/snapping.ts's own header comment): a pick (stage 1) tells us which * feature + viewport are under the cursor; `resolveSnap` (stage 2) * refines that to the nearest vertex/edge/midpoint. Falls back to the * plain pick on no config, no layer/viewport (an empty-map pick), a * `snap="off"` layer, or no candidate within tolerance — `resolveSnap`'s * own `null` covers the last case AND "geometry shape this module * doesn't understand" (e.g. a raw meter-offsets PathLayer row) * identically, which is the right behavior either way: nothing to add, * use the raw pick. * * Stage 1 does NOT simply reuse `info` (deck's own top-level pick that * already drove `onMapPoint`/`onSelectionChange`) whenever terrain is * active — confirmed live, terrain wins deck's pick-buffer resolution * against ANY co-located pickable content, vector or BIM mesh alike, * even where that content visibly, correctly renders on top of it (the * SAME finding already documented on this session's z-tooltip work, one * layer up: there it was worked around with an unscoped hover behavior; * here, where snapping specifically needs to know WHICH non-terrain * feature is under the cursor, that workaround doesn't apply). Instead, * re-picks explicitly via `deck.pickObject`, `layerIds`-restricted to * everything EXCEPT the terrain layer — deliberately narrow (standalone * mode only, terrain's own required mode) rather than a general-purpose * picking API this codebase otherwise avoids. */ private resolveMapPoint; /** * See `resolveMapPoint`'s own doc comment for why this re-picks rather than * reusing deck's own top-level `info`. `radius` mirrors the configured * tolerance — a vertex sitting outside the tolerance couldn't win anyway, * and this keeps the explicit re-pick's own search window consistent with * the resolver's. * * The `layerIds` list is memoized against the layer ARRAY's identity: this * runs on every pointer move while snapping is on, and deck only hands out * a new array when the layer set actually changes, so the filter+map runs * per layer-set change instead of per mousemove. (The `pickObject` call * itself is a synchronous GPU readback and remains the real cost here — * this just stops adding avoidable per-move allocation on top of it.) */ private pickForSnap; private drainSnapshotCaptures; /** Settle queued snapshot promises when the renderer they were waiting on goes away (mode flip / destroy) — never leave a caller hanging. */ private cancelSnapshotCaptures; /** * Canvas-only scene snapshot (spec: "Snapshot API") — the composite of * basemap + deck canvases at device pixels, captured right after a forced * repaint (no preserveDrawingBuffer in either context). DOM widgets, * overlays, the badge, and the attribution control are NOT captured — * consumers rendering exports must add provider credits themselves. * Headless rejects (no renderer); pre-ready rejects (await map.ready). */ /** The basemap's own settle state (style + tiles for the current view) — true when standalone/headless or the chunk hasn't landed. */ basemapIdle(): boolean; /** deck's own per-layer readiness (async layer/pipeline init) — whenSettled gates on this too; a capture before it is stably missing content (issue #37's flaky first frame). Headless/basemap-pending count as ready. */ layersReady(): boolean; /** Force one renderer draw without capturing (issue #37 — whenSettled's completed-render guarantee; a fresh page's first capture otherwise misses content that has never drawn). */ forceDraw(): void; snapshot(): Promise; /** * Live basemap change (spec: "Basemap presets & switching"), both paths: * - maplibre → maplibre: `map.setStyle()` — deck layers survive (the * overlay is a control, not style layers) and the camera is untouched. * - renderer-MODE change (none ↔ maplibre, or a mapbox fallback): full * remount seeded with the current camera; the retained IRs are re-handed * to the new renderer, so the scene carries over. * Headless ignores it (no renderer to switch); `om-map-ready` semantics * are unaffected (readiness settles once, at initial load). */ setBasemap(attr: string | null): void; private teardownRenderer; /** * Rebuilds every deck.gl Layer instance from the current IR and hands the * full array to deck.gl (or the basemap's MapboxOverlay) on every * reconcile — the underlying layer manager's own id-keyed prop diffing * decides what actually needs to change. Accessor props carry * `updateTriggers` fingerprints (Layer Reconciliation) so deck.gl * recomputes an attribute iff the accessor's source actually changed, * rather than never (deck.gl ignores accessor function identity on its * own) or on every pass. */ reconcile(irs: LayerIR[]): void; private buildLayers; private applyLayers; /** * The sanctioned per-frame DOM bypass (spec: "Map Stories / per-frame * channel"): merge plain-prop patches into a layer's descriptor and * re-apply immediately from the retained IRs — rebuilding deck layer * instances per frame is the deck-idiomatic rAF pattern. `null` clears a * layer's patches. Headless stores the patches (inspectable via the map's * internal getter) with no renderer to drive. */ patchAnimatedProps(layerId: string, props: Record | null): void; clearAnimatedProps(): void; getAnimatedProps(layerId: string): Record | undefined; /** Live Tileset3D per currently-present tiles layer — the warm-tiles surface and the paced gate. Pruned only when the IR set actually changed: the paced driver polls this every rAF of a tile hold, and re-proving an unchanged layer set per poll is pure waste. */ getLiveTilesets(): Map; /** * The current resolved viewport (for `ctx.viewport`'s bounds/project) — * always fresh, never cached. Standalone: `Deck#getViewports()` asserts * its internal `viewManager` is set, which only happens after Deck's own * async init — `isInitialized` guards the window between construction * (widgets can register and request a ctx synchronously, before that init * resolves) and the first frame. With a basemap: the MapLibre-backed * adapter, once the basemap's style has loaded. */ getViewport(): MapViewport | undefined; /** Renderer init state — feeds the `om-map-ready` signal. Headless has nothing async to wait for. */ isReady(): boolean; /** * Synthetic pick injection (spec: "Consumer Testing Surface") — invokes * the SAME onSelectionChange callback deck.gl's real picks arrive through, * so flattening, columnar row materialization, widget notification, * behavior dispatch, and the overlay flush are same-path by construction. * Available in every mode (a browser-level test may use it too). */ injectPick(selection: Selection | null, pickType?: "hover" | "click"): void; getViewState(): Readonly; /** * Sketch capture (spec: "Manual Drawing", D4) — while a draw tool is active, * disable the map's double-click-zoom so double-clicking to close a * line/polygon doesn't also zoom. Honored in both mount paths. Headless is a * no-op (no controller). Deliberately narrow: click-to-place-vertex doesn't * fight drag-pan, so pan stays live. */ setDrawCapture(active: boolean): void; /** * Suspend/restore drag-to-pan while a gizmo is grabbed — otherwise a drag * on the gizmo also pans the camera underneath it. Originally standalone- * only (the volume tool's own height gizmo requires terrain, which always * forces standalone mode — see setTerrain — so it never hit basemap mode * in practice); the clip-box face-handle gizmo (issue #34) has no such * precondition and runs fine with a plain basemap, which surfaced the gap * for real (reported: dragging a handle panned the map underneath it and * the two fighting over every mousemove read as the whole page hanging). * `basemap.setInteractive` is the SAME mechanism setDrawCapture already * uses for doubleClickZoom, just a different MapLibre gesture handler. */ setDragPan(active: boolean): void; private standaloneControllerOverrides; /** * Used by the built-in `zoom-controls` widget's emitted zoom-in/zoom-out * intents. Programmatic, so — unlike a user drag/scroll — standalone mode * never goes through deck.gl's own `onViewStateChange` controller * callback; fire `onViewportChange` directly so `viewport`-watching * widgets (scale-bar) still pick up the change. With a basemap, MapLibre's * own `jumpTo` already fires a `move` event (wired to `onViewportChange` * in the constructor), so no manual notification is needed there. */ zoomBy(delta: number): void; /** * Recenters (and optionally rezooms) the view — `map.flyTo(coords)` * (spec: "Runtime Core" programmatic API) and the `zoom-to-feature` * action's point form both go through this. Instant, not an animated * fly, in both modes — smooth transitions are a future addition (deck.gl * `transitions` / MapLibre's own animated `flyTo`), not implemented here. */ flyTo(center: [number, number], zoom?: number, opts?: CameraOptions): void; /** * Merges a partial view state — the harness's `setView` (and the only * path that reaches pitch/bearing, which `flyTo`/`zoomBy` don't touch). * Standalone/headless update the tracked state directly; with a basemap, * center/zoom delegate to the basemap's camera (which owns the view) and * pitch/bearing are a documented not-yet gap there. */ setDeterministic(on: boolean): void; isDeterministic(): boolean; setViewState(partial: Partial, opts?: CameraOptions): void; /** * Recenters and rezooms to fit a `[[minLng,minLat],[maxLng,maxLat]]` box — * the `zoom-to-feature` action's bbox form (GeoJSON polygons/lines). * Standalone falls back to a plain `flyTo` at the box's midpoint if the * viewport isn't initialized yet (mirrors `getViewport()`'s own guard); * with a basemap, MapLibre's own `fitBounds` handles this directly. */ flyToBounds(bounds: [[number, number], [number, number]], padding?: number, opts?: CameraOptions): void; destroy(): void; }