@orbat-mapper/tactical-draw 0.2.0-alpha.0
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/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/index.d.mts +950 -0
- package/dist/index.mjs +2855 -0
- package/package.json +58 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
import { ControlMeasure, ControlMeasureKind, ControlMeasureSnapshot, ControlMeasureStyle } from "@orbat-mapper/control-measures";
|
|
2
|
+
import { Feature, Position } from "geojson";
|
|
3
|
+
|
|
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
|
+
/** Engine-native event, for drivers that need to call preventDefault etc. */
|
|
16
|
+
native?: unknown;
|
|
17
|
+
}
|
|
18
|
+
interface PointerCallback {
|
|
19
|
+
(pixel: PixelCoordinate, info?: PointerEventInfo): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Engine-agnostic pointer driver consumed by `TacticalDraw`'s edit controller.
|
|
23
|
+
* The controller owns handle rendering and hit-tests handles in lonLat space;
|
|
24
|
+
* the driver only carries raw pointer events, drag-pan toggle, and pixel
|
|
25
|
+
* projection.
|
|
26
|
+
*/
|
|
27
|
+
interface EditPointerDriver {
|
|
28
|
+
onPointerDown(cb: PointerCallback): () => void;
|
|
29
|
+
onPointerMove(cb: PointerCallback): () => void;
|
|
30
|
+
onPointerUp(cb: PointerCallback): () => void;
|
|
31
|
+
setDragPanEnabled(enabled: boolean): void;
|
|
32
|
+
toPixel(lonLat: Position): PixelCoordinate | null;
|
|
33
|
+
fromPixel(pixel: PixelCoordinate): Position;
|
|
34
|
+
/** Idempotent. */
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/map-adapter.d.ts
|
|
39
|
+
type PixelCoordinate = [number, number];
|
|
40
|
+
type MapCursor = "" | "default" | "crosshair" | "grab" | "grabbing" | "pointer";
|
|
41
|
+
interface MapEvent {
|
|
42
|
+
type: string;
|
|
43
|
+
coordinate: Position;
|
|
44
|
+
pixel: PixelCoordinate;
|
|
45
|
+
originalEvent: unknown;
|
|
46
|
+
}
|
|
47
|
+
type MapEventHandler = (event: MapEvent) => void;
|
|
48
|
+
interface StyleOptions {
|
|
49
|
+
strokeColor?: string;
|
|
50
|
+
strokeWidth?: number;
|
|
51
|
+
strokeDash?: number[];
|
|
52
|
+
fillColor?: string;
|
|
53
|
+
pointFillColor?: string;
|
|
54
|
+
pointRadius?: number;
|
|
55
|
+
text?: TextStyleOptions;
|
|
56
|
+
}
|
|
57
|
+
interface TextStyleOptions {
|
|
58
|
+
text?: string;
|
|
59
|
+
labelProperty?: string;
|
|
60
|
+
font?: string;
|
|
61
|
+
fillColor?: string;
|
|
62
|
+
strokeColor?: string;
|
|
63
|
+
strokeWidth?: number;
|
|
64
|
+
rotation?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Per-feature style for edit handles (vertex / midpoint / translate / rotate).
|
|
68
|
+
* The producer (`TacticalDraw`) rides this on `properties.style`; the adapter
|
|
69
|
+
* reads it back when rendering the handle, so it is part of the adapter
|
|
70
|
+
* contract rather than the tactical-draw authoring surface. `pointRadius` is a
|
|
71
|
+
* UI affordance not present on `ControlMeasureStyle`. See ADR-0006.
|
|
72
|
+
*/
|
|
73
|
+
interface HandleStyle {
|
|
74
|
+
strokeColor?: string;
|
|
75
|
+
strokeWidth?: number;
|
|
76
|
+
strokeDash?: readonly number[];
|
|
77
|
+
fillColor?: string;
|
|
78
|
+
pointRadius?: number;
|
|
79
|
+
}
|
|
80
|
+
type LayerId = string;
|
|
81
|
+
/**
|
|
82
|
+
* Click-style hit event delivered by {@link MapAdapter.onPick}.
|
|
83
|
+
*
|
|
84
|
+
* `id` is the control measure id, already split off the feature id by the
|
|
85
|
+
* adapter (see `controlMeasureIdFromFeature`).
|
|
86
|
+
*
|
|
87
|
+
* `originalEvent` is whatever the engine surfaces — OpenLayers normalises to
|
|
88
|
+
* `PointerEvent`, MapLibre delivers `MouseEvent`, Leaflet delivers
|
|
89
|
+
* `MouseEvent | TouchEvent`. Do not narrow to `PointerEvent` at the consumer.
|
|
90
|
+
*/
|
|
91
|
+
interface PickEvent {
|
|
92
|
+
id: string;
|
|
93
|
+
feature: Feature;
|
|
94
|
+
pixel: PixelCoordinate;
|
|
95
|
+
coordinate: Position;
|
|
96
|
+
originalEvent: MouseEvent | PointerEvent | TouchEvent;
|
|
97
|
+
}
|
|
98
|
+
type PickHandler = (event: PickEvent) => void;
|
|
99
|
+
interface PickOptions {
|
|
100
|
+
signal?: AbortSignal;
|
|
101
|
+
}
|
|
102
|
+
interface MapAdapter {
|
|
103
|
+
/**
|
|
104
|
+
* Add a vector layer to the map.
|
|
105
|
+
* @returns unique layer ID
|
|
106
|
+
*/
|
|
107
|
+
addVectorLayer(options?: {
|
|
108
|
+
style?: StyleOptions;
|
|
109
|
+
}): LayerId;
|
|
110
|
+
/**
|
|
111
|
+
* Remove a layer from the map.
|
|
112
|
+
*/
|
|
113
|
+
removeLayer(layerId: LayerId): void;
|
|
114
|
+
/**
|
|
115
|
+
* Set the style for a specific layer.
|
|
116
|
+
*/
|
|
117
|
+
setLayerStyle(layerId: LayerId, style: StyleOptions): void;
|
|
118
|
+
/**
|
|
119
|
+
* Set features on a vector layer.
|
|
120
|
+
* Features should be standard GeoJSON Features.
|
|
121
|
+
*/
|
|
122
|
+
setLayerFeatures(layerId: LayerId, features: Feature[]): void;
|
|
123
|
+
/**
|
|
124
|
+
* Clear all features from a layer.
|
|
125
|
+
*/
|
|
126
|
+
clearLayer(layerId: LayerId): void;
|
|
127
|
+
/**
|
|
128
|
+
* Reconcile features on a layer against the incoming set, keyed by `id`.
|
|
129
|
+
*
|
|
130
|
+
* Semantics:
|
|
131
|
+
* - features with an `id` that already exists are updated in place;
|
|
132
|
+
* - features with a fresh `id` are added;
|
|
133
|
+
* - features in the layer whose `id` is missing from the incoming set are
|
|
134
|
+
* removed.
|
|
135
|
+
*
|
|
136
|
+
* Features must carry a stable `id`. Use this when you can compute the
|
|
137
|
+
* complete desired state of a layer in one go; for partial removals use
|
|
138
|
+
* {@link removeFeatures}.
|
|
139
|
+
*/
|
|
140
|
+
updateLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
|
|
141
|
+
/**
|
|
142
|
+
* Remove features by `id` from a layer. Ids that are not present are
|
|
143
|
+
* silently ignored.
|
|
144
|
+
*/
|
|
145
|
+
removeFeatures(layerId: LayerId, ids: readonly string[]): void;
|
|
146
|
+
/**
|
|
147
|
+
* Subscribe to click-style "pick" events for a specific layer. Basemap and
|
|
148
|
+
* other-layer clicks never fire the handler.
|
|
149
|
+
*
|
|
150
|
+
* For control-measure features, adapters must set `PickEvent.id` to the
|
|
151
|
+
* originating control-measure id (not the rendered part's feature id).
|
|
152
|
+
* Features that do not satisfy the control-measure feature-id contract must
|
|
153
|
+
* be ignored rather than dispatched.
|
|
154
|
+
*
|
|
155
|
+
* Bound to the engine's tap-suppressing click event (OL `singleclick`,
|
|
156
|
+
* MapLibre `click`, Leaflet `click`) so the handler does not fire at the
|
|
157
|
+
* end of a pan or pinch-zoom gesture.
|
|
158
|
+
*
|
|
159
|
+
* Hit tolerance is pointer-type aware: 4 px for mouse/pen, 12 px for touch.
|
|
160
|
+
*
|
|
161
|
+
* Returns an unsubscribe function. The returned function and the optional
|
|
162
|
+
* `signal` are both honoured; either teardown path stops further events,
|
|
163
|
+
* and both are idempotent.
|
|
164
|
+
*/
|
|
165
|
+
onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
166
|
+
/**
|
|
167
|
+
* Map Event Listeners
|
|
168
|
+
*/
|
|
169
|
+
on(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
|
|
170
|
+
off(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
|
|
171
|
+
/**
|
|
172
|
+
* Projection / Coordinate Utilities
|
|
173
|
+
*/
|
|
174
|
+
toLonLat(coordinate: number[]): Position;
|
|
175
|
+
fromLonLat(position: Position): number[];
|
|
176
|
+
getPixelFromCoordinate(position: Position): PixelCoordinate | null;
|
|
177
|
+
/**
|
|
178
|
+
* Get current viewport size in CSS pixels.
|
|
179
|
+
*/
|
|
180
|
+
getViewportSize(): {
|
|
181
|
+
width: number;
|
|
182
|
+
height: number;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Pan the map by pixel delta.
|
|
186
|
+
* Positive delta means desired on-screen feature shift (right/down).
|
|
187
|
+
*/
|
|
188
|
+
panByPixels(delta: PixelCoordinate, options?: {
|
|
189
|
+
durationMs?: number;
|
|
190
|
+
}): void;
|
|
191
|
+
/**
|
|
192
|
+
* Set the map viewport cursor. Pass an empty string to restore engine default styling.
|
|
193
|
+
*/
|
|
194
|
+
setCursor(cursor: MapCursor): void;
|
|
195
|
+
/**
|
|
196
|
+
* Toggle the engine's native double-click-to-zoom gesture. `TacticalDraw`
|
|
197
|
+
* disables this for the lifetime of a variable-length draw so the commit
|
|
198
|
+
* `dblclick` doesn't also zoom the map.
|
|
199
|
+
*/
|
|
200
|
+
setDoubleClickZoomEnabled(enabled: boolean): void;
|
|
201
|
+
/**
|
|
202
|
+
* Get current view resolution (meters per pixel at center or generally).
|
|
203
|
+
* returns undefined if not supported or not ready.
|
|
204
|
+
*/
|
|
205
|
+
getResolution(): number | undefined;
|
|
206
|
+
/**
|
|
207
|
+
* Get current map zoom level
|
|
208
|
+
*/
|
|
209
|
+
getZoom(): number | undefined;
|
|
210
|
+
/**
|
|
211
|
+
* Listen for view changes (zoom/resolution)
|
|
212
|
+
*/
|
|
213
|
+
onViewChange(handler: () => void): void;
|
|
214
|
+
offViewChange(handler: () => void): void;
|
|
215
|
+
/**
|
|
216
|
+
* Construct an {@link EditPointerDriver} for `TacticalDraw`'s edit
|
|
217
|
+
* controller. The controller subscribes to pointer events on the returned
|
|
218
|
+
* driver to wire handle drags (vertex / midpoint / translate / rotate).
|
|
219
|
+
*/
|
|
220
|
+
createEditPointerDriver(): EditPointerDriver;
|
|
221
|
+
/**
|
|
222
|
+
* Release all adapter-owned map resources. Adapters must not be used after
|
|
223
|
+
* destruction.
|
|
224
|
+
*/
|
|
225
|
+
destroy(): void;
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/base-map-adapter.d.ts
|
|
229
|
+
interface FeatureOps {
|
|
230
|
+
adds: Feature[];
|
|
231
|
+
updates: Feature[];
|
|
232
|
+
/** Features whose id was absent from the incoming set. */
|
|
233
|
+
removes?: Feature[];
|
|
234
|
+
full: Feature[];
|
|
235
|
+
replace?: boolean;
|
|
236
|
+
}
|
|
237
|
+
type NormalizedEventHandler = (lonLat: Position, pixel: PixelCoordinate, originalEvent: unknown) => void;
|
|
238
|
+
interface NativeEventEntry<TEventToken> {
|
|
239
|
+
type: string;
|
|
240
|
+
token: TEventToken;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Abstract base class shared by all `MapAdapter` implementations.
|
|
244
|
+
*
|
|
245
|
+
* Owns shared bookkeeping (layer registry, feature cache, event-token
|
|
246
|
+
* tracking) and orchestrates the public methods over a small
|
|
247
|
+
* protected primitive surface that subclasses implement. See
|
|
248
|
+
* `docs/adr/0001-map-adapter-base-class.md` for the rationale, in particular
|
|
249
|
+
* around the `applyFeatureOps` primitive shape.
|
|
250
|
+
*/
|
|
251
|
+
declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> implements MapAdapter {
|
|
252
|
+
protected layers: Map<string, TLayerHandle>;
|
|
253
|
+
protected eventTokens: Map<MapEventHandler, NativeEventEntry<TEventToken>>;
|
|
254
|
+
protected viewChangeTokens: Map<() => void, TViewToken>;
|
|
255
|
+
protected featureCache: Map<string, Feature<import("geojson").Geometry, import("geojson").GeoJsonProperties>[]>;
|
|
256
|
+
protected featuresChangedListeners: Map<string, Set<(features: Feature[]) => void>>;
|
|
257
|
+
private layerCounter;
|
|
258
|
+
private destroyed;
|
|
259
|
+
protected nextLayerId(): LayerId;
|
|
260
|
+
addVectorLayer(options?: {
|
|
261
|
+
style?: StyleOptions;
|
|
262
|
+
}): LayerId;
|
|
263
|
+
removeLayer(layerId: LayerId): void;
|
|
264
|
+
setLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
|
|
265
|
+
updateLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
|
|
266
|
+
removeFeatures(layerId: LayerId, ids: readonly string[]): void;
|
|
267
|
+
abstract onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
268
|
+
abstract createEditPointerDriver(): EditPointerDriver;
|
|
269
|
+
clearLayer(layerId: LayerId): void;
|
|
270
|
+
on(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
|
|
271
|
+
off(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
|
|
272
|
+
onViewChange(handler: () => void): void;
|
|
273
|
+
offViewChange(handler: () => void): void;
|
|
274
|
+
destroy(): void;
|
|
275
|
+
protected emitLayerFeaturesChanged(layerId: LayerId, features: Feature[]): void;
|
|
276
|
+
protected onLayerFeaturesChanged(layerId: LayerId, listener: (features: Feature[]) => void): () => void;
|
|
277
|
+
protected destroyEngineResources(): void;
|
|
278
|
+
protected abstract createLayer(layerId: LayerId, style?: StyleOptions): TLayerHandle;
|
|
279
|
+
protected abstract destroyLayer(handle: TLayerHandle): void;
|
|
280
|
+
protected abstract applyFeatureOps(layerId: LayerId, ops: FeatureOps): void;
|
|
281
|
+
protected abstract attachNativeEvent(type: "click" | "pointermove" | "dblclick", onEvent: NormalizedEventHandler): NativeEventEntry<TEventToken>;
|
|
282
|
+
protected abstract detachNativeEvent(entry: NativeEventEntry<TEventToken>): void;
|
|
283
|
+
protected abstract attachViewChange(handler: () => void): TViewToken;
|
|
284
|
+
protected abstract detachViewChange(token: TViewToken): void;
|
|
285
|
+
abstract setLayerStyle(layerId: LayerId, style: StyleOptions): void;
|
|
286
|
+
abstract toLonLat(coordinate: number[]): Position;
|
|
287
|
+
abstract fromLonLat(position: Position): number[];
|
|
288
|
+
abstract getPixelFromCoordinate(position: Position): PixelCoordinate | null;
|
|
289
|
+
abstract getViewportSize(): {
|
|
290
|
+
width: number;
|
|
291
|
+
height: number;
|
|
292
|
+
};
|
|
293
|
+
abstract panByPixels(delta: PixelCoordinate, options?: {
|
|
294
|
+
durationMs?: number;
|
|
295
|
+
}): void;
|
|
296
|
+
abstract setCursor(cursor: MapCursor): void;
|
|
297
|
+
abstract setDoubleClickZoomEnabled(enabled: boolean): void;
|
|
298
|
+
abstract getResolution(): number | undefined;
|
|
299
|
+
abstract getZoom(): number | undefined;
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/pick-utils.d.ts
|
|
303
|
+
declare function pickHitTolerance(originalEvent: unknown): number;
|
|
304
|
+
declare function tryControlMeasureIdFromFeature(feature: Feature): string | null;
|
|
305
|
+
/**
|
|
306
|
+
* Wire a subscription to honour both an optional AbortSignal and a returned
|
|
307
|
+
* dispose function. `attach` runs immediately (unless the signal is already
|
|
308
|
+
* aborted) and must return its own teardown function. The returned dispose is
|
|
309
|
+
* idempotent.
|
|
310
|
+
*/
|
|
311
|
+
declare function bindAbortable(signal: AbortSignal | undefined, attach: () => () => void): () => void;
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/feature-style.d.ts
|
|
314
|
+
/**
|
|
315
|
+
* Coerce a raw `properties.style` value into a `HandleStyle` for the
|
|
316
|
+
* per-feature interaction-affordance overlay (ADR-0006). Returns `undefined`
|
|
317
|
+
* when missing or shaped wrong so callers fall through to the layer style.
|
|
318
|
+
*/
|
|
319
|
+
declare function coerceHandleStyle(raw: unknown): HandleStyle | undefined;
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/gestures/hit-test.d.ts
|
|
322
|
+
type ToPixel = (lonLat: Position) => PixelCoordinate | null;
|
|
323
|
+
/**
|
|
324
|
+
* Test whether `pixel` hits any feature in `features`, in pixel space.
|
|
325
|
+
*
|
|
326
|
+
* Hit semantics:
|
|
327
|
+
* - Point: within `hitTolerance` of the projected vertex
|
|
328
|
+
* - LineString / MultiLineString: within `hitTolerance` of any segment
|
|
329
|
+
* - Polygon / MultiPolygon: inside the projected ring, OR within
|
|
330
|
+
* `hitTolerance` of a ring segment (so border hits work outside fill)
|
|
331
|
+
*
|
|
332
|
+
* Returns the first matching feature in iteration order, or null.
|
|
333
|
+
*/
|
|
334
|
+
declare function hitTestFeature(pixel: PixelCoordinate, features: Feature[], hitTolerance: number, toPixel: ToPixel): Feature | null;
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/gestures/hit-tolerance.d.ts
|
|
337
|
+
declare const TOUCH_HIT_TOLERANCE_PX = 22;
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/gestures/pointer-callback-hub.d.ts
|
|
340
|
+
/**
|
|
341
|
+
* The subscriber registry behind every {@link EditPointerDriver}.
|
|
342
|
+
*
|
|
343
|
+
* Each engine adapter sources pointer events differently — OpenLayers from
|
|
344
|
+
* unified DOM `PointerEvent`s, MapLibre and Leaflet from split mouse/touch
|
|
345
|
+
* streams — and each owns its own multi-touch capture FSM. What does *not*
|
|
346
|
+
* vary is the registry: three callback sets, the subscribe-returns-disposer
|
|
347
|
+
* contract, the snapshot-before-dispatch semantics, and teardown. That lived
|
|
348
|
+
* in four hand-rolled copies (the three adapters plus the test fake); it now
|
|
349
|
+
* lives here once.
|
|
350
|
+
*
|
|
351
|
+
* Adapters fan their normalized events in through {@link fireDown} /
|
|
352
|
+
* {@link fireMove} / {@link fireUp} and expose the `onPointer*` methods on
|
|
353
|
+
* their driver; teardown calls {@link clear}.
|
|
354
|
+
*/
|
|
355
|
+
declare class PointerCallbackHub {
|
|
356
|
+
private readonly downCbs;
|
|
357
|
+
private readonly moveCbs;
|
|
358
|
+
private readonly upCbs;
|
|
359
|
+
onPointerDown(cb: PointerCallback): () => void;
|
|
360
|
+
onPointerMove(cb: PointerCallback): () => void;
|
|
361
|
+
onPointerUp(cb: PointerCallback): () => void;
|
|
362
|
+
fireDown(pixel: PixelCoordinate, info?: PointerEventInfo): void;
|
|
363
|
+
fireMove(pixel: PixelCoordinate, info?: PointerEventInfo): void;
|
|
364
|
+
fireUp(pixel: PixelCoordinate, info?: PointerEventInfo): void;
|
|
365
|
+
/** Drop every subscriber. Idempotent; call from the driver's `dispose`. */
|
|
366
|
+
clear(): void;
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/tactical-draw/abort.d.ts
|
|
370
|
+
/**
|
|
371
|
+
* AbortPlumbing for the TacticalDraw façade.
|
|
372
|
+
*
|
|
373
|
+
* Owns the abort contract for `TacticalDraw` so the rest of the façade
|
|
374
|
+
* (controllers, sessions) does not invent ad-hoc reason strings or error
|
|
375
|
+
* subclasses. See issue #43 / PRD #40.
|
|
376
|
+
*/
|
|
377
|
+
type TacticalDrawAbortReason = "escape" | "signal" | "preempted" | "destroyed" | "session" | "removed";
|
|
378
|
+
interface TacticalDrawAbortErrorOptions {
|
|
379
|
+
cause?: unknown;
|
|
380
|
+
message?: string;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Façade abort error. Extends `DOMException` with `name: "AbortError"` so it
|
|
384
|
+
* still matches platform `AbortSignal` semantics and `instanceof DOMException`.
|
|
385
|
+
*/
|
|
386
|
+
declare class TacticalDrawAbortError extends DOMException {
|
|
387
|
+
readonly name: "AbortError";
|
|
388
|
+
readonly reason: TacticalDrawAbortReason;
|
|
389
|
+
constructor(reason: TacticalDrawAbortReason, options?: TacticalDrawAbortErrorOptions);
|
|
390
|
+
}
|
|
391
|
+
declare function isTacticalDrawAbortError(error: unknown): error is TacticalDrawAbortError;
|
|
392
|
+
/**
|
|
393
|
+
* Swallow façade `TacticalDrawAbortError` *and* plain DOM `AbortError`s;
|
|
394
|
+
* rethrow every other value unchanged. Designed for use as
|
|
395
|
+
* `promise.catch(ignoreAbort)`.
|
|
396
|
+
*/
|
|
397
|
+
declare function ignoreAbort(error: unknown): void;
|
|
398
|
+
/**
|
|
399
|
+
* Combined-signal handle returned by `combineWithHostSignal`. The façade uses
|
|
400
|
+
* `signal` for the live interaction and `abort` for programmatic cancellation
|
|
401
|
+
* with a closed-enum reason. `dispose` detaches the host-signal listener;
|
|
402
|
+
* controllers should call it when the interaction settles.
|
|
403
|
+
*/
|
|
404
|
+
interface CombinedAbort {
|
|
405
|
+
readonly signal: AbortSignal;
|
|
406
|
+
abort(reason: TacticalDrawAbortReason, cause?: unknown): void;
|
|
407
|
+
dispose(): void;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Compose the façade's internal abort controller with an optional host
|
|
411
|
+
* `AbortSignal`. Host aborts surface as `reason: "signal"` with the host's
|
|
412
|
+
* `signal.reason` on `error.cause`. An already-aborted host signal triggers
|
|
413
|
+
* a next-microtask abort (never synchronous), so callers can register await
|
|
414
|
+
* handlers before the rejection fires.
|
|
415
|
+
*/
|
|
416
|
+
declare function combineWithHostSignal(hostSignal?: AbortSignal): CombinedAbort;
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/tactical-draw/measure-options.d.ts
|
|
419
|
+
/**
|
|
420
|
+
* Where a control measure's size is anchored (ADR-0020). `ground` (the
|
|
421
|
+
* default) means a meter size fixed to the terrain — static, never re-rendered
|
|
422
|
+
* on zoom. `screen` means a pixel size held constant on screen — re-rendered
|
|
423
|
+
* against the live `metersPerPixel` on every view change. A draw/edit drawn in
|
|
424
|
+
* pixels resolves (`bakes`) to a `ground` anchor on commit unless the caller
|
|
425
|
+
* asks to stay `screen`.
|
|
426
|
+
*/
|
|
427
|
+
type SizeAnchor = "ground" | "screen";
|
|
428
|
+
/**
|
|
429
|
+
* The [[SizeAnchor]] a measure currently encodes (ADR-0020): `screen` when it
|
|
430
|
+
* carries its kind's pixel-denominated size option, `ground` otherwise. Keys off
|
|
431
|
+
* the same `PIXEL_SIZE_OPTIONS` table as the bake, so a consumer deciding how to
|
|
432
|
+
* re-edit a graphic never has to re-derive the per-kind pixel-option knowledge
|
|
433
|
+
* (e.g. via a fragile key-name heuristic). Accepts the minimal `{ kind, options }`
|
|
434
|
+
* shape, mirroring [[measureUsesAdapterResolution]].
|
|
435
|
+
*/
|
|
436
|
+
declare function getSizeAnchor(measure: Pick<ControlMeasure, "kind" | "options">): SizeAnchor;
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/tactical-draw/handles.d.ts
|
|
439
|
+
/**
|
|
440
|
+
* Line-only style surface for the rubber-band guide (ADR-0004). Slot inside
|
|
441
|
+
* `InteractionStyle.guide`. Applied as the guide layer's layer style by the
|
|
442
|
+
* façade; not per-feature.
|
|
443
|
+
*/
|
|
444
|
+
interface GuideStyle {
|
|
445
|
+
strokeColor?: string;
|
|
446
|
+
strokeWidth?: number;
|
|
447
|
+
strokeDash?: readonly number[];
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Unified interaction-affordance style surface (ADR-0006). One option object
|
|
451
|
+
* for both the guide line and every handle slot. `guide` is delivered as a
|
|
452
|
+
* layer style; handle slots ride per-feature on `properties.style`.
|
|
453
|
+
*/
|
|
454
|
+
interface InteractionStyle {
|
|
455
|
+
guide?: GuideStyle;
|
|
456
|
+
vertexHandle?: HandleStyle;
|
|
457
|
+
midpointHandle?: HandleStyle;
|
|
458
|
+
translateHandle?: HandleStyle;
|
|
459
|
+
rotateHandle?: HandleStyle;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Reserved enum from PRD #40. `reshape`, `translate`, and `rotate` produce
|
|
463
|
+
* handles. `delete` is an explicit destructive mode: it renders the vertex
|
|
464
|
+
* handles (so they can be tapped to remove) but no midpoint/translate/rotate
|
|
465
|
+
* grips, and the edit controller deletes the tapped vertex instead of dragging
|
|
466
|
+
* it. `scale` is reserved and throws elsewhere.
|
|
467
|
+
*/
|
|
468
|
+
type EditMode = "reshape" | "translate" | "rotate" | "scale" | "delete";
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/tactical-draw/edit-session.d.ts
|
|
471
|
+
/**
|
|
472
|
+
* Surface delivered via `td.edit(measure, opts).onSession` and synchronously
|
|
473
|
+
* available from `td.activeSession` while the edit is live.
|
|
474
|
+
*
|
|
475
|
+
* `measure` is the immutable edit-start input. `controlPoints` is live working
|
|
476
|
+
* state. After the session settles (`close` / `abort`), every method is a
|
|
477
|
+
* silent no-op.
|
|
478
|
+
*/
|
|
479
|
+
interface EditSession {
|
|
480
|
+
/** Immutable edit-start input. */
|
|
481
|
+
readonly measure: ControlMeasure;
|
|
482
|
+
/** Live working state. Copy on read. */
|
|
483
|
+
readonly controlPoints: readonly Position[];
|
|
484
|
+
/** Live working options. Copy on read. */
|
|
485
|
+
readonly options: ControlMeasure["options"];
|
|
486
|
+
/** Live working style. Copy on read. */
|
|
487
|
+
readonly style: ControlMeasureStyle | undefined;
|
|
488
|
+
/** `true` after the first working-copy mutation; back to `false` within tolerance. */
|
|
489
|
+
readonly dirty: boolean;
|
|
490
|
+
readonly modes: readonly EditMode[];
|
|
491
|
+
/**
|
|
492
|
+
* Live size anchor for the measure under edit (ADR-0020). `"ground"` bakes a
|
|
493
|
+
* pixel size to meters on commit; `"screen"` keeps it zoom-aware. Initialised
|
|
494
|
+
* from `EditOptions.sizeAnchor` and mutated by [[setSizeAnchor]].
|
|
495
|
+
*/
|
|
496
|
+
readonly sizeAnchor: SizeAnchor;
|
|
497
|
+
/**
|
|
498
|
+
* Resolve the outer `edit` promise with a `ControlMeasureSnapshot` of the
|
|
499
|
+
* current working state.
|
|
500
|
+
*/
|
|
501
|
+
close(): void;
|
|
502
|
+
/** Reject the outer `edit` promise with reason `"session"`. */
|
|
503
|
+
abort(): void;
|
|
504
|
+
/**
|
|
505
|
+
* Switch the active edit-mode set. `"reshape"`, `"translate"`, and `"rotate"`
|
|
506
|
+
* produce handles; `"delete"` is exclusive (vertex-tap removal) and collapses
|
|
507
|
+
* the set to delete-only. `"scale"` is rejected with `TypeError` per PRD #40.
|
|
508
|
+
*/
|
|
509
|
+
setModes(modes: readonly EditMode[]): void;
|
|
510
|
+
/** Shallow-merge per-measure generator options into the live working copy. */
|
|
511
|
+
setOptions(partial: ControlMeasure["options"]): void;
|
|
512
|
+
/**
|
|
513
|
+
* Flip the size anchor (ADR-0020). Takes effect on the next emitted snapshot
|
|
514
|
+
* and on `close()`. A no-op for measures with no pixel-denominated size. The
|
|
515
|
+
* in-flight preview is unaffected (it renders from the working options), so a
|
|
516
|
+
* screen-locked symbol stays screen-locked until commit.
|
|
517
|
+
*/
|
|
518
|
+
setSizeAnchor(anchor: SizeAnchor): void;
|
|
519
|
+
/** Shallow-merge per-measure style into the live working copy. */
|
|
520
|
+
setStyle(partial: ControlMeasureStyle): void;
|
|
521
|
+
/**
|
|
522
|
+
* Subscribe to working-copy changes. The listener receives an
|
|
523
|
+
* [[EditChangeEvent]] for each completed gesture (drag end, insert/delete
|
|
524
|
+
* via handle) — never on rubber-band ticks. All currently-registered
|
|
525
|
+
* listeners receive the same event instance in registration order.
|
|
526
|
+
*
|
|
527
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
528
|
+
* rethrown (one if single, aggregated otherwise). `event.reject()` /
|
|
529
|
+
* `event.close()` / `event.abort()` take effect only after all listeners
|
|
530
|
+
* have run.
|
|
531
|
+
*/
|
|
532
|
+
onChange(listener: (event: EditChangeEvent) => void): () => void;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Delivered to `EditSession.onChange` listeners once per completed gesture.
|
|
536
|
+
*
|
|
537
|
+
* Listeners see the *same* event instance in registration order. Per-gesture
|
|
538
|
+
* effects (`reject`, `close`, `abort`) are recorded during emission and
|
|
539
|
+
* applied after every listener has run, so a downstream listener cannot
|
|
540
|
+
* observe partial state. See PRD #40 user-stories 23–24.
|
|
541
|
+
*/
|
|
542
|
+
interface EditChangeEvent {
|
|
543
|
+
/** Working state after the completed gesture. */
|
|
544
|
+
readonly measure: ControlMeasureSnapshot;
|
|
545
|
+
/** State immediately before this gesture. */
|
|
546
|
+
readonly previous: ControlMeasureSnapshot;
|
|
547
|
+
/** The session the event belongs to. */
|
|
548
|
+
readonly session: EditSession;
|
|
549
|
+
/**
|
|
550
|
+
* Roll the working preview back to `previous` after all listeners have run.
|
|
551
|
+
* Multiple calls collapse to a single rollback. Overridden by `close()` or
|
|
552
|
+
* `abort()` if either is also called during this emission.
|
|
553
|
+
*/
|
|
554
|
+
reject(): void;
|
|
555
|
+
/** Shortcut for `event.session.close()`, deferred until the emission ends. */
|
|
556
|
+
close(): void;
|
|
557
|
+
/** Shortcut for `event.session.abort()`, deferred until the emission ends. */
|
|
558
|
+
abort(): void;
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/tactical-draw/session.d.ts
|
|
562
|
+
/**
|
|
563
|
+
* Scoped surface delivered via `td.draw({ onSession })` for variable-length
|
|
564
|
+
* kinds. Also available from `td.activeSession` while the draw is live.
|
|
565
|
+
*
|
|
566
|
+
* After the draw has settled — either via `commit()` returning `true` or
|
|
567
|
+
* `abort()` resolving — every method is a silent no-op.
|
|
568
|
+
*/
|
|
569
|
+
interface DrawSession {
|
|
570
|
+
/** Live snapshot of the committed control points. Copy on read. */
|
|
571
|
+
readonly controlPoints: readonly Position[];
|
|
572
|
+
/** `true` when the current point count is in `[min, max]`. */
|
|
573
|
+
readonly canCommit: boolean;
|
|
574
|
+
readonly minControlPoints: number;
|
|
575
|
+
readonly maxControlPoints?: number;
|
|
576
|
+
/**
|
|
577
|
+
* Commit if `canCommit` is true. Returns `false` (no-op) if not committable.
|
|
578
|
+
* On success resolves the outer `draw` promise on the next microtask.
|
|
579
|
+
*/
|
|
580
|
+
commit(): boolean;
|
|
581
|
+
/** Reject the outer `draw` promise with reason `"session"`. */
|
|
582
|
+
abort(): void;
|
|
583
|
+
/**
|
|
584
|
+
* Subscribe to control-point add/remove changes. Rubber-band pointer ticks
|
|
585
|
+
* do not fire listeners. Returns an unsubscribe function. Listener errors
|
|
586
|
+
* are collected and rethrown after all listeners run.
|
|
587
|
+
*/
|
|
588
|
+
onChange(listener: (session: DrawSession) => void): () => void;
|
|
589
|
+
}
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/tactical-draw/tactical-draw.d.ts
|
|
592
|
+
/**
|
|
593
|
+
* Host-supplied layer slot ids. Any omitted slot is allocated by the façade and
|
|
594
|
+
* released on `destroy()`.
|
|
595
|
+
*/
|
|
596
|
+
interface TacticalDrawLayerOptions {
|
|
597
|
+
graphics?: LayerId;
|
|
598
|
+
preview?: LayerId;
|
|
599
|
+
guide?: LayerId;
|
|
600
|
+
handles?: LayerId;
|
|
601
|
+
}
|
|
602
|
+
interface TacticalDrawOptions {
|
|
603
|
+
layers?: TacticalDrawLayerOptions;
|
|
604
|
+
/**
|
|
605
|
+
* Façade-level default style passed through to `renderControlMeasure` as the
|
|
606
|
+
* third (lowest priority) style layer per StyleResolver (#44). Shallow-merged
|
|
607
|
+
* over `BUILT_IN_GRAPHICS_STYLE` so every map engine renders the same default
|
|
608
|
+
* appearance when neither the host nor the measure supplies a style.
|
|
609
|
+
*/
|
|
610
|
+
graphicsStyle?: ControlMeasureStyle;
|
|
611
|
+
/**
|
|
612
|
+
* Façade-level default interaction-affordance style. Per-slot, per-property
|
|
613
|
+
* merged with `DrawOptions.interactionStyle` / `EditOptions.interactionStyle`
|
|
614
|
+
* on top of built-in defaults. The `guide` slot is delivered as a layer
|
|
615
|
+
* style on the guide layer; handle slots ride per-feature. See ADR-0006.
|
|
616
|
+
*/
|
|
617
|
+
interactionStyle?: InteractionStyle;
|
|
618
|
+
/**
|
|
619
|
+
* Optional id generator for measures produced by `draw()`. Defaults to a
|
|
620
|
+
* monotonically increasing `td-${n}` slug. Caller-supplied measure ids on
|
|
621
|
+
* the draw API itself are deferred (PRD #40 user-story-5 note).
|
|
622
|
+
*/
|
|
623
|
+
generateId?: () => string;
|
|
624
|
+
}
|
|
625
|
+
interface EditOptions {
|
|
626
|
+
signal?: AbortSignal;
|
|
627
|
+
/**
|
|
628
|
+
* Called synchronously once when the edit starts, before any pointer event
|
|
629
|
+
* is processed. Lets a host wire commit / cancel buttons to the active edit.
|
|
630
|
+
*/
|
|
631
|
+
onSession?: (session: EditSession) => void;
|
|
632
|
+
/**
|
|
633
|
+
* Default `true`. When `true`, a click on empty map (or on another graphics
|
|
634
|
+
* layer measure) closes the active edit. Clicks on preview / handle
|
|
635
|
+
* features are consumed and keep the edit open. Hosts that own their own
|
|
636
|
+
* close policy pass `false`.
|
|
637
|
+
*/
|
|
638
|
+
closeOnClickAway?: boolean;
|
|
639
|
+
/**
|
|
640
|
+
* Active edit modes. Defaults to `["reshape"]`. `reshape` renders vertex /
|
|
641
|
+
* midpoint handles; `translate` renders a grip on the centroid pivot; and
|
|
642
|
+
* `rotate` renders a grip offset just outside the shape (so it has a lever
|
|
643
|
+
* arm and orbits with the geometry). The three can be combined freely.
|
|
644
|
+
* `"scale"` is reserved and throws.
|
|
645
|
+
*/
|
|
646
|
+
modes?: readonly EditMode[];
|
|
647
|
+
/**
|
|
648
|
+
* Per-call interaction style override scoped to this edit. Per-slot,
|
|
649
|
+
* per-property merged over the façade-level default and the built-ins.
|
|
650
|
+
* See ADR-0006.
|
|
651
|
+
*/
|
|
652
|
+
interactionStyle?: InteractionStyle;
|
|
653
|
+
/**
|
|
654
|
+
* When `true` (default), the preview layer carries a rubber-band guide
|
|
655
|
+
* polyline sourced from the working control points alongside the working
|
|
656
|
+
* geometry preview. For `geometry === "area"` kinds, a separate
|
|
657
|
+
* closing-segment `LineString` is also emitted once `workingPoints.length >= 3`.
|
|
658
|
+
* The guide is suppressed mechanically when `geometry === "point"` or
|
|
659
|
+
* `minCoordinates === maxCoordinates`, regardless of this flag. See ADR-0004.
|
|
660
|
+
*/
|
|
661
|
+
guide?: boolean;
|
|
662
|
+
/**
|
|
663
|
+
* Size anchor for the committed measure (ADR-0020). Symmetric with
|
|
664
|
+
* `DrawOptions.sizeAnchor`: editing a screen-anchored graphic as `"ground"`
|
|
665
|
+
* bakes/freezes it to meters; editing a ground graphic whose working options
|
|
666
|
+
* have been switched to pixels as `"screen"` keeps the pixel size. Default
|
|
667
|
+
* `"ground"`. Applied to the resolved snapshot only — the in-flight preview
|
|
668
|
+
* stays screen-anchored throughout the edit.
|
|
669
|
+
*/
|
|
670
|
+
sizeAnchor?: SizeAnchor;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Draft control measure handed to `TacticalDraw.draw`. `kind` is required up
|
|
674
|
+
* front so the façade can resolve metadata, draw rule, and controller choice
|
|
675
|
+
* before any pointer event is observed. Everything else is optional and is
|
|
676
|
+
* stamped onto the in-flight preview and final committed measure (see slices
|
|
677
|
+
* 2-7).
|
|
678
|
+
*/
|
|
679
|
+
type DrawMeasureDraft<K extends ControlMeasureKind = ControlMeasureKind> = {
|
|
680
|
+
kind: K;
|
|
681
|
+
} & Partial<Omit<ControlMeasure<K>, "kind" | "controlPoints">> & {
|
|
682
|
+
controlPoints?: readonly Position[];
|
|
683
|
+
};
|
|
684
|
+
/**
|
|
685
|
+
* Interaction-only options for `TacticalDraw.draw`. Measure data — seed
|
|
686
|
+
* points, per-kind options, style, properties — lives on the `DrawMeasureDraft`
|
|
687
|
+
* passed as the first argument.
|
|
688
|
+
*/
|
|
689
|
+
interface DrawOptions {
|
|
690
|
+
signal?: AbortSignal;
|
|
691
|
+
/**
|
|
692
|
+
* Called synchronously once when a variable-length draw starts, before any
|
|
693
|
+
* pointer event is processed. The session lets a host wire a Done/Cancel
|
|
694
|
+
* toolbar to the active draw. Ignored for fixed-length kinds.
|
|
695
|
+
*/
|
|
696
|
+
onSession?: (session: DrawSession) => void;
|
|
697
|
+
/**
|
|
698
|
+
* When `true` (default), the preview layer carries a rubber-band guide
|
|
699
|
+
* polyline alongside the kind's generator preview. For `geometry === "area"`
|
|
700
|
+
* kinds, a separate closing-segment `LineString` is also emitted once the
|
|
701
|
+
* total point count (cursor included) is ≥ 3. The guide is suppressed
|
|
702
|
+
* mechanically when `geometry === "point"` or `minCoordinates === maxCoordinates`,
|
|
703
|
+
* regardless of this flag. See ADR-0004.
|
|
704
|
+
*/
|
|
705
|
+
guide?: boolean;
|
|
706
|
+
/**
|
|
707
|
+
* Per-call interaction style override scoped to this draw. Only the `guide`
|
|
708
|
+
* slot is meaningful for draws today — handle slots are reserved for the
|
|
709
|
+
* edit path. See ADR-0006.
|
|
710
|
+
*/
|
|
711
|
+
interactionStyle?: InteractionStyle;
|
|
712
|
+
/**
|
|
713
|
+
* Size anchor for the committed measure (ADR-0020). `"ground"` (default)
|
|
714
|
+
* bakes any pixel-denominated size to meters at the finishing zoom, so the
|
|
715
|
+
* graphic becomes static. `"screen"` keeps the pixel size, so the graphic
|
|
716
|
+
* holds a constant on-screen size and is re-rendered on every zoom. Only
|
|
717
|
+
* meaningful for pixel-sizable kinds (FLOT, fortified, antitank); a no-op
|
|
718
|
+
* otherwise. Not persisted — the resulting options shape encodes the anchor.
|
|
719
|
+
*/
|
|
720
|
+
sizeAnchor?: SizeAnchor;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* `TacticalDraw` façade — slice #6 (issue #45).
|
|
724
|
+
*
|
|
725
|
+
* Provides the constructor + lifecycle + `render(measures)` reconcile path.
|
|
726
|
+
* Interactive draw and edit live in later slices; the shape is in place so
|
|
727
|
+
* the surface is stable.
|
|
728
|
+
*/
|
|
729
|
+
declare class TacticalDraw {
|
|
730
|
+
private readonly adapter;
|
|
731
|
+
private readonly graphicsLayer;
|
|
732
|
+
private readonly previewLayer;
|
|
733
|
+
private readonly guideLayer;
|
|
734
|
+
private readonly handlesLayer;
|
|
735
|
+
private readonly ownedLayers;
|
|
736
|
+
private readonly graphicsStyle;
|
|
737
|
+
private readonly defaultInteractionStyle;
|
|
738
|
+
private readonly generateId;
|
|
739
|
+
private destroyed;
|
|
740
|
+
private hasRendered;
|
|
741
|
+
private readonly activeInteraction;
|
|
742
|
+
/**
|
|
743
|
+
* Tracked alongside `activeInteraction` whenever the interaction is an edit. The
|
|
744
|
+
* `render()` path needs the edited measure's id (to exclude it from
|
|
745
|
+
* reconcile) and a hook to surface external style changes — both live on
|
|
746
|
+
* `EditHandle` so the controller stays the single owner of edit state.
|
|
747
|
+
*/
|
|
748
|
+
private activeEdit;
|
|
749
|
+
/**
|
|
750
|
+
* Host pick handlers registered via `td.onPick` / `td.onMeasurePick`. The
|
|
751
|
+
* façade subscribes once to `adapter.onPick` per layer and routes events
|
|
752
|
+
* through this map so that close-then-pick ordering during an active edit can
|
|
753
|
+
* be enforced (issue #52).
|
|
754
|
+
*/
|
|
755
|
+
private readonly hostPickHandlers;
|
|
756
|
+
private readonly adapterPickUnsubs;
|
|
757
|
+
/**
|
|
758
|
+
* Captured graphics-layer pick that arrived during an active edit on a
|
|
759
|
+
* *different* measure. The edit controller closes the active session and
|
|
760
|
+
* then asks the façade to re-dispatch this pick to host handlers so that
|
|
761
|
+
* `td.edit(otherMeasure)` from a host pick handler runs as a fresh edit,
|
|
762
|
+
* not as a programmatic preemption.
|
|
763
|
+
*/
|
|
764
|
+
private deferredGraphicsPick;
|
|
765
|
+
private idCounter;
|
|
766
|
+
private editCounter;
|
|
767
|
+
/**
|
|
768
|
+
* Snapshot of the host's last `render()` argument. The edit slice uses it
|
|
769
|
+
* to recompute the graphics-layer state during a lift / restore — the
|
|
770
|
+
* façade owns the policy because controllers shouldn't need to know the
|
|
771
|
+
* surrounding collection state.
|
|
772
|
+
*/
|
|
773
|
+
private lastRenderedMeasures;
|
|
774
|
+
constructor(adapter: MapAdapter, options?: TacticalDrawOptions);
|
|
775
|
+
/**
|
|
776
|
+
* Render `measures` into features and write them to the graphics layer.
|
|
777
|
+
* Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
|
|
778
|
+
* resolved against the live map. Callers own any edit-exclusion policy — see
|
|
779
|
+
* `render()` (excludes the edited measure, owned by the preview layer) versus
|
|
780
|
+
* `restoreOriginalToGraphics` (no exclusion, the edit is ending).
|
|
781
|
+
*/
|
|
782
|
+
private renderMeasuresToGraphics;
|
|
783
|
+
/**
|
|
784
|
+
* Re-render the graphics layer on a view (zoom) change so pixel-denominated
|
|
785
|
+
* measures rescale live. Skipped entirely when nothing rendered carries a
|
|
786
|
+
* resolution-dependent size — meter geometry is zoom-independent, so a rebuild
|
|
787
|
+
* would be wasted layer writes. During an active edit the edited measure lives
|
|
788
|
+
* on the preview layer (lifted from graphics), so it is excluded here exactly
|
|
789
|
+
* as in `render()`; the preview's own view-change subscription rescales it.
|
|
790
|
+
*/
|
|
791
|
+
private readonly handleViewChange;
|
|
792
|
+
/**
|
|
793
|
+
* Reconcile the graphics layer against the authoritative list of measures.
|
|
794
|
+
* Idempotent. Throws synchronously on duplicate measure ids *before*
|
|
795
|
+
* mutating any layer. An empty list before any prior render is a no-op.
|
|
796
|
+
*
|
|
797
|
+
* Cross-layer semantics with an active edit (issue #51):
|
|
798
|
+
* - The measure under edit is **excluded** from the reconcile pass — the
|
|
799
|
+
* preview layer owns its working copy and the lift on the graphics layer
|
|
800
|
+
* must stay in place.
|
|
801
|
+
* - If the edited measure is absent from `measures`, the pending `edit`
|
|
802
|
+
* promise rejects on the **next microtask** with reason `"removed"`.
|
|
803
|
+
* `render` itself returns normally so host collection ops stay synchronous.
|
|
804
|
+
* - If the edited measure is present and its `style` changed, the preview
|
|
805
|
+
* picks up the new style on this render. `controlPoints` changes on the
|
|
806
|
+
* edited measure are routed nowhere — the user's in-flight drag is never
|
|
807
|
+
* yanked out from under them.
|
|
808
|
+
*
|
|
809
|
+
* Cross-layer semantics with an active draw: `render` only touches the
|
|
810
|
+
* graphics layer, so the draw's preview is undisturbed.
|
|
811
|
+
*
|
|
812
|
+
* Reentrancy: calling `td.render(...)` from inside `EditSession.onChange` is
|
|
813
|
+
* safe — `render` never re-enters the active edit or its preview / handle
|
|
814
|
+
* layers (the only edit-facing side effect, `notifyExternalMeasure`, is a
|
|
815
|
+
* pure style replay).
|
|
816
|
+
*/
|
|
817
|
+
render(measures: readonly ControlMeasure[]): void;
|
|
818
|
+
/**
|
|
819
|
+
* Nested-call guard + preempt, shared by `draw` and `edit`. Returns a
|
|
820
|
+
* pre-rejected promise when called re-entrantly from inside another
|
|
821
|
+
* interaction's `onSession` (the outer is still mid-construction, so the
|
|
822
|
+
* nested one is preempted synchronously — #52). Otherwise preempts whichever
|
|
823
|
+
* draw / edit is in flight and returns `null`, signalling the caller to
|
|
824
|
+
* proceed. One interaction at a time per PRD #40.
|
|
825
|
+
*/
|
|
826
|
+
private guardAndPreempt;
|
|
827
|
+
/**
|
|
828
|
+
* Reserve the single interaction slot around `create`, shared by `draw` and
|
|
829
|
+
* `edit`. Reserves the active tracker (and `activeEdit` for edits) synchronously so
|
|
830
|
+
* `onSettled` callbacks see a coherent state; the handle's settle path calls
|
|
831
|
+
* the wrapped `onSettled` which clears the slot for us, avoiding the
|
|
832
|
+
* "promise.then(clearActive)" microtask gap that left the interaction set across
|
|
833
|
+
* re-dispatched pick handlers. The backstop `.then` covers a controller that
|
|
834
|
+
* settled synchronously (already-aborted signal returns a pre-settled handle),
|
|
835
|
+
* whose `onSettled` fired before `active` was assigned. `extraSettle` runs the
|
|
836
|
+
* caller's own teardown (guide-style revert, dbl-click-zoom re-enable).
|
|
837
|
+
*/
|
|
838
|
+
private runWithSlot;
|
|
839
|
+
/**
|
|
840
|
+
* Wrap a host `onSession` callback so the session is recorded on the tracker
|
|
841
|
+
* (exposing it via `activeSession`) before the host's own handler runs.
|
|
842
|
+
*/
|
|
843
|
+
private trackSession;
|
|
844
|
+
/**
|
|
845
|
+
* Start an interactive draw. Fixed-length kinds commit automatically on the
|
|
846
|
+
* last required click. Variable-length kinds surface a `DrawSession` via
|
|
847
|
+
* `opts.onSession` and commit on `session.commit()`, `dblclick`, or a second
|
|
848
|
+
* click on the last added control point. All paths reject with
|
|
849
|
+
* `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
|
|
850
|
+
*/
|
|
851
|
+
draw<K extends ControlMeasureKind>(draft: DrawMeasureDraft<K>, options?: DrawOptions): Promise<ControlMeasureSnapshot<K>>;
|
|
852
|
+
/**
|
|
853
|
+
* Start an interactive edit on a single control measure. Returns a Promise
|
|
854
|
+
* that resolves with a `ControlMeasureSnapshot` of the working state on
|
|
855
|
+
* `session.close()` and rejects with `TacticalDrawAbortError` on
|
|
856
|
+
* signal / Escape / destroy / preempt / `session.abort()`.
|
|
857
|
+
*
|
|
858
|
+
* Default click-away (`closeOnClickAway: true`) closes the edit on a click
|
|
859
|
+
* outside the preview / handle features. Hosts that own close policy pass
|
|
860
|
+
* `false`.
|
|
861
|
+
*/
|
|
862
|
+
edit(measure: ControlMeasure, options?: EditOptions): Promise<ControlMeasureSnapshot>;
|
|
863
|
+
/**
|
|
864
|
+
* Subscribe to picks on committed control measures.
|
|
865
|
+
*
|
|
866
|
+
* The graphics layer is pre-bound, and `event.id` is the originating control
|
|
867
|
+
* measure id. Events retain the same close-then-pick ordering as
|
|
868
|
+
* {@link onPick}: when another measure is picked during an active edit, the
|
|
869
|
+
* current edit closes before the handler runs.
|
|
870
|
+
*
|
|
871
|
+
* Returns an idempotent unsubscribe function. An optional `signal` removes
|
|
872
|
+
* the handler when aborted.
|
|
873
|
+
*/
|
|
874
|
+
onMeasurePick(handler: PickHandler, opts?: PickOptions): () => void;
|
|
875
|
+
/**
|
|
876
|
+
* Subscribe to pick events on `layerId`. Routed through the façade so the
|
|
877
|
+
* close-then-pick contract (issue #52) is observable: when an active edit
|
|
878
|
+
* sees a click on a *different* graphics-layer measure, the edit closes
|
|
879
|
+
* first and only then are pick events delivered to host handlers — letting
|
|
880
|
+
* a host `td.edit(other)` from a pick handler run as a fresh edit instead
|
|
881
|
+
* of a programmatic preemption.
|
|
882
|
+
*
|
|
883
|
+
* Returns an unsubscribe function. Optional `signal` mirrors `adapter.onPick`.
|
|
884
|
+
*/
|
|
885
|
+
onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
886
|
+
/**
|
|
887
|
+
* Abort the in-flight draw or edit. The default `"session"` reason matches
|
|
888
|
+
* `DrawSession.abort()` / `EditSession.abort()`.
|
|
889
|
+
*
|
|
890
|
+
* Returns `true` when an interaction was active, otherwise `false`.
|
|
891
|
+
*/
|
|
892
|
+
cancel(reason?: TacticalDrawAbortReason): boolean;
|
|
893
|
+
/**
|
|
894
|
+
* The live variable-draw or edit session, or `null` while idle and during
|
|
895
|
+
* fixed-length draws. Safe to read after `destroy()`.
|
|
896
|
+
*/
|
|
897
|
+
get activeSession(): DrawSession | EditSession | null;
|
|
898
|
+
/**
|
|
899
|
+
* Tear down the façade. Releases only layer slots the façade created; host
|
|
900
|
+
* supplied layer ids are left untouched. Safe to call multiple times. After
|
|
901
|
+
* the first call every public method throws `TacticalDrawDestroyedError`.
|
|
902
|
+
*/
|
|
903
|
+
destroy(): void;
|
|
904
|
+
/**
|
|
905
|
+
* Decide whether to deliver `event` to host pick handlers immediately or
|
|
906
|
+
* defer it until after the active edit closes. The "defer" branch is the
|
|
907
|
+
* close-then-pick path: the edit controller's click handler will close the
|
|
908
|
+
* session and then invoke `dispatchHostPick(graphicsLayer, event)` via
|
|
909
|
+
* `onClickAwayGraphicsPick` to redeliver — guaranteeing `td.edit(other)`
|
|
910
|
+
* from a host pick handler observes a settled session.
|
|
911
|
+
*/
|
|
912
|
+
private routePick;
|
|
913
|
+
private dispatchHostPick;
|
|
914
|
+
/** @internal — exposed for tests and future controllers. */
|
|
915
|
+
get layerIds(): {
|
|
916
|
+
graphics: LayerId;
|
|
917
|
+
preview: LayerId;
|
|
918
|
+
guide: LayerId;
|
|
919
|
+
handles: LayerId;
|
|
920
|
+
};
|
|
921
|
+
/**
|
|
922
|
+
* Apply the resolved guide-slot style for an interaction and return a
|
|
923
|
+
* revert closure to re-apply the base (built-in + façade) style on settle.
|
|
924
|
+
* Per-call overrides are scoped to a single interaction (ADR-0004/0006).
|
|
925
|
+
*/
|
|
926
|
+
private applyGuideStyle;
|
|
927
|
+
private baseGuideStyle;
|
|
928
|
+
/**
|
|
929
|
+
* Resolve handle slots across built-in → façade → per-call with per-slot,
|
|
930
|
+
* per-property shallow merge. Explicit `undefined` at a higher-priority
|
|
931
|
+
* layer unsets the corresponding lower-priority field (ADR-0006).
|
|
932
|
+
*
|
|
933
|
+
* Returns `undefined` only when no slot would carry any value — keeps the
|
|
934
|
+
* downstream "no style on the feature" path intact for tests that assert
|
|
935
|
+
* the absence of `properties.style`.
|
|
936
|
+
*/
|
|
937
|
+
private resolveInteractionStyle;
|
|
938
|
+
private assertAlive;
|
|
939
|
+
}
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region src/tactical-draw/errors.d.ts
|
|
942
|
+
/**
|
|
943
|
+
* Error thrown when any `TacticalDraw` method is called after `destroy()`.
|
|
944
|
+
* See PRD #40 / issue #45.
|
|
945
|
+
*/
|
|
946
|
+
declare class TacticalDrawDestroyedError extends Error {
|
|
947
|
+
constructor(message?: string);
|
|
948
|
+
}
|
|
949
|
+
//#endregion
|
|
950
|
+
export { BaseMapAdapter, type CombinedAbort, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, type InteractionStyle, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, type PixelCoordinate, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, pickHitTolerance, tryControlMeasureIdFromFeature };
|