Keep Draw rows in place across board changes, and frame lines across the antimeridian

The shape list rebuilt every row on each board change. A name being typed was
lost when a voice mark landed or expired, and a click right after typing only
committed the name, because the button under the pointer was replaced. Rows
are now updated in place, a re-entrant render is deferred, picking a colour or
Esc returns focus to the swatch, and keys on the list no longer reach Draw.

Fly-to on a line across the antimeridian flew to longitude 0. A drawn mark
and a voice mark are no longer deduplicated into one another, so a voice TTL
cannot expire a drawn pin, and a new pin on a hidden one is drawn.

The engine tests now check effects (renderer calls, alpha, notifications on
clear and expiry, the framed centre), not only return values.
This commit is contained in:
Said Turksever committed 2026-10-30 15:23:56 +00:00
1 parent 94470ff00e
commit bdbbc3d767
6 files changed
+428 -108

No files matched your search

+2 -2
View File
@@ -5420,8 +5420,8 @@ request viewbox and continues searching without that geographic preference.
### Manual Whiteboard Drawing (September 2026)
- UTILITIES ▸ **Draw** turns the globe into a whiteboard drawn by hand. Pick a shape (icon buttons for Area, Line and Pin) and a colour swatch for the next shape, click the vertices on the real world, and finish. **Keyboard contract, live only while Draw is on and never while you are typing in another field:** Enter finishes the shape (as does a double-click, and the two cannot both submit it — the session is replaced before the annotate call is awaited); Backspace removes the last vertex; Esc cancels the shape in progress, and a second Esc — Esc on an empty session — leaves draw mode. **Clear** wipes the board (the shape in progress and every placed mark), so removing marks does not require the console or the microphone.
- **Drawn shapes are listed under Draw like Import's files** (`src/annotations/drawMarkList.js`): one row per hand-drawn mark (`manual: true`; voice marks are not listed) with its colour swatch, its name, and show/hide, fly-to and remove. The name is typed in the row (Enter or leaving the field saves it, Esc restores it and stays in the field); an unnamed shape shows its kind, and an unnamed line its length. The swatch opens the five colours in the row. The list reads the engine's board and redraws on `onChange`; it asks for changes through the engine's per-mark `remove`, `setHidden`, `frame` and `restyle`. A hidden mark stays on the board at alpha 0, so it keeps its place and comes back at once; `restyle` re-adds the mark with the new name or colour, and a renamed line keeps its length (`Name — 1.2 km`). Fly-to frames a line by its whole path, not its first point.
- Runtime entry: `createApplicationTools` (`src/app/tools.js`, behind the `src/standalone/tools.js` shim) calls `initDrawTool({ viewer, annotations })` after `initAnnotations` and defers its `destroy()`, so disposing the shell returns every DOM listener, the Cesium handler, the preview data source, the borrowed click actions and the `window.__gevDrawTool` handle. Its markup lives in the UTILITIES panel's component template (`src/ui/templates/layer-panels.html`), assembled into `index.html` at build time. That handle (`setActive`, `setShape`, `addVertex(lon, lat)`, `finish`, `cancel`, `clearAll`, `diagnostics`) is the no-mouse test seam.
- **Drawn shapes are listed under Draw like Import's files** (`src/annotations/drawMarkList.js`): one row per hand-drawn mark (`manual: true`; voice marks are not listed) with its colour swatch, its name, and show/hide, fly-to and remove. The name is typed in the row (Enter or leaving the field saves it, Esc restores it and leaves the field without leaving Draw); an unnamed shape shows its kind, and an unnamed line its length. The swatch opens the five colours in the row; choosing one, or Esc, closes them and returns focus to the swatch. The list reads the engine's board on `onChange` and updates its rows in place rather than rebuilding them, so a click that is half done, or a name being typed, survives a board change; keys on the list's controls are the list's, not the drawing's. it asks for changes through the engine's per-mark `remove`, `setHidden`, `frame` and `restyle`. A hidden mark stays on the board at alpha 0, so it keeps its place and comes back at once; `restyle` re-adds the mark with the new name or colour, and a renamed line keeps its length (`Name — 1.2 km`). Fly-to frames a line by its whole path, not its first point, and centres a line across the antimeridian there. A drawn mark and a voice mark are never deduplicated into one another, and a hidden mark is never refreshed by a new one.
- Runtime entry: `createApplicationTools` (`src/app/tools.js`, behind the `src/standalone/tools.js` shim) calls `initDrawTool({ viewer, annotations })` after `initAnnotations` and defers its `destroy()`, so disposing the shell returns every DOM listener, the Cesium handler, the preview data source, the borrowed click actions and the `window.__gevDrawTool` handle. Its markup lives in the UTILITIES panel's component template (`src/ui/templates/layer-panels.html`), assembled into `index.html` at build time. That handle (`setActive`, `setShape`, `addVertex(lon, lat)`, `finish`, `cancel`, `clearAll`, `marks`, `setColor`, `diagnostics`) is 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 clear with `clear_annotations` / `.clear()`.
- **A vertex is a lon/lat, and the mark is DRAPED.** Clicks are picked through `pickWorldFromScreen` — one of two scene helpers the annotation resolver factory (`src/annotations/resolver.js`) exposes alongside `sampleGroundHeight`, so the app has ONE depth-aware pick cascade rather than a second written beside it — which returns the height the pick landed on as well as the coordinate, so the live preview hangs on the roof or hillside under the pointer instead of sinking to sea level. The world renderer draws areas and routes with `clampToGround`, so the height is dropped at finish in one place (`finishSpec`) rather than being carried downstream and discarded silently: what you get is the outline you drew, lying on the surface beneath it.
- **Draped marks classify onto `ClassificationType.BOTH`** — terrain AND 3D tiles — not onto 3D tiles alone. With only `CESIUM_3D_TILE`, every area fill, area outline, route and arrow rendered to nothing on a keyless boot, where Cesium's own globe carries Esri/OSM imagery and there are no tiles to classify against; the labels appeared and the geometry did not. That hit spoken annotations exactly as hard as hand-drawn ones. `scripts/qa-draw-tool.mjs` counts the mark's own colour in the rendered frame before and after a shape is finished, on a keyed AND a keyless server, so "the board says there is a mark" can no longer pass for "the mark is on screen".