From 4a444a7a26fc262c3eeed2f2d1b8ba3ead34aa5b Mon Sep 17 00:00:00 2001 From: Clint Borle Date: Wed, 9 Sep 2026 10:59:48 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20draw=20on=20the=20world=20by=20hand=20(?= =?UTF-8?q?DISPLAY=20=E2=96=B8=20Draw)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pick Area, Line or Pin, click the vertices on the real world, double-click or press Enter to finish, label and colour it. A drawn shape goes through the same annotationEngine.annotate() the voice agent uses, with its geometry supplied and manual: true, so it renders with the whiteboard look, persists, de-dups, exports to GeoJSON and clears with the board. No geocoder, Places or Overpass call is made for a drawn shape. - drawMode.js: pure session, minimum vertices, double-click de-dup, length and area, hint text, isDrawModeActive(); unit-tested - drawTool.js: Cesium + DOM half; vertices world-anchored through the resolver's pickWorldFromScreen; dashed preview; stock double-click removed for the session and restored on exit; window.__gevDrawTool seam - annotationEngine: manual specs resolve without any fetch; a drawn line is a route with mode manual, labelled with its length, never a travel time - trackingClickGesture: yields while a draw session is open, so a vertex over a contact is a vertex, not a selection - index.html / style.css: the Draw group in the DISPLAY panel - docs: CURRENT-STATE, CHANGELOG, README --- CHANGELOG.md | 7 + README.md | 2 + docs/CURRENT-STATE.md | 9 + index.html | 25 +++ src/annotations/annotationEngine.js | 49 +++++- src/annotations/annotationResolver.js | 4 +- src/annotations/drawMode.js | 169 ++++++++++++++++++ src/annotations/drawMode.test.mjs | 105 ++++++++++++ src/annotations/drawTool.js | 235 ++++++++++++++++++++++++++ src/annotations/drawTool.test.mjs | 55 ++++++ src/data/trackingClickGesture.js | 4 + src/main.js | 3 + style.css | 29 ++++ 13 files changed, 693 insertions(+), 3 deletions(-) create mode 100644 src/annotations/drawMode.js create mode 100644 src/annotations/drawMode.test.mjs create mode 100644 src/annotations/drawTool.js create mode 100644 src/annotations/drawTool.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 21544fa..a732be4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ## [Unreleased] +### Added + +- DISPLAY ▸ Draw: draw on the world by hand. Pick Area, Line or Pin, click the + vertices, double-click or press Enter to finish, label and colour it. Drawn + shapes go through the same annotation engine as spoken ones, so they render + with the whiteboard look, persist, export to GeoJSON and clear with the board. + ### Fixed - Mapped-site outages show their scheduled retry countdown and distinguish diff --git a/README.md b/README.md index 86f69c5..cadc6bb 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,8 @@ Twenty-eight tools, four jobs — the commands below come straight from the prod **🖊️ Annotate it** — a whiteboard over the real world: > 🗣️ *"Outline the state of Texas."* · *"Annotate the Texas State Capitol and its grounds"* — it draws the **actual enclosing boundary**, not a circle. · *"How far is the Eiffel Tower from the Louvre?"* — a connector arrow appears and it speaks the distance. Everything persists until you say *"clear the map."* +**✍️ Or draw it yourself** — DISPLAY ▸ **Draw**: pick Area, Line or Pin, click the vertices on the real world, double-click to finish, label it. Same whiteboard, same persistence and export, no microphone needed. + ![Zilker Park and Lady Bird Lake drawing onto the 3D city as persistent vector annotations, by voice](docs/media/01-voice-annotate-zilker.gif) ![A spoken distance measurement spanning an airport, inspected from orbit](docs/media/04-airport-distance.gif) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 1f4d861..8b00695 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2218,6 +2218,15 @@ silently demoting every later lookup for the session. - Console/dev API: `window.__gevAnnotations.tour()`, `.demo()`, `.annotate()`, `.clear()`, `.count()`, and `.list()` are the deterministic no-mic test surface. - Current known resolver gap: mall/lifestyle districts such as "The Domain, Austin" can prefer a named building over the broader retail envelope. Product decision is that districts should become envelope + key buildings, but the scoring change still needs a careful multi-case validation pass. +### Manual Whiteboard Drawing (September 2026) + +- DISPLAY ▸ **Draw** turns the globe into a whiteboard drawn by hand. Pick a shape (Area, Line, Pin), click the vertices on the real world, double-click or press Enter to finish, type an optional label and pick a colour first. Backspace removes the last vertex; Esc cancels the shape in progress, a second Esc leaves draw mode. +- Runtime entry: `src/main.js` calls `initDrawTool({ viewer, annotations })` immediately after `initAnnotations`; the handle is exposed as `window.__gevDrawTool` (`setActive`, `setShape`, `addVertex(lon, lat)`, `finish`, `cancel`) as the no-mouse test seam. +- Contract: a finished shape is submitted through the SAME `annotationEngine.annotate()` the voice agent uses, with geometry supplied and `manual: true` (`type: area` + `ring`, `type: route` + `path`, `type: pin` + coordinates). `resolveSpec` short-circuits manual specs (`resolveManualSpec`) so no geocoder, Places or Overpass call is made. A drawn area is a real (not synthesized) outline and drapes solid; a drawn line is a `route` with `mode: 'manual'`, labelled with its length and never a travel time; a pin is a point. Drawn marks therefore render with the whiteboard look, persist, de-dup, appear in `.list()` and the GeoJSON interchange, and clear with `clear_annotations` / `.clear()`. +- Vertices are world-anchored through `pickWorldFromScreen` (exported from the resolver), the same depth-aware pick cascade the voice screen fallback uses, so a vertex on a roof lands on the roof. +- Click ownership: while a session is open `isDrawModeActive()` (`src/annotations/drawMode.js`) is true and `bindTrackingClickGesture` returns before `onClick`, so a vertex over an aircraft, military contact or camera is a vertex, not a selection. The viewer's stock LEFT_DOUBLE_CLICK action is removed for the session and restored on exit. +- Pure half: `src/annotations/drawMode.js` (session, minimum vertices, double-click de-dup at 0.5 m, length/area, hint text). Regression surface: `src/annotations/drawMode.test.mjs`, `src/annotations/drawTool.test.mjs`. + ### 3D Aircraft + Tracking (June 2026) - **The TRACKED contact's 2D↔3D handoff is DEFAULT behaviour (2026-08-19), driven by camera distance alone.** It does NOT consult the DISPLAY-rail `3D` toggle, which continues to own the FLEET (the un-instanced draw-call budget stays the operator's decision). Policy lives in `src/data/trackedModelRegime.js` and is shared by both layers: enter below `TRACKED_MODEL_ENTER_ALT_M` = 150,000 m and hand back to the billboard only above `TRACKED_MODEL_EXIT_ALT_M` = 172,500 m. **The swap distance was set by playtesting on 2026-08-20:** a first pass at 1,000,000 m switched too early; 2D reads correctly at ~600 km and the handoff belongs at ~150 km. **Consequence, recorded on purpose:** the tracked contact now enters 3D NEARER than the FLEET does (`MODEL_ALT_CEIL_M` = 800,000 m, unchanged), so with the DISPLAY-rail `3D` toggle on, 150–800 km draws surrounding contacts as models while the selected one is still a glyph. Nothing double-draws (the fleet pass skips the tracked icao) and aligning the two is a fleet-side decision, deliberately out of scope. **The two thresholds are asymmetric on purpose:** a single threshold makes a tracked orbit sitting ON the boundary strobe billboard↔model as the camera's altitude wobbles across it. Do not collapse them. The latch is scoped per selection, so a new target re-evaluates against the ENTER ceiling rather than inheriting the previous target's exit band. Exactly ONE model is involved; it loads on demand when the regime opens, is HIDDEN (not released) on regime exit so re-entry has no load gap, and is released by the existing teardown on deselect/re-track/destroy. Cockpit and TR-3B suppression are unchanged. **Two invariants around it:** (a) the hysteresis latch AND the load-failure latch are per-selection state cleared by `_resetTrackedSelectionState()` in the tracking lifecycle (deselect / re-track / cross-layer / init / destroy) — the predicate's icao-change guard is defence only, since it needs a drawn frame while nothing is selected and the render governor's idle mode does not promise one; (b) on-demand loading is bounded at 3 attempts per selection with a 1.5 s backoff and one console warning naming the asset — the driver runs every `scene.preUpdate`, so an unbounded catch means a missing GLB spins load→reject at frame rate. The billboard stays the visual throughout a failed load. diff --git a/index.html b/index.html index e8b0b8d..371034a 100644 --- a/index.html +++ b/index.html @@ -444,6 +444,31 @@ 11% +
+ +
+ Shape +
+ + + +
+
+
+ + + +
+