@orbat-mapper/tactical-draw 0.2.0-alpha.31 → 0.2.0-alpha.33
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 +196 -3
- package/dist/index.mjs +607 -109
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -166,6 +166,17 @@ interface IconResource {
|
|
|
166
166
|
width: number;
|
|
167
167
|
height: number;
|
|
168
168
|
anchor: [number, number];
|
|
169
|
+
/**
|
|
170
|
+
* Direct visible mark used to rank icon picks, in unscaled canvas coordinates.
|
|
171
|
+
* Hits elsewhere in the full canvas measure their distance from this rectangle;
|
|
172
|
+
* omit it to preserve whole-canvas distance-zero behavior.
|
|
173
|
+
*/
|
|
174
|
+
coreRect?: {
|
|
175
|
+
x: number;
|
|
176
|
+
y: number;
|
|
177
|
+
width: number;
|
|
178
|
+
height: number;
|
|
179
|
+
};
|
|
169
180
|
}
|
|
170
181
|
/**
|
|
171
182
|
* Click-style hit event delivered by {@link MapAdapter.onPick}.
|
|
@@ -254,6 +265,9 @@ interface MapAdapter {
|
|
|
254
265
|
* end of a pan or pinch-zoom gesture.
|
|
255
266
|
*
|
|
256
267
|
* Hit tolerance is pointer-type aware: 4 px for mouse/pen, 12 px for touch.
|
|
268
|
+
* Outline-only polygon interiors additionally have bounded inward selection
|
|
269
|
+
* reach from visible viewport-clipped boundaries: 24 px for mouse/pen and
|
|
270
|
+
* 36 px for touch. A visible solid or patterned fill owns its full interior.
|
|
257
271
|
*
|
|
258
272
|
* Returns an unsubscribe function. The returned function and the optional
|
|
259
273
|
* `signal` are both honoured; either teardown path stops further events,
|
|
@@ -443,6 +457,8 @@ declare abstract class BaseMapAdapter<TLayerHandle, TEventToken, TViewToken> imp
|
|
|
443
457
|
}
|
|
444
458
|
//#endregion
|
|
445
459
|
//#region src/pick-utils.d.ts
|
|
460
|
+
type PickPointerType = "mouse" | "pen" | "touch";
|
|
461
|
+
declare function pickPointerType(originalEvent: unknown): PickPointerType;
|
|
446
462
|
declare function pickHitTolerance(originalEvent: unknown): number;
|
|
447
463
|
declare function tryControlMeasureIdFromFeature(feature: Feature): string | null;
|
|
448
464
|
/**
|
|
@@ -453,6 +469,119 @@ declare function tryControlMeasureIdFromFeature(feature: Feature): string | null
|
|
|
453
469
|
*/
|
|
454
470
|
declare function bindAbortable(signal: AbortSignal | undefined, attach: () => () => void): () => void;
|
|
455
471
|
//#endregion
|
|
472
|
+
//#region src/pick-candidates.d.ts
|
|
473
|
+
interface ScreenBounds {
|
|
474
|
+
minX: number;
|
|
475
|
+
minY: number;
|
|
476
|
+
maxX: number;
|
|
477
|
+
maxY: number;
|
|
478
|
+
}
|
|
479
|
+
interface RotatedScreenBounds {
|
|
480
|
+
center: PixelCoordinate;
|
|
481
|
+
halfWidth: number;
|
|
482
|
+
halfHeight: number;
|
|
483
|
+
angle: number;
|
|
484
|
+
}
|
|
485
|
+
type LabelExtent = ScreenBounds | RotatedScreenBounds;
|
|
486
|
+
interface ResolvedPickPresentation {
|
|
487
|
+
stroke?: {
|
|
488
|
+
color?: string;
|
|
489
|
+
width?: number;
|
|
490
|
+
dash?: readonly number[];
|
|
491
|
+
};
|
|
492
|
+
fill?: {
|
|
493
|
+
color?: string;
|
|
494
|
+
pattern?: string;
|
|
495
|
+
opacity: number;
|
|
496
|
+
};
|
|
497
|
+
point?: {
|
|
498
|
+
fillColor?: string;
|
|
499
|
+
radius?: number;
|
|
500
|
+
};
|
|
501
|
+
text?: TextStyleOptions;
|
|
502
|
+
}
|
|
503
|
+
type PickCandidateHitKind = "native" | "point" | "line" | "polygon-boundary" | "polygon-interior" | "icon" | "label";
|
|
504
|
+
interface PickCandidateHit {
|
|
505
|
+
kind: PickCandidateHitKind;
|
|
506
|
+
/** CSS-pixel distance to the visible mark, or null when only the engine knows the hit. */
|
|
507
|
+
distance: number | null;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Engine-neutral description of one rendered part eligible at a pointer location.
|
|
511
|
+
* This is deliberately richer than PickEvent but remains adapter-author plumbing:
|
|
512
|
+
* ordered candidates are not exposed by the public picking event.
|
|
513
|
+
*/
|
|
514
|
+
interface PickCandidate {
|
|
515
|
+
feature: Feature;
|
|
516
|
+
graphicId: string | null;
|
|
517
|
+
renderedPartId: string | number | null;
|
|
518
|
+
hit: PickCandidateHit;
|
|
519
|
+
viewport: ScreenBounds;
|
|
520
|
+
presentation: ResolvedPickPresentation;
|
|
521
|
+
visualOrder: number;
|
|
522
|
+
iconFootprint?: readonly PixelCoordinate[];
|
|
523
|
+
labelExtent?: LabelExtent;
|
|
524
|
+
}
|
|
525
|
+
/** A rendered part supplied by an adapter or TacticalDraw for shared evaluation. */
|
|
526
|
+
interface PickCandidateInput {
|
|
527
|
+
feature: Feature;
|
|
528
|
+
/**
|
|
529
|
+
* Lower values mean visually topmost. Suppliers must normalize their
|
|
530
|
+
* native ordering to topmost-first before calling into evaluation.
|
|
531
|
+
*/
|
|
532
|
+
visualOrder: number;
|
|
533
|
+
/** The engine already reported this part as hit; retained as a fallback for engine-only marks. */
|
|
534
|
+
nativeHit?: boolean;
|
|
535
|
+
icon?: IconResource;
|
|
536
|
+
labelExtent?: LabelExtent;
|
|
537
|
+
presentation?: ResolvedPickPresentation;
|
|
538
|
+
layerStyle?: StyleOptions;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* The `visualOrder` for the feature at `index` of a render-ordered
|
|
542
|
+
* (bottom-first) list of `total` features: reversing the index makes the
|
|
543
|
+
* topmost-rendered feature the lowest, and therefore winning, order.
|
|
544
|
+
*/
|
|
545
|
+
declare function renderOrderToVisualOrder(index: number, total: number): number;
|
|
546
|
+
interface MergedPickFeature {
|
|
547
|
+
feature: Feature;
|
|
548
|
+
visualOrder: number;
|
|
549
|
+
nativeHit: boolean;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Preserve native visual ordering/hit evidence while keeping cached source
|
|
553
|
+
* geometry authoritative. Native polygon geometry may be tile-clipped and
|
|
554
|
+
* therefore contain artificial edges that are not part of the graphic.
|
|
555
|
+
*
|
|
556
|
+
* Normalizes to the shared `visualOrder` contract (lower = visually
|
|
557
|
+
* topmost): native hits already arrive topmost-first from the engines and
|
|
558
|
+
* keep that order at the front. Cached-only features (not among the native
|
|
559
|
+
* hits) are appended after them, in reverse cache order — cache arrays are
|
|
560
|
+
* render order (bottom-first), so reversing yields topmost-first — with
|
|
561
|
+
* `visualOrder` numbering continuing on from the native section.
|
|
562
|
+
*/
|
|
563
|
+
declare function mergeNativeAndCachedPickFeatures(nativeFeatures: readonly Feature[], cachedFeatures: readonly Feature[]): MergedPickFeature[];
|
|
564
|
+
interface PickEvaluationContext {
|
|
565
|
+
pixel: PixelCoordinate;
|
|
566
|
+
tolerance: number;
|
|
567
|
+
pointerType?: PickPointerType;
|
|
568
|
+
toPixel: (position: Position) => PixelCoordinate | null;
|
|
569
|
+
viewport: ScreenBounds;
|
|
570
|
+
bearing?: number;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Evaluate every relevant rendered part in screen space, then return the
|
|
574
|
+
* primary candidate under the visible-intent ranking policy: direct visible
|
|
575
|
+
* marks (strokes, points, icons, labels) outrank visibly filled interiors,
|
|
576
|
+
* which outrank inferred outline-only reach; competing direct marks rank by
|
|
577
|
+
* screen-space distance; visual topmost order is only the final tie-break.
|
|
578
|
+
* See `rankPickCandidates` for the full ordered list — that ordering stays
|
|
579
|
+
* internal adapter-author plumbing; the public pick event exposes only this
|
|
580
|
+
* primary candidate.
|
|
581
|
+
*/
|
|
582
|
+
declare function evaluatePickCandidates(inputs: readonly PickCandidateInput[], context: PickEvaluationContext): PickCandidate | null;
|
|
583
|
+
declare function resolvePickPresentation(feature: Feature, layerStyle?: StyleOptions): ResolvedPickPresentation;
|
|
584
|
+
//#endregion
|
|
456
585
|
//#region src/feature-style.d.ts
|
|
457
586
|
/**
|
|
458
587
|
* Coerce a raw `properties.style` value into a `HandleStyle` for the
|
|
@@ -748,6 +877,15 @@ interface InteractionStyle {
|
|
|
748
877
|
scaleHandle?: HandleStyle;
|
|
749
878
|
rotateHandle?: HandleStyle;
|
|
750
879
|
boxOutline?: GuideStyle;
|
|
880
|
+
/**
|
|
881
|
+
* Line style for the passive, non-interactive selection highlight rendered
|
|
882
|
+
* by {@link TacticalDraw.setHighlightedGraphics}. A dashed, muted variant of
|
|
883
|
+
* `boxOutline` so a locked/highlighted selection reads as visually distinct
|
|
884
|
+
* from an active edit's oriented transform box. Delivered per-feature via
|
|
885
|
+
* `properties.style` on the highlight outline `LineString`, resolved through
|
|
886
|
+
* the same built-in → façade → per-call slot system as every other field.
|
|
887
|
+
*/
|
|
888
|
+
highlightOutline?: GuideStyle;
|
|
751
889
|
/**
|
|
752
890
|
* Pixel padding added on every side of the fitted transform box
|
|
753
891
|
* (`fitTransformBox`, ADR-0017) so the box doesn't tightly hug the rendered
|
|
@@ -1418,6 +1556,12 @@ type DrawPointSymbolDraft = Pick<PointSymbol, "kind" | "sidc"> & Partial<Omit<Po
|
|
|
1418
1556
|
*/
|
|
1419
1557
|
interface TacticalDrawLayerOptions {
|
|
1420
1558
|
graphics?: LayerId;
|
|
1559
|
+
/**
|
|
1560
|
+
* Passive selection-highlight outlines ({@link TacticalDraw.setHighlightedGraphics}).
|
|
1561
|
+
* Resolved immediately above `graphics` so highlight boxes stack over base
|
|
1562
|
+
* graphics but below the preview / guide / handles interaction chrome.
|
|
1563
|
+
*/
|
|
1564
|
+
highlight?: LayerId;
|
|
1421
1565
|
preview?: LayerId;
|
|
1422
1566
|
guide?: LayerId;
|
|
1423
1567
|
handles?: LayerId;
|
|
@@ -1604,6 +1748,7 @@ interface DrawOptions {
|
|
|
1604
1748
|
declare class TacticalDraw {
|
|
1605
1749
|
private readonly adapter;
|
|
1606
1750
|
private readonly graphicsLayer;
|
|
1751
|
+
private readonly highlightLayer;
|
|
1607
1752
|
private readonly previewLayer;
|
|
1608
1753
|
private readonly guideLayer;
|
|
1609
1754
|
private readonly handlesLayer;
|
|
@@ -1665,6 +1810,20 @@ declare class TacticalDraw {
|
|
|
1665
1810
|
*/
|
|
1666
1811
|
private lastRenderedMeasures;
|
|
1667
1812
|
private snapGeometryFeatures;
|
|
1813
|
+
/**
|
|
1814
|
+
* Ids of graphics currently carrying a passive selection-highlight outline
|
|
1815
|
+
* ({@link setHighlightedGraphics}). Declarative set-replace, no session —
|
|
1816
|
+
* re-projected on every view change and re-evaluated at the end of every
|
|
1817
|
+
* `render()` so outlines track geometry, style, and dropped graphics.
|
|
1818
|
+
*/
|
|
1819
|
+
private highlightedIds;
|
|
1820
|
+
/**
|
|
1821
|
+
* Whether the highlight layer currently holds any outline. Lets
|
|
1822
|
+
* `renderHighlights` skip a redundant empty write on every view change /
|
|
1823
|
+
* render while no highlights are set, but still emit the one clearing write
|
|
1824
|
+
* that removes outlines when the set transitions to empty.
|
|
1825
|
+
*/
|
|
1826
|
+
private hasHighlightOutlines;
|
|
1668
1827
|
constructor(adapter: MapAdapter, options?: TacticalDrawOptions);
|
|
1669
1828
|
/** Replace snapping configuration without recreating the map adapter or façade. */
|
|
1670
1829
|
setSnappingOptions(options?: SnappingOptions): void;
|
|
@@ -1729,6 +1888,34 @@ declare class TacticalDraw {
|
|
|
1729
1888
|
* pure style replay).
|
|
1730
1889
|
*/
|
|
1731
1890
|
render(measures: readonly Graphic[]): void;
|
|
1891
|
+
/**
|
|
1892
|
+
* Replace the passive selection-highlight set with `ids`. Declarative and
|
|
1893
|
+
* idempotent (mirrors {@link render}): a non-interactive outline box is drawn
|
|
1894
|
+
* around each still-rendered graphic on the dedicated highlight layer — no
|
|
1895
|
+
* draw / edit / transform session, no promise, no pick subscription. Passing
|
|
1896
|
+
* `[]` clears every highlight. Unknown ids and ids currently lifted into an
|
|
1897
|
+
* active edit / transform session are skipped (the live session's own visuals
|
|
1898
|
+
* supersede the passive outline). Hosts use it for Excalidraw-style
|
|
1899
|
+
* "locked selection" chrome.
|
|
1900
|
+
*/
|
|
1901
|
+
setHighlightedGraphics(ids: readonly string[]): void;
|
|
1902
|
+
/**
|
|
1903
|
+
* Project the current {@link highlightedIds} into one outline `LineString`
|
|
1904
|
+
* per graphic and reconcile them onto the highlight layer. Idempotent and
|
|
1905
|
+
* id-keyed via `adapter.updateLayerFeatures`, so a set-replace removes stale
|
|
1906
|
+
* outlines and an unchanged set is a cheap no-op write. Skips ids that are
|
|
1907
|
+
* unknown (not in the last render) or lifted into an active session, and
|
|
1908
|
+
* bails gracefully when the adapter cannot invert pixels to coordinates
|
|
1909
|
+
* (`getCoordinateFromPixel` is optional in the MapAdapter ABI).
|
|
1910
|
+
*/
|
|
1911
|
+
private renderHighlights;
|
|
1912
|
+
/**
|
|
1913
|
+
* Fit the passive-highlight transform box for one graphic: the point-symbol
|
|
1914
|
+
* footprint for symbols, the snug text footprint for Text graphics, and the
|
|
1915
|
+
* projected-geometry bounding box (at the current bearing) for every other
|
|
1916
|
+
* control measure. Returns `null` when nothing projects this frame.
|
|
1917
|
+
*/
|
|
1918
|
+
private highlightBoxForGraphic;
|
|
1732
1919
|
/**
|
|
1733
1920
|
* Nested-call guard + preempt, shared by `draw` and `edit`. Returns a
|
|
1734
1921
|
* pre-rejected promise when called re-entrantly from inside another
|
|
@@ -1892,7 +2079,11 @@ declare class TacticalDraw {
|
|
|
1892
2079
|
* then `previewLayer`, then `graphicsLayer` — against each layer's current
|
|
1893
2080
|
* `adapter.getLayerFeatures(...)` cache. The `guideLayer` is deliberately
|
|
1894
2081
|
* excluded: it is non-interactive draw-guide scaffolding, never a
|
|
1895
|
-
* legitimate "owned" click target.
|
|
2082
|
+
* legitimate "owned" click target. Within each layer, candidates follow
|
|
2083
|
+
* the shared visible-intent ranking (direct marks, then filled interiors,
|
|
2084
|
+
* then inferred outline-only reach, distance within direct marks, then
|
|
2085
|
+
* visual topmost order) — this explicit-affordance layer order is the
|
|
2086
|
+
* tier above all of it.
|
|
1896
2087
|
*
|
|
1897
2088
|
* `preview` and `graphics` each get a label text-extent fallback after the
|
|
1898
2089
|
* geometry pass misses on that layer: for every `Point` feature carrying a
|
|
@@ -1903,7 +2094,7 @@ declare class TacticalDraw {
|
|
|
1903
2094
|
* This matters most for the active preview, since a click can land
|
|
1904
2095
|
* anywhere across a rotated/offset label during an edit.
|
|
1905
2096
|
*
|
|
1906
|
-
* `tolerance` defaults to `
|
|
2097
|
+
* `tolerance` defaults to `pickHitTolerance(options?.originalEvent)` —
|
|
1907
2098
|
* pass the host's raw click event as `originalEvent` to get the same
|
|
1908
2099
|
* pointer-type-aware tolerance (4px mouse/pen, 12px touch) `onPick` uses;
|
|
1909
2100
|
* pass an explicit `tolerance` to override it outright.
|
|
@@ -1960,6 +2151,7 @@ declare class TacticalDraw {
|
|
|
1960
2151
|
/** @internal — exposed for tests and future controllers. */
|
|
1961
2152
|
get layerIds(): {
|
|
1962
2153
|
graphics: LayerId;
|
|
2154
|
+
highlight: LayerId;
|
|
1963
2155
|
preview: LayerId;
|
|
1964
2156
|
guide: LayerId;
|
|
1965
2157
|
handles: LayerId;
|
|
@@ -1971,6 +2163,7 @@ declare class TacticalDraw {
|
|
|
1971
2163
|
*/
|
|
1972
2164
|
private applyGuideStyle;
|
|
1973
2165
|
private baseGuideStyle;
|
|
2166
|
+
private baseHighlightStyle;
|
|
1974
2167
|
/**
|
|
1975
2168
|
* Resolve handle slots (plus `boxOutline`) across built-in → façade →
|
|
1976
2169
|
* per-call with per-slot, per-property shallow merge. Explicit `undefined`
|
|
@@ -2063,4 +2256,4 @@ declare function nearestWorldCopyLng(lng: number, refLng: number): number;
|
|
|
2063
2256
|
/** `nearestWorldCopyLng` applied to a lon/lat position. */
|
|
2064
2257
|
declare function nearestWorldCopyPosition(position: Position, refLng: number): Position;
|
|
2065
2258
|
//#endregion
|
|
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 };
|
|
2259
|
+
export { BaseMapAdapter, type CombinedAbort, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, type DrawMeasureDraft, type DrawOptions, type DrawPointSymbolDraft, type DrawSession, type EditChangeEvent, type EditManyOptions, type EditMode, type EditOptions, type EditPointerDriver, type EditSession, type FeatureOps, type Graphic, type GraphicEditChangeEvent, type GraphicEditSession, type GraphicGeometrySnappingOptions, type GraphicRender, type GraphicSnapshot, type GuideStyle, HATCH_PATTERNS, type HandleStyle, type HatchPattern, type IconResource, type InteractionHit, type InteractionStyle, type LabelExtent, type LayerId, type MapAdapter, type MapCursor, type MapEvent, type MapEventHandler, type MergedPickFeature, type NativeEventEntry, type PickCandidate, type PickCandidateHit, type PickCandidateHitKind, type PickCandidateInput, type PickEvaluationContext, type PickEvent, type PickHandler, type PickOptions, type PickPointerType, type PixelCoordinate, type PointSymbol, type PointSymbolAnchor, type PointSymbolCapability, PointSymbolCapabilityRequiredError, type PointSymbolColors, type PointSymbolFootprint, type PointSymbolGraphicModifiers, type PointSymbolRender, type PointSymbolSize, type PointSymbolStyle, type PointerCallback, PointerCallbackHub, type PointerEventInfo, type ResolvedPickPresentation, type RotatedScreenBounds, type ScreenBounds, type SessionChangeSource, type SessionHistory, type SessionHistoryState, type SettleReason, type SizeAnchor, type SnapCandidate, type SnapCandidateProvider, type SnapCandidateRequest, type SnapInteraction, type SnapResult, type SnappingOptions, type StyleOptions, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, type TacticalDrawAbortReason, TacticalDrawDestroyedError, type TacticalDrawLayerOptions, type TacticalDrawOptions, type TextSizeProps, type TextStyleOptions, type ToPixel, type TransformBox, type TransformChangeEvent, type TransformSession, type TransientChangeEvent, type TransientPhase, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pickPointerType, pointSymbolFootprint, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolveTextSizePixels, tryControlMeasureIdFromFeature };
|
package/dist/index.mjs
CHANGED
|
@@ -218,26 +218,17 @@ var BaseMapAdapter = class {
|
|
|
218
218
|
return this.featureCache.get(layerId) ?? [];
|
|
219
219
|
}
|
|
220
220
|
};
|
|
221
|
-
function
|
|
222
|
-
if (typeof PointerEvent !== "undefined" && originalEvent instanceof PointerEvent)
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
function pickPointerType(originalEvent) {
|
|
222
|
+
if (typeof PointerEvent !== "undefined" && originalEvent instanceof PointerEvent) {
|
|
223
|
+
if (originalEvent.pointerType === "touch") return "touch";
|
|
224
|
+
return originalEvent.pointerType === "pen" ? "pen" : "mouse";
|
|
225
|
+
}
|
|
226
|
+
if (typeof TouchEvent !== "undefined" && originalEvent instanceof TouchEvent) return "touch";
|
|
227
|
+
if (originalEvent && typeof originalEvent === "object" && "originalEvent" in originalEvent) return pickPointerType(originalEvent.originalEvent);
|
|
228
|
+
return "mouse";
|
|
225
229
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* `MapAdapter.on(...)` pipeline. Unlike {@link PickEvent}, that pipeline forwards
|
|
229
|
-
* the *engine wrapper* event (OpenLayers `MapBrowserEvent`, MapLibre
|
|
230
|
-
* `MapMouseEvent`, Leaflet `LeafletMouseEvent`), each of which carries the raw
|
|
231
|
-
* DOM event on a `.originalEvent` property. {@link pickHitTolerance} only
|
|
232
|
-
* recognizes a real `PointerEvent` / `TouchEvent`, so unwrap one level when the
|
|
233
|
-
* wrapper itself is not recognized as touch. Falls back to the mouse tolerance
|
|
234
|
-
* when the input type cannot be determined.
|
|
235
|
-
*/
|
|
236
|
-
function mapEventHitTolerance(originalEvent) {
|
|
237
|
-
const direct = pickHitTolerance(originalEvent);
|
|
238
|
-
if (direct === 12) return direct;
|
|
239
|
-
if (originalEvent && typeof originalEvent === "object" && "originalEvent" in originalEvent) return pickHitTolerance(originalEvent.originalEvent);
|
|
240
|
-
return direct;
|
|
230
|
+
function pickHitTolerance(originalEvent) {
|
|
231
|
+
return pickPointerType(originalEvent) === "touch" ? 12 : 4;
|
|
241
232
|
}
|
|
242
233
|
function tryControlMeasureIdFromFeature(feature) {
|
|
243
234
|
const graphicId = feature.properties?.graphicId;
|
|
@@ -268,6 +259,392 @@ function bindAbortable(signal, attach) {
|
|
|
268
259
|
return dispose;
|
|
269
260
|
}
|
|
270
261
|
//#endregion
|
|
262
|
+
//#region src/pick-candidates.ts
|
|
263
|
+
/**
|
|
264
|
+
* The `visualOrder` for the feature at `index` of a render-ordered
|
|
265
|
+
* (bottom-first) list of `total` features: reversing the index makes the
|
|
266
|
+
* topmost-rendered feature the lowest, and therefore winning, order.
|
|
267
|
+
*/
|
|
268
|
+
function renderOrderToVisualOrder(index, total) {
|
|
269
|
+
return total - 1 - index;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Preserve native visual ordering/hit evidence while keeping cached source
|
|
273
|
+
* geometry authoritative. Native polygon geometry may be tile-clipped and
|
|
274
|
+
* therefore contain artificial edges that are not part of the graphic.
|
|
275
|
+
*
|
|
276
|
+
* Normalizes to the shared `visualOrder` contract (lower = visually
|
|
277
|
+
* topmost): native hits already arrive topmost-first from the engines and
|
|
278
|
+
* keep that order at the front. Cached-only features (not among the native
|
|
279
|
+
* hits) are appended after them, in reverse cache order — cache arrays are
|
|
280
|
+
* render order (bottom-first), so reversing yields topmost-first — with
|
|
281
|
+
* `visualOrder` numbering continuing on from the native section.
|
|
282
|
+
*/
|
|
283
|
+
function mergeNativeAndCachedPickFeatures(nativeFeatures, cachedFeatures) {
|
|
284
|
+
const cachedById = /* @__PURE__ */ new Map();
|
|
285
|
+
for (const feature of cachedFeatures) if (feature.id != null) cachedById.set(feature.id, feature);
|
|
286
|
+
const merged = [];
|
|
287
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
288
|
+
for (const nativeFeature of nativeFeatures) {
|
|
289
|
+
if (nativeFeature.id != null && seenIds.has(nativeFeature.id)) continue;
|
|
290
|
+
const feature = nativeFeature.id == null ? nativeFeature : cachedById.get(nativeFeature.id) ?? nativeFeature;
|
|
291
|
+
merged.push({
|
|
292
|
+
feature,
|
|
293
|
+
visualOrder: merged.length,
|
|
294
|
+
nativeHit: true
|
|
295
|
+
});
|
|
296
|
+
if (nativeFeature.id != null) seenIds.add(nativeFeature.id);
|
|
297
|
+
}
|
|
298
|
+
for (let i = cachedFeatures.length - 1; i >= 0; i--) {
|
|
299
|
+
const feature = cachedFeatures[i];
|
|
300
|
+
if (feature.id != null && seenIds.has(feature.id)) continue;
|
|
301
|
+
merged.push({
|
|
302
|
+
feature,
|
|
303
|
+
visualOrder: merged.length,
|
|
304
|
+
nativeHit: false
|
|
305
|
+
});
|
|
306
|
+
if (feature.id != null) seenIds.add(feature.id);
|
|
307
|
+
}
|
|
308
|
+
return merged;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Evaluate every relevant rendered part in screen space, then return the
|
|
312
|
+
* primary candidate under the visible-intent ranking policy: direct visible
|
|
313
|
+
* marks (strokes, points, icons, labels) outrank visibly filled interiors,
|
|
314
|
+
* which outrank inferred outline-only reach; competing direct marks rank by
|
|
315
|
+
* screen-space distance; visual topmost order is only the final tie-break.
|
|
316
|
+
* See `rankPickCandidates` for the full ordered list — that ordering stays
|
|
317
|
+
* internal adapter-author plumbing; the public pick event exposes only this
|
|
318
|
+
* primary candidate.
|
|
319
|
+
*/
|
|
320
|
+
function evaluatePickCandidates(inputs, context) {
|
|
321
|
+
let best = null;
|
|
322
|
+
for (const input of inputs) {
|
|
323
|
+
const candidate = buildCandidate(input, context);
|
|
324
|
+
if (candidate && (best === null || compareCandidates(candidate, best) < 0)) best = candidate;
|
|
325
|
+
}
|
|
326
|
+
return best;
|
|
327
|
+
}
|
|
328
|
+
function buildCandidate(input, context) {
|
|
329
|
+
const presentation = input.presentation ?? resolvePickPresentation(input.feature, input.layerStyle);
|
|
330
|
+
const evaluated = evaluateInput(input, presentation, context);
|
|
331
|
+
if (!evaluated) return null;
|
|
332
|
+
return {
|
|
333
|
+
feature: input.feature,
|
|
334
|
+
graphicId: tryControlMeasureIdFromFeature(input.feature),
|
|
335
|
+
renderedPartId: typeof input.feature.id === "string" || typeof input.feature.id === "number" ? input.feature.id : null,
|
|
336
|
+
hit: evaluated.hit,
|
|
337
|
+
viewport: context.viewport,
|
|
338
|
+
presentation,
|
|
339
|
+
visualOrder: input.visualOrder,
|
|
340
|
+
...evaluated.iconFootprint ? { iconFootprint: evaluated.iconFootprint } : {},
|
|
341
|
+
...input.labelExtent ? { labelExtent: input.labelExtent } : {}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function compareCandidates(a, b) {
|
|
345
|
+
const rankA = selectionSurfaceRank(a);
|
|
346
|
+
const rankDifference = rankA - selectionSurfaceRank(b);
|
|
347
|
+
if (rankDifference !== 0) return rankDifference;
|
|
348
|
+
if (rankA === 0 && a.hit.distance !== null && b.hit.distance !== null) {
|
|
349
|
+
const distanceDifference = a.hit.distance - b.hit.distance;
|
|
350
|
+
if (distanceDifference !== 0) return distanceDifference;
|
|
351
|
+
}
|
|
352
|
+
return a.visualOrder - b.visualOrder;
|
|
353
|
+
}
|
|
354
|
+
function selectionSurfaceRank(candidate) {
|
|
355
|
+
if (candidate.hit.kind !== "polygon-interior") return 0;
|
|
356
|
+
return (candidate.presentation.fill?.opacity ?? 0) > 0 ? 1 : 2;
|
|
357
|
+
}
|
|
358
|
+
function resolvePickPresentation(feature, layerStyle) {
|
|
359
|
+
const featureStyle = asRecord(feature.properties?.style);
|
|
360
|
+
const strokeColor = stringValue(featureStyle?.strokeColor) ?? layerStyle?.strokeColor;
|
|
361
|
+
const strokeWidth = numberValue(featureStyle?.strokeWidth) ?? layerStyle?.strokeWidth;
|
|
362
|
+
const strokeDash = numberArray(featureStyle?.strokeDash) ?? layerStyle?.strokeDash;
|
|
363
|
+
const fillColor = stringValue(featureStyle?.fillColor) ?? layerStyle?.fillColor;
|
|
364
|
+
const fillPattern = stringValue(featureStyle?.fillPattern);
|
|
365
|
+
const pointFillColor = stringValue(featureStyle?.pointFillColor) ?? stringValue(featureStyle?.fillColor) ?? layerStyle?.pointFillColor ?? layerStyle?.fillColor;
|
|
366
|
+
const pointRadius = numberValue(featureStyle?.pointRadius) ?? layerStyle?.pointRadius;
|
|
367
|
+
const presentation = {};
|
|
368
|
+
if (strokeColor !== void 0 || strokeWidth !== void 0 || strokeDash !== void 0) presentation.stroke = {
|
|
369
|
+
...strokeColor !== void 0 ? { color: strokeColor } : {},
|
|
370
|
+
...strokeWidth !== void 0 ? { width: strokeWidth } : {},
|
|
371
|
+
...strokeDash !== void 0 ? { dash: strokeDash } : {}
|
|
372
|
+
};
|
|
373
|
+
if (fillColor !== void 0 || fillPattern !== void 0) presentation.fill = {
|
|
374
|
+
...fillColor !== void 0 ? { color: fillColor } : {},
|
|
375
|
+
...fillPattern !== void 0 ? { pattern: fillPattern } : {},
|
|
376
|
+
opacity: colorOpacity(fillColor)
|
|
377
|
+
};
|
|
378
|
+
if (feature.geometry.type === "Point" && (pointFillColor !== void 0 || pointRadius !== void 0)) presentation.point = {
|
|
379
|
+
...pointFillColor !== void 0 ? { fillColor: pointFillColor } : {},
|
|
380
|
+
...pointRadius !== void 0 ? { radius: pointRadius } : {}
|
|
381
|
+
};
|
|
382
|
+
if (layerStyle?.text !== void 0) presentation.text = { ...layerStyle.text };
|
|
383
|
+
return presentation;
|
|
384
|
+
}
|
|
385
|
+
function evaluateInput(input, presentation, context) {
|
|
386
|
+
if (input.labelExtent && pointInExtent(context.pixel, input.labelExtent)) return { hit: {
|
|
387
|
+
kind: "label",
|
|
388
|
+
distance: 0
|
|
389
|
+
} };
|
|
390
|
+
if (input.icon) return evaluateIcon(input.feature, input.icon, context);
|
|
391
|
+
const geometryHit = evaluateGeometry(input.feature, presentation, context);
|
|
392
|
+
if (geometryHit) return { hit: geometryHit };
|
|
393
|
+
if (input.feature.geometry.type === "Polygon" || input.feature.geometry.type === "MultiPolygon") return null;
|
|
394
|
+
return input.nativeHit ? { hit: {
|
|
395
|
+
kind: inferNativeHitKind(input.feature),
|
|
396
|
+
distance: null
|
|
397
|
+
} } : null;
|
|
398
|
+
}
|
|
399
|
+
function inferNativeHitKind(feature) {
|
|
400
|
+
if (typeof feature.properties?.iconId === "string") return "icon";
|
|
401
|
+
if (typeof feature.properties?.text === "string") return "label";
|
|
402
|
+
if (feature.geometry.type === "Point") return "point";
|
|
403
|
+
if (feature.geometry.type === "LineString" || feature.geometry.type === "MultiLineString") return "line";
|
|
404
|
+
return "native";
|
|
405
|
+
}
|
|
406
|
+
function evaluateGeometry(feature, presentation, context) {
|
|
407
|
+
const geometry = feature.geometry;
|
|
408
|
+
const { pixel, tolerance, toPixel } = context;
|
|
409
|
+
if (geometry.type === "Point") {
|
|
410
|
+
const projected = toPixel(geometry.coordinates);
|
|
411
|
+
if (!projected) return null;
|
|
412
|
+
const distance = pointDistance(pixel, projected);
|
|
413
|
+
return distance <= tolerance ? {
|
|
414
|
+
kind: "point",
|
|
415
|
+
distance
|
|
416
|
+
} : null;
|
|
417
|
+
}
|
|
418
|
+
if (geometry.type === "LineString") {
|
|
419
|
+
const distance = distanceToLine(pixel, geometry.coordinates, toPixel);
|
|
420
|
+
return distance !== null && distance <= tolerance ? {
|
|
421
|
+
kind: "line",
|
|
422
|
+
distance
|
|
423
|
+
} : null;
|
|
424
|
+
}
|
|
425
|
+
if (geometry.type === "MultiLineString") {
|
|
426
|
+
const distance = minimum(geometry.coordinates.map((line) => distanceToLine(pixel, line, toPixel)));
|
|
427
|
+
return distance !== null && distance <= tolerance ? {
|
|
428
|
+
kind: "line",
|
|
429
|
+
distance
|
|
430
|
+
} : null;
|
|
431
|
+
}
|
|
432
|
+
if (geometry.type === "Polygon") return evaluatePolygon(pixel, geometry.coordinates, presentation, context);
|
|
433
|
+
if (geometry.type === "MultiPolygon") return bestPolygonHit(geometry.coordinates.map((polygon) => evaluatePolygon(pixel, polygon, presentation, context)).filter((hit) => hit !== null));
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
function evaluatePolygon(pixel, rings, presentation, context) {
|
|
437
|
+
const { tolerance, toPixel, viewport } = context;
|
|
438
|
+
const projectedRings = rings.map((ring) => ring.map(toPixel));
|
|
439
|
+
const distance = hasVisibleBoundary(presentation) ? minimum(projectedRings.map((points) => minVisibleSegmentDistance(pixel, points, viewport))) : null;
|
|
440
|
+
if (distance !== null && distance <= tolerance) return {
|
|
441
|
+
kind: "polygon-boundary",
|
|
442
|
+
distance
|
|
443
|
+
};
|
|
444
|
+
if (!(projectedRings.length > 0 && pointInProjectedRing(pixel, projectedRings[0]) && !projectedRings.slice(1).some((points) => pointInProjectedRing(pixel, points)))) return null;
|
|
445
|
+
const visiblyFilled = (presentation.fill?.opacity ?? 0) > 0;
|
|
446
|
+
const inwardReach = context.pointerType === "touch" ? 36 : 24;
|
|
447
|
+
if (visiblyFilled || distance !== null && distance <= inwardReach) return {
|
|
448
|
+
kind: "polygon-interior",
|
|
449
|
+
distance
|
|
450
|
+
};
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
function bestPolygonHit(hits) {
|
|
454
|
+
if (hits.length === 0) return null;
|
|
455
|
+
const boundary = hits.filter((hit) => hit.kind === "polygon-boundary");
|
|
456
|
+
if (boundary.length > 0) return boundary.reduce((best, hit) => (hit.distance ?? Infinity) < (best.distance ?? Infinity) ? hit : best);
|
|
457
|
+
return hits[0];
|
|
458
|
+
}
|
|
459
|
+
function evaluateIcon(feature, resource, context) {
|
|
460
|
+
if (feature.geometry.type !== "Point") return null;
|
|
461
|
+
const anchor = context.toPixel(feature.geometry.coordinates);
|
|
462
|
+
if (!anchor) return null;
|
|
463
|
+
const rawScale = feature.properties?.iconScale;
|
|
464
|
+
const scale = typeof rawScale === "number" ? rawScale : 1;
|
|
465
|
+
if (!Number.isFinite(scale) || scale <= 0) return null;
|
|
466
|
+
const rawRotation = feature.properties?.iconRotation;
|
|
467
|
+
const angle = (typeof rawRotation === "number" && Number.isFinite(rawRotation) ? rawRotation : 0) - (context.bearing ?? 0);
|
|
468
|
+
const cos = Math.cos(angle);
|
|
469
|
+
const sin = Math.sin(angle);
|
|
470
|
+
const minX = -resource.anchor[0] * scale;
|
|
471
|
+
const maxX = (resource.width - resource.anchor[0]) * scale;
|
|
472
|
+
const minY = -resource.anchor[1] * scale;
|
|
473
|
+
const maxY = (resource.height - resource.anchor[1]) * scale;
|
|
474
|
+
const dx = context.pixel[0] - anchor[0];
|
|
475
|
+
const dy = context.pixel[1] - anchor[1];
|
|
476
|
+
const localX = dx * cos + dy * sin;
|
|
477
|
+
const localY = -dx * sin + dy * cos;
|
|
478
|
+
const outsideX = Math.max(minX - localX, 0, localX - maxX);
|
|
479
|
+
const outsideY = Math.max(minY - localY, 0, localY - maxY);
|
|
480
|
+
if (outsideX > context.tolerance || outsideY > context.tolerance) return null;
|
|
481
|
+
const coreRect = resource.coreRect ?? {
|
|
482
|
+
x: 0,
|
|
483
|
+
y: 0,
|
|
484
|
+
width: resource.width,
|
|
485
|
+
height: resource.height
|
|
486
|
+
};
|
|
487
|
+
const coreMinX = (coreRect.x - resource.anchor[0]) * scale;
|
|
488
|
+
const coreMaxX = (coreRect.x + coreRect.width - resource.anchor[0]) * scale;
|
|
489
|
+
const coreMinY = (coreRect.y - resource.anchor[1]) * scale;
|
|
490
|
+
const coreMaxY = (coreRect.y + coreRect.height - resource.anchor[1]) * scale;
|
|
491
|
+
const coreOutsideX = Math.max(coreMinX - localX, 0, localX - coreMaxX);
|
|
492
|
+
const coreOutsideY = Math.max(coreMinY - localY, 0, localY - coreMaxY);
|
|
493
|
+
const distance = Math.hypot(coreOutsideX, coreOutsideY);
|
|
494
|
+
const corner = (x, y) => [anchor[0] + x * cos - y * sin, anchor[1] + x * sin + y * cos];
|
|
495
|
+
return {
|
|
496
|
+
hit: {
|
|
497
|
+
kind: "icon",
|
|
498
|
+
distance
|
|
499
|
+
},
|
|
500
|
+
iconFootprint: [
|
|
501
|
+
corner(minX, minY),
|
|
502
|
+
corner(maxX, minY),
|
|
503
|
+
corner(maxX, maxY),
|
|
504
|
+
corner(minX, maxY)
|
|
505
|
+
]
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function distanceToLine(pixel, coordinates, toPixel) {
|
|
509
|
+
return minSegmentDistance(pixel, coordinates.map(toPixel));
|
|
510
|
+
}
|
|
511
|
+
/** Nearest distance from `pixel` to any segment of a pre-projected polyline. */
|
|
512
|
+
function minSegmentDistance(pixel, points) {
|
|
513
|
+
let closest = null;
|
|
514
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
515
|
+
const a = points[i];
|
|
516
|
+
const b = points[i + 1];
|
|
517
|
+
if (!a || !b) continue;
|
|
518
|
+
const distance = distanceToSegment(pixel, a, b);
|
|
519
|
+
if (distance !== null && (closest === null || distance < closest)) closest = distance;
|
|
520
|
+
}
|
|
521
|
+
return closest;
|
|
522
|
+
}
|
|
523
|
+
/** As `minSegmentDistance`, but each segment is first clipped to `viewport`. */
|
|
524
|
+
function minVisibleSegmentDistance(pixel, points, viewport) {
|
|
525
|
+
let closest = null;
|
|
526
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
527
|
+
const a = points[i];
|
|
528
|
+
const b = points[i + 1];
|
|
529
|
+
if (!a || !b) continue;
|
|
530
|
+
const visible = clipSegmentToBounds(a, b, viewport);
|
|
531
|
+
if (!visible) continue;
|
|
532
|
+
const [visibleA, visibleB] = visible;
|
|
533
|
+
const distance = visibleA[0] === visibleB[0] && visibleA[1] === visibleB[1] ? pointDistance(pixel, visibleA) : distanceToSegment(pixel, visibleA, visibleB);
|
|
534
|
+
if (distance !== null && (closest === null || distance < closest)) closest = distance;
|
|
535
|
+
}
|
|
536
|
+
return closest;
|
|
537
|
+
}
|
|
538
|
+
function clipSegmentToBounds(a, b, bounds) {
|
|
539
|
+
const dx = b[0] - a[0];
|
|
540
|
+
const dy = b[1] - a[1];
|
|
541
|
+
let start = 0;
|
|
542
|
+
let end = 1;
|
|
543
|
+
const edges = [
|
|
544
|
+
[-dx, a[0] - bounds.minX],
|
|
545
|
+
[dx, bounds.maxX - a[0]],
|
|
546
|
+
[-dy, a[1] - bounds.minY],
|
|
547
|
+
[dy, bounds.maxY - a[1]]
|
|
548
|
+
];
|
|
549
|
+
for (const [direction, offset] of edges) {
|
|
550
|
+
if (direction === 0) {
|
|
551
|
+
if (offset < 0) return null;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const ratio = offset / direction;
|
|
555
|
+
if (direction < 0) start = Math.max(start, ratio);
|
|
556
|
+
else end = Math.min(end, ratio);
|
|
557
|
+
if (start > end) return null;
|
|
558
|
+
}
|
|
559
|
+
return [[a[0] + start * dx, a[1] + start * dy], [a[0] + end * dx, a[1] + end * dy]];
|
|
560
|
+
}
|
|
561
|
+
function distanceToSegment(pixel, a, b) {
|
|
562
|
+
const dx = b[0] - a[0];
|
|
563
|
+
const dy = b[1] - a[1];
|
|
564
|
+
const lengthSq = dx * dx + dy * dy;
|
|
565
|
+
if (lengthSq === 0) return null;
|
|
566
|
+
const unclamped = ((pixel[0] - a[0]) * dx + (pixel[1] - a[1]) * dy) / lengthSq;
|
|
567
|
+
const t = Math.max(0, Math.min(1, unclamped));
|
|
568
|
+
return Math.hypot(pixel[0] - (a[0] + t * dx), pixel[1] - (a[1] + t * dy));
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Even-odd point-in-ring test over a pre-projected ring. Any vertex that
|
|
572
|
+
* failed to project makes the whole ring inconclusive, matching the prior
|
|
573
|
+
* "project every vertex or bail" contract.
|
|
574
|
+
*/
|
|
575
|
+
function pointInProjectedRing(pixel, points) {
|
|
576
|
+
let inside = false;
|
|
577
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
578
|
+
const pi = points[i];
|
|
579
|
+
const pj = points[j];
|
|
580
|
+
if (!pi || !pj) return false;
|
|
581
|
+
const [xi, yi] = pi;
|
|
582
|
+
const [xj, yj] = pj;
|
|
583
|
+
if (yi > pixel[1] !== yj > pixel[1] && pixel[0] < (xj - xi) * (pixel[1] - yi) / (yj - yi) + xi) inside = !inside;
|
|
584
|
+
}
|
|
585
|
+
return inside;
|
|
586
|
+
}
|
|
587
|
+
function pointInExtent(pixel, extent) {
|
|
588
|
+
if ("minX" in extent) return pixel[0] >= extent.minX && pixel[0] <= extent.maxX && pixel[1] >= extent.minY && pixel[1] <= extent.maxY;
|
|
589
|
+
const dx = pixel[0] - extent.center[0];
|
|
590
|
+
const dy = pixel[1] - extent.center[1];
|
|
591
|
+
const cos = Math.cos(-extent.angle);
|
|
592
|
+
const sin = Math.sin(-extent.angle);
|
|
593
|
+
const localX = dx * cos - dy * sin;
|
|
594
|
+
const localY = dx * sin + dy * cos;
|
|
595
|
+
return Math.abs(localX) <= extent.halfWidth && Math.abs(localY) <= extent.halfHeight;
|
|
596
|
+
}
|
|
597
|
+
function pointDistance(a, b) {
|
|
598
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
599
|
+
}
|
|
600
|
+
function minimum(values) {
|
|
601
|
+
let result = null;
|
|
602
|
+
for (const value of values) if (value !== null && (result === null || value < result)) result = value;
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
function colorOpacity(color) {
|
|
606
|
+
if (color === void 0) return 0;
|
|
607
|
+
const normalized = color.trim().toLowerCase();
|
|
608
|
+
if (normalized === "transparent") return 0;
|
|
609
|
+
if (/^#[0-9a-f]{4}$/i.test(normalized)) return parseInt(normalized[4], 16) / 15;
|
|
610
|
+
if (/^#[0-9a-f]{8}$/i.test(normalized)) return parseInt(normalized.slice(7, 9), 16) / 255;
|
|
611
|
+
const functional = normalized.match(/^[a-z]+\((.*)\)$/);
|
|
612
|
+
if (functional) {
|
|
613
|
+
const body = functional[1];
|
|
614
|
+
const slashAlpha = body.match(/\/\s*([+-]?(?:\d*\.)?\d+)(%)?\s*$/);
|
|
615
|
+
if (slashAlpha) return normalizedAlpha(slashAlpha[1], slashAlpha[2] === "%");
|
|
616
|
+
const commaParts = body.split(",").map((part) => part.trim());
|
|
617
|
+
if (commaParts.length === 4) {
|
|
618
|
+
const alpha = commaParts[3].match(/^([+-]?(?:\d*\.)?\d+)(%)?$/);
|
|
619
|
+
if (alpha) return normalizedAlpha(alpha[1], alpha[2] === "%");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return 1;
|
|
623
|
+
}
|
|
624
|
+
function normalizedAlpha(value, percentage) {
|
|
625
|
+
const alpha = Number(value);
|
|
626
|
+
if (!Number.isFinite(alpha)) return 1;
|
|
627
|
+
return Math.max(0, Math.min(1, percentage ? alpha / 100 : alpha));
|
|
628
|
+
}
|
|
629
|
+
function hasVisibleBoundary(presentation) {
|
|
630
|
+
const stroke = presentation.stroke;
|
|
631
|
+
if (!stroke || stroke.color === void 0) return false;
|
|
632
|
+
if (stroke.width !== void 0 && stroke.width <= 0) return false;
|
|
633
|
+
return colorOpacity(stroke.color) > 0;
|
|
634
|
+
}
|
|
635
|
+
function asRecord(value) {
|
|
636
|
+
return value !== null && typeof value === "object" ? value : void 0;
|
|
637
|
+
}
|
|
638
|
+
function stringValue(value) {
|
|
639
|
+
return typeof value === "string" ? value : void 0;
|
|
640
|
+
}
|
|
641
|
+
function numberValue(value) {
|
|
642
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
643
|
+
}
|
|
644
|
+
function numberArray(value) {
|
|
645
|
+
return Array.isArray(value) && value.every((item) => typeof item === "number") ? [...value] : void 0;
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
271
648
|
//#region src/feature-style.ts
|
|
272
649
|
/**
|
|
273
650
|
* Coerce a raw `properties.style` value into a `HandleStyle` for the
|
|
@@ -334,41 +711,6 @@ function clampedSegmentParameter(pixel, a, b) {
|
|
|
334
711
|
const t = ((pixel[0] - a[0]) * dx + (pixel[1] - a[1]) * dy) / lengthSq;
|
|
335
712
|
return Math.max(0, Math.min(1, t));
|
|
336
713
|
}
|
|
337
|
-
/** Distance from `pixel` to the segment p1→p2 in pixel space, or null if either endpoint fails to project or the segment has zero length. */
|
|
338
|
-
function pixelDistanceToSegment(pixel, p1, p2, toPixel) {
|
|
339
|
-
const a = toPixel(p1);
|
|
340
|
-
const b = toPixel(p2);
|
|
341
|
-
if (!a || !b) return null;
|
|
342
|
-
const t = clampedSegmentParameter(pixel, a, b);
|
|
343
|
-
if (t === null) return null;
|
|
344
|
-
const ddx = pixel[0] - (a[0] + t * (b[0] - a[0]));
|
|
345
|
-
const ddy = pixel[1] - (a[1] + t * (b[1] - a[1]));
|
|
346
|
-
return Math.sqrt(ddx * ddx + ddy * ddy);
|
|
347
|
-
}
|
|
348
|
-
/** Even-odd point-in-ring test in pixel space. */
|
|
349
|
-
function pixelPointInRing(pixel, ring, toPixel) {
|
|
350
|
-
const projected = [];
|
|
351
|
-
for (const c of ring) {
|
|
352
|
-
const p = toPixel(c);
|
|
353
|
-
if (!p) return false;
|
|
354
|
-
projected.push(p);
|
|
355
|
-
}
|
|
356
|
-
const [px, py] = pixel;
|
|
357
|
-
let inside = false;
|
|
358
|
-
for (let i = 0, j = projected.length - 1; i < projected.length; j = i++) {
|
|
359
|
-
const xi = projected[i][0], yi = projected[i][1];
|
|
360
|
-
const xj = projected[j][0], yj = projected[j][1];
|
|
361
|
-
if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) inside = !inside;
|
|
362
|
-
}
|
|
363
|
-
return inside;
|
|
364
|
-
}
|
|
365
|
-
function isNearAnySegment(pixel, ring, hitTolerance, toPixel) {
|
|
366
|
-
for (let i = 0; i < ring.length - 1; i++) {
|
|
367
|
-
const d = pixelDistanceToSegment(pixel, ring[i], ring[i + 1], toPixel);
|
|
368
|
-
if (d !== null && d <= hitTolerance) return true;
|
|
369
|
-
}
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
714
|
/**
|
|
373
715
|
* Test whether `pixel` hits any feature in `features`, in pixel space.
|
|
374
716
|
*
|
|
@@ -381,23 +723,28 @@ function isNearAnySegment(pixel, ring, hitTolerance, toPixel) {
|
|
|
381
723
|
* Returns the first matching feature in iteration order, or null.
|
|
382
724
|
*/
|
|
383
725
|
function hitTestFeature(pixel, features, hitTolerance, toPixel) {
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
726
|
+
for (const feature of features) {
|
|
727
|
+
if (!feature) continue;
|
|
728
|
+
const candidate = evaluatePickCandidates([{
|
|
729
|
+
feature,
|
|
730
|
+
visualOrder: 0,
|
|
731
|
+
...feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon" ? { presentation: {
|
|
732
|
+
stroke: {
|
|
733
|
+
color: "#000",
|
|
734
|
+
width: 1
|
|
735
|
+
},
|
|
736
|
+
fill: {
|
|
737
|
+
color: "#000",
|
|
738
|
+
opacity: 1
|
|
739
|
+
}
|
|
740
|
+
} } : {}
|
|
741
|
+
}], {
|
|
742
|
+
pixel,
|
|
743
|
+
tolerance: hitTolerance,
|
|
744
|
+
toPixel,
|
|
745
|
+
viewport: UNBOUNDED_VIEWPORT
|
|
746
|
+
});
|
|
747
|
+
if (candidate) return candidate.feature;
|
|
401
748
|
}
|
|
402
749
|
return null;
|
|
403
750
|
}
|
|
@@ -408,26 +755,24 @@ function hitTestFeature(pixel, features, hitTolerance, toPixel) {
|
|
|
408
755
|
* unscaled SVG viewport. A positive tolerance pads all four sides.
|
|
409
756
|
*/
|
|
410
757
|
function hitTestIconFeature(pixel, feature, resource, hitTolerance, toPixel, bearing = 0) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const dy = pixel[1] - anchor[1];
|
|
423
|
-
const localX = dx * cos + dy * sin;
|
|
424
|
-
const localY = -dx * sin + dy * cos;
|
|
425
|
-
const minX = -resource.anchor[0] * scale - hitTolerance;
|
|
426
|
-
const maxX = (resource.width - resource.anchor[0]) * scale + hitTolerance;
|
|
427
|
-
const minY = -resource.anchor[1] * scale - hitTolerance;
|
|
428
|
-
const maxY = (resource.height - resource.anchor[1]) * scale + hitTolerance;
|
|
429
|
-
return localX >= minX && localX <= maxX && localY >= minY && localY <= maxY;
|
|
758
|
+
return evaluatePickCandidates([{
|
|
759
|
+
feature,
|
|
760
|
+
visualOrder: 0,
|
|
761
|
+
icon: resource
|
|
762
|
+
}], {
|
|
763
|
+
pixel,
|
|
764
|
+
tolerance: hitTolerance,
|
|
765
|
+
toPixel,
|
|
766
|
+
viewport: UNBOUNDED_VIEWPORT,
|
|
767
|
+
bearing
|
|
768
|
+
}) !== null;
|
|
430
769
|
}
|
|
770
|
+
const UNBOUNDED_VIEWPORT = {
|
|
771
|
+
minX: Number.NEGATIVE_INFINITY,
|
|
772
|
+
minY: Number.NEGATIVE_INFINITY,
|
|
773
|
+
maxX: Number.POSITIVE_INFINITY,
|
|
774
|
+
maxY: Number.POSITIVE_INFINITY
|
|
775
|
+
};
|
|
431
776
|
//#endregion
|
|
432
777
|
//#region src/gestures/hit-tolerance.ts
|
|
433
778
|
const TOUCH_HIT_TOLERANCE_PX = 22;
|
|
@@ -1809,7 +2154,7 @@ function runVariableLengthDraw(options) {
|
|
|
1809
2154
|
if (lastSpineIdx >= 0 && canCommitNow()) for (const finishIndex of finishIndices) {
|
|
1810
2155
|
const finishPixel = adapter.getPixelFromCoordinate(committed[finishIndex]);
|
|
1811
2156
|
if (!finishPixel) continue;
|
|
1812
|
-
const tol =
|
|
2157
|
+
const tol = pickHitTolerance(event.originalEvent);
|
|
1813
2158
|
const dx = event.pixel[0] - finishPixel[0];
|
|
1814
2159
|
const dy = event.pixel[1] - finishPixel[1];
|
|
1815
2160
|
if (dx * dx + dy * dy <= tol * tol) {
|
|
@@ -4363,7 +4708,13 @@ function renderPointSymbolForAdapter(adapter, capability, symbol) {
|
|
|
4363
4708
|
svg: render.svg,
|
|
4364
4709
|
width: render.width,
|
|
4365
4710
|
height: render.height,
|
|
4366
|
-
anchor: [render.anchor.x, render.anchor.y]
|
|
4711
|
+
anchor: [render.anchor.x, render.anchor.y],
|
|
4712
|
+
coreRect: {
|
|
4713
|
+
x: render.octagonAnchor.x - render.octagonSize / 2,
|
|
4714
|
+
y: render.octagonAnchor.y - render.octagonSize / 2,
|
|
4715
|
+
width: render.octagonSize,
|
|
4716
|
+
height: render.octagonSize
|
|
4717
|
+
}
|
|
4367
4718
|
},
|
|
4368
4719
|
feature: pointSymbolFeature(symbol, render, adapter.getResolution())
|
|
4369
4720
|
};
|
|
@@ -5492,6 +5843,7 @@ function runPointSymbolEdit(opts) {
|
|
|
5492
5843
|
var TacticalDraw = class {
|
|
5493
5844
|
adapter;
|
|
5494
5845
|
graphicsLayer;
|
|
5846
|
+
highlightLayer;
|
|
5495
5847
|
previewLayer;
|
|
5496
5848
|
guideLayer;
|
|
5497
5849
|
handlesLayer;
|
|
@@ -5553,6 +5905,20 @@ var TacticalDraw = class {
|
|
|
5553
5905
|
*/
|
|
5554
5906
|
lastRenderedMeasures = [];
|
|
5555
5907
|
snapGeometryFeatures = [];
|
|
5908
|
+
/**
|
|
5909
|
+
* Ids of graphics currently carrying a passive selection-highlight outline
|
|
5910
|
+
* ({@link setHighlightedGraphics}). Declarative set-replace, no session —
|
|
5911
|
+
* re-projected on every view change and re-evaluated at the end of every
|
|
5912
|
+
* `render()` so outlines track geometry, style, and dropped graphics.
|
|
5913
|
+
*/
|
|
5914
|
+
highlightedIds = [];
|
|
5915
|
+
/**
|
|
5916
|
+
* Whether the highlight layer currently holds any outline. Lets
|
|
5917
|
+
* `renderHighlights` skip a redundant empty write on every view change /
|
|
5918
|
+
* render while no highlights are set, but still emit the one clearing write
|
|
5919
|
+
* that removes outlines when the set transitions to empty.
|
|
5920
|
+
*/
|
|
5921
|
+
hasHighlightOutlines = false;
|
|
5556
5922
|
constructor(adapter, options) {
|
|
5557
5923
|
this.adapter = adapter;
|
|
5558
5924
|
this.graphicsStyle = resolveStyleHints(BUILT_IN_GRAPHICS_STYLE, options?.graphicsStyle, void 0);
|
|
@@ -5564,6 +5930,7 @@ var TacticalDraw = class {
|
|
|
5564
5930
|
const owned = /* @__PURE__ */ new Set();
|
|
5565
5931
|
const provided = options?.layers ?? {};
|
|
5566
5932
|
this.graphicsLayer = resolveSlot(adapter, provided.graphics, owned);
|
|
5933
|
+
this.highlightLayer = resolveSlot(adapter, provided.highlight, owned);
|
|
5567
5934
|
this.previewLayer = resolveSlot(adapter, provided.preview, owned);
|
|
5568
5935
|
this.guideLayer = resolveSlot(adapter, provided.guide, owned);
|
|
5569
5936
|
this.handlesLayer = resolveSlot(adapter, provided.handles, owned);
|
|
@@ -5572,6 +5939,7 @@ var TacticalDraw = class {
|
|
|
5572
5939
|
this.adapter.setLayerStyle(this.graphicsLayer, graphicsLayerStyle);
|
|
5573
5940
|
this.adapter.setLayerStyle(this.previewLayer, graphicsLayerStyle);
|
|
5574
5941
|
this.adapter.setLayerStyle(this.guideLayer, guideToStyleOptions(this.baseGuideStyle()));
|
|
5942
|
+
this.adapter.setLayerStyle(this.highlightLayer, guideToStyleOptions(this.baseHighlightStyle()));
|
|
5575
5943
|
this.adapter.onViewChange(this.handleViewChange);
|
|
5576
5944
|
}
|
|
5577
5945
|
/** Replace snapping configuration without recreating the map adapter or façade. */
|
|
@@ -5619,6 +5987,7 @@ var TacticalDraw = class {
|
|
|
5619
5987
|
*/
|
|
5620
5988
|
handleViewChange = () => {
|
|
5621
5989
|
if (this.destroyed || !this.hasRendered) return;
|
|
5990
|
+
this.renderHighlights();
|
|
5622
5991
|
const excluded = this.excludedInteractionIds();
|
|
5623
5992
|
const visible = excluded.size > 0 ? this.lastRenderedMeasures.filter((m) => !excluded.has(m.id)) : this.lastRenderedMeasures;
|
|
5624
5993
|
const nativeGroundIconScaling = this.adapter.scalesGroundPointSymbolsNatively === true;
|
|
@@ -5716,6 +6085,88 @@ var TacticalDraw = class {
|
|
|
5716
6085
|
if (handle.graphicIds.some((id) => !rendered.has(id))) handle.abort("removed");
|
|
5717
6086
|
});
|
|
5718
6087
|
}
|
|
6088
|
+
this.renderHighlights();
|
|
6089
|
+
}
|
|
6090
|
+
/**
|
|
6091
|
+
* Replace the passive selection-highlight set with `ids`. Declarative and
|
|
6092
|
+
* idempotent (mirrors {@link render}): a non-interactive outline box is drawn
|
|
6093
|
+
* around each still-rendered graphic on the dedicated highlight layer — no
|
|
6094
|
+
* draw / edit / transform session, no promise, no pick subscription. Passing
|
|
6095
|
+
* `[]` clears every highlight. Unknown ids and ids currently lifted into an
|
|
6096
|
+
* active edit / transform session are skipped (the live session's own visuals
|
|
6097
|
+
* supersede the passive outline). Hosts use it for Excalidraw-style
|
|
6098
|
+
* "locked selection" chrome.
|
|
6099
|
+
*/
|
|
6100
|
+
setHighlightedGraphics(ids) {
|
|
6101
|
+
this.assertAlive();
|
|
6102
|
+
this.highlightedIds = ids.slice();
|
|
6103
|
+
this.renderHighlights();
|
|
6104
|
+
}
|
|
6105
|
+
/**
|
|
6106
|
+
* Project the current {@link highlightedIds} into one outline `LineString`
|
|
6107
|
+
* per graphic and reconcile them onto the highlight layer. Idempotent and
|
|
6108
|
+
* id-keyed via `adapter.updateLayerFeatures`, so a set-replace removes stale
|
|
6109
|
+
* outlines and an unchanged set is a cheap no-op write. Skips ids that are
|
|
6110
|
+
* unknown (not in the last render) or lifted into an active session, and
|
|
6111
|
+
* bails gracefully when the adapter cannot invert pixels to coordinates
|
|
6112
|
+
* (`getCoordinateFromPixel` is optional in the MapAdapter ABI).
|
|
6113
|
+
*/
|
|
6114
|
+
renderHighlights() {
|
|
6115
|
+
if (this.destroyed) return;
|
|
6116
|
+
if (this.highlightedIds.length === 0 && !this.hasHighlightOutlines) return;
|
|
6117
|
+
const fromPixel = this.adapter.getCoordinateFromPixel?.bind(this.adapter);
|
|
6118
|
+
if (!fromPixel) return;
|
|
6119
|
+
const resolved = this.resolveInteractionStyle(void 0);
|
|
6120
|
+
const style = resolved.highlightOutline;
|
|
6121
|
+
const padding = resolved.boxPadding ?? 0;
|
|
6122
|
+
const excluded = this.excludedInteractionIds();
|
|
6123
|
+
const bearing = this.adapter.getBearing();
|
|
6124
|
+
const features = [];
|
|
6125
|
+
for (const id of this.highlightedIds) {
|
|
6126
|
+
if (excluded.has(id)) continue;
|
|
6127
|
+
const graphic = this.lastRenderedMeasures.find((g) => g.id === id);
|
|
6128
|
+
if (!graphic) continue;
|
|
6129
|
+
const box = this.highlightBoxForGraphic(graphic, bearing, padding);
|
|
6130
|
+
if (!box) continue;
|
|
6131
|
+
let invertFailed = false;
|
|
6132
|
+
const invert = (pixel) => {
|
|
6133
|
+
const coord = fromPixel(pixel);
|
|
6134
|
+
if (!coord) {
|
|
6135
|
+
invertFailed = true;
|
|
6136
|
+
return [pixel[0], pixel[1]];
|
|
6137
|
+
}
|
|
6138
|
+
return coord;
|
|
6139
|
+
};
|
|
6140
|
+
const feature = boxOutlineFeature(box, invert, `__td_highlight__:${id}`, style);
|
|
6141
|
+
if (!invertFailed) features.push(feature);
|
|
6142
|
+
}
|
|
6143
|
+
this.adapter.updateLayerFeatures(this.highlightLayer, features);
|
|
6144
|
+
this.hasHighlightOutlines = features.length > 0;
|
|
6145
|
+
}
|
|
6146
|
+
/**
|
|
6147
|
+
* Fit the passive-highlight transform box for one graphic: the point-symbol
|
|
6148
|
+
* footprint for symbols, the snug text footprint for Text graphics, and the
|
|
6149
|
+
* projected-geometry bounding box (at the current bearing) for every other
|
|
6150
|
+
* control measure. Returns `null` when nothing projects this frame.
|
|
6151
|
+
*/
|
|
6152
|
+
highlightBoxForGraphic(graphic, bearing, padding) {
|
|
6153
|
+
if (isPointSymbol(graphic)) {
|
|
6154
|
+
if (!this.pointSymbols) return null;
|
|
6155
|
+
return pointSymbolFootprint(graphic, this.pointSymbols.render(graphic), this.adapter)?.box ?? null;
|
|
6156
|
+
}
|
|
6157
|
+
const render = renderMeasureForAdapter(this.adapter, graphic, { graphicsStyle: this.graphicsStyle });
|
|
6158
|
+
if (graphic.kind === "text") return fitTextFootprintFromFeatures(render.features, (lonLat) => this.adapter.getPixelFromCoordinate(lonLat), {
|
|
6159
|
+
resolution: this.adapter.getResolution(),
|
|
6160
|
+
bearing
|
|
6161
|
+
});
|
|
6162
|
+
const positions = [];
|
|
6163
|
+
for (const feature of render.features) if (feature.geometry) collectFeaturePositions(feature.geometry, positions);
|
|
6164
|
+
const pixels = [];
|
|
6165
|
+
for (const position of positions) {
|
|
6166
|
+
const pixel = this.adapter.getPixelFromCoordinate(position);
|
|
6167
|
+
if (pixel) pixels.push(pixel);
|
|
6168
|
+
}
|
|
6169
|
+
return fitTransformBox(pixels, bearing, padding);
|
|
5719
6170
|
}
|
|
5720
6171
|
/**
|
|
5721
6172
|
* Nested-call guard + preempt, shared by `draw` and `edit`. Returns a
|
|
@@ -5770,6 +6221,7 @@ var TacticalDraw = class {
|
|
|
5770
6221
|
clearSlot();
|
|
5771
6222
|
this.clearSnapResult();
|
|
5772
6223
|
if (extraSettle) extraSettle();
|
|
6224
|
+
this.renderHighlights();
|
|
5773
6225
|
};
|
|
5774
6226
|
this.activeInteraction.begin();
|
|
5775
6227
|
try {
|
|
@@ -5781,7 +6233,10 @@ var TacticalDraw = class {
|
|
|
5781
6233
|
}
|
|
5782
6234
|
if (!settledDuringCreate) {
|
|
5783
6235
|
this.activeInteraction.set(handle);
|
|
5784
|
-
if (activate && !handle.settled)
|
|
6236
|
+
if (activate && !handle.settled) {
|
|
6237
|
+
deactivate = activate(handle) ?? null;
|
|
6238
|
+
this.renderHighlights();
|
|
6239
|
+
}
|
|
5785
6240
|
}
|
|
5786
6241
|
handle.promise.then(clearSlot, clearSlot);
|
|
5787
6242
|
return handle.promise;
|
|
@@ -6116,7 +6571,11 @@ var TacticalDraw = class {
|
|
|
6116
6571
|
* then `previewLayer`, then `graphicsLayer` — against each layer's current
|
|
6117
6572
|
* `adapter.getLayerFeatures(...)` cache. The `guideLayer` is deliberately
|
|
6118
6573
|
* excluded: it is non-interactive draw-guide scaffolding, never a
|
|
6119
|
-
* legitimate "owned" click target.
|
|
6574
|
+
* legitimate "owned" click target. Within each layer, candidates follow
|
|
6575
|
+
* the shared visible-intent ranking (direct marks, then filled interiors,
|
|
6576
|
+
* then inferred outline-only reach, distance within direct marks, then
|
|
6577
|
+
* visual topmost order) — this explicit-affordance layer order is the
|
|
6578
|
+
* tier above all of it.
|
|
6120
6579
|
*
|
|
6121
6580
|
* `preview` and `graphics` each get a label text-extent fallback after the
|
|
6122
6581
|
* geometry pass misses on that layer: for every `Point` feature carrying a
|
|
@@ -6127,7 +6586,7 @@ var TacticalDraw = class {
|
|
|
6127
6586
|
* This matters most for the active preview, since a click can land
|
|
6128
6587
|
* anywhere across a rotated/offset label during an edit.
|
|
6129
6588
|
*
|
|
6130
|
-
* `tolerance` defaults to `
|
|
6589
|
+
* `tolerance` defaults to `pickHitTolerance(options?.originalEvent)` —
|
|
6131
6590
|
* pass the host's raw click event as `originalEvent` to get the same
|
|
6132
6591
|
* pointer-type-aware tolerance (4px mouse/pen, 12px touch) `onPick` uses;
|
|
6133
6592
|
* pass an explicit `tolerance` to override it outright.
|
|
@@ -6143,7 +6602,7 @@ var TacticalDraw = class {
|
|
|
6143
6602
|
*/
|
|
6144
6603
|
ownsInteractionAt(pixel, options) {
|
|
6145
6604
|
this.assertAlive();
|
|
6146
|
-
const tolerance = options?.tolerance ??
|
|
6605
|
+
const tolerance = options?.tolerance ?? pickHitTolerance(options?.originalEvent);
|
|
6147
6606
|
const toPixel = (lonLat) => this.adapter.getPixelFromCoordinate(lonLat);
|
|
6148
6607
|
const layers = [
|
|
6149
6608
|
{
|
|
@@ -6162,20 +6621,48 @@ var TacticalDraw = class {
|
|
|
6162
6621
|
labelFallback: true
|
|
6163
6622
|
}
|
|
6164
6623
|
];
|
|
6624
|
+
const size = this.adapter.getViewportSize();
|
|
6625
|
+
const context = {
|
|
6626
|
+
pixel,
|
|
6627
|
+
pointerType: pickPointerType(options?.originalEvent),
|
|
6628
|
+
tolerance,
|
|
6629
|
+
toPixel,
|
|
6630
|
+
viewport: {
|
|
6631
|
+
minX: 0,
|
|
6632
|
+
minY: 0,
|
|
6633
|
+
maxX: size.width,
|
|
6634
|
+
maxY: size.height
|
|
6635
|
+
},
|
|
6636
|
+
bearing: this.adapter.getBearing()
|
|
6637
|
+
};
|
|
6165
6638
|
for (const { layer, layerId, labelFallback } of layers) {
|
|
6166
6639
|
const features = this.adapter.getLayerFeatures(layerId);
|
|
6167
|
-
const
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
const
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6640
|
+
const geometryCandidate = evaluatePickCandidates(features.map((feature, index) => ({
|
|
6641
|
+
feature,
|
|
6642
|
+
visualOrder: renderOrderToVisualOrder(index, features.length)
|
|
6643
|
+
})), context);
|
|
6644
|
+
if (geometryCandidate) return this.toInteractionHit(layer, geometryCandidate.feature);
|
|
6645
|
+
if (labelFallback) {
|
|
6646
|
+
const labelCandidates = [];
|
|
6647
|
+
for (const feature of features) {
|
|
6648
|
+
if (feature.geometry?.type !== "Point") continue;
|
|
6649
|
+
if (typeof feature.properties?.text !== "string") continue;
|
|
6650
|
+
const anchorPixel = toPixel(feature.geometry.coordinates);
|
|
6651
|
+
if (!anchorPixel) continue;
|
|
6652
|
+
const box = fitLabelBoxFromFeature(feature, anchorPixel, {
|
|
6653
|
+
resolution: this.adapter.getResolution(),
|
|
6654
|
+
bearing: this.adapter.getBearing()
|
|
6655
|
+
});
|
|
6656
|
+
if (box) labelCandidates.push({
|
|
6657
|
+
feature,
|
|
6658
|
+
labelExtent: box
|
|
6659
|
+
});
|
|
6660
|
+
}
|
|
6661
|
+
const labelCandidate = evaluatePickCandidates(labelCandidates.map((candidate, index) => ({
|
|
6662
|
+
...candidate,
|
|
6663
|
+
visualOrder: renderOrderToVisualOrder(index, labelCandidates.length)
|
|
6664
|
+
})), context);
|
|
6665
|
+
if (labelCandidate) return this.toInteractionHit(layer, labelCandidate.feature);
|
|
6179
6666
|
}
|
|
6180
6667
|
}
|
|
6181
6668
|
return null;
|
|
@@ -6265,6 +6752,7 @@ var TacticalDraw = class {
|
|
|
6265
6752
|
get layerIds() {
|
|
6266
6753
|
return {
|
|
6267
6754
|
graphics: this.graphicsLayer,
|
|
6755
|
+
highlight: this.highlightLayer,
|
|
6268
6756
|
preview: this.previewLayer,
|
|
6269
6757
|
guide: this.guideLayer,
|
|
6270
6758
|
handles: this.handlesLayer
|
|
@@ -6287,6 +6775,9 @@ var TacticalDraw = class {
|
|
|
6287
6775
|
baseGuideStyle() {
|
|
6288
6776
|
return mergeSlot(BUILT_IN_INTERACTION_STYLE.guide, this.defaultInteractionStyle?.guide);
|
|
6289
6777
|
}
|
|
6778
|
+
baseHighlightStyle() {
|
|
6779
|
+
return mergeSlot(BUILT_IN_INTERACTION_STYLE.highlightOutline, this.defaultInteractionStyle?.highlightOutline);
|
|
6780
|
+
}
|
|
6290
6781
|
/**
|
|
6291
6782
|
* Resolve handle slots (plus `boxOutline`) across built-in → façade →
|
|
6292
6783
|
* per-call with per-slot, per-property shallow merge. Explicit `undefined`
|
|
@@ -6314,6 +6805,8 @@ var TacticalDraw = class {
|
|
|
6314
6805
|
}
|
|
6315
6806
|
const boxOutline = mergeSlot(mergeSlot(BUILT_IN_INTERACTION_STYLE.boxOutline, this.defaultInteractionStyle?.boxOutline), perCall?.boxOutline);
|
|
6316
6807
|
if (boxOutline) out.boxOutline = boxOutline;
|
|
6808
|
+
const highlightOutline = mergeSlot(mergeSlot(BUILT_IN_INTERACTION_STYLE.highlightOutline, this.defaultInteractionStyle?.highlightOutline), perCall?.highlightOutline);
|
|
6809
|
+
if (highlightOutline) out.highlightOutline = highlightOutline;
|
|
6317
6810
|
const boxPadding = perCall && "boxPadding" in perCall ? perCall.boxPadding : this.defaultInteractionStyle && "boxPadding" in this.defaultInteractionStyle ? this.defaultInteractionStyle.boxPadding : void 0;
|
|
6318
6811
|
if (typeof boxPadding === "number") out.boxPadding = boxPadding;
|
|
6319
6812
|
return out;
|
|
@@ -6354,6 +6847,11 @@ const BUILT_IN_INTERACTION_STYLE = {
|
|
|
6354
6847
|
strokeColor: "#4f7cff",
|
|
6355
6848
|
strokeWidth: 1
|
|
6356
6849
|
},
|
|
6850
|
+
highlightOutline: {
|
|
6851
|
+
strokeColor: "#8ea3c9",
|
|
6852
|
+
strokeWidth: 1.5,
|
|
6853
|
+
strokeDash: [4, 4]
|
|
6854
|
+
},
|
|
6357
6855
|
vertexHandle: {
|
|
6358
6856
|
pointRadius: 6,
|
|
6359
6857
|
fillColor: "#ffffff",
|
|
@@ -6593,4 +7091,4 @@ function nearestWorldCopyPosition(position, refLng) {
|
|
|
6593
7091
|
return [nearestWorldCopyLng(position[0], refLng), ...position.slice(1)];
|
|
6594
7092
|
}
|
|
6595
7093
|
//#endregion
|
|
6596
|
-
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pointSymbolFootprint, resizePointSymbolSize, resolveGroundTextSizePixels, resolveTextSizePixels, tryControlMeasureIdFromFeature };
|
|
7094
|
+
export { BaseMapAdapter, DEFAULT_LABEL_SIZE_PIXELS, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointSymbolCapabilityRequiredError, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, TacticalDraw, TacticalDrawAbortError, TacticalDrawDestroyedError, bindAbortable, clonePointSymbol, coerceHandleStyle, combineWithHostSignal, effectivePointSymbolPixelSize, evaluatePickCandidates, fillPatternImageDimension, fillPatternImageId, getSizeAnchor, hitTestFeature, hitTestIconFeature, ignoreAbort, isPointSymbol, isTacticalDrawAbortError, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, nearestWorldCopyPosition, pickHitTolerance, pickPointerType, pointSymbolFootprint, renderOrderToVisualOrder, resizePointSymbolSize, resolveGroundTextSizePixels, resolvePickPresentation, resolveTextSizePixels, tryControlMeasureIdFromFeature };
|
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.33",
|
|
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",
|