@orbat-mapper/tactical-draw 0.2.0-alpha.33 → 0.2.0-alpha.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,383 +1,8 @@
1
- import { AmplifierPlacement, AmplifierPlacements, ControlMeasure, ControlMeasureKind, ControlMeasureRender, ControlMeasureStyle, FillPattern, TextAmplifiers } from "@orbat-mapper/control-measures";
1
+ import { S as PointerEventInfo, _ as fillPatternImageDimension, a as MapAdapter, b as EditPointerDriver, c as MapEventHandler, d as PickOptions, f as PixelCoordinate, g as HatchPattern, h as HATCH_PATTERNS, i as LayerId, l as PickEvent, m as TextStyleOptions, n as HandleStyle, o as MapCursor, p as StyleOptions, r as IconResource, s as MapEvent, t as FeatureGroup, u as PickHandler, v as fillPatternImageId, x as PointerCallback, y as makeFillPatternImage } from "./map-adapter-meRo5VY2.mjs";
2
+ import { AmplifierPlacement, AmplifierPlacements, ControlMeasure, ControlMeasureKind, ControlMeasureRender, ControlMeasureStyle, TextAmplifiers } from "@orbat-mapper/control-measures";
2
3
  import { Feature, Position } from "geojson";
3
4
 
4
- //#region src/gestures/types.d.ts
5
- interface PointerEventInfo {
6
- /** Per-event hit-tolerance override (e.g. larger for touch). */
7
- hitTolerance?: number;
8
- /**
9
- * Whether the Alt/Option modifier was held for this pointer event. The edit
10
- * controller reads this on pointer-down to turn a vertex hit into a delete
11
- * (the GIS-standard Alt+click-to-delete gesture) instead of a drag. Only the
12
- * mouse paths populate it; touch leaves it `undefined`.
13
- */
14
- altKey?: boolean;
15
- /**
16
- * Whether Shift was held for this pointer event. Rotation gestures use the
17
- * live value to constrain transform-box angles to 15-degree increments.
18
- */
19
- shiftKey?: boolean;
20
- /** Engine-native event, for drivers that need to call preventDefault etc. */
21
- native?: unknown;
22
- }
23
- interface PointerCallback {
24
- (pixel: PixelCoordinate, info?: PointerEventInfo): void;
25
- }
26
- /**
27
- * Engine-agnostic pointer driver consumed by `TacticalDraw`'s edit controller.
28
- * The controller owns handle rendering and hit-tests handles in lonLat space;
29
- * the driver only carries raw pointer events, drag-pan toggle, and pixel
30
- * projection.
31
- */
32
- interface EditPointerDriver {
33
- onPointerDown(cb: PointerCallback): () => void;
34
- onPointerMove(cb: PointerCallback): () => void;
35
- onPointerUp(cb: PointerCallback): () => void;
36
- setDragPanEnabled(enabled: boolean): void;
37
- toPixel(lonLat: Position): PixelCoordinate | null;
38
- fromPixel(pixel: PixelCoordinate): Position;
39
- /** Idempotent. */
40
- dispose(): void;
41
- }
42
- //#endregion
43
- //#region src/fill-pattern.d.ts
44
- /**
45
- * Shared hatch fill-pattern tiles for area control-measure fills (ADR-0035
46
- * `FillPattern`, excluding `"solid"` which every adapter already renders as a
47
- * flat color). Both the MapLibre adapter (raster `addImage` pattern) and the
48
- * OpenLayers adapter (canvas `Pattern` fill) need the *same* diagonal-hachure
49
- * texture, so it is rasterized once here in pure math — no `HTMLCanvasElement`
50
- * — so it works in adapter code and in jsdom/node tests alike.
51
- */
52
- type HatchPattern = Exclude<FillPattern, "solid">;
53
- /**
54
- * Every non-solid `FillPattern` as a runtime list — the value-level companion
55
- * to {@link HatchPattern}, so adapters can test membership and build filter
56
- * expressions without re-spelling the pattern vocabulary.
57
- */
58
- declare const HATCH_PATTERNS: readonly HatchPattern[];
59
- /**
60
- * Stable id for a rasterized tile, keyed by pattern and color. Adapters use
61
- * this as the image-registry key (MapLibre `addImage` id, OpenLayers pattern
62
- * cache key) so the same color+pattern never gets rasterized twice. Color is
63
- * lowercased so `"#FF0000"` and `"#ff0000"` share one texture.
64
- */
65
- declare function fillPatternImageId(color: string, pattern: HatchPattern): string;
66
- /**
67
- * Tile edge length in device pixels for a given `pixelRatio`. Must be a power
68
- * of two — MapLibre requires POT dimensions for seamless WebGL texture wrap —
69
- * and scales linearly with `pixelRatio` for the ratios that matter in
70
- * practice (1x/2x displays: 32 -> 64, both already POT). Non-POT products
71
- * (e.g. a 3x ratio, 32*3=96) round up to the next POT rather than losing the
72
- * power-of-two guarantee.
73
- */
74
- declare function fillPatternImageDimension(pixelRatio: number): number;
75
- /**
76
- * Rasterize one seamless hatch tile as a straight RGBA pixel buffer — no
77
- * canvas involved, so this runs identically in a browser adapter and in
78
- * jsdom/node tests. Diagonal lines are snapped to slope ±1 (45°) so the
79
- * mod-based coverage math in {@link lineCoverage} tiles exactly. `"hachure"`
80
- * draws the "/" diagonal, `"backhachure"` its "\" mirror, and `"cross"` unions
81
- * both via `Math.max` coverage.
82
- */
83
- declare function makeFillPatternImage(color: string, pattern: HatchPattern, pixelRatio?: number): {
84
- width: number;
85
- height: number;
86
- data: Uint8ClampedArray;
87
- };
88
- //#endregion
89
- //#region src/map-adapter.d.ts
90
- type PixelCoordinate = [number, number];
91
- type MapCursor = "" | "default" | "crosshair" | "grab" | "grabbing" | "pointer";
92
- interface MapEvent {
93
- type: string;
94
- coordinate: Position;
95
- pixel: PixelCoordinate;
96
- originalEvent: unknown;
97
- }
98
- type MapEventHandler = (event: MapEvent) => void;
99
- interface StyleOptions {
100
- strokeColor?: string;
101
- strokeWidth?: number;
102
- strokeDash?: number[];
103
- fillColor?: string;
104
- pointFillColor?: string;
105
- pointRadius?: number;
106
- text?: TextStyleOptions;
107
- }
108
- /**
109
- * Text (label) style for a layer. When `labelProperty` is set, adapters also
110
- * read per-feature label properties: `rotation` (radians, rendered as
111
- * `π − rotation`), the size anchor's `textSizeMeters` (ground) or
112
- * `textSizePixels` (screen) (ADR-0028), and `textAnchor` (`"start"` = text
113
- * begins at the label point, `"end"` = text ends there; absent = centered).
114
- * A ground-anchored label renders at
115
- * `clamp(textSizeMeters / resolution, textSizeMinPixels, textSizeMaxPixels)`;
116
- * a screen-anchored one at a constant `textSizePixels`, unclamped.
117
- */
118
- interface TextStyleOptions {
119
- text?: string;
120
- labelProperty?: string;
121
- font?: string;
122
- fillColor?: string;
123
- strokeColor?: string;
124
- strokeWidth?: number;
125
- rotation?: number;
126
- /**
127
- * Lower edge of the ground-anchored label clamp band (ADR-0028): text never
128
- * renders below this pixel size, however far the view zooms out.
129
- * @default 8
130
- */
131
- textSizeMinPixels?: number;
132
- /**
133
- * Upper edge of the ground-anchored label clamp band (ADR-0028): text never
134
- * renders above this pixel size, however far the view zooms in.
135
- * @default 24
136
- */
137
- textSizeMaxPixels?: number;
138
- }
139
- /**
140
- * Per-feature style for edit handles (vertex / midpoint / translate / rotate).
141
- * The producer (`TacticalDraw`) rides this on `properties.style`; the adapter
142
- * reads it back when rendering the handle, so it is part of the adapter
143
- * contract rather than the tactical-draw authoring surface. `pointRadius` is a
144
- * UI affordance not present on `ControlMeasureStyle`. See ADR-0006.
145
- */
146
- interface HandleStyle {
147
- strokeColor?: string;
148
- strokeWidth?: number;
149
- strokeDash?: readonly number[];
150
- fillColor?: string;
151
- /**
152
- * How `fillColor` paints a filled part's interior. Absent or `"solid"` is
153
- * today's flat fill; `"hachure"`/`"backhachure"`/`"cross"` request the shared hatch texture
154
- * ({@link makeFillPatternImage}). Alpha rides in `fillColor` — there is no
155
- * separate opacity field. Adapters that cannot rasterize a pattern degrade
156
- * to solid. Per-feature only: layer-level `StyleOptions` has no equivalent.
157
- */
158
- fillPattern?: "solid" | HatchPattern;
159
- pointRadius?: number;
160
- }
161
- type LayerId = string;
162
- /** Canonical SVG resource shared by point features through `iconId`. */
163
- interface IconResource {
164
- id: string;
165
- svg: string;
166
- width: number;
167
- height: number;
168
- anchor: [number, number];
169
- /**
170
- * Direct visible mark used to rank icon picks, in unscaled canvas coordinates.
171
- * Hits elsewhere in the full canvas measure their distance from this rectangle;
172
- * omit it to preserve whole-canvas distance-zero behavior.
173
- */
174
- coreRect?: {
175
- x: number;
176
- y: number;
177
- width: number;
178
- height: number;
179
- };
180
- }
181
- /**
182
- * Click-style hit event delivered by {@link MapAdapter.onPick}.
183
- *
184
- * `id` is the control measure id, already split off the feature id by the
185
- * adapter (see `controlMeasureIdFromFeature`).
186
- *
187
- * `originalEvent` is whatever the engine surfaces — OpenLayers normalises to
188
- * `PointerEvent`, MapLibre delivers `MouseEvent`, Leaflet delivers
189
- * `MouseEvent | TouchEvent`. Do not narrow to `PointerEvent` at the consumer.
190
- */
191
- interface PickEvent {
192
- id: string;
193
- feature: Feature;
194
- pixel: PixelCoordinate;
195
- coordinate: Position;
196
- originalEvent: MouseEvent | PointerEvent | TouchEvent;
197
- }
198
- type PickHandler = (event: PickEvent) => void;
199
- interface PickOptions {
200
- signal?: AbortSignal;
201
- }
202
- interface MapAdapter {
203
- /**
204
- * Add a vector layer to the map.
205
- * @returns unique layer ID
206
- */
207
- addVectorLayer(options?: {
208
- style?: StyleOptions;
209
- }): LayerId;
210
- /**
211
- * Remove a layer from the map.
212
- */
213
- removeLayer(layerId: LayerId): void;
214
- /**
215
- * Set the style for a specific layer.
216
- */
217
- setLayerStyle(layerId: LayerId, style: StyleOptions): void;
218
- /** Reconcile the layer's desired shared icon resources. */
219
- reconcileLayerIconResources(layerId: LayerId, resources: readonly IconResource[]): void;
220
- /**
221
- * Set features on a vector layer.
222
- * Features should be standard GeoJSON Features.
223
- */
224
- setLayerFeatures(layerId: LayerId, features: Feature[]): void;
225
- /**
226
- * Clear all features from a layer.
227
- */
228
- clearLayer(layerId: LayerId): void;
229
- /**
230
- * Reconcile features on a layer against the incoming set, keyed by `id`.
231
- *
232
- * Semantics:
233
- * - features with an `id` that already exists are updated in place;
234
- * - features with a fresh `id` are added;
235
- * - features in the layer whose `id` is missing from the incoming set are
236
- * removed.
237
- *
238
- * Features must carry a stable `id`. Use this when you can compute the
239
- * complete desired state of a layer in one go; for partial removals use
240
- * {@link removeFeatures}.
241
- */
242
- updateLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
243
- /**
244
- * Remove features by `id` from a layer. Ids that are not present are
245
- * silently ignored.
246
- */
247
- removeFeatures(layerId: LayerId, ids: readonly string[]): void;
248
- /**
249
- * Defer an upcoming removal from `layerId` until the most recent update to
250
- * `updatedLayerId` has completed in the engine. Optional because synchronous
251
- * adapters already make cross-layer write order visible atomically.
252
- */
253
- deferLayerRemovalUntilLayerUpdate?(layerId: LayerId, updatedLayerId: LayerId): void;
254
- /**
255
- * Subscribe to click-style "pick" events for a specific layer. Basemap and
256
- * other-layer clicks never fire the handler.
257
- *
258
- * For control-measure features, adapters must set `PickEvent.id` to the
259
- * originating control-measure id (not the rendered part's feature id).
260
- * Features that do not satisfy the control-measure feature-id contract must
261
- * be ignored rather than dispatched.
262
- *
263
- * Bound to the engine's tap-suppressing click event (OL `singleclick`,
264
- * MapLibre `click`, Leaflet `click`) so the handler does not fire at the
265
- * end of a pan or pinch-zoom gesture.
266
- *
267
- * Hit tolerance is pointer-type aware: 4 px for mouse/pen, 12 px for touch.
268
- * Outline-only polygon interiors additionally have bounded inward selection
269
- * reach from visible viewport-clipped boundaries: 24 px for mouse/pen and
270
- * 36 px for touch. A visible solid or patterned fill owns its full interior.
271
- *
272
- * Returns an unsubscribe function. The returned function and the optional
273
- * `signal` are both honoured; either teardown path stops further events,
274
- * and both are idempotent.
275
- */
276
- onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
277
- /**
278
- * Map Event Listeners
279
- *
280
- * Ordering contract: a `"click"` handler and the `onPick` handlers for the
281
- * same user click MUST observe the pick first — adapters deliver picks and
282
- * `"click"` from the same underlying engine event, in registration order.
283
- * The draw/edit controllers register their pick subscriptions before their
284
- * click handler and read the stashed pick inside it (click-away close /
285
- * close-then-pick re-dispatch); an adapter that fires `"click"` from an
286
- * earlier engine event than its picks breaks that contract (e.g. OL
287
- * `click` vs `singleclick`).
288
- */
289
- on(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
290
- off(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
291
- /**
292
- * Projection / Coordinate Utilities
293
- */
294
- toLonLat(coordinate: number[]): Position;
295
- fromLonLat(position: Position): number[];
296
- getPixelFromCoordinate(position: Position): PixelCoordinate | null;
297
- /**
298
- * Convert a viewport pixel to longitude/latitude. Optional so third-party
299
- * adapters compiled against an older ABI remain valid.
300
- */
301
- getCoordinateFromPixel?(pixel: PixelCoordinate): Position | null;
302
- /**
303
- * Get current viewport size in CSS pixels.
304
- */
305
- getViewportSize(): {
306
- width: number;
307
- height: number;
308
- };
309
- /**
310
- * Pan the map by pixel delta.
311
- * Positive delta means desired on-screen feature shift (right/down).
312
- */
313
- panByPixels(delta: PixelCoordinate, options?: {
314
- durationMs?: number;
315
- }): void;
316
- /**
317
- * Set the map viewport cursor. Pass an empty string to restore engine default styling.
318
- */
319
- setCursor(cursor: MapCursor): void;
320
- /**
321
- * Toggle the engine's native double-click-to-zoom gesture. `TacticalDraw`
322
- * disables this for the lifetime of a variable-length draw so the commit
323
- * `dblclick` doesn't also zoom the map.
324
- */
325
- setDoubleClickZoomEnabled(enabled: boolean): void;
326
- /**
327
- * Get current view resolution (meters per pixel at center or generally).
328
- * returns undefined if not supported or not ready.
329
- */
330
- getResolution(): number | undefined;
331
- /**
332
- * Get current map zoom level
333
- */
334
- getZoom(): number | undefined;
335
- /**
336
- * Get current view bearing, radians — the clockwise-from-north compass
337
- * direction that screen-up points to (MapLibre-bearing semantics). `0` for
338
- * engines/views that don't rotate.
339
- */
340
- getBearing(): number;
341
- /**
342
- * Listen for view changes (zoom/resolution)
343
- */
344
- onViewChange(handler: () => void): void;
345
- offViewChange(handler: () => void): void;
346
- /**
347
- * The engine evaluates the meters→pixels clamp for ground-sized point
348
- * symbols inside its own style layer, so meters-sized point symbols don't
349
- * need view-change re-renders for size.
350
- */
351
- readonly scalesGroundPointSymbolsNatively?: boolean;
352
- /**
353
- * Construct an {@link EditPointerDriver} for `TacticalDraw`'s edit
354
- * controller. The controller subscribes to pointer events on the returned
355
- * driver to wire handle drags (vertex / midpoint / translate / rotate).
356
- */
357
- createEditPointerDriver(): EditPointerDriver;
358
- /**
359
- * Release all adapter-owned map resources. Adapters must not be used after
360
- * destruction.
361
- */
362
- destroy(): void;
363
- /**
364
- * Returns the features currently set on the layer — the adapter-side cache
365
- * of the last `setLayerFeatures`/`updateLayerFeatures`/`removeFeatures`/
366
- * `clearLayer` state. Empty array for an unknown layer. Callers must not
367
- * mutate the returned array or features.
368
- */
369
- getLayerFeatures(layerId: LayerId): readonly Feature[];
370
- }
371
- //#endregion
372
5
  //#region src/base-map-adapter.d.ts
373
- interface FeatureOps {
374
- adds: Feature[];
375
- updates: Feature[];
376
- /** Features whose id was absent from the incoming set. */
377
- removes?: Feature[];
378
- full: Feature[];
379
- replace?: boolean;
380
- }
381
6
  type NormalizedEventHandler = (lonLat: Position, pixel: PixelCoordinate, originalEvent: unknown) => void;
382
7
  interface NativeEventEntry<TEventToken> {
383
8
  type: string;
@@ -387,10 +12,8 @@ interface NativeEventEntry<TEventToken> {
387
12
  * Abstract base class shared by all `MapAdapter` implementations.
388
13
  *
389
14
  * Owns shared bookkeeping (layer registry, feature cache, event-token
390
- * tracking) and orchestrates the public methods over a small
391
- * protected primitive surface that subclasses implement. See
392
- * `docs/adr/0001-map-adapter-base-class.md` for the rationale, in particular
393
- * around the `applyFeatureOps` primitive shape.
15
+ * tracking) and orchestrates the public methods over a small protected
16
+ * grouped desired-state primitive that subclasses implement.
394
17
  */
395
18
  declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> implements MapAdapter {
396
19
  protected layers: Map<string, TLayerHandle>;
@@ -399,6 +22,7 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
399
22
  protected featureCache: Map<string, Feature<import("geojson").Geometry, import("geojson").GeoJsonProperties>[]>;
400
23
  protected featuresChangedListeners: Map<string, Set<(features: Feature[]) => void>>;
401
24
  protected iconResources: Map<string, IconResource>;
25
+ private featureGroupState;
402
26
  private layerIconResourceIds;
403
27
  private layerCounter;
404
28
  private destroyed;
@@ -407,13 +31,9 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
407
31
  style?: StyleOptions;
408
32
  }): LayerId;
409
33
  removeLayer(layerId: LayerId): void;
410
- reconcileLayerIconResources(layerId: LayerId, resources: readonly IconResource[]): void;
411
- setLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
412
- updateLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
413
- removeFeatures(layerId: LayerId, ids: readonly string[]): void;
34
+ setLayerFeatureGroups(layerId: LayerId, groups: readonly FeatureGroup[]): void;
414
35
  abstract onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
415
36
  abstract createEditPointerDriver(): EditPointerDriver;
416
- clearLayer(layerId: LayerId): void;
417
37
  on(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
418
38
  off(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
419
39
  onViewChange(handler: () => void): void;
@@ -424,11 +44,13 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
424
44
  protected destroyEngineResources(): void;
425
45
  protected applyIconResources(resources: readonly IconResource[]): void;
426
46
  protected getIconResource(id: string): IconResource | undefined;
47
+ protected getFeatureGroupOrder(layerId: LayerId, featureId: string | number | undefined): number | undefined;
427
48
  protected removeIconResource(id: string): void;
428
49
  private pruneIconResources;
429
50
  protected abstract createLayer(layerId: LayerId, style?: StyleOptions): TLayerHandle;
430
51
  protected abstract destroyLayer(handle: TLayerHandle): void;
431
- protected abstract applyFeatureOps(layerId: LayerId, ops: FeatureOps): void;
52
+ /** Apply one validated, complete grouped desired state. */
53
+ protected abstract applyFeatureGroups(layerId: LayerId, groups: readonly FeatureGroup[]): void;
432
54
  protected abstract attachNativeEvent(type: "click" | "pointermove" | "dblclick", onEvent: NormalizedEventHandler): NativeEventEntry<TEventToken>;
433
55
  protected abstract detachNativeEvent(entry: NativeEventEntry<TEventToken>): void;
434
56
  protected abstract attachViewChange(handler: () => void): TViewToken;
@@ -519,6 +141,8 @@ interface PickCandidate {
519
141
  viewport: ScreenBounds;
520
142
  presentation: ResolvedPickPresentation;
521
143
  visualOrder: number;
144
+ /** Lower values mean a visually higher atomic feature group. */
145
+ featureGroupOrder?: number;
522
146
  iconFootprint?: readonly PixelCoordinate[];
523
147
  labelExtent?: LabelExtent;
524
148
  }
@@ -530,6 +154,11 @@ interface PickCandidateInput {
530
154
  * native ordering to topmost-first before calling into evaluation.
531
155
  */
532
156
  visualOrder: number;
157
+ /**
158
+ * Authoritative bottom-to-top group order normalized to lower = visually
159
+ * higher. Unlike `visualOrder`, native engine query order cannot override it.
160
+ */
161
+ featureGroupOrder?: number;
533
162
  /** The engine already reported this part as hit; retained as a fallback for engine-only marks. */
534
163
  nativeHit?: boolean;
535
164
  icon?: IconResource;
@@ -635,6 +264,15 @@ interface TextSizeProps {
635
264
  * - no usable size or resolution: `null`.
636
265
  */
637
266
  declare function resolveTextSizePixels(props: TextSizeProps, resolution: number): number | null;
267
+ /** Fallback halo color when a `textHalo` feature carries no `textHaloColor`. */
268
+ declare const TEXT_HALO_COLOR = "#ffffff";
269
+ /**
270
+ * Outward depth (px) of a `textHalo` halo from the glyph edge, scaled to font
271
+ * size. This is the halo's *outward* reach, not a stroke width — a canvas
272
+ * stroke straddles the glyph edge (half in, half out), so a caller drawing
273
+ * one must double this value to get the same visible depth.
274
+ */
275
+ declare function resolveTextHaloWidth(textSizePixels?: number): number;
638
276
  //#endregion
639
277
  //#region src/gestures/hit-test.d.ts
640
278
  type ToPixel = (lonLat: Position) => PixelCoordinate | null;
@@ -1262,8 +900,7 @@ interface TransformSession {
1262
900
  * - **Removed** members (id in the current set, absent from the next) are
1263
901
  * committed at their current working state.
1264
902
  * - **Added** members (id only in the next set) join at their passed input
1265
- * state; if currently on the graphics layer they are lifted off it exactly
1266
- * like initial members.
903
+ * state and use their host-provided Graphic-stack slot when present.
1267
904
  *
1268
905
  * The transform box re-derives from the next member set at the session's
1269
906
  * current box angle. Session-scoped state (`sizeAnchor`, box angle,
@@ -1280,11 +917,12 @@ interface TransformSession {
1280
917
  * size left untouched, then cloned and rendered); an empty array when nothing
1281
918
  * was removed. The outer `editMany` promise resolves with the snapshots of
1282
919
  * the members in the set **at close time only**. Removed members are NOT
1283
- * written back to the graphics layer by the controller — the host folds the
1284
- * returned snapshots into its document and calls `td.render()`, the same
1285
- * contract as [[close]]. A removed member's exit snapshot is also delivered
1286
- * through [[onCommit]] — the recommended channel, since it covers this return
1287
- * value and the close-time promise resolution through a single subscription.
920
+ * retained in its Graphic-stack slot until the host acknowledges it by
921
+ * folding the returned snapshot into its document and calling `td.render()`,
922
+ * the same contract as [[close]]. A removed member's exit snapshot is also
923
+ * delivered through [[onCommit]] — the recommended channel, since it covers
924
+ * this return value and the close-time promise resolution through a single
925
+ * subscription.
1288
926
  * Returns `undefined` (a silent no-op) after the session has settled.
1289
927
  *
1290
928
  * `setGraphics([])` on a live session delegates to [[close]]: it commits every
@@ -1359,9 +997,8 @@ interface TransformSession {
1359
997
  * emission, so a listener that clears selection state observes a document
1360
998
  * that already contains the exit snapshots.
1361
999
  * - On every path it fires **after** the facade has already cleared its
1362
- * slot — `td.activeTransformSession` is `null` and the members are no
1363
- * longer excluded from `td.render()` reconciliation, so fold-then-render
1364
- * works from inside the handler.
1000
+ * slot — `td.activeTransformSession` is `null`, so fold-then-render works
1001
+ * from inside the handler.
1365
1002
  * - `reason` distinguishes commit from abort but is deliberately coarse —
1366
1003
  * no payload; snapshots were already delivered through [[onCommit]].
1367
1004
  *
@@ -1759,6 +1396,7 @@ declare class TacticalDraw {
1759
1396
  private readonly pointSymbols;
1760
1397
  private snappingOptions;
1761
1398
  private readonly resolveSnap;
1399
+ private readonly graphicStack;
1762
1400
  private destroyed;
1763
1401
  private hasRendered;
1764
1402
  private readonly activeInteraction;
@@ -1802,12 +1440,7 @@ declare class TacticalDraw {
1802
1440
  private idCounter;
1803
1441
  private editCounter;
1804
1442
  private transformCounter;
1805
- /**
1806
- * Snapshot of the host's last `render()` argument. The edit slice uses it
1807
- * to recompute the graphics-layer state during a lift / restore — the
1808
- * façade owns the policy because controllers shouldn't need to know the
1809
- * surrounding collection state.
1810
- */
1443
+ /** Snapshot of the host's latest authoritative Graphic membership and order. */
1811
1444
  private lastRenderedMeasures;
1812
1445
  private snapGeometryFeatures;
1813
1446
  /**
@@ -1829,31 +1462,31 @@ declare class TacticalDraw {
1829
1462
  setSnappingOptions(options?: SnappingOptions): void;
1830
1463
  /** Clear transient host affordances when snapping or its interaction ends. */
1831
1464
  private clearSnapResult;
1832
- /**
1833
- * Render `measures` into features and write them to the graphics layer.
1834
- * Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
1835
- * resolved against the live map. Callers own any edit-exclusion policy — see
1836
- * `render()` (excludes the edited measure, owned by the preview layer) versus
1837
- * `settleToGraphics` (no exclusion, the edit is ending).
1838
- */
1839
- private renderMeasuresToGraphics;
1465
+ /** Render one atomic Graphic group for the engine-neutral graphics stack. */
1466
+ private renderGraphicGroup;
1467
+ /** Submit one complete grouped desired state for the host stack plus overrides. */
1468
+ private reconcileGraphicsStack;
1469
+ /** Atomically publish several live renders into their host-owned Graphic slots. */
1470
+ private publishWorkingGraphics;
1471
+ /** Settle several stack overrides in one graphics-layer reconciliation. */
1472
+ private settleWorkingGraphics;
1840
1473
  /**
1841
1474
  * Re-render the graphics layer on a view (zoom) change so pixel-denominated
1842
1475
  * and not-yet-committed label-sized measures rescale live. Skipped entirely
1843
1476
  * when nothing rendered still reads live view state — meter geometry with a
1844
1477
  * captured label size is zoom-independent, so a rebuild would be wasted
1845
- * layer writes. During an active edit the edited measure lives on the
1846
- * preview layer (lifted from graphics), so it is excluded here exactly as
1847
- * in `render()`; the preview's own view-change subscription rescales it.
1478
+ * layer writes. A single-control-measure edit publishes through the stack
1479
+ * coordinator; point symbols and transform members already present in the
1480
+ * host stack do the same. Graphics without a host slot remain on preview.
1848
1481
  */
1849
1482
  private readonly handleViewChange;
1850
1483
  /**
1851
- * Every measure id currently lifted onto the preview layer by an active
1852
- * edit or group transform — excluded from `render()` / view-change
1853
- * reconciliation because the preview layer (not graphics) owns their
1854
- * working copy. Empty while idle.
1484
+ * Every Graphic id owned by an active interaction. Used for highlight and
1485
+ * pick routing; not every such Graphic is excluded from stack reconciliation.
1855
1486
  */
1856
1487
  private excludedInteractionIds;
1488
+ /** Interaction ids still rendered on the legacy preview surface. */
1489
+ private graphicsStackExcludedIds;
1857
1490
  /**
1858
1491
  * Live member ids of the active group transform, or `undefined` when none.
1859
1492
  * Falls back to the session ref because `onSession` fires from inside the
@@ -1867,24 +1500,24 @@ declare class TacticalDraw {
1867
1500
  * Idempotent. Throws synchronously on duplicate measure ids *before*
1868
1501
  * mutating any layer. An empty list before any prior render is a no-op.
1869
1502
  *
1870
- * Cross-layer semantics with an active edit (issue #51):
1871
- * - The measure under edit is **excluded** from the reconcile pass — the
1872
- * preview layer owns its working copy and the lift on the graphics layer
1873
- * must stay in place.
1503
+ * Cross-layer semantics with an active edit:
1504
+ * - The stack coordinator replaces the edited Graphic's host-owned slot
1505
+ * with its working render; membership and order remain host-authoritative.
1874
1506
  * - If the edited measure is absent from `measures`, the pending `edit`
1875
1507
  * promise rejects on the **next microtask** with reason `"removed"`.
1876
1508
  * `render` itself returns normally so host collection ops stay synchronous.
1877
- * - If the edited measure is present and its `style` changed, the preview
1878
- * picks up the new style on this render. `controlPoints` changes on the
1509
+ * - If the edited measure is present and its `style` changed, its working
1510
+ * group picks up the new style on this render. `controlPoints` changes on the
1879
1511
  * edited measure are routed nowhere — the user's in-flight drag is never
1880
- * yanked out from under them.
1512
+ * yanked out from under them. A point symbol's complete working group remains
1513
+ * authoritative until settle, including any changed icon resource.
1881
1514
  *
1882
1515
  * Cross-layer semantics with an active draw: `render` only touches the
1883
1516
  * graphics layer, so the draw's preview is undisturbed.
1884
1517
  *
1885
1518
  * Reentrancy: calling `td.render(...)` from inside `EditSession.onChange` is
1886
- * safe — `render` never re-enters the active edit or its preview / handle
1887
- * layers (the only edit-facing side effect, `notifyExternalMeasure`, is a
1519
+ * safe — `render` never re-enters the active edit or its handle layer (the
1520
+ * only edit-facing side effect, `notifyExternalMeasure`, is a
1888
1521
  * pure style replay).
1889
1522
  */
1890
1523
  render(measures: readonly Graphic[]): void;
@@ -1893,7 +1526,7 @@ declare class TacticalDraw {
1893
1526
  * idempotent (mirrors {@link render}): a non-interactive outline box is drawn
1894
1527
  * around each still-rendered graphic on the dedicated highlight layer — no
1895
1528
  * draw / edit / transform session, no promise, no pick subscription. Passing
1896
- * `[]` clears every highlight. Unknown ids and ids currently lifted into an
1529
+ * `[]` clears every highlight. Unknown ids and ids currently active in an
1897
1530
  * active edit / transform session are skipped (the live session's own visuals
1898
1531
  * supersede the passive outline). Hosts use it for Excalidraw-style
1899
1532
  * "locked selection" chrome.
@@ -1902,9 +1535,9 @@ declare class TacticalDraw {
1902
1535
  /**
1903
1536
  * Project the current {@link highlightedIds} into one outline `LineString`
1904
1537
  * per graphic and reconcile them onto the highlight layer. Idempotent and
1905
- * id-keyed via `adapter.updateLayerFeatures`, so a set-replace removes stale
1906
- * outlines and an unchanged set is a cheap no-op write. Skips ids that are
1907
- * unknown (not in the last render) or lifted into an active session, and
1538
+ * grouped by graphic id, so desired-state replacement removes stale outlines
1539
+ * and an unchanged set is a cheap no-op write. Skips ids that are
1540
+ * unknown (not in the last render) or active in an interaction session, and
1908
1541
  * bails gracefully when the adapter cannot invert pixels to coordinates
1909
1542
  * (`getCoordinateFromPixel` is optional in the MapAdapter ABI).
1910
1543
  */
@@ -1985,9 +1618,10 @@ declare class TacticalDraw {
1985
1618
  edit(measure: ControlMeasure, options?: EditOptions): Promise<GraphicSnapshot<ControlMeasure>>;
1986
1619
  /**
1987
1620
  * Start an interactive group transform (multi-select) over `measures`
1988
- * (ADR-0017 "Multi-select" slice). Every member is lifted onto the shared
1989
- * preview layer; the box (corner scale, rotate grip, drag-anywhere body
1990
- * translate) applies rigidly to all of them at once. Returns a Promise that
1621
+ * (ADR-0017 "Multi-select" slice). Every host-provided member stays in its
1622
+ * Graphic-stack slot; only a member without a host slot uses preview. The box
1623
+ * (corner scale, rotate grip, drag-anywhere body translate) applies rigidly
1624
+ * to all of them at once. Returns a Promise that
1991
1625
  * resolves with `GraphicSnapshot<ControlMeasure>[]` on `session.close()` and rejects
1992
1626
  * with `TacticalDrawAbortError` on signal / Escape / destroy / preempt /
1993
1627
  * `session.abort()`. Key the resolved snapshots by `snapshot.graphic.id`:
@@ -2256,4 +1890,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
2256
1890
  /** `nearestWorldCopyLng` applied to a lon/lat position. */
2257
1891
  declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
2258
1892
  //#endregion
2259
- export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, type GuideStyle, HATCH_PATTERNS, type HandleStyle, type HatchPattern, type IconResource, type InteractionHit, type InteractionStyle, type LabelExtent, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type MergedPickFeature, type NativeEventEntry, type PickCandidate, type PickCandidateHit, type PickCandidateHitKind, type PickCandidateInput, type PickEvaluationContext, type PickEvent, type PickHandler, type PickOptions, type PickPointerType, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type ResolvedPickPresentation, type RotatedScreenBounds, type ScreenBounds, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextSizeProps, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, type TransientChangeEvent, type TransientPhase, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pickPointerType, pointSymbolFootprint, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolveTextSizePixels, tryControlMeasureIdFromFeature };
1893
+ export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureGroup, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, type GuideStyle, HATCH_PATTERNS, type HandleStyle, type HatchPattern, type IconResource, type InteractionHit, type InteractionStyle, type LabelExtent, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type MergedPickFeature, type NativeEventEntry, type PickCandidate, type PickCandidateHit, type PickCandidateHitKind, type PickCandidateInput, type PickEvaluationContext, type PickEvent, type PickHandler, type PickOptions, type PickPointerType, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type ResolvedPickPresentation, type RotatedScreenBounds, type ScreenBounds, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, type StyleOptions, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextSizeProps, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, type TransientChangeEvent, type TransientPhase, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pickPointerType, pointSymbolFootprint, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolveTextHaloWidth, resolveTextSizePixels, tryControlMeasureIdFromFeature };