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

+12 -2
View File
@@ -828,6 +828,10 @@ export function createAnnotationEngine({
};
for (const ex of annotations.values()) {
if (ex.type !== anno.type || !ex.anchor) continue;
// A shape drawn by hand and a voice mark are never the same mark: a
// match would let a voice TTL expire the drawn one, or replace it. A
// hidden mark is not refreshed either; a new shape there is new.
if (Boolean(ex.manual) !== Boolean(anno.manual) || ex.hidden) continue;
// Identity is GEOMETRY, not label: "Marina" and "Marina District" resolve to the SAME
// polygon/anchor and must collapse to one mark (the caller replaces label/color in place
// so the latest caption wins). Resolution is deterministic, so the same place re-resolves
@@ -1054,15 +1058,21 @@ export function createAnnotationEngine({
if (!anno) return false;
if (anno.type === 'route' && anno.path?.length) {
const ring = anno.path.map((p) => [p.lon, p.lat]);
const lons = ring.map(([lon]) => lon);
// Unwrap around the first vertex, as ringRange does, so a line across
// the antimeridian is centred on it, not on the far side of the globe.
const ref = ring[0][0];
const lons = ring.map(
([lon]) => lon - 360 * Math.round((lon - ref) / 360),
);
const lats = ring.map(([, lat]) => lat);
const midLon = (Math.min(...lons) + Math.max(...lons)) / 2;
frameAnnotation({
...anno,
to: null,
ring,
polygons: null,
anchor: {
lon: (Math.min(...lons) + Math.max(...lons)) / 2,
lon: ((((midLon + 180) % 360) + 360) % 360) - 180,
lat: (Math.min(...lats) + Math.max(...lats)) / 2,
height: 0,
},