@orbat-mapper/tactical-draw 0.2.0-alpha.3 → 0.2.0-alpha.31
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/README.md +27 -7
- package/dist/index.d.mts +1177 -61
- package/dist/index.mjs +5038 -1309
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ControlMeasure, ControlMeasureKind,
|
|
1
|
+
import { AmplifierPlacement, AmplifierPlacements, ControlMeasure, ControlMeasureKind, ControlMeasureRender, ControlMeasureStyle, FillPattern, TextAmplifiers } from "@orbat-mapper/control-measures";
|
|
2
2
|
import { Feature, Position } from "geojson";
|
|
3
3
|
|
|
4
4
|
//#region src/gestures/types.d.ts
|
|
@@ -12,6 +12,11 @@ interface PointerEventInfo {
|
|
|
12
12
|
* mouse paths populate it; touch leaves it `undefined`.
|
|
13
13
|
*/
|
|
14
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;
|
|
15
20
|
/** Engine-native event, for drivers that need to call preventDefault etc. */
|
|
16
21
|
native?: unknown;
|
|
17
22
|
}
|
|
@@ -35,6 +40,52 @@ interface EditPointerDriver {
|
|
|
35
40
|
dispose(): void;
|
|
36
41
|
}
|
|
37
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
|
|
38
89
|
//#region src/map-adapter.d.ts
|
|
39
90
|
type PixelCoordinate = [number, number];
|
|
40
91
|
type MapCursor = "" | "default" | "crosshair" | "grab" | "grabbing" | "pointer";
|
|
@@ -54,6 +105,16 @@ interface StyleOptions {
|
|
|
54
105
|
pointRadius?: number;
|
|
55
106
|
text?: TextStyleOptions;
|
|
56
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
|
+
*/
|
|
57
118
|
interface TextStyleOptions {
|
|
58
119
|
text?: string;
|
|
59
120
|
labelProperty?: string;
|
|
@@ -62,6 +123,18 @@ interface TextStyleOptions {
|
|
|
62
123
|
strokeColor?: string;
|
|
63
124
|
strokeWidth?: number;
|
|
64
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;
|
|
65
138
|
}
|
|
66
139
|
/**
|
|
67
140
|
* Per-feature style for edit handles (vertex / midpoint / translate / rotate).
|
|
@@ -75,9 +148,25 @@ interface HandleStyle {
|
|
|
75
148
|
strokeWidth?: number;
|
|
76
149
|
strokeDash?: readonly number[];
|
|
77
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;
|
|
78
159
|
pointRadius?: number;
|
|
79
160
|
}
|
|
80
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
|
+
}
|
|
81
170
|
/**
|
|
82
171
|
* Click-style hit event delivered by {@link MapAdapter.onPick}.
|
|
83
172
|
*
|
|
@@ -115,6 +204,8 @@ interface MapAdapter {
|
|
|
115
204
|
* Set the style for a specific layer.
|
|
116
205
|
*/
|
|
117
206
|
setLayerStyle(layerId: LayerId, style: StyleOptions): void;
|
|
207
|
+
/** Reconcile the layer's desired shared icon resources. */
|
|
208
|
+
reconcileLayerIconResources(layerId: LayerId, resources: readonly IconResource[]): void;
|
|
118
209
|
/**
|
|
119
210
|
* Set features on a vector layer.
|
|
120
211
|
* Features should be standard GeoJSON Features.
|
|
@@ -143,6 +234,12 @@ interface MapAdapter {
|
|
|
143
234
|
* silently ignored.
|
|
144
235
|
*/
|
|
145
236
|
removeFeatures(layerId: LayerId, ids: readonly string[]): void;
|
|
237
|
+
/**
|
|
238
|
+
* Defer an upcoming removal from `layerId` until the most recent update to
|
|
239
|
+
* `updatedLayerId` has completed in the engine. Optional because synchronous
|
|
240
|
+
* adapters already make cross-layer write order visible atomically.
|
|
241
|
+
*/
|
|
242
|
+
deferLayerRemovalUntilLayerUpdate?(layerId: LayerId, updatedLayerId: LayerId): void;
|
|
146
243
|
/**
|
|
147
244
|
* Subscribe to click-style "pick" events for a specific layer. Basemap and
|
|
148
245
|
* other-layer clicks never fire the handler.
|
|
@@ -165,6 +262,15 @@ interface MapAdapter {
|
|
|
165
262
|
onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
166
263
|
/**
|
|
167
264
|
* Map Event Listeners
|
|
265
|
+
*
|
|
266
|
+
* Ordering contract: a `"click"` handler and the `onPick` handlers for the
|
|
267
|
+
* same user click MUST observe the pick first — adapters deliver picks and
|
|
268
|
+
* `"click"` from the same underlying engine event, in registration order.
|
|
269
|
+
* The draw/edit controllers register their pick subscriptions before their
|
|
270
|
+
* click handler and read the stashed pick inside it (click-away close /
|
|
271
|
+
* close-then-pick re-dispatch); an adapter that fires `"click"` from an
|
|
272
|
+
* earlier engine event than its picks breaks that contract (e.g. OL
|
|
273
|
+
* `click` vs `singleclick`).
|
|
168
274
|
*/
|
|
169
275
|
on(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
|
|
170
276
|
off(eventType: "click" | "pointermove" | "dblclick", handler: MapEventHandler): void;
|
|
@@ -174,6 +280,11 @@ interface MapAdapter {
|
|
|
174
280
|
toLonLat(coordinate: number[]): Position;
|
|
175
281
|
fromLonLat(position: Position): number[];
|
|
176
282
|
getPixelFromCoordinate(position: Position): PixelCoordinate | null;
|
|
283
|
+
/**
|
|
284
|
+
* Convert a viewport pixel to longitude/latitude. Optional so third-party
|
|
285
|
+
* adapters compiled against an older ABI remain valid.
|
|
286
|
+
*/
|
|
287
|
+
getCoordinateFromPixel?(pixel: PixelCoordinate): Position | null;
|
|
177
288
|
/**
|
|
178
289
|
* Get current viewport size in CSS pixels.
|
|
179
290
|
*/
|
|
@@ -207,11 +318,23 @@ interface MapAdapter {
|
|
|
207
318
|
* Get current map zoom level
|
|
208
319
|
*/
|
|
209
320
|
getZoom(): number | undefined;
|
|
321
|
+
/**
|
|
322
|
+
* Get current view bearing, radians — the clockwise-from-north compass
|
|
323
|
+
* direction that screen-up points to (MapLibre-bearing semantics). `0` for
|
|
324
|
+
* engines/views that don't rotate.
|
|
325
|
+
*/
|
|
326
|
+
getBearing(): number;
|
|
210
327
|
/**
|
|
211
328
|
* Listen for view changes (zoom/resolution)
|
|
212
329
|
*/
|
|
213
330
|
onViewChange(handler: () => void): void;
|
|
214
331
|
offViewChange(handler: () => void): void;
|
|
332
|
+
/**
|
|
333
|
+
* The engine evaluates the meters→pixels clamp for ground-sized point
|
|
334
|
+
* symbols inside its own style layer, so meters-sized point symbols don't
|
|
335
|
+
* need view-change re-renders for size.
|
|
336
|
+
*/
|
|
337
|
+
readonly scalesGroundPointSymbolsNatively?: boolean;
|
|
215
338
|
/**
|
|
216
339
|
* Construct an {@link EditPointerDriver} for `TacticalDraw`'s edit
|
|
217
340
|
* controller. The controller subscribes to pointer events on the returned
|
|
@@ -223,6 +346,13 @@ interface MapAdapter {
|
|
|
223
346
|
* destruction.
|
|
224
347
|
*/
|
|
225
348
|
destroy(): void;
|
|
349
|
+
/**
|
|
350
|
+
* Returns the features currently set on the layer — the adapter-side cache
|
|
351
|
+
* of the last `setLayerFeatures`/`updateLayerFeatures`/`removeFeatures`/
|
|
352
|
+
* `clearLayer` state. Empty array for an unknown layer. Callers must not
|
|
353
|
+
* mutate the returned array or features.
|
|
354
|
+
*/
|
|
355
|
+
getLayerFeatures(layerId: LayerId): readonly Feature[];
|
|
226
356
|
}
|
|
227
357
|
//#endregion
|
|
228
358
|
//#region src/base-map-adapter.d.ts
|
|
@@ -254,6 +384,8 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
|
|
|
254
384
|
protected viewChangeTokens: Map<() => void, TViewToken>;
|
|
255
385
|
protected featureCache: Map<string, Feature<import("geojson").Geometry, import("geojson").GeoJsonProperties>[]>;
|
|
256
386
|
protected featuresChangedListeners: Map<string, Set<(features: Feature[]) => void>>;
|
|
387
|
+
protected iconResources: Map<string, IconResource>;
|
|
388
|
+
private layerIconResourceIds;
|
|
257
389
|
private layerCounter;
|
|
258
390
|
private destroyed;
|
|
259
391
|
protected nextLayerId(): LayerId;
|
|
@@ -261,6 +393,7 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
|
|
|
261
393
|
style?: StyleOptions;
|
|
262
394
|
}): LayerId;
|
|
263
395
|
removeLayer(layerId: LayerId): void;
|
|
396
|
+
reconcileLayerIconResources(layerId: LayerId, resources: readonly IconResource[]): void;
|
|
264
397
|
setLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
|
|
265
398
|
updateLayerFeatures(layerId: LayerId, features: readonly Feature[]): void;
|
|
266
399
|
removeFeatures(layerId: LayerId, ids: readonly string[]): void;
|
|
@@ -275,6 +408,10 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
|
|
|
275
408
|
protected emitLayerFeaturesChanged(layerId: LayerId, features: Feature[]): void;
|
|
276
409
|
protected onLayerFeaturesChanged(layerId: LayerId, listener: (features: Feature[]) => void): () => void;
|
|
277
410
|
protected destroyEngineResources(): void;
|
|
411
|
+
protected applyIconResources(resources: readonly IconResource[]): void;
|
|
412
|
+
protected getIconResource(id: string): IconResource | undefined;
|
|
413
|
+
protected removeIconResource(id: string): void;
|
|
414
|
+
private pruneIconResources;
|
|
278
415
|
protected abstract createLayer(layerId: LayerId, style?: StyleOptions): TLayerHandle;
|
|
279
416
|
protected abstract destroyLayer(handle: TLayerHandle): void;
|
|
280
417
|
protected abstract applyFeatureOps(layerId: LayerId, ops: FeatureOps): void;
|
|
@@ -297,6 +434,12 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
|
|
|
297
434
|
abstract setDoubleClickZoomEnabled(enabled: boolean): void;
|
|
298
435
|
abstract getResolution(): number | undefined;
|
|
299
436
|
abstract getZoom(): number | undefined;
|
|
437
|
+
/**
|
|
438
|
+
* Default for engines/views that don't rotate (Leaflet). Rotating engines
|
|
439
|
+
* (MapLibre, OpenLayers) override this.
|
|
440
|
+
*/
|
|
441
|
+
getBearing(): number;
|
|
442
|
+
getLayerFeatures(layerId: LayerId): readonly Feature[];
|
|
300
443
|
}
|
|
301
444
|
//#endregion
|
|
302
445
|
//#region src/pick-utils.d.ts
|
|
@@ -317,6 +460,52 @@ declare function bindAbortable(signal: AbortSignal | undefined, attach: () => ()
|
|
|
317
460
|
* when missing or shaped wrong so callers fall through to the layer style.
|
|
318
461
|
*/
|
|
319
462
|
declare function coerceHandleStyle(raw: unknown): HandleStyle | undefined;
|
|
463
|
+
/** Default lower edge of the ground-anchored label clamp band (ADR-0028). */
|
|
464
|
+
declare const DEFAULT_TEXT_SIZE_MIN_PIXELS = 8;
|
|
465
|
+
/** Default upper edge of the ground-anchored label clamp band (ADR-0028). */
|
|
466
|
+
declare const DEFAULT_TEXT_SIZE_MAX_PIXELS = 24;
|
|
467
|
+
/**
|
|
468
|
+
* Rendered pixel size of a ground-anchored label (ADR-0028):
|
|
469
|
+
* `textSizeMeters / resolution`, clamped to the layer's text-style band.
|
|
470
|
+
* Within the band the text scales with its graphic; past either edge it pins
|
|
471
|
+
* to the bound, so zooming out never yields unreadable smudges and zooming in
|
|
472
|
+
* never yields screen-covering text. Shared by the adapters that resolve text
|
|
473
|
+
* size in code (OpenLayers, Leaflet); the MapLibre adapter evaluates the same
|
|
474
|
+
* clamp inside its per-zoom-stop style expression.
|
|
475
|
+
*/
|
|
476
|
+
declare function resolveGroundTextSizePixels(textSizeMeters: number, resolution: number, text?: Pick<TextStyleOptions, "textSizeMinPixels" | "textSizeMaxPixels">): number;
|
|
477
|
+
/**
|
|
478
|
+
* The size properties a Text graphic's feature carries (see `text.ts` /
|
|
479
|
+
* `FeaturePartProps`). Mutually exclusive size anchors: `textSizePixels`
|
|
480
|
+
* (screen-anchored) or `textSizeMeters` (ground-anchored). `textMaxSizePixels`
|
|
481
|
+
* is the ADR-0037 hide threshold; the min/max band fields are the amplifier
|
|
482
|
+
* -label clamp bounds, applied only when no max-size threshold is present.
|
|
483
|
+
*/
|
|
484
|
+
interface TextSizeProps {
|
|
485
|
+
textSizeMeters?: number;
|
|
486
|
+
textSizePixels?: number;
|
|
487
|
+
textMaxSizePixels?: number;
|
|
488
|
+
textSizeMinPixels?: number;
|
|
489
|
+
textSizeMaxPixels?: number;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Resolve a Text graphic's on-screen size in pixels, honoring the
|
|
493
|
+
* `textMaxSizePixels` hide threshold (ADR-0037). Returns `null` — the "hidden"
|
|
494
|
+
* signal — when the graphic should render no text this frame; adapters emit no
|
|
495
|
+
* text style for a `null`. Distinct from [[resolveGroundTextSizePixels]], which
|
|
496
|
+
* keeps the 8–24 px clamp band for amplifier labels: this helper is the Text
|
|
497
|
+
* graphic's own sizing rule.
|
|
498
|
+
*
|
|
499
|
+
* - screen-anchored (`textSizePixels`): the fixed pixel size, never scaled or
|
|
500
|
+
* hidden by zoom.
|
|
501
|
+
* - ground-anchored (`textSizeMeters`) with a `textMaxSizePixels` threshold:
|
|
502
|
+
* *unclamped* `meters / resolution` (the 8–24 band does NOT apply), hidden
|
|
503
|
+
* (`null`) once that exceeds the threshold — text shrinks freely zooming out
|
|
504
|
+
* and vanishes rather than clamps zooming in.
|
|
505
|
+
* - ground-anchored without a threshold: falls back to the clamped band.
|
|
506
|
+
* - no usable size or resolution: `null`.
|
|
507
|
+
*/
|
|
508
|
+
declare function resolveTextSizePixels(props: TextSizeProps, resolution: number): number | null;
|
|
320
509
|
//#endregion
|
|
321
510
|
//#region src/gestures/hit-test.d.ts
|
|
322
511
|
type ToPixel = (lonLat: Position) => PixelCoordinate | null;
|
|
@@ -331,7 +520,14 @@ type ToPixel = (lonLat: Position) => PixelCoordinate | null;
|
|
|
331
520
|
*
|
|
332
521
|
* Returns the first matching feature in iteration order, or null.
|
|
333
522
|
*/
|
|
334
|
-
declare function hitTestFeature(pixel: PixelCoordinate, features: Feature[], hitTolerance: number, toPixel: ToPixel): Feature | null;
|
|
523
|
+
declare function hitTestFeature(pixel: PixelCoordinate, features: readonly Feature[], hitTolerance: number, toPixel: ToPixel): Feature | null;
|
|
524
|
+
/**
|
|
525
|
+
* Test a rendered icon-backed Point feature against its actual pixel footprint.
|
|
526
|
+
* The feature carries the adapter-neutral `iconScale` / `iconRotation` values
|
|
527
|
+
* emitted by TacticalDraw; `resource.anchor` is the placement anchor within the
|
|
528
|
+
* unscaled SVG viewport. A positive tolerance pads all four sides.
|
|
529
|
+
*/
|
|
530
|
+
declare function hitTestIconFeature(pixel: PixelCoordinate, feature: Feature, resource: IconResource, hitTolerance: number, toPixel: ToPixel, bearing?: number): boolean;
|
|
335
531
|
//#endregion
|
|
336
532
|
//#region src/gestures/hit-tolerance.d.ts
|
|
337
533
|
declare const TOUCH_HIT_TOLERANCE_PX = 22;
|
|
@@ -366,6 +562,95 @@ declare class PointerCallbackHub {
|
|
|
366
562
|
clear(): void;
|
|
367
563
|
}
|
|
368
564
|
//#endregion
|
|
565
|
+
//#region src/point-symbol.d.ts
|
|
566
|
+
type PointSymbolSize = {
|
|
567
|
+
value: number;
|
|
568
|
+
unit: "pixels";
|
|
569
|
+
} | {
|
|
570
|
+
value: number;
|
|
571
|
+
unit: "meters"; /** Floor for the on-screen projection of the ground-based size; none when omitted. */
|
|
572
|
+
minPixels?: number; /** Cap on the on-screen projection of the ground-based size — typically the pixel size the symbol had when anchored to ground; unclamped when omitted. */
|
|
573
|
+
maxPixels?: number;
|
|
574
|
+
};
|
|
575
|
+
interface PointSymbolGraphicModifiers {
|
|
576
|
+
/** Radians clockwise, relative to the point symbol's own orientation. */
|
|
577
|
+
direction?: number;
|
|
578
|
+
}
|
|
579
|
+
interface PointSymbolColors {
|
|
580
|
+
fill?: string;
|
|
581
|
+
frame?: string;
|
|
582
|
+
icon?: string;
|
|
583
|
+
info?: string;
|
|
584
|
+
outline?: string;
|
|
585
|
+
}
|
|
586
|
+
interface PointSymbolStyle {
|
|
587
|
+
colors?: PointSymbolColors;
|
|
588
|
+
monoColor?: string;
|
|
589
|
+
opacity?: number;
|
|
590
|
+
outlineWidth?: number;
|
|
591
|
+
fill?: boolean;
|
|
592
|
+
frame?: boolean;
|
|
593
|
+
icon?: boolean;
|
|
594
|
+
}
|
|
595
|
+
interface PointSymbol {
|
|
596
|
+
id: string;
|
|
597
|
+
kind: "point-symbol";
|
|
598
|
+
sidc: string;
|
|
599
|
+
position: Position;
|
|
600
|
+
/** Normalized radians clockwise from geographic north. */
|
|
601
|
+
rotation: number;
|
|
602
|
+
/** Milsymbol's octagon-based L dimension. */
|
|
603
|
+
size: PointSymbolSize;
|
|
604
|
+
textAmplifiers?: TextAmplifiers;
|
|
605
|
+
graphicModifiers?: PointSymbolGraphicModifiers;
|
|
606
|
+
style?: PointSymbolStyle;
|
|
607
|
+
rendererOptions?: {
|
|
608
|
+
milsymbol?: Record<string, unknown>;
|
|
609
|
+
};
|
|
610
|
+
properties?: Record<string, unknown>;
|
|
611
|
+
schemaVersion?: 1;
|
|
612
|
+
}
|
|
613
|
+
interface PointSymbolAnchor {
|
|
614
|
+
x: number;
|
|
615
|
+
y: number;
|
|
616
|
+
}
|
|
617
|
+
/** Canonical-size, renderer-neutral point-symbol output. */
|
|
618
|
+
interface PointSymbolRender {
|
|
619
|
+
/** Content-derived identity for adapter-wide native-resource reuse. */
|
|
620
|
+
resourceId: string;
|
|
621
|
+
svg: string;
|
|
622
|
+
/** Renderer-space octagon size used to interpret persisted symbol size. */
|
|
623
|
+
octagonSize: number;
|
|
624
|
+
width: number;
|
|
625
|
+
height: number;
|
|
626
|
+
anchor: PointSymbolAnchor;
|
|
627
|
+
octagonAnchor: PointSymbolAnchor;
|
|
628
|
+
valid: boolean;
|
|
629
|
+
validation?: unknown;
|
|
630
|
+
}
|
|
631
|
+
interface PointSymbolCapability {
|
|
632
|
+
readonly defaultSize: PointSymbolSize;
|
|
633
|
+
readonly defaultRotation: number;
|
|
634
|
+
render(symbol: PointSymbol): PointSymbolRender;
|
|
635
|
+
invalidate(): void;
|
|
636
|
+
}
|
|
637
|
+
declare function isPointSymbol(graphic: unknown): graphic is PointSymbol;
|
|
638
|
+
declare function clonePointSymbol(symbol: PointSymbol): PointSymbol;
|
|
639
|
+
/**
|
|
640
|
+
* Replace a point-symbol size value as a user-driven resize. Ground-size display
|
|
641
|
+
* bounds scale with the value so they continue to constrain zoom changes rather
|
|
642
|
+
* than preventing the resize itself.
|
|
643
|
+
*/
|
|
644
|
+
declare function resizePointSymbolSize(size: PointSymbolSize, value: number): PointSymbolSize;
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/graphic.d.ts
|
|
647
|
+
type Graphic = ControlMeasure | PointSymbol;
|
|
648
|
+
type GraphicRender = ControlMeasureRender | PointSymbolRender;
|
|
649
|
+
interface GraphicSnapshot<G extends Graphic = Graphic> {
|
|
650
|
+
graphic: G;
|
|
651
|
+
render: G extends PointSymbol ? PointSymbolRender : ControlMeasureRender;
|
|
652
|
+
}
|
|
653
|
+
//#endregion
|
|
369
654
|
//#region src/tactical-draw/abort.d.ts
|
|
370
655
|
/**
|
|
371
656
|
* AbortPlumbing for the TacticalDraw façade.
|
|
@@ -416,6 +701,7 @@ interface CombinedAbort {
|
|
|
416
701
|
declare function combineWithHostSignal(hostSignal?: AbortSignal): CombinedAbort;
|
|
417
702
|
//#endregion
|
|
418
703
|
//#region src/tactical-draw/measure-options.d.ts
|
|
704
|
+
declare const DEFAULT_LABEL_SIZE_PIXELS = 14;
|
|
419
705
|
/**
|
|
420
706
|
* Where a control measure's size is anchored (ADR-0020). `ground` (the
|
|
421
707
|
* default) means a meter size fixed to the terrain — static, never re-rendered
|
|
@@ -437,9 +723,10 @@ declare function getSizeAnchor(measure: Pick<ControlMeasure, "kind" | "options">
|
|
|
437
723
|
//#endregion
|
|
438
724
|
//#region src/tactical-draw/handles.d.ts
|
|
439
725
|
/**
|
|
440
|
-
* Line-only style surface for the rubber-band guide (ADR-0004)
|
|
441
|
-
*
|
|
442
|
-
*
|
|
726
|
+
* Line-only style surface for the rubber-band guide (ADR-0004) and the
|
|
727
|
+
* transform-box outline. Slot inside `InteractionStyle.guide` /
|
|
728
|
+
* `InteractionStyle.boxOutline`. Applied as a layer style for the guide, and
|
|
729
|
+
* per-feature (`properties.style`) for the box outline — see ADR-0017.
|
|
443
730
|
*/
|
|
444
731
|
interface GuideStyle {
|
|
445
732
|
strokeColor?: string;
|
|
@@ -447,25 +734,96 @@ interface GuideStyle {
|
|
|
447
734
|
strokeDash?: readonly number[];
|
|
448
735
|
}
|
|
449
736
|
/**
|
|
450
|
-
* Unified interaction-affordance style surface (ADR-0006
|
|
451
|
-
* for
|
|
452
|
-
*
|
|
737
|
+
* Unified interaction-affordance style surface (ADR-0006, extended by
|
|
738
|
+
* ADR-0017 for the transform box). One option object for the guide line,
|
|
739
|
+
* the box outline, and every handle slot. `guide` and `boxOutline` are line
|
|
740
|
+
* styles; handle slots ride per-feature on `properties.style`.
|
|
453
741
|
*/
|
|
454
742
|
interface InteractionStyle {
|
|
455
743
|
guide?: GuideStyle;
|
|
456
744
|
vertexHandle?: HandleStyle;
|
|
457
745
|
midpointHandle?: HandleStyle;
|
|
458
|
-
|
|
746
|
+
/** Visible grab target for a draggable rendered text label. */
|
|
747
|
+
labelHandle?: HandleStyle;
|
|
748
|
+
scaleHandle?: HandleStyle;
|
|
459
749
|
rotateHandle?: HandleStyle;
|
|
750
|
+
boxOutline?: GuideStyle;
|
|
751
|
+
/**
|
|
752
|
+
* Pixel padding added on every side of the fitted transform box
|
|
753
|
+
* (`fitTransformBox`, ADR-0017) so the box doesn't tightly hug the rendered
|
|
754
|
+
* geometry. Default `0`. A scalar, not a style object, but it participates
|
|
755
|
+
* in the same built-in → façade → per-call resolution slot system as every
|
|
756
|
+
* other `InteractionStyle` field (see `TacticalDraw.resolveInteractionStyle`).
|
|
757
|
+
*/
|
|
758
|
+
boxPadding?: number;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Reserved enum from PRD #40, replaced under ADR-0017 by the transform-box UI.
|
|
762
|
+
* `reshape` renders vertex/midpoint handles; `transform` renders the oriented
|
|
763
|
+
* selection box (4 corner scale handles + a rotate grip + drag-anywhere
|
|
764
|
+
* body translate) via `transform-box.ts`. The two can be combined freely —
|
|
765
|
+
* single-measure edits show both by default. `labeldrag` enables dragging
|
|
766
|
+
* rendered text labels. `delete` is an explicit
|
|
767
|
+
* destructive mode: it renders the vertex handles (so they can be tapped to
|
|
768
|
+
* remove) but no midpoints or box, and the edit controller deletes the tapped
|
|
769
|
+
* vertex instead of dragging it.
|
|
770
|
+
*/
|
|
771
|
+
type EditMode = "reshape" | "transform" | "labeldrag" | "delete";
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/tactical-draw/session-events.d.ts
|
|
774
|
+
/** A gesture's phase within a stream of {@link TransientChangeEvent}s. */
|
|
775
|
+
type TransientPhase = "start" | "move" | "end";
|
|
776
|
+
/**
|
|
777
|
+
* An in-flight mutation of a graphic's working geometry during a gesture —
|
|
778
|
+
* streamed continuously while the gesture runs (`EditSession.onTransientChange`
|
|
779
|
+
* / `DrawSession.onTransientChange`), distinct from the once-per-completed-
|
|
780
|
+
* gesture `onChange`. Hosts use it for live feedback, e.g. a radius readout
|
|
781
|
+
* while dragging a circle handle.
|
|
782
|
+
*/
|
|
783
|
+
interface TransientChangeEvent {
|
|
784
|
+
/** Gesture phase. `end` fires exactly once per started gesture — on commit AND on cancel/abort. */
|
|
785
|
+
readonly phase: TransientPhase;
|
|
786
|
+
/** The full working control points at this instant (lon/lat). Fresh array per emission. */
|
|
787
|
+
readonly points: readonly Position[];
|
|
788
|
+
/**
|
|
789
|
+
* Index of the live point in `points` — the dragged vertex in edit, the
|
|
790
|
+
* rubber-band point in draw — or `null` when the whole geometry moves at
|
|
791
|
+
* once (box translate / rotate / scale).
|
|
792
|
+
*/
|
|
793
|
+
readonly pointIndex: number | null;
|
|
460
794
|
}
|
|
461
795
|
/**
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
796
|
+
* The reason a session (`TransformSession`, `EditSession`, or `DrawSession`)
|
|
797
|
+
* settled, delivered to that session's `onSettled` handler (#191 / #194 /
|
|
798
|
+
* #197). `"close"` covers every commit path of the owning session —
|
|
799
|
+
* `TransformSession.close()` / `EditSession.close()` / `DrawSession.commit()`,
|
|
800
|
+
* a click-away close, a gesture commit (dblclick, a second click/tap on the
|
|
801
|
+
* finishing vertex, a click on another graphics-layer measure), or
|
|
802
|
+
* `TransformSession.setGraphics([])`. `"error"` is the commit-time snapshot
|
|
803
|
+
* build throwing (the outer promise rejects with that error, not a
|
|
804
|
+
* `TacticalDrawAbortError`). The remaining values replay
|
|
805
|
+
* `TacticalDrawAbortError.reason` verbatim: `"escape"`, `"signal"`,
|
|
806
|
+
* `"session"` (`session.abort()` / `event.abort()`), `"preempted"`,
|
|
807
|
+
* `"destroyed"`, and `"removed"` (edit / transform only — a measure dropped
|
|
808
|
+
* from `td.render()`'s measure set).
|
|
467
809
|
*/
|
|
468
|
-
type
|
|
810
|
+
type SettleReason = "close" | "error" | TacticalDrawAbortReason;
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/tactical-draw/session-history.d.ts
|
|
813
|
+
type SessionChangeSource = "gesture" | "undo" | "redo" | "programmatic";
|
|
814
|
+
interface SessionHistoryState {
|
|
815
|
+
readonly canUndo: boolean;
|
|
816
|
+
readonly canRedo: boolean;
|
|
817
|
+
readonly undoLabel?: string;
|
|
818
|
+
readonly redoLabel?: string;
|
|
819
|
+
}
|
|
820
|
+
interface SessionHistory {
|
|
821
|
+
/** Current availability and optional labels. Each session retains at most 100 completed steps. */
|
|
822
|
+
readonly state: SessionHistoryState;
|
|
823
|
+
undo(): boolean;
|
|
824
|
+
redo(): boolean;
|
|
825
|
+
subscribe(listener: (state: SessionHistoryState) => void): () => void;
|
|
826
|
+
}
|
|
469
827
|
//#endregion
|
|
470
828
|
//#region src/tactical-draw/edit-session.d.ts
|
|
471
829
|
/**
|
|
@@ -474,17 +832,31 @@ type EditMode = "reshape" | "translate" | "rotate" | "scale" | "delete";
|
|
|
474
832
|
*
|
|
475
833
|
* `measure` is the immutable edit-start input. `controlPoints` is live working
|
|
476
834
|
* state. After the session settles (`close` / `abort`), every method is a
|
|
477
|
-
* silent no-op
|
|
835
|
+
* silent no-op (except [[onSettled]] — late registration still delivers,
|
|
836
|
+
* asynchronously).
|
|
478
837
|
*/
|
|
479
838
|
interface EditSession {
|
|
839
|
+
/** Undo/redo for completed mutations within this live session. */
|
|
840
|
+
readonly history: SessionHistory;
|
|
480
841
|
/** Immutable edit-start input. */
|
|
481
|
-
readonly
|
|
842
|
+
readonly graphic: ControlMeasure;
|
|
843
|
+
/** Live complete working graphic. Copy on read. */
|
|
844
|
+
readonly workingGraphic: ControlMeasure;
|
|
482
845
|
/** Live working state. Copy on read. */
|
|
483
846
|
readonly controlPoints: readonly Position[];
|
|
484
847
|
/** Live working options. Copy on read. */
|
|
485
848
|
readonly options: ControlMeasure["options"];
|
|
486
849
|
/** Live working style. Copy on read. */
|
|
487
850
|
readonly style: ControlMeasureStyle | undefined;
|
|
851
|
+
/**
|
|
852
|
+
* Live working text amplifiers (MIL-STD-2525E Appendix L Table L-II). Copy
|
|
853
|
+
* on read. `undefined` when the edit-start measure carried none and none
|
|
854
|
+
* have been set since. Keys are stored as given — canonicalisation to
|
|
855
|
+
* doctrinal field codes happens in the core render seam, not here.
|
|
856
|
+
*/
|
|
857
|
+
readonly textAmplifiers: TextAmplifiers | undefined;
|
|
858
|
+
/** Live user-adjusted anchors for individually rendered text amplifiers. */
|
|
859
|
+
readonly amplifierPlacements: AmplifierPlacements | undefined;
|
|
488
860
|
/** `true` after the first working-copy mutation; back to `false` within tolerance. */
|
|
489
861
|
readonly dirty: boolean;
|
|
490
862
|
readonly modes: readonly EditMode[];
|
|
@@ -495,18 +867,24 @@ interface EditSession {
|
|
|
495
867
|
*/
|
|
496
868
|
readonly sizeAnchor: SizeAnchor;
|
|
497
869
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
870
|
+
* Commit: returns the `GraphicSnapshot<ControlMeasure>` of the current working state
|
|
871
|
+
* and resolves the outer `edit` promise with the same value (the same
|
|
872
|
+
* reference — no double-bake of ground-anchored sizes). Returns `undefined`
|
|
873
|
+
* after the session has settled (idempotent no-op).
|
|
500
874
|
*/
|
|
501
|
-
close():
|
|
875
|
+
close(): GraphicSnapshot<ControlMeasure> | undefined;
|
|
502
876
|
/** Reject the outer `edit` promise with reason `"session"`. */
|
|
503
877
|
abort(): void;
|
|
504
878
|
/**
|
|
505
|
-
* Switch the active edit-mode set. `"reshape"
|
|
506
|
-
*
|
|
507
|
-
*
|
|
879
|
+
* Switch the active edit-mode set. `"reshape"` produces vertex/midpoint
|
|
880
|
+
* handles; `"transform"` renders the oriented selection box (corner scale
|
|
881
|
+
* handles, rotate grip, body translate — ADR-0017); `"delete"` is exclusive
|
|
882
|
+
* (vertex-tap removal) and collapses the set to delete-only. An unknown mode
|
|
883
|
+
* is rejected with `TypeError`.
|
|
508
884
|
*/
|
|
509
885
|
setModes(modes: readonly EditMode[]): void;
|
|
886
|
+
/** Atomically replace the live working graphic while preserving id and kind. */
|
|
887
|
+
updateGraphic(updater: (graphic: ControlMeasure) => ControlMeasure): void;
|
|
510
888
|
/** Shallow-merge per-measure generator options into the live working copy. */
|
|
511
889
|
setOptions(partial: ControlMeasure["options"]): void;
|
|
512
890
|
/**
|
|
@@ -518,6 +896,22 @@ interface EditSession {
|
|
|
518
896
|
setSizeAnchor(anchor: SizeAnchor): void;
|
|
519
897
|
/** Shallow-merge per-measure style into the live working copy. */
|
|
520
898
|
setStyle(partial: ControlMeasureStyle): void;
|
|
899
|
+
/**
|
|
900
|
+
* Shallow-merge into the live working text amplifiers. A value of
|
|
901
|
+
* `undefined` or an empty/whitespace-only string deletes that key; the
|
|
902
|
+
* working record collapses back to `undefined` once it holds no keys. A
|
|
903
|
+
* silent no-op after the session has settled (like [[setOptions]]).
|
|
904
|
+
*/
|
|
905
|
+
setTextAmplifiers(partial: Record<string, string | undefined>): void;
|
|
906
|
+
/**
|
|
907
|
+
* Merge field-keyed label overrides (position and/or rotation). The merge is
|
|
908
|
+
* **field-wise per key**: a bare `Position` value means `{ position }`;
|
|
909
|
+
* fields absent from the incoming value (e.g. `rotation` when only
|
|
910
|
+
* `position` is supplied) are preserved from the existing entry; an
|
|
911
|
+
* explicit `undefined` value deletes the key entirely — a full reset to the
|
|
912
|
+
* doctrinal placement (both position and rotation).
|
|
913
|
+
*/
|
|
914
|
+
setAmplifierPlacements(partial: Record<string, AmplifierPlacement | undefined>): void;
|
|
521
915
|
/**
|
|
522
916
|
* Subscribe to working-copy changes. The listener receives an
|
|
523
917
|
* [[EditChangeEvent]] for each completed gesture (drag end, insert/delete
|
|
@@ -530,6 +924,69 @@ interface EditSession {
|
|
|
530
924
|
* have run.
|
|
531
925
|
*/
|
|
532
926
|
onChange(listener: (event: EditChangeEvent) => void): () => void;
|
|
927
|
+
/**
|
|
928
|
+
* Subscribe to the committed snapshot — the single delivery channel for the
|
|
929
|
+
* `GraphicSnapshot<ControlMeasure>` this session ever commits (#197, mirroring
|
|
930
|
+
* `TransformSession.onCommit` #191). Fires exactly once, synchronously from
|
|
931
|
+
* within the [[close]] call that produced it (`session.close()`,
|
|
932
|
+
* `event.close()` from [[onChange]], a click-away close, or a click on
|
|
933
|
+
* another graphics-layer measure), with the same reference [[close]]
|
|
934
|
+
* returns and the outer `edit` promise resolves with. Never fires on abort.
|
|
935
|
+
*
|
|
936
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
937
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
938
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
939
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
940
|
+
* fires.
|
|
941
|
+
*/
|
|
942
|
+
onCommit(handler: (snapshot: GraphicSnapshot<ControlMeasure>) => void): () => void;
|
|
943
|
+
/**
|
|
944
|
+
* Subscribe to the session's settlement — the lifecycle complement of
|
|
945
|
+
* [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
|
|
946
|
+
* the session is *over* and why (#197, mirroring
|
|
947
|
+
* `TransformSession.onSettled` #194). Fires exactly once, when the session
|
|
948
|
+
* settles by any path, commit or abort, with a `SettleReason`.
|
|
949
|
+
*
|
|
950
|
+
* - On the commit path it fires **after** [[onCommit]]'s emission, so a
|
|
951
|
+
* listener that clears selection state observes a document that already
|
|
952
|
+
* contains the committed snapshot.
|
|
953
|
+
* - On every path it fires **after** the facade has already cleared
|
|
954
|
+
* `td.activeSession` — so fold-then-render works from inside the handler.
|
|
955
|
+
* - `reason` distinguishes commit from abort but is deliberately coarse —
|
|
956
|
+
* no payload; the snapshot was already delivered through [[onCommit]].
|
|
957
|
+
*
|
|
958
|
+
* Registering after the session has already settled is **not** a no-op
|
|
959
|
+
* (unlike [[onChange]] / [[onCommit]]): the handler is invoked
|
|
960
|
+
* asynchronously (on a microtask) exactly once with the settle reason;
|
|
961
|
+
* unsubscribing before that microtask runs cancels the delivery.
|
|
962
|
+
*
|
|
963
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
964
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
965
|
+
* [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
|
|
966
|
+
*/
|
|
967
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
968
|
+
/**
|
|
969
|
+
* Subscribe to the continuous stream of in-flight geometry mutations —
|
|
970
|
+
* vertex/midpoint drags and box scale/rotate/translate gestures — fired
|
|
971
|
+
* throughout the gesture rather than once at its end (contrast
|
|
972
|
+
* [[onChange]]). `start` fires at gesture begin, `move` on every pointer
|
|
973
|
+
* tick, and `end` fires exactly once when the gesture finishes by any
|
|
974
|
+
* path: commit, a click-slop no-op (a box body click that never becomes a
|
|
975
|
+
* drag), or session abort/teardown mid-drag. `end` fires **before** the
|
|
976
|
+
* gesture's [[onChange]] emission (and, on the commit path, before
|
|
977
|
+
* [[onCommit]]/[[onSettled]] too), so a listener observing both channels
|
|
978
|
+
* sees the transient stream close first.
|
|
979
|
+
*
|
|
980
|
+
* Label-placement drags (`labeldrag` mode) do not fire this — they move
|
|
981
|
+
* amplifier placements, not control points.
|
|
982
|
+
*
|
|
983
|
+
* Listener exceptions are collected; after every listener has run, they
|
|
984
|
+
* are rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
985
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
986
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
987
|
+
* fires.
|
|
988
|
+
*/
|
|
989
|
+
onTransientChange(listener: (event: TransientChangeEvent) => void): () => void;
|
|
533
990
|
}
|
|
534
991
|
/**
|
|
535
992
|
* Delivered to `EditSession.onChange` listeners once per completed gesture.
|
|
@@ -540,10 +997,12 @@ interface EditSession {
|
|
|
540
997
|
* observe partial state. See PRD #40 user-stories 23–24.
|
|
541
998
|
*/
|
|
542
999
|
interface EditChangeEvent {
|
|
1000
|
+
/** How this working-copy transition was initiated. */
|
|
1001
|
+
readonly source: SessionChangeSource;
|
|
543
1002
|
/** Working state after the completed gesture. */
|
|
544
|
-
readonly
|
|
1003
|
+
readonly graphic: GraphicSnapshot<ControlMeasure>;
|
|
545
1004
|
/** State immediately before this gesture. */
|
|
546
|
-
readonly previous:
|
|
1005
|
+
readonly previous: GraphicSnapshot<ControlMeasure>;
|
|
547
1006
|
/** The session the event belongs to. */
|
|
548
1007
|
readonly session: EditSession;
|
|
549
1008
|
/**
|
|
@@ -558,13 +1017,268 @@ interface EditChangeEvent {
|
|
|
558
1017
|
abort(): void;
|
|
559
1018
|
}
|
|
560
1019
|
//#endregion
|
|
1020
|
+
//#region src/tactical-draw/graphic-edit-session.d.ts
|
|
1021
|
+
interface GraphicEditChangeEvent<G extends Graphic = Graphic> {
|
|
1022
|
+
readonly source: SessionChangeSource;
|
|
1023
|
+
readonly graphic: GraphicSnapshot<G>;
|
|
1024
|
+
readonly previous: GraphicSnapshot<G>;
|
|
1025
|
+
readonly session: GraphicEditSession<G>;
|
|
1026
|
+
reject(): void;
|
|
1027
|
+
close(): void;
|
|
1028
|
+
abort(): void;
|
|
1029
|
+
}
|
|
1030
|
+
/** Common selection/editing contract shared by control measures and point symbols. */
|
|
1031
|
+
interface GraphicEditSession<G extends Graphic = Graphic> {
|
|
1032
|
+
readonly history: SessionHistory;
|
|
1033
|
+
/** Immutable edit-start input. */
|
|
1034
|
+
readonly graphic: G;
|
|
1035
|
+
/** Live working graphic. Copy on read. */
|
|
1036
|
+
readonly workingGraphic: G;
|
|
1037
|
+
readonly dirty: boolean;
|
|
1038
|
+
readonly modes: readonly EditMode[];
|
|
1039
|
+
close(): GraphicSnapshot<G> | undefined;
|
|
1040
|
+
abort(): void;
|
|
1041
|
+
setModes(modes: readonly EditMode[]): void;
|
|
1042
|
+
/** Atomically replace any part of the live working graphic. */
|
|
1043
|
+
updateGraphic(updater: (graphic: G) => G): void;
|
|
1044
|
+
onChange(listener: (event: GraphicEditChangeEvent<G>) => void): () => void;
|
|
1045
|
+
onCommit(handler: (snapshot: GraphicSnapshot<G>) => void): () => void;
|
|
1046
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
1047
|
+
}
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region src/tactical-draw/transform-session.d.ts
|
|
1050
|
+
/**
|
|
1051
|
+
* Surface delivered via `td.editMany(graphics, opts).onSession` and
|
|
1052
|
+
* synchronously available from `td.activeTransformSession` while the group
|
|
1053
|
+
* transform is live. The group sibling of `EditSession` (`edit-session.ts`):
|
|
1054
|
+
* no reshape, no per-measure `setOptions`/`setStyle` — only the shared
|
|
1055
|
+
* transform box's corner-scale / rotate / body-translate gestures, applied to
|
|
1056
|
+
* every member at once. See the Phase 3 ("Multi-select") slice of ADR-0017.
|
|
1057
|
+
*
|
|
1058
|
+
* Typed on `Graphic` (ADR-0032): a member is either a `ControlMeasure` or a
|
|
1059
|
+
* point symbol, and the two coexist in one session. Hosts narrow with
|
|
1060
|
+
* `isPointSymbol`. `controlPointsFor` is a control-measure accessor and returns
|
|
1061
|
+
* `undefined` for a point-symbol id.
|
|
1062
|
+
*
|
|
1063
|
+
* The member set is mutable via [[setGraphics]] (#187): members can join or
|
|
1064
|
+
* leave a live session without a session boundary — point symbols exactly like
|
|
1065
|
+
* measures. Because membership changes over the session's lifetime, snapshot
|
|
1066
|
+
* arrays are keyed by **graphic id**, not by index — a snapshot's identity is
|
|
1067
|
+
* `snapshot.graphic.id`. `graphics` is a live view of the current members'
|
|
1068
|
+
* join-time inputs; a `TransformChangeEvent`'s arrays are index-aligned with
|
|
1069
|
+
* `session.graphics` *as of that emission*. After the session settles (`close`
|
|
1070
|
+
* / `abort` / an emptying `setGraphics`), every method is a silent no-op
|
|
1071
|
+
* (except [[onSettled]] — late registration still delivers, asynchronously).
|
|
1072
|
+
*/
|
|
1073
|
+
interface TransformSession {
|
|
1074
|
+
/** Undo/redo for completed gestures within the current membership baseline. */
|
|
1075
|
+
readonly history: SessionHistory;
|
|
1076
|
+
/**
|
|
1077
|
+
* Live view of the current members' join-time input graphics — a fresh array
|
|
1078
|
+
* on each read, in current membership order. Grows/shrinks as [[setGraphics]]
|
|
1079
|
+
* adds or removes members. Not stable across membership changes: key
|
|
1080
|
+
* snapshots by `snapshot.graphic.id` rather than relying on index alignment.
|
|
1081
|
+
*/
|
|
1082
|
+
readonly graphics: readonly Graphic[];
|
|
1083
|
+
/**
|
|
1084
|
+
* Live view of the current members' working graphics — a fresh array on each
|
|
1085
|
+
* read, in current membership order, reflecting every gesture applied so far.
|
|
1086
|
+
* Measures carry their working control points (and any transform-frozen
|
|
1087
|
+
* options); point symbols carry their working position, size value, and
|
|
1088
|
+
* rotation. Copy on read.
|
|
1089
|
+
*/
|
|
1090
|
+
readonly workingGraphics: readonly Graphic[];
|
|
1091
|
+
/**
|
|
1092
|
+
* Live working control points for the control-measure member whose id is
|
|
1093
|
+
* `graphicId`, or `undefined` when no such member carries that id — including
|
|
1094
|
+
* when `graphicId` names a point symbol (which has no control points; read
|
|
1095
|
+
* its working `position` from [[workingGraphics]] instead). Copy on read.
|
|
1096
|
+
*/
|
|
1097
|
+
controlPointsFor(graphicId: string): readonly Position[] | undefined;
|
|
1098
|
+
/** `true` when any member's working state differs from its edit-start input beyond tolerance. */
|
|
1099
|
+
readonly dirty: boolean;
|
|
1100
|
+
/**
|
|
1101
|
+
* Live size anchor, applied group-wide to the **control-measure** members
|
|
1102
|
+
* only (ADR-0020). `"ground"` bakes each measure's pixel size to meters on
|
|
1103
|
+
* commit; `"screen"` keeps it zoom-aware. A point symbol's persisted
|
|
1104
|
+
* pixel/meter size is its own anchor and is never rewritten by this setting.
|
|
1105
|
+
* Initialised from `EditManyOptions.sizeAnchor` and mutated by [[setSizeAnchor]].
|
|
1106
|
+
*/
|
|
1107
|
+
readonly sizeAnchor: SizeAnchor;
|
|
1108
|
+
/**
|
|
1109
|
+
* Switch the group's size anchor (ADR-0020) for its control-measure members.
|
|
1110
|
+
* Takes effect on the next emitted snapshot and on `close()`. Point-symbol
|
|
1111
|
+
* members are unaffected.
|
|
1112
|
+
*/
|
|
1113
|
+
setSizeAnchor(anchor: SizeAnchor): void;
|
|
1114
|
+
/**
|
|
1115
|
+
* Replace the member set in place — the primitive behind shift-click toggle,
|
|
1116
|
+
* marquee replace, and single-edit → group folding, without tearing the
|
|
1117
|
+
* session down (#187). Members (measures and point symbols alike) are matched
|
|
1118
|
+
* to the current set by graphic id:
|
|
1119
|
+
*
|
|
1120
|
+
* - **Retained** members (id in both the current and next sets) carry their
|
|
1121
|
+
* current working state forward; the passed graphic object is ignored for
|
|
1122
|
+
* them. Their `dirty` baseline stays their original join-time input, so a
|
|
1123
|
+
* session that has already moved things stays dirty across the call.
|
|
1124
|
+
* - **Removed** members (id in the current set, absent from the next) are
|
|
1125
|
+
* committed at their current working state.
|
|
1126
|
+
* - **Added** members (id only in the next set) join at their passed input
|
|
1127
|
+
* state; if currently on the graphics layer they are lifted off it exactly
|
|
1128
|
+
* like initial members.
|
|
1129
|
+
*
|
|
1130
|
+
* The transform box re-derives from the next member set at the session's
|
|
1131
|
+
* current box angle. Session-scoped state (`sizeAnchor`, box angle,
|
|
1132
|
+
* non-geometry dirty) is unchanged.
|
|
1133
|
+
*
|
|
1134
|
+
* Calling this establishes a new session-history baseline: both undo and
|
|
1135
|
+
* redo are cleared after retained members carry their working state
|
|
1136
|
+
* forward. Membership changes and already-emitted exit snapshots are not
|
|
1137
|
+
* undoable within the live session.
|
|
1138
|
+
*
|
|
1139
|
+
* **Return value** — the committed exit snapshots of the members removed *by
|
|
1140
|
+
* this call*, built exactly like [[close]]'s snapshots (a measure's pixel
|
|
1141
|
+
* size baked to ground unless `sizeAnchor` is `"screen"`, a point symbol's
|
|
1142
|
+
* size left untouched, then cloned and rendered); an empty array when nothing
|
|
1143
|
+
* was removed. The outer `editMany` promise resolves with the snapshots of
|
|
1144
|
+
* the members in the set **at close time only**. Removed members are NOT
|
|
1145
|
+
* written back to the graphics layer by the controller — the host folds the
|
|
1146
|
+
* returned snapshots into its document and calls `td.render()`, the same
|
|
1147
|
+
* contract as [[close]]. A removed member's exit snapshot is also delivered
|
|
1148
|
+
* through [[onCommit]] — the recommended channel, since it covers this return
|
|
1149
|
+
* value and the close-time promise resolution through a single subscription.
|
|
1150
|
+
* Returns `undefined` (a silent no-op) after the session has settled.
|
|
1151
|
+
*
|
|
1152
|
+
* `setGraphics([])` on a live session delegates to [[close]]: it commits every
|
|
1153
|
+
* current member, resolves the promise with their snapshots, and returns that
|
|
1154
|
+
* same array. Duplicate ids in `graphics` throw `TypeError` synchronously,
|
|
1155
|
+
* without mutating session state. A point-symbol member entering a session
|
|
1156
|
+
* whose controller was created without a point-symbol capability throws
|
|
1157
|
+
* `PointSymbolCapabilityRequiredError` synchronously, also without mutating
|
|
1158
|
+
* session state.
|
|
1159
|
+
*/
|
|
1160
|
+
setGraphics(graphics: readonly Graphic[]): GraphicSnapshot[] | undefined;
|
|
1161
|
+
/**
|
|
1162
|
+
* Commit: returns the members' `GraphicSnapshot[]` (the members in the set at
|
|
1163
|
+
* close time) and resolves the outer `editMany` promise with the same value
|
|
1164
|
+
* (the same reference — no double-bake of ground-anchored measure sizes). A
|
|
1165
|
+
* member removed mid-session via [[setGraphics]] is not included here; its exit
|
|
1166
|
+
* snapshot was returned from that `setGraphics` call (and delivered to
|
|
1167
|
+
* [[onCommit]] at that time). Returns `undefined` after the session has
|
|
1168
|
+
* settled (idempotent no-op).
|
|
1169
|
+
*/
|
|
1170
|
+
close(): GraphicSnapshot[] | undefined;
|
|
1171
|
+
/** Reject the outer `editMany` promise with reason `"session"`. */
|
|
1172
|
+
abort(): void;
|
|
1173
|
+
/**
|
|
1174
|
+
* Subscribe to working-copy changes. The listener receives a
|
|
1175
|
+
* [[TransformChangeEvent]] for each completed group gesture (corner scale,
|
|
1176
|
+
* rotate, or body-translate drag end) — never on rubber-band ticks. All
|
|
1177
|
+
* currently-registered listeners receive the same event instance, in
|
|
1178
|
+
* registration order.
|
|
1179
|
+
*
|
|
1180
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
1181
|
+
* rethrown (one if single, aggregated otherwise). `event.reject()` /
|
|
1182
|
+
* `event.close()` / `event.abort()` take effect only after all listeners
|
|
1183
|
+
* have run — mirrors `EditSession.onChange` (issue #49).
|
|
1184
|
+
*/
|
|
1185
|
+
onChange(listener: (event: TransformChangeEvent) => void): () => void;
|
|
1186
|
+
/**
|
|
1187
|
+
* Subscribe to committed exit-snapshot batches — the single delivery channel
|
|
1188
|
+
* for every `GraphicSnapshot[]` this session ever commits (#191). Fires
|
|
1189
|
+
* exactly once per batch:
|
|
1190
|
+
*
|
|
1191
|
+
* - The members [[setGraphics]] removes, but only when that call actually
|
|
1192
|
+
* removed at least one member.
|
|
1193
|
+
* - The close-time members on any commit path: [[close]], `setGraphics([])`,
|
|
1194
|
+
* a click-away close, or `TransformChangeEvent.close()`.
|
|
1195
|
+
*
|
|
1196
|
+
* Never fires on an abort/reject path — pair it with [[onSettled]] to also
|
|
1197
|
+
* observe abort settles. Fires synchronously from within the
|
|
1198
|
+
* `setGraphics`/`close` call that produced the batch, before that call
|
|
1199
|
+
* returns, and the array delivered here is the same reference that call
|
|
1200
|
+
* returns (and, for the close batch, the same reference the `editMany`
|
|
1201
|
+
* promise resolves with). This is the recommended single fold path for exit
|
|
1202
|
+
* state — subscribe once in `onSession` and every committed snapshot, from
|
|
1203
|
+
* either delivery site, arrives here exactly once; the `setGraphics` return
|
|
1204
|
+
* value and the promise resolution are unchanged and remain valid for
|
|
1205
|
+
* backward compatibility.
|
|
1206
|
+
*
|
|
1207
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
1208
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
1209
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
1210
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
1211
|
+
* fires.
|
|
1212
|
+
*/
|
|
1213
|
+
onCommit(handler: (snapshots: GraphicSnapshot[]) => void): () => void;
|
|
1214
|
+
/**
|
|
1215
|
+
* Subscribe to the session's settlement — the lifecycle complement of
|
|
1216
|
+
* [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
|
|
1217
|
+
* the session is *over* and why (#194). Fires exactly once, when the
|
|
1218
|
+
* session settles by any path, commit or abort, with a [[SettleReason]].
|
|
1219
|
+
*
|
|
1220
|
+
* - On commit paths it fires **after** the close batch's `onCommit`
|
|
1221
|
+
* emission, so a listener that clears selection state observes a document
|
|
1222
|
+
* that already contains the exit snapshots.
|
|
1223
|
+
* - On every path it fires **after** the facade has already cleared its
|
|
1224
|
+
* slot — `td.activeTransformSession` is `null` and the members are no
|
|
1225
|
+
* longer excluded from `td.render()` reconciliation, so fold-then-render
|
|
1226
|
+
* works from inside the handler.
|
|
1227
|
+
* - `reason` distinguishes commit from abort but is deliberately coarse —
|
|
1228
|
+
* no payload; snapshots were already delivered through [[onCommit]].
|
|
1229
|
+
*
|
|
1230
|
+
* Registering after the session has already settled is **not** a no-op
|
|
1231
|
+
* (unlike [[onChange]] / [[onCommit]]): the handler is invoked
|
|
1232
|
+
* asynchronously (on a microtask) exactly once with the settle reason;
|
|
1233
|
+
* unsubscribing before that microtask runs cancels the delivery.
|
|
1234
|
+
*
|
|
1235
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
1236
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
1237
|
+
* [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
|
|
1238
|
+
*/
|
|
1239
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Delivered to `TransformSession.onChange` listeners once per completed group
|
|
1243
|
+
* gesture. Both `graphics` and `previous` are index-aligned with
|
|
1244
|
+
* `session.graphics` *as of this emission*. Key by `graphic.id` rather than
|
|
1245
|
+
* relying on index alignment across emissions. A `setGraphics` call from within
|
|
1246
|
+
* a listener takes effect immediately and supersedes this emission's
|
|
1247
|
+
* `reject()` (the rollback baseline no longer matches the member set);
|
|
1248
|
+
* `close()` and `abort()` still apply, against the new membership. See
|
|
1249
|
+
* `EditChangeEvent` for the identical emission contract this mirrors.
|
|
1250
|
+
*/
|
|
1251
|
+
interface TransformChangeEvent {
|
|
1252
|
+
/** How this working-copy transition was initiated. */
|
|
1253
|
+
readonly source: SessionChangeSource;
|
|
1254
|
+
/** Working state after the completed gesture, index-aligned with `session.graphics` as of this emission. */
|
|
1255
|
+
readonly graphics: GraphicSnapshot[];
|
|
1256
|
+
/** State immediately before this gesture, index-aligned with `session.graphics` as of this emission. */
|
|
1257
|
+
readonly previous: GraphicSnapshot[];
|
|
1258
|
+
/** The session the event belongs to. */
|
|
1259
|
+
readonly session: TransformSession;
|
|
1260
|
+
/**
|
|
1261
|
+
* Roll every member's working preview back to `previous` after all
|
|
1262
|
+
* listeners have run. Multiple calls collapse to a single rollback.
|
|
1263
|
+
* Overridden by `close()` or `abort()` if either is also called during this
|
|
1264
|
+
* emission, and dropped if a listener called `session.setGraphics` (the
|
|
1265
|
+
* rollback baseline no longer matches the member set).
|
|
1266
|
+
*/
|
|
1267
|
+
reject(): void;
|
|
1268
|
+
/** Shortcut for `event.session.close()`, deferred until the emission ends. */
|
|
1269
|
+
close(): void;
|
|
1270
|
+
/** Shortcut for `event.session.abort()`, deferred until the emission ends. */
|
|
1271
|
+
abort(): void;
|
|
1272
|
+
}
|
|
1273
|
+
//#endregion
|
|
561
1274
|
//#region src/tactical-draw/session.d.ts
|
|
562
1275
|
/**
|
|
563
1276
|
* Scoped surface delivered via `td.draw({ onSession })` for variable-length
|
|
564
1277
|
* kinds. Also available from `td.activeSession` while the draw is live.
|
|
565
1278
|
*
|
|
566
1279
|
* After the draw has settled — either via `commit()` returning `true` or
|
|
567
|
-
* `abort()` resolving — every method is a silent no-op
|
|
1280
|
+
* `abort()` resolving — every method is a silent no-op (except [[onSettled]]
|
|
1281
|
+
* — late registration still delivers, asynchronously).
|
|
568
1282
|
*/
|
|
569
1283
|
interface DrawSession {
|
|
570
1284
|
/** Live snapshot of the committed control points. Copy on read. */
|
|
@@ -586,8 +1300,117 @@ interface DrawSession {
|
|
|
586
1300
|
* are collected and rethrown after all listeners run.
|
|
587
1301
|
*/
|
|
588
1302
|
onChange(listener: (session: DrawSession) => void): () => void;
|
|
1303
|
+
/**
|
|
1304
|
+
* Subscribe to the committed snapshot — the single delivery channel for the
|
|
1305
|
+
* `GraphicSnapshot<ControlMeasure>` this session ever commits (#197, mirroring
|
|
1306
|
+
* `TransformSession.onCommit` #191). Fires exactly once, synchronously from
|
|
1307
|
+
* within the commit that produced it (`session.commit()`, dblclick, or a
|
|
1308
|
+
* second click/tap on the finishing vertex), with the same reference the
|
|
1309
|
+
* outer `draw` promise resolves with — the committed snapshot, not just the
|
|
1310
|
+
* measure. Never fires on abort.
|
|
1311
|
+
*
|
|
1312
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
1313
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
1314
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
1315
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
1316
|
+
* fires.
|
|
1317
|
+
*/
|
|
1318
|
+
onCommit(handler: (snapshot: GraphicSnapshot<ControlMeasure>) => void): () => void;
|
|
1319
|
+
/**
|
|
1320
|
+
* Subscribe to the session's settlement — the lifecycle complement of
|
|
1321
|
+
* [[onCommit]]: `onCommit` delivers *what* was committed, `onSettled` says
|
|
1322
|
+
* the session is *over* and why (#197, mirroring
|
|
1323
|
+
* `TransformSession.onSettled` #194). Fires exactly once, when the session
|
|
1324
|
+
* settles by any path, commit or abort, with a `SettleReason`.
|
|
1325
|
+
*
|
|
1326
|
+
* - On the commit path it fires **after** [[onCommit]]'s emission.
|
|
1327
|
+
* - On every path it fires **after** the facade has already cleared
|
|
1328
|
+
* `td.activeSession` — so fold-then-render works from inside the handler.
|
|
1329
|
+
* - `reason` distinguishes commit from abort but is deliberately coarse —
|
|
1330
|
+
* no payload; the snapshot was already delivered through [[onCommit]].
|
|
1331
|
+
*
|
|
1332
|
+
* Registering after the session has already settled is **not** a no-op
|
|
1333
|
+
* (unlike [[onChange]] / [[onCommit]]): the handler is invoked
|
|
1334
|
+
* asynchronously (on a microtask) exactly once with the settle reason;
|
|
1335
|
+
* unsubscribing before that microtask runs cancels the delivery.
|
|
1336
|
+
*
|
|
1337
|
+
* Listener exceptions are collected; after every listener has run, they are
|
|
1338
|
+
* rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
1339
|
+
* [[onChange]] / [[onCommit]]. Returns an unsubscribe function.
|
|
1340
|
+
*/
|
|
1341
|
+
onSettled(handler: (reason: SettleReason) => void): () => void;
|
|
1342
|
+
/**
|
|
1343
|
+
* Subscribe to the continuous stream of rubber-band pointer ticks while the
|
|
1344
|
+
* draw is live — contrast [[onChange]], which fires once per committed
|
|
1345
|
+
* point. `start` fires on the first rubber-band tick, `move` on every
|
|
1346
|
+
* subsequent tick; a click that commits a point does not itself emit — the
|
|
1347
|
+
* next rubber-band tick after it does. `end` fires exactly once, when the
|
|
1348
|
+
* session settles (commit or abort).
|
|
1349
|
+
*
|
|
1350
|
+
* `points` is the committed points with the live rubber-band point spliced
|
|
1351
|
+
* in at its insertion index; `pointIndex` is that index.
|
|
1352
|
+
*
|
|
1353
|
+
* Listener exceptions are collected; after every listener has run, they
|
|
1354
|
+
* are rethrown (one if single, `AggregateError` otherwise) — mirrors
|
|
1355
|
+
* [[onChange]]. Returns an unsubscribe function; registering after the
|
|
1356
|
+
* session has settled returns a no-op unsubscribe and the listener never
|
|
1357
|
+
* fires.
|
|
1358
|
+
*/
|
|
1359
|
+
onTransientChange(listener: (event: TransientChangeEvent) => void): () => void;
|
|
589
1360
|
}
|
|
590
1361
|
//#endregion
|
|
1362
|
+
//#region src/tactical-draw/snapping.d.ts
|
|
1363
|
+
type SnapInteraction = "draw" | "reshape" | "translate" | "label-drag";
|
|
1364
|
+
interface SnapCandidateRequest {
|
|
1365
|
+
coordinate: Position;
|
|
1366
|
+
pixel: PixelCoordinate;
|
|
1367
|
+
interaction: SnapInteraction;
|
|
1368
|
+
graphicId?: string;
|
|
1369
|
+
controlPointIndex?: number;
|
|
1370
|
+
originalEvent?: unknown;
|
|
1371
|
+
}
|
|
1372
|
+
interface SnapCandidate {
|
|
1373
|
+
id: string;
|
|
1374
|
+
coordinate: Position;
|
|
1375
|
+
kind: string;
|
|
1376
|
+
graphicId?: string;
|
|
1377
|
+
controlPointIndex?: number;
|
|
1378
|
+
priority?: number;
|
|
1379
|
+
}
|
|
1380
|
+
type SnapCandidateProvider = (request: SnapCandidateRequest) => readonly SnapCandidate[];
|
|
1381
|
+
interface SnapResult {
|
|
1382
|
+
coordinate: Position;
|
|
1383
|
+
pixel: PixelCoordinate;
|
|
1384
|
+
candidate: SnapCandidate;
|
|
1385
|
+
distancePixels: number;
|
|
1386
|
+
}
|
|
1387
|
+
interface GraphicGeometrySnappingOptions {
|
|
1388
|
+
/** Snap to the nearest point along generated linework and polygon boundaries. Default `true`. */
|
|
1389
|
+
edges?: boolean;
|
|
1390
|
+
/** Snap to generated geometry vertices. Default `false`. */
|
|
1391
|
+
vertices?: boolean;
|
|
1392
|
+
}
|
|
1393
|
+
interface SnappingOptions {
|
|
1394
|
+
enabled?: boolean;
|
|
1395
|
+
tolerancePixels?: number;
|
|
1396
|
+
sources?: {
|
|
1397
|
+
graphics?: boolean;
|
|
1398
|
+
/**
|
|
1399
|
+
* Snap to generated control-measure geometry. `true` enables edges with
|
|
1400
|
+
* vertices disabled; pass an object to select either candidate type.
|
|
1401
|
+
* Text/label point features and the active graphic are excluded.
|
|
1402
|
+
*/
|
|
1403
|
+
graphicGeometry?: boolean | GraphicGeometrySnappingOptions;
|
|
1404
|
+
external?: SnapCandidateProvider;
|
|
1405
|
+
};
|
|
1406
|
+
bypass?: (originalEvent: unknown) => boolean;
|
|
1407
|
+
/** Called for each resolution attempt; `null` clears a host-rendered indicator. */
|
|
1408
|
+
onSnap?: (result: SnapResult | null) => void;
|
|
1409
|
+
}
|
|
1410
|
+
//#endregion
|
|
1411
|
+
//#region src/tactical-draw/point-symbol-draw-controller.d.ts
|
|
1412
|
+
type DrawPointSymbolDraft = Pick<PointSymbol, "kind" | "sidc"> & Partial<Omit<PointSymbol, "kind" | "sidc">>;
|
|
1413
|
+
//#endregion
|
|
591
1414
|
//#region src/tactical-draw/tactical-draw.d.ts
|
|
592
1415
|
/**
|
|
593
1416
|
* Host-supplied layer slot ids. Any omitted slot is allocated by the façade and
|
|
@@ -599,8 +1422,22 @@ interface TacticalDrawLayerOptions {
|
|
|
599
1422
|
guide?: LayerId;
|
|
600
1423
|
handles?: LayerId;
|
|
601
1424
|
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Result of {@link TacticalDraw.ownsInteractionAt} — which layer claimed the
|
|
1427
|
+
* pixel and the feature that matched.
|
|
1428
|
+
*/
|
|
1429
|
+
interface InteractionHit {
|
|
1430
|
+
/** Which TacticalDraw layer owns the pixel — topmost match wins. */
|
|
1431
|
+
layer: "handles" | "preview" | "graphics";
|
|
1432
|
+
/** The hit feature (do not mutate). */
|
|
1433
|
+
feature: Feature;
|
|
1434
|
+
/** Control-measure id when the hit feature belongs to one (graphics/preview parts). */
|
|
1435
|
+
measureId?: string;
|
|
1436
|
+
}
|
|
602
1437
|
interface TacticalDrawOptions {
|
|
603
1438
|
layers?: TacticalDrawLayerOptions;
|
|
1439
|
+
/** Optional renderer capability for first-class point symbols (ADR-0029). */
|
|
1440
|
+
pointSymbols?: PointSymbolCapability;
|
|
604
1441
|
/**
|
|
605
1442
|
* Façade-level default style passed through to `renderControlMeasure` as the
|
|
606
1443
|
* third (lowest priority) style layer per StyleResolver (#44). Shallow-merged
|
|
@@ -621,6 +1458,8 @@ interface TacticalDrawOptions {
|
|
|
621
1458
|
* the draw API itself are deferred (PRD #40 user-story-5 note).
|
|
622
1459
|
*/
|
|
623
1460
|
generateId?: () => string;
|
|
1461
|
+
/** Optional engine-level snapping for draw and edit interactions. */
|
|
1462
|
+
snapping?: SnappingOptions;
|
|
624
1463
|
}
|
|
625
1464
|
interface EditOptions {
|
|
626
1465
|
signal?: AbortSignal;
|
|
@@ -628,7 +1467,7 @@ interface EditOptions {
|
|
|
628
1467
|
* Called synchronously once when the edit starts, before any pointer event
|
|
629
1468
|
* is processed. Lets a host wire commit / cancel buttons to the active edit.
|
|
630
1469
|
*/
|
|
631
|
-
onSession?: (session: EditSession) => void;
|
|
1470
|
+
onSession?: (session: EditSession | GraphicEditSession<PointSymbol>) => void;
|
|
632
1471
|
/**
|
|
633
1472
|
* Default `true`. When `true`, a click on empty map (or on another graphics
|
|
634
1473
|
* layer measure) closes the active edit. Clicks on preview / handle
|
|
@@ -637,11 +1476,13 @@ interface EditOptions {
|
|
|
637
1476
|
*/
|
|
638
1477
|
closeOnClickAway?: boolean;
|
|
639
1478
|
/**
|
|
640
|
-
* Active edit modes. Defaults to `["reshape"]`. `reshape`
|
|
641
|
-
* midpoint handles; `
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
* `"
|
|
1479
|
+
* Active edit modes. Defaults to `["reshape", "transform"]`. `reshape`
|
|
1480
|
+
* renders vertex / midpoint handles; `transform` renders the oriented
|
|
1481
|
+
* selection box — 4 corner scale handles, a rotate grip above the top edge,
|
|
1482
|
+
* and drag-anywhere-inside-the-box translate (ADR-0017). The two can be
|
|
1483
|
+
* combined freely (the default does). `"labeldrag"` opt-in enables repositioning
|
|
1484
|
+
* rendered text labels. `"delete"` is exclusive: it collapses
|
|
1485
|
+
* the set to vertex-tap removal only. An unknown mode throws `TypeError`.
|
|
645
1486
|
*/
|
|
646
1487
|
modes?: readonly EditMode[];
|
|
647
1488
|
/**
|
|
@@ -655,8 +1496,10 @@ interface EditOptions {
|
|
|
655
1496
|
* polyline sourced from the working control points alongside the working
|
|
656
1497
|
* geometry preview. For `geometry === "area"` kinds, a separate
|
|
657
1498
|
* closing-segment `LineString` is also emitted once `workingPoints.length >= 3`.
|
|
658
|
-
* The guide is suppressed mechanically when `geometry === "point"
|
|
659
|
-
* `minCoordinates === maxCoordinates
|
|
1499
|
+
* The guide is suppressed mechanically when `geometry === "point"`, and for
|
|
1500
|
+
* fixed-length kinds (`minCoordinates === maxCoordinates` or `canonicalPointCount`
|
|
1501
|
+
* set) unless the kind's draw rule opts back in via `showGuide`, regardless of
|
|
1502
|
+
* this flag. See ADR-0004.
|
|
660
1503
|
*/
|
|
661
1504
|
guide?: boolean;
|
|
662
1505
|
/**
|
|
@@ -669,6 +1512,32 @@ interface EditOptions {
|
|
|
669
1512
|
*/
|
|
670
1513
|
sizeAnchor?: SizeAnchor;
|
|
671
1514
|
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Options for `TacticalDraw.editMany` — the multi-select group transform
|
|
1517
|
+
* (ADR-0017 "Multi-select" slice). Leaner than `EditOptions`: there is no
|
|
1518
|
+
* `modes` (a group transform is always the oriented box — no reshape), no
|
|
1519
|
+
* `guide` (no rubber-band content for a group), and no per-measure `setOptions`
|
|
1520
|
+
* / `setStyle` surface on the resulting session.
|
|
1521
|
+
*/
|
|
1522
|
+
interface EditManyOptions {
|
|
1523
|
+
signal?: AbortSignal;
|
|
1524
|
+
/**
|
|
1525
|
+
* Called synchronously once when the group transform starts, before any
|
|
1526
|
+
* pointer event is processed. Lets a host wire Done/Cancel/Delete buttons to
|
|
1527
|
+
* the active session.
|
|
1528
|
+
*/
|
|
1529
|
+
onSession?: (session: TransformSession) => void;
|
|
1530
|
+
/**
|
|
1531
|
+
* Default `true`. When `true`, a click on empty map (or on a graphics-layer
|
|
1532
|
+
* measure outside the group) closes the active transform. Clicks on preview
|
|
1533
|
+
* / handle features are consumed and keep it open.
|
|
1534
|
+
*/
|
|
1535
|
+
closeOnClickAway?: boolean;
|
|
1536
|
+
/** Per-call interaction style override scoped to this transform. See ADR-0006. */
|
|
1537
|
+
interactionStyle?: InteractionStyle;
|
|
1538
|
+
/** Size anchor for the committed measures (ADR-0020), applied group-wide. Default `"ground"`. */
|
|
1539
|
+
sizeAnchor?: SizeAnchor;
|
|
1540
|
+
}
|
|
672
1541
|
/**
|
|
673
1542
|
* Draft control measure handed to `TacticalDraw.draw`. `kind` is required up
|
|
674
1543
|
* front so the façade can resolve metadata, draw rule, and controller choice
|
|
@@ -689,9 +1558,13 @@ type DrawMeasureDraft<K extends ControlMeasureKind = ControlMeasureKind> = {
|
|
|
689
1558
|
interface DrawOptions {
|
|
690
1559
|
signal?: AbortSignal;
|
|
691
1560
|
/**
|
|
692
|
-
* Called synchronously once when a
|
|
693
|
-
*
|
|
694
|
-
*
|
|
1561
|
+
* Called synchronously once when a draw starts, before any pointer event
|
|
1562
|
+
* is processed. The session lets a host wire a Done/Cancel toolbar to the
|
|
1563
|
+
* active draw and subscribe to live control-point / transient-geometry
|
|
1564
|
+
* feedback. Surfaced for both variable- and fixed-length kinds — a
|
|
1565
|
+
* fixed-length session auto-commits on the last required click, so its
|
|
1566
|
+
* main value is `onTransientChange` (e.g. a live radius readout) and
|
|
1567
|
+
* `abort()`.
|
|
695
1568
|
*/
|
|
696
1569
|
onSession?: (session: DrawSession) => void;
|
|
697
1570
|
/**
|
|
@@ -699,8 +1572,10 @@ interface DrawOptions {
|
|
|
699
1572
|
* polyline alongside the kind's generator preview. For `geometry === "area"`
|
|
700
1573
|
* kinds, a separate closing-segment `LineString` is also emitted once the
|
|
701
1574
|
* total point count (cursor included) is ≥ 3. The guide is suppressed
|
|
702
|
-
* mechanically when `geometry === "point"
|
|
703
|
-
*
|
|
1575
|
+
* mechanically when `geometry === "point"`, and for fixed-length kinds
|
|
1576
|
+
* (`minCoordinates === maxCoordinates` or `canonicalPointCount` set) unless
|
|
1577
|
+
* the kind's draw rule opts back in via `showGuide`, regardless of this
|
|
1578
|
+
* flag. See ADR-0004.
|
|
704
1579
|
*/
|
|
705
1580
|
guide?: boolean;
|
|
706
1581
|
/**
|
|
@@ -736,6 +1611,9 @@ declare class TacticalDraw {
|
|
|
736
1611
|
private readonly graphicsStyle;
|
|
737
1612
|
private readonly defaultInteractionStyle;
|
|
738
1613
|
private readonly generateId;
|
|
1614
|
+
private readonly pointSymbols;
|
|
1615
|
+
private snappingOptions;
|
|
1616
|
+
private readonly resolveSnap;
|
|
739
1617
|
private destroyed;
|
|
740
1618
|
private hasRendered;
|
|
741
1619
|
private readonly activeInteraction;
|
|
@@ -747,7 +1625,21 @@ declare class TacticalDraw {
|
|
|
747
1625
|
*/
|
|
748
1626
|
private activeEdit;
|
|
749
1627
|
/**
|
|
750
|
-
*
|
|
1628
|
+
* Tracked alongside `activeInteraction` whenever the interaction is a group
|
|
1629
|
+
* transform (`editMany`). The `render()` / view-change paths need every
|
|
1630
|
+
* member's id (to exclude all of them from reconcile) — mirrors `activeEdit`
|
|
1631
|
+
* but for `TransformHandle`'s `graphicIds` array.
|
|
1632
|
+
*/
|
|
1633
|
+
private activeTransform;
|
|
1634
|
+
/**
|
|
1635
|
+
* The live `TransformSession`, exposed via `activeTransformSession`. Set
|
|
1636
|
+
* directly by `editMany`'s `onSession` wrapper (not routed through
|
|
1637
|
+
* `ActiveInteractionTracker.session`, whose type is `DrawSession | EditSession`)
|
|
1638
|
+
* and cleared on settle.
|
|
1639
|
+
*/
|
|
1640
|
+
private activeTransformSessionRef;
|
|
1641
|
+
/**
|
|
1642
|
+
* Host pick handlers registered via `td.onPick` / `td.onGraphicPick`. The
|
|
751
1643
|
* façade subscribes once to `adapter.onPick` per layer and routes events
|
|
752
1644
|
* through this map so that close-then-pick ordering during an active edit can
|
|
753
1645
|
* be enforced (issue #52).
|
|
@@ -764,6 +1656,7 @@ declare class TacticalDraw {
|
|
|
764
1656
|
private deferredGraphicsPick;
|
|
765
1657
|
private idCounter;
|
|
766
1658
|
private editCounter;
|
|
1659
|
+
private transformCounter;
|
|
767
1660
|
/**
|
|
768
1661
|
* Snapshot of the host's last `render()` argument. The edit slice uses it
|
|
769
1662
|
* to recompute the graphics-layer state during a lift / restore — the
|
|
@@ -771,24 +1664,45 @@ declare class TacticalDraw {
|
|
|
771
1664
|
* surrounding collection state.
|
|
772
1665
|
*/
|
|
773
1666
|
private lastRenderedMeasures;
|
|
1667
|
+
private snapGeometryFeatures;
|
|
774
1668
|
constructor(adapter: MapAdapter, options?: TacticalDrawOptions);
|
|
1669
|
+
/** Replace snapping configuration without recreating the map adapter or façade. */
|
|
1670
|
+
setSnappingOptions(options?: SnappingOptions): void;
|
|
1671
|
+
/** Clear transient host affordances when snapping or its interaction ends. */
|
|
1672
|
+
private clearSnapResult;
|
|
775
1673
|
/**
|
|
776
1674
|
* Render `measures` into features and write them to the graphics layer.
|
|
777
1675
|
* Routes every measure through [[renderMeasureForAdapter]] so pixel sizing is
|
|
778
1676
|
* resolved against the live map. Callers own any edit-exclusion policy — see
|
|
779
1677
|
* `render()` (excludes the edited measure, owned by the preview layer) versus
|
|
780
|
-
* `
|
|
1678
|
+
* `settleToGraphics` (no exclusion, the edit is ending).
|
|
781
1679
|
*/
|
|
782
1680
|
private renderMeasuresToGraphics;
|
|
783
1681
|
/**
|
|
784
1682
|
* Re-render the graphics layer on a view (zoom) change so pixel-denominated
|
|
785
|
-
* measures rescale live. Skipped entirely
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
1683
|
+
* and not-yet-committed label-sized measures rescale live. Skipped entirely
|
|
1684
|
+
* when nothing rendered still reads live view state — meter geometry with a
|
|
1685
|
+
* captured label size is zoom-independent, so a rebuild would be wasted
|
|
1686
|
+
* layer writes. During an active edit the edited measure lives on the
|
|
1687
|
+
* preview layer (lifted from graphics), so it is excluded here exactly as
|
|
1688
|
+
* in `render()`; the preview's own view-change subscription rescales it.
|
|
790
1689
|
*/
|
|
791
1690
|
private readonly handleViewChange;
|
|
1691
|
+
/**
|
|
1692
|
+
* Every measure id currently lifted onto the preview layer by an active
|
|
1693
|
+
* edit or group transform — excluded from `render()` / view-change
|
|
1694
|
+
* reconciliation because the preview layer (not graphics) owns their
|
|
1695
|
+
* working copy. Empty while idle.
|
|
1696
|
+
*/
|
|
1697
|
+
private excludedInteractionIds;
|
|
1698
|
+
/**
|
|
1699
|
+
* Live member ids of the active group transform, or `undefined` when none.
|
|
1700
|
+
* Falls back to the session ref because `onSession` fires from inside the
|
|
1701
|
+
* controller's attach, before `runWithSlot` has assigned `activeTransform` —
|
|
1702
|
+
* the session ref is registered first (`trackTransformSession`), so a
|
|
1703
|
+
* `render()` from inside `onSession` still sees live membership.
|
|
1704
|
+
*/
|
|
1705
|
+
private transformMemberIds;
|
|
792
1706
|
/**
|
|
793
1707
|
* Reconcile the graphics layer against the authoritative list of measures.
|
|
794
1708
|
* Idempotent. Throws synchronously on duplicate measure ids *before*
|
|
@@ -814,7 +1728,7 @@ declare class TacticalDraw {
|
|
|
814
1728
|
* layers (the only edit-facing side effect, `notifyExternalMeasure`, is a
|
|
815
1729
|
* pure style replay).
|
|
816
1730
|
*/
|
|
817
|
-
render(measures: readonly
|
|
1731
|
+
render(measures: readonly Graphic[]): void;
|
|
818
1732
|
/**
|
|
819
1733
|
* Nested-call guard + preempt, shared by `draw` and `edit`. Returns a
|
|
820
1734
|
* pre-rejected promise when called re-entrantly from inside another
|
|
@@ -842,24 +1756,108 @@ declare class TacticalDraw {
|
|
|
842
1756
|
*/
|
|
843
1757
|
private trackSession;
|
|
844
1758
|
/**
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
* `TacticalDrawAbortError` on signal / Escape / destroy / `session.abort()`.
|
|
1759
|
+
* Wrap a host `onSession` callback for `editMany`, recording the session on
|
|
1760
|
+
* `activeTransformSessionRef` (exposed via `activeTransformSession`) before
|
|
1761
|
+
* the host's own handler runs. Separate from `trackSession` because
|
|
1762
|
+
* `ActiveInteractionTracker.session` is typed `DrawSession | EditSession`.
|
|
850
1763
|
*/
|
|
851
|
-
|
|
1764
|
+
private trackTransformSession;
|
|
1765
|
+
/**
|
|
1766
|
+
* Start an interactive draw. Both fixed- and variable-length kinds surface
|
|
1767
|
+
* a `DrawSession` via `opts.onSession`. Fixed-length kinds commit
|
|
1768
|
+
* automatically on the last required click; variable-length kinds commit
|
|
1769
|
+
* on `session.commit()`, `dblclick`, or a second click on the last added
|
|
1770
|
+
* control point. All paths reject with `TacticalDrawAbortError` on
|
|
1771
|
+
* signal / Escape / destroy / `session.abort()`.
|
|
1772
|
+
*
|
|
1773
|
+
* `DrawSession.onCommit` is the single delivery channel for the committed
|
|
1774
|
+
* snapshot and `DrawSession.onSettled` is the lifecycle signal for when the
|
|
1775
|
+
* session is over and why (#197) — together they make the returned promise
|
|
1776
|
+
* non-load-bearing for hosts that adopt them. `DrawSession.onTransientChange`
|
|
1777
|
+
* streams live rubber-band geometry while the draw is in flight, for both
|
|
1778
|
+
* kinds of session.
|
|
1779
|
+
*/
|
|
1780
|
+
draw(draft: DrawPointSymbolDraft, options?: DrawOptions): Promise<GraphicSnapshot<PointSymbol>>;
|
|
1781
|
+
draw<K extends ControlMeasureKind>(draft: DrawMeasureDraft<K>, options?: DrawOptions): Promise<GraphicSnapshot<ControlMeasure<K>>>;
|
|
852
1782
|
/**
|
|
853
1783
|
* Start an interactive edit on a single control measure. Returns a Promise
|
|
854
|
-
* that resolves with a `
|
|
1784
|
+
* that resolves with a `GraphicSnapshot<ControlMeasure>` of the working state on
|
|
855
1785
|
* `session.close()` and rejects with `TacticalDrawAbortError` on
|
|
856
1786
|
* signal / Escape / destroy / preempt / `session.abort()`.
|
|
857
1787
|
*
|
|
858
1788
|
* Default click-away (`closeOnClickAway: true`) closes the edit on a click
|
|
859
1789
|
* outside the preview / handle features. Hosts that own close policy pass
|
|
860
1790
|
* `false`.
|
|
1791
|
+
*
|
|
1792
|
+
* `EditSession.onCommit` is the single delivery channel for the committed
|
|
1793
|
+
* snapshot and `EditSession.onSettled` is the lifecycle signal for when the
|
|
1794
|
+
* session is over and why (#197) — together they make the returned promise
|
|
1795
|
+
* non-load-bearing for hosts that adopt them.
|
|
861
1796
|
*/
|
|
862
|
-
edit(
|
|
1797
|
+
edit(graphic: PointSymbol, options?: EditOptions): Promise<GraphicSnapshot<PointSymbol>>;
|
|
1798
|
+
edit(measure: ControlMeasure, options?: EditOptions): Promise<GraphicSnapshot<ControlMeasure>>;
|
|
1799
|
+
/**
|
|
1800
|
+
* Start an interactive group transform (multi-select) over `measures`
|
|
1801
|
+
* (ADR-0017 "Multi-select" slice). Every member is lifted onto the shared
|
|
1802
|
+
* preview layer; the box (corner scale, rotate grip, drag-anywhere body
|
|
1803
|
+
* translate) applies rigidly to all of them at once. Returns a Promise that
|
|
1804
|
+
* resolves with `GraphicSnapshot<ControlMeasure>[]` on `session.close()` and rejects
|
|
1805
|
+
* with `TacticalDrawAbortError` on signal / Escape / destroy / preempt /
|
|
1806
|
+
* `session.abort()`. Key the resolved snapshots by `snapshot.graphic.id`:
|
|
1807
|
+
* the member set is mutable (`session.setGraphics`, #187) so snapshot arrays
|
|
1808
|
+
* are id-keyed, not index-aligned with this input. The promise resolves with
|
|
1809
|
+
* the members in the set at close time; a member removed mid-session had its
|
|
1810
|
+
* exit snapshot returned from the `setGraphics` call that removed it.
|
|
1811
|
+
*
|
|
1812
|
+
* `measures.length` must be >= 1; a single-element array behaves like an
|
|
1813
|
+
* `edit` with only `transform` mode. Throws `TypeError` synchronously (before
|
|
1814
|
+
* any preemption) for an empty array or duplicate ids.
|
|
1815
|
+
*
|
|
1816
|
+
* For a selection that changes over time (shift-click toggle, marquee
|
|
1817
|
+
* replace), prefer [[syncTransformGraphics]] over hand-rolling the
|
|
1818
|
+
* live-session-vs-fresh-`editMany` branch, and subscribe to
|
|
1819
|
+
* `TransformSession.onCommit` as the single fold path for exit snapshots
|
|
1820
|
+
* and `TransformSession.onSettled` as the lifecycle signal for when the
|
|
1821
|
+
* session is over and why.
|
|
1822
|
+
*/
|
|
1823
|
+
editMany(measures: ControlMeasure[], options?: EditManyOptions): Promise<GraphicSnapshot<ControlMeasure>[]>;
|
|
1824
|
+
editMany(graphics: Graphic[], options?: EditManyOptions): Promise<GraphicSnapshot[]>;
|
|
1825
|
+
/**
|
|
1826
|
+
* One-call selection-sync: mutate a live transform's member set in place, or
|
|
1827
|
+
* start a fresh `editMany` when none is live (#191). Any mix of control
|
|
1828
|
+
* measures and point symbols is handled by the one Graphic-typed session
|
|
1829
|
+
* (ADR-0032): a point symbol joins or leaves the live session exactly like a
|
|
1830
|
+
* measure via `setGraphics`, preserving box angle, size anchor, and history
|
|
1831
|
+
* baseline — no preempt-and-restart. A point symbol in the selection with no
|
|
1832
|
+
* point-symbol capability configured throws `PointSymbolCapabilityRequiredError`
|
|
1833
|
+
* synchronously (from `setGraphics` on a live session, or from `editMany` on a
|
|
1834
|
+
* fresh one).
|
|
1835
|
+
*
|
|
1836
|
+
* The facade knows session liveness synchronously — `activeTransformSessionRef`
|
|
1837
|
+
* is cleared inside the settle path, before control returns to host code — so
|
|
1838
|
+
* unlike a host's own `activeTransformSession ? setGraphics : editMany` check,
|
|
1839
|
+
* this can never observe a stale ref pointing at an already-settled session.
|
|
1840
|
+
* The settled-race fall-through hosts had to hand-roll for that gap is owned
|
|
1841
|
+
* here instead: a `setGraphics` call that (implausibly) still returns
|
|
1842
|
+
* `undefined` falls through to a fresh `editMany` rather than silently
|
|
1843
|
+
* dropping the selection.
|
|
1844
|
+
*
|
|
1845
|
+
* `graphics: []` closes any live session (committing every member) and is a
|
|
1846
|
+
* no-op while idle. With a live session, `options` is ignored — session-scoped
|
|
1847
|
+
* state (`sizeAnchor`, box angle, close-on-click-away policy) is sticky per
|
|
1848
|
+
* `setGraphics`'s contract; `options` applies only when this call starts a
|
|
1849
|
+
* fresh `editMany`. Exit snapshots are delivered via the session's
|
|
1850
|
+
* `onCommit` — subscribe once in `options.onSession`. Session lifecycle —
|
|
1851
|
+
* including an Escape / preempt / destroy abort the host did not itself
|
|
1852
|
+
* initiate — is observed via the session's `onSettled` (#194); together
|
|
1853
|
+
* `onCommit` and `onSettled` make the underlying `editMany` promise
|
|
1854
|
+
* irrelevant to hosts. That promise is owned by the facade; its abort
|
|
1855
|
+
* rejections are swallowed here (non-abort errors surface as unhandled
|
|
1856
|
+
* rejections). Throws `TypeError` synchronously on duplicate ids, from
|
|
1857
|
+
* either path. Starting a fresh transform preempts any other in-flight
|
|
1858
|
+
* draw/edit exactly like `editMany`.
|
|
1859
|
+
*/
|
|
1860
|
+
syncTransformGraphics(graphics: readonly Graphic[], options?: EditManyOptions): void;
|
|
863
1861
|
/**
|
|
864
1862
|
* Subscribe to picks on committed control measures.
|
|
865
1863
|
*
|
|
@@ -871,7 +1869,7 @@ declare class TacticalDraw {
|
|
|
871
1869
|
* Returns an idempotent unsubscribe function. An optional `signal` removes
|
|
872
1870
|
* the handler when aborted.
|
|
873
1871
|
*/
|
|
874
|
-
|
|
1872
|
+
onGraphicPick(handler: PickHandler, opts?: PickOptions): () => void;
|
|
875
1873
|
/**
|
|
876
1874
|
* Subscribe to pick events on `layerId`. Routed through the façade so the
|
|
877
1875
|
* close-then-pick contract (issue #52) is observable: when an active edit
|
|
@@ -883,6 +1881,47 @@ declare class TacticalDraw {
|
|
|
883
1881
|
* Returns an unsubscribe function. Optional `signal` mirrors `adapter.onPick`.
|
|
884
1882
|
*/
|
|
885
1883
|
onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
1884
|
+
/**
|
|
1885
|
+
* Stateless hit test answering "does TacticalDraw own the pixel at
|
|
1886
|
+
* `pixel`?" — for hosts composing their own click-away / deselection logic
|
|
1887
|
+
* (issue #216) alongside `TacticalDraw`'s draw/edit sessions. Today hosts
|
|
1888
|
+
* hack this together from the `@internal` `layerIds` plus engine-specific
|
|
1889
|
+
* layer queries; this is the supported replacement.
|
|
1890
|
+
*
|
|
1891
|
+
* Tests, topmost first, the three interactive layers — `handlesLayer`,
|
|
1892
|
+
* then `previewLayer`, then `graphicsLayer` — against each layer's current
|
|
1893
|
+
* `adapter.getLayerFeatures(...)` cache. The `guideLayer` is deliberately
|
|
1894
|
+
* excluded: it is non-interactive draw-guide scaffolding, never a
|
|
1895
|
+
* legitimate "owned" click target.
|
|
1896
|
+
*
|
|
1897
|
+
* `preview` and `graphics` each get a label text-extent fallback after the
|
|
1898
|
+
* geometry pass misses on that layer: for every `Point` feature carrying a
|
|
1899
|
+
* string `properties.text`, the anchor is projected and
|
|
1900
|
+
* `fitLabelBoxFromFeature` (shared with the edit controller's `pickLabel`,
|
|
1901
|
+
* see commit f908434) estimates the rendered text's box; a pixel inside
|
|
1902
|
+
* that box counts as owned even though it's off the anchor point itself.
|
|
1903
|
+
* This matters most for the active preview, since a click can land
|
|
1904
|
+
* anywhere across a rotated/offset label during an edit.
|
|
1905
|
+
*
|
|
1906
|
+
* `tolerance` defaults to `mapEventHitTolerance(options?.originalEvent)` —
|
|
1907
|
+
* pass the host's raw click event as `originalEvent` to get the same
|
|
1908
|
+
* pointer-type-aware tolerance (4px mouse/pen, 12px touch) `onPick` uses;
|
|
1909
|
+
* pass an explicit `tolerance` to override it outright.
|
|
1910
|
+
*
|
|
1911
|
+
* `measureId` is set when the hit feature resolves to a control-measure id
|
|
1912
|
+
* via `tryControlMeasureIdFromFeature` — present for graphics/preview
|
|
1913
|
+
* parts, generally absent for handles.
|
|
1914
|
+
*
|
|
1915
|
+
* This is a pure synchronous query over adapter-cached state: it does not
|
|
1916
|
+
* depend on `onPick`/`"click"` dispatch having run first, so it is safe to
|
|
1917
|
+
* call from any host click handler regardless of ordering relative to
|
|
1918
|
+
* TacticalDraw's own pick/click pipeline.
|
|
1919
|
+
*/
|
|
1920
|
+
ownsInteractionAt(pixel: PixelCoordinate, options?: {
|
|
1921
|
+
tolerance?: number;
|
|
1922
|
+
originalEvent?: unknown;
|
|
1923
|
+
}): InteractionHit | null;
|
|
1924
|
+
private toInteractionHit;
|
|
886
1925
|
/**
|
|
887
1926
|
* Abort the in-flight draw or edit. The default `"session"` reason matches
|
|
888
1927
|
* `DrawSession.abort()` / `EditSession.abort()`.
|
|
@@ -894,7 +1933,14 @@ declare class TacticalDraw {
|
|
|
894
1933
|
* The live variable-draw or edit session, or `null` while idle and during
|
|
895
1934
|
* fixed-length draws. Safe to read after `destroy()`.
|
|
896
1935
|
*/
|
|
897
|
-
get activeSession(): DrawSession | EditSession | null;
|
|
1936
|
+
get activeSession(): DrawSession | EditSession | GraphicEditSession<PointSymbol> | null;
|
|
1937
|
+
/**
|
|
1938
|
+
* The live `editMany` group-transform session, or `null` while idle (and
|
|
1939
|
+
* during any other interaction). Separate from `activeSession` because
|
|
1940
|
+
* `TransformSession` isn't part of its `DrawSession | EditSession` union —
|
|
1941
|
+
* see `activeTransformSessionRef`. Safe to read after `destroy()`.
|
|
1942
|
+
*/
|
|
1943
|
+
get activeTransformSession(): TransformSession | null;
|
|
898
1944
|
/**
|
|
899
1945
|
* Tear down the façade. Releases only layer slots the façade created; host
|
|
900
1946
|
* supplied layer ids are left untouched. Safe to call multiple times. After
|
|
@@ -926,9 +1972,19 @@ declare class TacticalDraw {
|
|
|
926
1972
|
private applyGuideStyle;
|
|
927
1973
|
private baseGuideStyle;
|
|
928
1974
|
/**
|
|
929
|
-
* Resolve handle slots across built-in → façade →
|
|
930
|
-
* per-property shallow merge. Explicit `undefined`
|
|
931
|
-
* layer unsets the corresponding lower-priority field
|
|
1975
|
+
* Resolve handle slots (plus `boxOutline`) across built-in → façade →
|
|
1976
|
+
* per-call with per-slot, per-property shallow merge. Explicit `undefined`
|
|
1977
|
+
* at a higher-priority layer unsets the corresponding lower-priority field
|
|
1978
|
+
* (ADR-0006). `boxOutline` rides this same per-feature merge path (not the
|
|
1979
|
+
* `guide` layer-style path) — the edit controller reads it off
|
|
1980
|
+
* `properties.style` on the box outline `LineString` (ADR-0017).
|
|
1981
|
+
*
|
|
1982
|
+
* `boxPadding` is a scalar, not a mergeable style object, so it resolves
|
|
1983
|
+
* separately with the same façade → per-call precedence and the same
|
|
1984
|
+
* explicit-undefined-wins rule: a `boxPadding` key present at a
|
|
1985
|
+
* higher-priority layer (even set to `undefined`) wins over a lower layer's
|
|
1986
|
+
* value. There is no built-in `boxPadding` — an absent value at every layer
|
|
1987
|
+
* falls through to the controllers' own `?? 0` default.
|
|
932
1988
|
*
|
|
933
1989
|
* Returns `undefined` only when no slot would carry any value — keeps the
|
|
934
1990
|
* downstream "no style on the feature" path intact for tests that assert
|
|
@@ -946,5 +2002,65 @@ declare class TacticalDraw {
|
|
|
946
2002
|
declare class TacticalDrawDestroyedError extends Error {
|
|
947
2003
|
constructor(message?: string);
|
|
948
2004
|
}
|
|
2005
|
+
declare class PointSymbolCapabilityRequiredError extends Error {
|
|
2006
|
+
constructor();
|
|
2007
|
+
}
|
|
2008
|
+
//#endregion
|
|
2009
|
+
//#region src/tactical-draw/transform-box.d.ts
|
|
2010
|
+
/**
|
|
2011
|
+
* The Excalidraw/Felt-style transform box: the oriented bounding box drawn
|
|
2012
|
+
* around a selection, with 4 corner scale handles, a rotate grip above the
|
|
2013
|
+
* top edge, and drag-anywhere-inside-the-box translate. This module is the
|
|
2014
|
+
* single source of truth for box *placement* (`fitTransformBox`,
|
|
2015
|
+
* `boxCorners`, `rotateGripPixel`) AND box *picking* (`pointInBox`,
|
|
2016
|
+
* `pickBoxHandle`) — mirroring the ADR-0016 discipline that keeps
|
|
2017
|
+
* `handles.ts` owning vertex/midpoint placement/pick. Everything here is pure
|
|
2018
|
+
* pixel-space math: no projection, no rendering, no session state.
|
|
2019
|
+
*
|
|
2020
|
+
* **Angle convention**: `angle` is in radians, measured in screen pixel space
|
|
2021
|
+
* (y grows downward). A positive angle rotates the box clockwise as drawn on
|
|
2022
|
+
* screen — the same convention `rotatePointsAround` (`handle-drag.ts`) uses,
|
|
2023
|
+
* so box rotation and control-point rotation stay in lockstep.
|
|
2024
|
+
*
|
|
2025
|
+
* **Corner ordering**: `boxCorners` always returns
|
|
2026
|
+
* `[topLeft, topRight, bottomRight, bottomLeft]` in the box's own (unrotated)
|
|
2027
|
+
* local frame, then rotated into place. "Top" means the smaller-y edge before
|
|
2028
|
+
* rotation — i.e. the edge the rotate grip floats above. The order is stable
|
|
2029
|
+
* across calls so a scale handle's `index` (0–3) always means the same
|
|
2030
|
+
* corner, and its opposite corner is always `(index + 2) % 4`.
|
|
2031
|
+
*/
|
|
2032
|
+
interface TransformBox {
|
|
2033
|
+
/** Box center, pixel space. */
|
|
2034
|
+
center: PixelCoordinate;
|
|
2035
|
+
/** Half-width along the box's local x-axis (pre-rotation), in pixels. */
|
|
2036
|
+
halfWidth: number;
|
|
2037
|
+
/** Half-height along the box's local y-axis (pre-rotation), in pixels. */
|
|
2038
|
+
halfHeight: number;
|
|
2039
|
+
/** Radians, screen space, positive = clockwise on screen. */
|
|
2040
|
+
angle: number;
|
|
2041
|
+
}
|
|
2042
|
+
//#endregion
|
|
2043
|
+
//#region src/tactical-draw/point-symbol-rendering.d.ts
|
|
2044
|
+
interface PointSymbolFootprint {
|
|
2045
|
+
box: TransformBox;
|
|
2046
|
+
corners: PixelCoordinate[];
|
|
2047
|
+
}
|
|
2048
|
+
declare function effectivePointSymbolPixelSize(symbol: PointSymbol, resolution: number | undefined): number;
|
|
2049
|
+
/** Complete rotated logical footprint for selection and transform chrome. */
|
|
2050
|
+
declare function pointSymbolFootprint(symbol: PointSymbol, render: PointSymbolRender, adapter: Pick<MapAdapter, "getBearing" | "getPixelFromCoordinate" | "getResolution">): PointSymbolFootprint | null;
|
|
2051
|
+
//#endregion
|
|
2052
|
+
//#region src/world-wrap.d.ts
|
|
2053
|
+
/**
|
|
2054
|
+
* Shift `lng` by whole world widths (360°) onto the copy nearest `refLng`.
|
|
2055
|
+
*
|
|
2056
|
+
* Map engines that wrap pointer longitudes into [-180, 180] (OpenLayers'
|
|
2057
|
+
* `toLonLat`, MapLibre's globe unproject) make positions jump ~360° when a
|
|
2058
|
+
* gesture crosses the antimeridian. Adapters use this to keep delivered
|
|
2059
|
+
* longitudes continuous with the view center, and to project coordinates on
|
|
2060
|
+
* the world copy the view is actually looking at.
|
|
2061
|
+
*/
|
|
2062
|
+
declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
2063
|
+
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
2064
|
+
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
949
2065
|
//#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 };
|
|
2066
|
+
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 LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type NativeEventEntry, type PickEvent, type PickHandler, type PickOptions, 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 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, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, resolveTextSizePixels, tryControlMeasureIdFromFeature };
|