@orbat-mapper/tactical-draw 0.2.0-alpha.18 → 0.2.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +63 -2
- package/dist/index.mjs +119 -16
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -260,6 +260,13 @@ interface MapAdapter {
|
|
|
260
260
|
* destruction.
|
|
261
261
|
*/
|
|
262
262
|
destroy(): void;
|
|
263
|
+
/**
|
|
264
|
+
* Returns the features currently set on the layer — the adapter-side cache
|
|
265
|
+
* of the last `setLayerFeatures`/`updateLayerFeatures`/`removeFeatures`/
|
|
266
|
+
* `clearLayer` state. Empty array for an unknown layer. Callers must not
|
|
267
|
+
* mutate the returned array or features.
|
|
268
|
+
*/
|
|
269
|
+
getLayerFeatures(layerId: LayerId): readonly Feature[];
|
|
263
270
|
}
|
|
264
271
|
//#endregion
|
|
265
272
|
//#region src/base-map-adapter.d.ts
|
|
@@ -339,6 +346,7 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
|
|
|
339
346
|
* (MapLibre, OpenLayers) override this.
|
|
340
347
|
*/
|
|
341
348
|
getBearing(): number;
|
|
349
|
+
getLayerFeatures(layerId: LayerId): readonly Feature[];
|
|
342
350
|
}
|
|
343
351
|
//#endregion
|
|
344
352
|
//#region src/pick-utils.d.ts
|
|
@@ -387,7 +395,7 @@ type ToPixel = (lonLat: Position) => PixelCoordinate | null;
|
|
|
387
395
|
*
|
|
388
396
|
* Returns the first matching feature in iteration order, or null.
|
|
389
397
|
*/
|
|
390
|
-
declare function hitTestFeature(pixel: PixelCoordinate, features: Feature[], hitTolerance: number, toPixel: ToPixel): Feature | null;
|
|
398
|
+
declare function hitTestFeature(pixel: PixelCoordinate, features: readonly Feature[], hitTolerance: number, toPixel: ToPixel): Feature | null;
|
|
391
399
|
//#endregion
|
|
392
400
|
//#region src/gestures/hit-tolerance.d.ts
|
|
393
401
|
declare const TOUCH_HIT_TOLERANCE_PX = 22;
|
|
@@ -993,6 +1001,18 @@ interface TacticalDrawLayerOptions {
|
|
|
993
1001
|
guide?: LayerId;
|
|
994
1002
|
handles?: LayerId;
|
|
995
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Result of {@link TacticalDraw.ownsInteractionAt} — which layer claimed the
|
|
1006
|
+
* pixel and the feature that matched.
|
|
1007
|
+
*/
|
|
1008
|
+
interface InteractionHit {
|
|
1009
|
+
/** Which TacticalDraw layer owns the pixel — topmost match wins. */
|
|
1010
|
+
layer: "handles" | "preview" | "graphics";
|
|
1011
|
+
/** The hit feature (do not mutate). */
|
|
1012
|
+
feature: Feature;
|
|
1013
|
+
/** Control-measure id when the hit feature belongs to one (graphics/preview parts). */
|
|
1014
|
+
measureId?: string;
|
|
1015
|
+
}
|
|
996
1016
|
interface TacticalDrawOptions {
|
|
997
1017
|
layers?: TacticalDrawLayerOptions;
|
|
998
1018
|
/**
|
|
@@ -1414,6 +1434,47 @@ declare class TacticalDraw {
|
|
|
1414
1434
|
* Returns an unsubscribe function. Optional `signal` mirrors `adapter.onPick`.
|
|
1415
1435
|
*/
|
|
1416
1436
|
onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
1437
|
+
/**
|
|
1438
|
+
* Stateless hit test answering "does TacticalDraw own the pixel at
|
|
1439
|
+
* `pixel`?" — for hosts composing their own click-away / deselection logic
|
|
1440
|
+
* (issue #216) alongside `TacticalDraw`'s draw/edit sessions. Today hosts
|
|
1441
|
+
* hack this together from the `@internal` `layerIds` plus engine-specific
|
|
1442
|
+
* layer queries; this is the supported replacement.
|
|
1443
|
+
*
|
|
1444
|
+
* Tests, topmost first, the three interactive layers — `handlesLayer`,
|
|
1445
|
+
* then `previewLayer`, then `graphicsLayer` — against each layer's current
|
|
1446
|
+
* `adapter.getLayerFeatures(...)` cache. The `guideLayer` is deliberately
|
|
1447
|
+
* excluded: it is non-interactive draw-guide scaffolding, never a
|
|
1448
|
+
* legitimate "owned" click target.
|
|
1449
|
+
*
|
|
1450
|
+
* `preview` and `graphics` each get a label text-extent fallback after the
|
|
1451
|
+
* geometry pass misses on that layer: for every `Point` feature carrying a
|
|
1452
|
+
* string `properties.text`, the anchor is projected and
|
|
1453
|
+
* `fitLabelBoxFromFeature` (shared with the edit controller's `pickLabel`,
|
|
1454
|
+
* see commit f908434) estimates the rendered text's box; a pixel inside
|
|
1455
|
+
* that box counts as owned even though it's off the anchor point itself.
|
|
1456
|
+
* This matters most for the active preview, since a click can land
|
|
1457
|
+
* anywhere across a rotated/offset label during an edit.
|
|
1458
|
+
*
|
|
1459
|
+
* `tolerance` defaults to `mapEventHitTolerance(options?.originalEvent)` —
|
|
1460
|
+
* pass the host's raw click event as `originalEvent` to get the same
|
|
1461
|
+
* pointer-type-aware tolerance (4px mouse/pen, 12px touch) `onPick` uses;
|
|
1462
|
+
* pass an explicit `tolerance` to override it outright.
|
|
1463
|
+
*
|
|
1464
|
+
* `measureId` is set when the hit feature resolves to a control-measure id
|
|
1465
|
+
* via `tryControlMeasureIdFromFeature` — present for graphics/preview
|
|
1466
|
+
* parts, generally absent for handles.
|
|
1467
|
+
*
|
|
1468
|
+
* This is a pure synchronous query over adapter-cached state: it does not
|
|
1469
|
+
* depend on `onPick`/`"click"` dispatch having run first, so it is safe to
|
|
1470
|
+
* call from any host click handler regardless of ordering relative to
|
|
1471
|
+
* TacticalDraw's own pick/click pipeline.
|
|
1472
|
+
*/
|
|
1473
|
+
ownsInteractionAt(pixel: PixelCoordinate, options?: {
|
|
1474
|
+
tolerance?: number;
|
|
1475
|
+
originalEvent?: unknown;
|
|
1476
|
+
}): InteractionHit | null;
|
|
1477
|
+
private toInteractionHit;
|
|
1417
1478
|
/**
|
|
1418
1479
|
* Abort the in-flight draw or edit. The default `"session"` reason matches
|
|
1419
1480
|
* `DrawSession.abort()` / `EditSession.abort()`.
|
|
@@ -1543,4 +1604,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
|
1543
1604
|
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
1544
1605
|
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
1545
1606
|
//#endregion
|
|
1546
|
-
export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditManyOptions, 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 SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
|
1607
|
+
export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type GuideStyle, type HandleStyle, 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 PointerCallback, PointerCallbackHub, type PointerEventInfo, type SettleReason, type SizeAnchor, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, bindAbortable, coerceHandleStyle, combineWithHostSignal, getSizeAnchor, hitTestFeature, ignoreAbort, isTacticalDrawAbortError, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, resolveGroundTextSizePixels, tryControlMeasureIdFromFeature };
|
package/dist/index.mjs
CHANGED
|
@@ -180,6 +180,9 @@ var BaseMapAdapter = class {
|
|
|
180
180
|
getBearing() {
|
|
181
181
|
return 0;
|
|
182
182
|
}
|
|
183
|
+
getLayerFeatures(layerId) {
|
|
184
|
+
return this.featureCache.get(layerId) ?? [];
|
|
185
|
+
}
|
|
183
186
|
};
|
|
184
187
|
function pickHitTolerance(originalEvent) {
|
|
185
188
|
if (typeof PointerEvent !== "undefined" && originalEvent instanceof PointerEvent) return originalEvent.pointerType === "touch" ? 12 : 4;
|
|
@@ -2140,6 +2143,28 @@ function fitLabelTransformBox(input) {
|
|
|
2140
2143
|
angle
|
|
2141
2144
|
};
|
|
2142
2145
|
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Read a label `Point` feature's rendered props (`text`, `rotation`,
|
|
2148
|
+
* `textAnchor`, `textSizePixels`, `textSizeMeters`) into a
|
|
2149
|
+
* {@link fitLabelTransformBox} input, anchored at `anchorPixel`. Shared by
|
|
2150
|
+
* the edit controller's label picking/dragging and `TacticalDraw`'s
|
|
2151
|
+
* `ownsInteractionAt` label-extent fallback — both need the exact same
|
|
2152
|
+
* text-extent estimate so a click that "owns" a label during a host
|
|
2153
|
+
* click-away check matches what the edit controller would pick.
|
|
2154
|
+
*/
|
|
2155
|
+
function fitLabelBoxFromFeature(feature, anchorPixel, view) {
|
|
2156
|
+
const props = feature.properties ?? {};
|
|
2157
|
+
return fitLabelTransformBox({
|
|
2158
|
+
text: typeof props.text === "string" ? props.text : void 0,
|
|
2159
|
+
rotation: typeof props.rotation === "number" ? props.rotation : void 0,
|
|
2160
|
+
textAnchor: props.textAnchor === "start" || props.textAnchor === "end" ? props.textAnchor : void 0,
|
|
2161
|
+
textSizePixels: typeof props.textSizePixels === "number" ? props.textSizePixels : void 0,
|
|
2162
|
+
textSizeMeters: typeof props.textSizeMeters === "number" ? props.textSizeMeters : void 0,
|
|
2163
|
+
anchor: anchorPixel,
|
|
2164
|
+
resolution: view.resolution,
|
|
2165
|
+
bearing: view.bearing
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2143
2168
|
//#endregion
|
|
2144
2169
|
//#region src/tactical-draw/edit-controller.ts
|
|
2145
2170
|
/**
|
|
@@ -2374,7 +2399,10 @@ function runEdit(opts) {
|
|
|
2374
2399
|
else {
|
|
2375
2400
|
const anchor = selectedLabelAnchor();
|
|
2376
2401
|
if (selectedLabelKey && !anchor) selectedLabelKey = null;
|
|
2377
|
-
else if (anchor) labelBox = fitLabelBoxFromFeature(anchor.feature, anchor.pixel
|
|
2402
|
+
else if (anchor) labelBox = fitLabelBoxFromFeature(anchor.feature, anchor.pixel, {
|
|
2403
|
+
resolution: adapter.getResolution(),
|
|
2404
|
+
bearing: adapter.getBearing()
|
|
2405
|
+
});
|
|
2378
2406
|
}
|
|
2379
2407
|
for (const feature of lastPreviewFeatures) {
|
|
2380
2408
|
const key = feature.properties?.labelPlacementKey;
|
|
@@ -2860,20 +2888,6 @@ function runEdit(opts) {
|
|
|
2860
2888
|
* click landed at within the box).
|
|
2861
2889
|
*/
|
|
2862
2890
|
let activeAmplifierDrag = null;
|
|
2863
|
-
/** Read a label feature's rendered props into a `fitLabelTransformBox` input, anchored at `anchorPixel`. */
|
|
2864
|
-
function fitLabelBoxFromFeature(feature, anchorPixel) {
|
|
2865
|
-
const props = feature.properties ?? {};
|
|
2866
|
-
return fitLabelTransformBox({
|
|
2867
|
-
text: typeof props.text === "string" ? props.text : void 0,
|
|
2868
|
-
rotation: typeof props.rotation === "number" ? props.rotation : void 0,
|
|
2869
|
-
textAnchor: props.textAnchor === "start" || props.textAnchor === "end" ? props.textAnchor : void 0,
|
|
2870
|
-
textSizePixels: typeof props.textSizePixels === "number" ? props.textSizePixels : void 0,
|
|
2871
|
-
textSizeMeters: typeof props.textSizeMeters === "number" ? props.textSizeMeters : void 0,
|
|
2872
|
-
anchor: anchorPixel,
|
|
2873
|
-
resolution: adapter.getResolution(),
|
|
2874
|
-
bearing: adapter.getBearing()
|
|
2875
|
-
});
|
|
2876
|
-
}
|
|
2877
2891
|
function pickLabel(pixel, tolerance) {
|
|
2878
2892
|
for (const feature of lastPreviewFeatures) {
|
|
2879
2893
|
const key = feature.properties?.labelPlacementKey;
|
|
@@ -2887,7 +2901,10 @@ function runEdit(opts) {
|
|
|
2887
2901
|
anchorPixel: projected
|
|
2888
2902
|
};
|
|
2889
2903
|
}
|
|
2890
|
-
const box = fitLabelBoxFromFeature(feature, projected
|
|
2904
|
+
const box = fitLabelBoxFromFeature(feature, projected, {
|
|
2905
|
+
resolution: adapter.getResolution(),
|
|
2906
|
+
bearing: adapter.getBearing()
|
|
2907
|
+
});
|
|
2891
2908
|
if (box && pointInBox(box, pixel)) return {
|
|
2892
2909
|
key,
|
|
2893
2910
|
anchorPixel: projected
|
|
@@ -4640,6 +4657,92 @@ var TacticalDraw = class {
|
|
|
4640
4657
|
return remove;
|
|
4641
4658
|
}
|
|
4642
4659
|
/**
|
|
4660
|
+
* Stateless hit test answering "does TacticalDraw own the pixel at
|
|
4661
|
+
* `pixel`?" — for hosts composing their own click-away / deselection logic
|
|
4662
|
+
* (issue #216) alongside `TacticalDraw`'s draw/edit sessions. Today hosts
|
|
4663
|
+
* hack this together from the `@internal` `layerIds` plus engine-specific
|
|
4664
|
+
* layer queries; this is the supported replacement.
|
|
4665
|
+
*
|
|
4666
|
+
* Tests, topmost first, the three interactive layers — `handlesLayer`,
|
|
4667
|
+
* then `previewLayer`, then `graphicsLayer` — against each layer's current
|
|
4668
|
+
* `adapter.getLayerFeatures(...)` cache. The `guideLayer` is deliberately
|
|
4669
|
+
* excluded: it is non-interactive draw-guide scaffolding, never a
|
|
4670
|
+
* legitimate "owned" click target.
|
|
4671
|
+
*
|
|
4672
|
+
* `preview` and `graphics` each get a label text-extent fallback after the
|
|
4673
|
+
* geometry pass misses on that layer: for every `Point` feature carrying a
|
|
4674
|
+
* string `properties.text`, the anchor is projected and
|
|
4675
|
+
* `fitLabelBoxFromFeature` (shared with the edit controller's `pickLabel`,
|
|
4676
|
+
* see commit f908434) estimates the rendered text's box; a pixel inside
|
|
4677
|
+
* that box counts as owned even though it's off the anchor point itself.
|
|
4678
|
+
* This matters most for the active preview, since a click can land
|
|
4679
|
+
* anywhere across a rotated/offset label during an edit.
|
|
4680
|
+
*
|
|
4681
|
+
* `tolerance` defaults to `mapEventHitTolerance(options?.originalEvent)` —
|
|
4682
|
+
* pass the host's raw click event as `originalEvent` to get the same
|
|
4683
|
+
* pointer-type-aware tolerance (4px mouse/pen, 12px touch) `onPick` uses;
|
|
4684
|
+
* pass an explicit `tolerance` to override it outright.
|
|
4685
|
+
*
|
|
4686
|
+
* `measureId` is set when the hit feature resolves to a control-measure id
|
|
4687
|
+
* via `tryControlMeasureIdFromFeature` — present for graphics/preview
|
|
4688
|
+
* parts, generally absent for handles.
|
|
4689
|
+
*
|
|
4690
|
+
* This is a pure synchronous query over adapter-cached state: it does not
|
|
4691
|
+
* depend on `onPick`/`"click"` dispatch having run first, so it is safe to
|
|
4692
|
+
* call from any host click handler regardless of ordering relative to
|
|
4693
|
+
* TacticalDraw's own pick/click pipeline.
|
|
4694
|
+
*/
|
|
4695
|
+
ownsInteractionAt(pixel, options) {
|
|
4696
|
+
this.assertAlive();
|
|
4697
|
+
const tolerance = options?.tolerance ?? mapEventHitTolerance(options?.originalEvent);
|
|
4698
|
+
const toPixel = (lonLat) => this.adapter.getPixelFromCoordinate(lonLat);
|
|
4699
|
+
const layers = [
|
|
4700
|
+
{
|
|
4701
|
+
layer: "handles",
|
|
4702
|
+
layerId: this.handlesLayer,
|
|
4703
|
+
labelFallback: false
|
|
4704
|
+
},
|
|
4705
|
+
{
|
|
4706
|
+
layer: "preview",
|
|
4707
|
+
layerId: this.previewLayer,
|
|
4708
|
+
labelFallback: true
|
|
4709
|
+
},
|
|
4710
|
+
{
|
|
4711
|
+
layer: "graphics",
|
|
4712
|
+
layerId: this.graphicsLayer,
|
|
4713
|
+
labelFallback: true
|
|
4714
|
+
}
|
|
4715
|
+
];
|
|
4716
|
+
for (const { layer, layerId, labelFallback } of layers) {
|
|
4717
|
+
const features = this.adapter.getLayerFeatures(layerId);
|
|
4718
|
+
const hit = hitTestFeature(pixel, features, tolerance, toPixel);
|
|
4719
|
+
if (hit) return this.toInteractionHit(layer, hit);
|
|
4720
|
+
if (labelFallback) for (const feature of features) {
|
|
4721
|
+
if (feature.geometry?.type !== "Point") continue;
|
|
4722
|
+
if (typeof feature.properties?.text !== "string") continue;
|
|
4723
|
+
const anchorPixel = toPixel(feature.geometry.coordinates);
|
|
4724
|
+
if (!anchorPixel) continue;
|
|
4725
|
+
const box = fitLabelBoxFromFeature(feature, anchorPixel, {
|
|
4726
|
+
resolution: this.adapter.getResolution(),
|
|
4727
|
+
bearing: this.adapter.getBearing()
|
|
4728
|
+
});
|
|
4729
|
+
if (box && pointInBox(box, pixel)) return this.toInteractionHit(layer, feature);
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
return null;
|
|
4733
|
+
}
|
|
4734
|
+
toInteractionHit(layer, feature) {
|
|
4735
|
+
const measureId = tryControlMeasureIdFromFeature(feature);
|
|
4736
|
+
return measureId != null ? {
|
|
4737
|
+
layer,
|
|
4738
|
+
feature,
|
|
4739
|
+
measureId
|
|
4740
|
+
} : {
|
|
4741
|
+
layer,
|
|
4742
|
+
feature
|
|
4743
|
+
};
|
|
4744
|
+
}
|
|
4745
|
+
/**
|
|
4643
4746
|
* Abort the in-flight draw or edit. The default `"session"` reason matches
|
|
4644
4747
|
* `DrawSession.abort()` / `EditSession.abort()`.
|
|
4645
4748
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.19",
|
|
4
4
|
"description": "Engine-agnostic draw/edit core for tactical control measures (MIL-STD-2525 / APP-6). The MapAdapter ABI and bring-your-own-engine entry point for the tactical-draw package family.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Orbat Mapper",
|