/** * Sketch-capture controller (spec: "Manual Drawing", phases D3/D4). The * stateful island that bridges the pure DrawSession (src/draw.ts) into the * runtime: it consumes map click/hover coordinates (routed from the core's * onMapPoint), drives the in-progress preview via the per-frame channel, * pushes committed features into the live `draw:` store, toggles * double-click-zoom while a tool is active, and saves to a local file. * * Owned per (the tooltip-overlay WeakMap precedent) and driven by * `draw-*` actions (src/actions.ts) plus keyboard — so the widget stays pure * UI and no "widget injects a layer" capability is needed. */ import { DrawSession, type DrawMode } from "./draw"; import type { RuntimeCore } from "./runtime-core"; export type SaveMode = "download" | "file-system" | "both"; /** `sketch` → `draw:sketch` (idempotent if already prefixed). */ export declare function drawUrl(target: string): string; export declare class DrawController { private readonly mapEl; private readonly core; private readonly sessions; private readonly autosaveKeys; private readonly observers; private active; /** * rAF handle coalescing hover-driven preview updates (spec: "Cut/fill * volume measurement" — found debugging click-reliability in standalone * mode). `point()` used to call `renderPreview()` (which patches the * per-frame channel and rebuilds every deck.gl layer) SYNCHRONOUSLY on * every raw mousemove — far more often than the "per-frame" name implies, * since mousemove fires at native pointer-event rate, not rAF rate. That * rebuild storm intermittently corrupted deck.gl's own click/pick pipeline * (mjolnir gesture recognition losing a click shortly after a hover-driven * rebuild) — confirmed by disabling hover-triggered rebuilds entirely and * watching the click-loss disappear. Coalescing to one rebuild per actual * animation frame is what `patchAnimatedProps`'s own doc comment already * describes as "the deck-idiomatic rAF pattern" — this was a gap between * that intent and this call site, not a design change. */ private hoverRaf; /** * The coordinate `commit()` just used to close a shape (its live cursor * position at that moment), or null. The native `dblclick` DOM event * (which drives `commit()`) fires as soon as the second tap's mouseup * lands — but deck.gl's own click-gesture recognition is independently * delayed (Hammer's Tap recognizer defers confirming a "click" until it's * sure a second tap isn't coming, per its `requireFailure`/`interval` * mechanics). One of the double-click's own two taps can therefore still * arrive here as a late, ordinary "click" a few ms *after* commit() already * ran, at ~that same coordinate — with pending now empty, session.click's * own same-spot dedup (see `point()`'s click branch) has nothing to * compare against, so it silently starts a fresh one-vertex shape at the * spot the user just finished closing. * * Guarding on position (not a time window) is what keeps this from * swallowing a genuinely new, deliberate click that happens to follow * immediately after a commit — e.g. redrawing a second shape back-to-back * in a test with no elapsed wall-clock time — since that click lands at a * different coordinate and passes straight through. The match is a small * PIXEL-radius tolerance (see `point()`), not exact equality: a real * double-click's two taps are two independent physical presses and rarely * land at the bit-identical screen pixel, let alone the bit-identical * unprojected lng/lat — exact equality let real hand jitter slip past the * guard even though a synthetic, pixel-perfect test wouldn't reveal that. * Consumed (cleared) by the very next click regardless of whether it matched. */ private lastClosePos; /** The previous click's coordinate + timestamp — the first half of a possible double-tap (see DOUBLE_TAP_MS). */ private lastTapPos; private lastTapTime; constructor(mapEl: Element, core: RuntimeCore); /** A draw tool is active — om-map routes points here and suppresses selection. */ isCapturing(): boolean; /** * Watch a target's session (spec: issue #20 measure widget). The callback * fires after every mutation of that target's session — vertex added, cursor * moved, commit, cancel, clear — so the measure controller can recompute * geodesic readouts + labels live off the shared draw stack. `null` unsubs. */ setObserver(target: string, cb: ((session: DrawSession) => void) | null): void; /** This map's own committed-feature count for `target` (spec: "Cut/fill volume measurement") — see DrawSession.count's doc comment for why this beats reading the shared getDrawData mirror. */ featureCount(target: string): number; private notify; /** Enable localStorage autosave for a target and restore any saved sketch, and/or an auto-styled committed-geometry layer (draw-config action). */ configure(target: string, opts: { autosave?: string; fillColor?: number[]; lineColor?: number[]; }): void; /** * Opt-in committed-geometry visualization (spec: issue #34's region-export * demo surfaced this — a closed shape with no author-added layer is * indistinguishable from "double-click didn't work," since the ONLY * built-in visual is the in-progress preview, which clears the instant a * shape commits). Bound directly to the SAME reactive `draw:` data * URL an author's own manual layer would use — this is pure convenience * layered on top, not a replacement; an author who wants custom styling * (or a non-GeoJsonLayer rendering) still adds their own layer instead, * and both can coexist. One committed layer per TARGET (not a singleton * like `PREVIEW_ID`), since multiple draw targets can exist on one map. * * `depthTest: false` (patched, not attribute-expressible — same non- * string-prop precedent as the volume gizmo's `mesh`): a flat 2D ring has * no z of its own, so on a page with 3D content (the exact case this was * built for — outlining a footprint over a BIM building to export) it can * sit at an elevation UNDER opaque building geometry, fully occluded from * a normal camera angle — the identical depth-occlusion class already * fixed for the measure tool's cut-prism guides. This is a SELECTION * indicator, not real in-world geometry, so it's correct for it to always * read on top regardless of what 3D content surrounds it. */ private ensureCommittedLayer; setMode(target: string, mode: DrawMode | null): void; private deactivate; point(coord: [number, number] | null, kind: "click" | "hover", pointerType?: string): void; /** Drop a queued hover-coalesced render (a following immediate render already supersedes it). */ private cancelHoverRaf; /** `CLOSE_GUARD_PIXELS` converted to real meters at the current zoom/latitude (standard Web Mercator meters-per-pixel). */ private closeGuardMeters; /** Close the in-progress line/polygon — from the double-click DOM event or Enter. */ commit(): void; cancel(): void; /** Enter closes the shape, Escape cancels it. */ handleKey(key: string): void; /** Remove the most recently drawn shape (v1 "delete last" — no per-feature selection yet). */ deleteLast(target: string): void; clear(target: string): void; save(target: string, mode?: SaveMode): Promise; private sessionFor; private renderPreview; private ensurePreviewLayer; private removePreviewLayer; } /** Get-or-create the controller for a map element. */ export declare function getDrawController(mapEl: Element, core: RuntimeCore): DrawController; /** Peek without creating — om-map's hot onMapPoint/selection paths use this. */ export declare function peekDrawController(mapEl: Element): DrawController | undefined;