@trackunit/react-map 0.1.3 → 0.1.5

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/package.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "name": "@trackunit/react-map",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
7
7
  "node": ">=24.x"
8
8
  },
9
9
  "dependencies": {
10
- "@trackunit/react-components": "2.1.22",
11
- "@trackunit/css-class-variance-utilities": "1.13.29",
12
- "@trackunit/react-form-components": "2.1.23",
13
- "@trackunit/react-core-hooks": "1.17.36",
14
- "@trackunit/geo-json-utils": "1.14.30",
15
- "@trackunit/i18n-library-translation": "2.0.24",
16
- "@trackunit/react-modal": "2.1.24",
10
+ "@trackunit/react-components": "2.1.24",
11
+ "@trackunit/css-class-variance-utilities": "1.13.30",
12
+ "@trackunit/react-form-components": "2.1.25",
13
+ "@trackunit/react-core-hooks": "1.17.38",
14
+ "@trackunit/geo-json-utils": "1.14.32",
15
+ "@trackunit/i18n-library-translation": "2.0.26",
16
+ "@trackunit/react-modal": "2.1.26",
17
17
  "react-minimal-pie-chart": "^8.4.0",
18
- "@trackunit/react-map-adapter-shared": "0.0.9",
19
- "@trackunit/ui-design-tokens": "1.13.29",
18
+ "@trackunit/react-map-adapter-shared": "0.0.11",
19
+ "@trackunit/ui-design-tokens": "1.13.30",
20
20
  "@floating-ui/react": "^0.26.25",
21
21
  "es-toolkit": "^1.39.10",
22
22
  "tailwind-merge": "^2.0.0",
package/src/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export type { ControlMiddleware, MiddlewareContext } from "./controls/renderingR
22
22
  export type { CategoryMetadata } from "./controls/collapseControls";
23
23
  export type { ClusterEntity, Entity, MapInteractionState, MarkerEntity, RouteEntity, ShapeEntity, ShapeType, } from "@trackunit/react-map-adapter-shared";
24
24
  export { geometryTypeToShapeType, INITIAL_INTERACTION_STATE } from "@trackunit/react-map-adapter-shared";
25
- export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterInfo, ClusterRenderState, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, FitParticipation, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, ImageOverlayHandle, LayerHandle, LayerMeta, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, MarkerLayerHandle, RenderConfig, RenderMedium, ResolutionContext, RouteLayerHandle, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeLayerHandle, ShapeStyle, ShapeStyleOverrides, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "./layers/types";
25
+ export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterInfo, ClusterRenderState, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, FitParticipation, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, ImageOverlayHandle, LayerHandle, LayerMeta, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, MarkerLayerHandle, RenderConfig, RenderMedium, ResolutionContext, RouteLayerHandle, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeLayerHandle, ShapeOverlapConfig, ShapeStyle, ShapeStyleOverrides, ShapesUnderCursorHit, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "./layers/types";
26
26
  export { computeMarkerDomPortalZIndex } from "./layers/types";
27
27
  export { ANCHOR_SELECTOR, HIT_SURFACE_SELECTOR } from "@trackunit/react-map-adapter-shared";
28
28
  export { ClusterMarker, type ClusterMarkerProps, type ClusterMarkerStickInput, type ClusterSegment, } from "./clusters/ClusterMarker";
@@ -46,6 +46,7 @@ export { type AutoPanContext, type AutoPanResult } from "./panel/utils/autoPan";
46
46
  export type { DecorationAnchor, EdgeSide, ShapeDecoration } from "./layers/useShapes/shapeDecorations";
47
47
  export { type AnnotationContext, type ShapeLabelPolicy } from "./layers/useShapes/shapeLabelPolicy";
48
48
  export type { ResolveShapeLabel, ShapeLabelResolution, ShapeLabelResolutionContext, } from "./layers/useShapes/shapeLabelResolution";
49
+ export { type ContendedShape, type ResolveShapeStackOrder, type ShapeStackContext, } from "./layers/useShapes/shapeStackOrder";
49
50
  export { useImageOverlay, type UseImageOverlayOptions, type UseImageOverlayReturn, } from "./layers/image-overlay/useImageOverlay";
50
51
  export { buildExpandedIds, useExpandedIds, type MapFocus, type MapFocusTier, type MapFocusTierDisplay, } from "./layers/mapFocus";
51
52
  export { useRoute, type UseRouteOptions, type UseRouteReturn } from "./layers/routes/useRoute";
@@ -1,8 +1,11 @@
1
- import type { GeoJsonBbox, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonPosition } from "@trackunit/geo-json-utils";
1
+ import type { GeoJsonBbox, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, GeoJsonPosition } from "@trackunit/geo-json-utils";
2
2
  import type { AdaptiveMarkerResolution, ClusterConfig, ClusterRenderConfig, RenderConfig, RouteStyle, ShapeInteractiveMode, ShapeStyle } from "@trackunit/react-map-adapter-shared";
3
3
  import type { CategoryKey, ControlConfig } from "../controls/types";
4
4
  import type { ShapeDecoration } from "./useShapes/shapeDecorations";
5
+ import type { ShapesUnderCursorHit } from "./useShapes/shapeFillTiling";
5
6
  import type { ShapeLabelResolutionContext } from "./useShapes/shapeLabelResolution";
7
+ import type { ResolveShapeStackOrder } from "./useShapes/shapeStackOrder";
8
+ export type { ShapesUnderCursorHit };
6
9
  export type { AdaptiveMarkerResolution, AdaptiveRenderConfig, AdaptiveRenderState, AdaptiveResolutionContext, CircleSymbolDescriptor, ClientClusterConfig, ClusterConfig, ClusterDomRenderConfig, ClusterInfo, ClusterRenderConfig, ClusterRenderState, ClusterSymbolRenderConfig, ClusterSymbolStyle, CommonRenderState, DomPortalStackingInput, DomPortalStackingResolver, DomPortalStackingResult, DomRenderConfig, DomRenderState, GeoJsonFeature, GeoJsonFeatureCollection, GeoJsonGeometry, MarkerAnchor, MarkerDomPortalStackGeometry, MarkerDomPortalStackPhase, PixelOffset, RenderConfig, RenderMedium, ResolutionContext, RouteStyle, ServerClusterConfig, ShapeInteractiveMode, ShapeStyle, ShapeStyleOverrides, SymbolDescriptor, SymbolRenderConfig, SymbolRenderState, } from "@trackunit/react-map-adapter-shared";
7
10
  export { computeMarkerDomPortalZIndex } from "@trackunit/react-map-adapter-shared";
8
11
  /**
@@ -13,6 +16,22 @@ export { computeMarkerDomPortalZIndex } from "@trackunit/react-map-adapter-share
13
16
  * - `"none"` — never participates in any fit operation.
14
17
  */
15
18
  export type FitParticipation = "all" | "initial" | "none";
19
+ /**
20
+ * Opt-in fill-tiling configuration for a shape layer (ADR-0021).
21
+ *
22
+ * When set, overlapping polygon fills are clipped so every pixel is painted by
23
+ * exactly one feature (no compounded translucency). Strokes always stay full.
24
+ * Ownership of shared regions is decided by `resolveStackOrder`, biased toward
25
+ * the feature the cursor is most inside.
26
+ *
27
+ * Orthogonal to `interactive`: this controls the *visual* tiling only and never
28
+ * implies hit-testing behaviour.
29
+ */
30
+ export type ShapeOverlapConfig = Readonly<{
31
+ mode: "tile";
32
+ /** Swappable ranking policy. Defaults to `defaultShapeStackOrder`. */
33
+ resolveStackOrder?: ResolveShapeStackOrder;
34
+ }>;
16
35
  /**
17
36
  * Metadata common to all layer types.
18
37
  * Every layer hook returns an object extending this shape.
@@ -93,12 +112,52 @@ export interface ShapeLayerHandle extends LayerMeta {
93
112
  readonly featureStyles?: ReadonlyMap<string, ShapeStyle>;
94
113
  /** Which parts of the shape respond to interaction */
95
114
  readonly interactive: ShapeInteractiveMode;
115
+ /**
116
+ * Fill-tiling configuration. When present (`mode: "tile"`), overlapping fills
117
+ * are clipped and ranked. Absent = legacy compounding fills. Orthogonal to
118
+ * `interactive`.
119
+ */
120
+ readonly overlap?: ShapeOverlapConfig;
121
+ /**
122
+ * Per-feature clipped fill geometry keyed by `String(feature.id)`, filled by
123
+ * `<Layers>` for layers with `overlap.mode === "tile"`. Strokes keep the full
124
+ * geometry; only the fill consumes this clipped geometry.
125
+ */
126
+ readonly featureFillGeometries?: ReadonlyMap<string, GeoJsonGeometry>;
127
+ /**
128
+ * Per-feature z-index override for fill rendering, filled by `<Layers>` for
129
+ * layers with `overlap.mode === "tile"` (ADR-0021 Path B). Higher value = visually
130
+ * on top. Only features in an overlap group receive an entry; the adapter uses
131
+ * the feature's default z-index for absent entries. The hook keeps resting values
132
+ * stable across hovers; only the promoted feature's value is boosted on hover.
133
+ */
134
+ readonly featureZIndexOverrides?: ReadonlyMap<string, number>;
96
135
  /**
97
136
  * Returns all decorations for a feature, including auto-generated ones
98
137
  * (e.g. multi-geometry "+" badges). Merging of custom and auto decorations
99
138
  * is handled by `useShapes` at construction time.
100
139
  */
101
140
  readonly getDecorations: (feature: GeoJsonFeature) => ReadonlyArray<ShapeDecoration>;
141
+ /**
142
+ * Geometric hit-test at the given map position. Returns all features whose
143
+ * fill or stroke region contains `position`, with fill/stroke flags and
144
+ * distance-to-boundary for tiebreaks.
145
+ *
146
+ * Injected by `<Layers>` with live viewport context (zoom + tile-size).
147
+ * `undefined` when the handle is used outside `<Layers>` (e.g. in unit tests).
148
+ */
149
+ readonly queryShapesAt?: (position: GeoJsonPosition) => ReadonlyArray<ShapesUnderCursorHit>;
150
+ /**
151
+ * Called by `<Layers>` after the pointer settle debounce fires and the
152
+ * stroke hit-test completes for this layer. Receives all fill + stroke hits
153
+ * (sorted by feature id). Wired from `UseShapesOptions.onShapesUnderCursor`;
154
+ * always present but is a no-op when that option is not provided.
155
+ *
156
+ * Only fires for layers with `overlap: { mode: "tile" }`.
157
+ */
158
+ readonly onShapesUnderCursor?: (hits: ReadonlyArray<ShapesUnderCursorHit>, ctx: Readonly<{
159
+ position: GeoJsonPosition;
160
+ }>) => void;
102
161
  }
103
162
  /**
104
163
  * Route layer handle -- returned by `useRoute`.
@@ -17,6 +17,9 @@ type InteractiveDecorationProps = Readonly<{
17
17
  * selection, mouseenter/mouseleave dispatches entity hover — both followed
18
18
  * by optional supplementary callbacks.
19
19
  *
20
+ * Double-click is detected via time-based logic (`useManualDblClick`) rather
21
+ * than the native `dblclick` event. See that hook for the rationale.
22
+ *
20
23
  * @internal
21
24
  */
22
25
  export declare const InteractiveDecoration: ({ entity, select, hover, onClick, onDblClick, onHover, children, }: InteractiveDecorationProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,114 @@
1
+ import { type GeoJsonBbox, type GeoJsonFeature, type GeoJsonGeometry, type GeoJsonMultiPolygon, type GeoJsonPosition } from "@trackunit/geo-json-utils";
2
+ import type { ShapeStyle } from "@trackunit/react-map-adapter-shared";
3
+ import type { ResolveShapeStackOrder } from "./shapeStackOrder";
4
+ /**
5
+ * Pure fill-tiling core for ADR-0021. Given a set of features, the current
6
+ * viewport, a stack-order resolver, and an optional cursor, it computes the
7
+ * clipped fill geometry for every feature whose fill overlaps a neighbour.
8
+ *
9
+ * Non-overlapping features and the top-ranked feature of each overlap group
10
+ * keep their full geometry (no entry returned → adapter falls back to full).
11
+ * Fully covered features map to an empty MultiPolygon (no fill painted).
12
+ *
13
+ * Kept free of React/rAF so it is unit-testable; the surrounding hook owns the
14
+ * pointermove throttling and emits the result through the adapter.
15
+ */
16
+ /** Empty fill = painted by nobody (fully covered loser or suppressed feature). */
17
+ export declare const EMPTY_FILL: GeoJsonMultiPolygon;
18
+ /**
19
+ * A feature's fill is suppressed when `fillOpacity` is exactly `0` on the
20
+ * resolved style. Suppressed features are excluded from tiling stack
21
+ * computation (no overlap ownership, no clip neighbour) and from fill
22
+ * hit-testing (stroke remains interactive).
23
+ */
24
+ export declare const isFillSuppressed: (style: ShapeStyle | undefined) => boolean;
25
+ /** Per-feature visible fill lookup for hit-testing (clipped geometry from tiling). */
26
+ export type VisibleFillLookup = (featureId: string) => GeoJsonGeometry | null | undefined;
27
+ export type FillTilingInput = Readonly<{
28
+ features: ReadonlyArray<GeoJsonFeature>;
29
+ /** Only features whose bbox intersects this are considered. */
30
+ viewportBounds: GeoJsonBbox;
31
+ resolveStackOrder: ResolveShapeStackOrder;
32
+ zoom: number;
33
+ /**
34
+ * Feature id that is currently selected. Selection forces the feature to the
35
+ * top of its overlap group, overriding resting order. Takes highest priority.
36
+ */
37
+ selectedFeatureId?: string | null;
38
+ /**
39
+ * Feature ids whose fill is suppressed (`fillOpacity === 0`). Suppressed
40
+ * features are excluded from the tiling stack (no overlap group, no clip
41
+ * of neighbours) and receive `EMPTY_FILL` when their bbox is in the
42
+ * viewport. They get no `featureToGroupKey` entry so promotion is a no-op.
43
+ */
44
+ suppressedFeatureIds?: ReadonlySet<string>;
45
+ }>;
46
+ export type FillTilingResult = Readonly<{
47
+ /** Feature id → clipped fill geometry. Only clipped/covered features appear. */
48
+ fillGeometries: ReadonlyMap<string, GeoJsonGeometry>;
49
+ /**
50
+ * Feature id → group key. Lets the hook look up which group a stroke-hover
51
+ * hit belongs to so it can set the promotion overlay on the next hover.
52
+ */
53
+ featureToGroupKey: ReadonlyMap<string, string>;
54
+ /**
55
+ * Resting z-index per feature in each overlap group (rank 0 = highest value =
56
+ * visually on top). Only features that belong to an overlap group are included.
57
+ * Used by Path B promotion: the hook keeps these stable across hover events and
58
+ * only overrides the promoted feature's value to raise it above its peers.
59
+ */
60
+ featureZIndex: ReadonlyMap<string, number>;
61
+ }>;
62
+ /**
63
+ * Planar area of any GeoJSON geometry (outer rings minus holes). Returns 0 for
64
+ * non-polygonal types (Point, LineString, …). Relative magnitude only — not
65
+ * geodesically accurate, but consistent enough for stack-order and tiebreak use.
66
+ */
67
+ export declare const geometryArea: (geometry: GeoJsonGeometry) => number;
68
+ /** Geographic degrees spanned by one screen pixel at a given zoom and tile size. */
69
+ export declare const degreesPerPixel: (zoom: number, tileSize: number) => number;
70
+ /** A single hit-test result from `shapesUnderCursor`. */
71
+ export type ShapesUnderCursorHit = Readonly<{
72
+ featureId: string;
73
+ /** True when the position is inside the polygon fill (holes respected). */
74
+ fill: boolean;
75
+ /** True when the position is within half the rendered stroke width of any boundary ring. */
76
+ stroke: boolean;
77
+ /**
78
+ * Minimum distance from `position` to the nearest boundary segment, in geographic
79
+ * degrees. `null` when the geometry could not be parsed. Used by callers for
80
+ * tiebreaks (e.g. "top stroke hit = smallest area").
81
+ */
82
+ distanceToBoundary: number | null;
83
+ }>;
84
+ /**
85
+ * Adapter-agnostic hit test: given a cursor `position` and a list of GeoJSON
86
+ * features, returns all features whose fill or stroke region contains the
87
+ * position, along with raw distance-to-boundary for caller tiebreaks.
88
+ *
89
+ * - **fill**: point-in-polygon on the feature's **visible** fill geometry when
90
+ * `visibleFillFor` is supplied (clipped tiling output); otherwise the full
91
+ * feature geometry. Stroke distance always uses the full outline.
92
+ * - **stroke**: `distanceToBoundary < max(strokeWidth, 3) / 2` pixels, converted
93
+ * to geographic degrees via `degreesPerPixel(zoom, tileSize)`.
94
+ * - Bbox-prefiltered for performance; results are in deterministic (feature-id) order.
95
+ */
96
+ export declare const shapesUnderCursor: (position: GeoJsonPosition, features: ReadonlyArray<GeoJsonFeature>, options: Readonly<{
97
+ zoom: number;
98
+ tileSize: number;
99
+ strokeWidthFor: (featureId: string) => number;
100
+ /**
101
+ * Optional per-feature visible fill geometry (from fill tiling). Absent entry
102
+ * = full geometry; null / empty MultiPolygon = no visible fill.
103
+ */
104
+ visibleFillFor?: VisibleFillLookup;
105
+ }>) => Array<ShapesUnderCursorHit>;
106
+ /**
107
+ * Hover-promotion overlay for one overlap group: the winner renders at full
108
+ * geometry (caller removes its fill override). Each peer is clipped against the
109
+ * winner's full outline starting from its **resting** fill (when present), not
110
+ * its full geometry — so peer-to-peer overlap ownership from resting tiling is
111
+ * preserved and translucent fills do not compound under the winner.
112
+ */
113
+ export declare const computePromotionGroupFills: (members: ReadonlyArray<GeoJsonFeature>, winnerId: string, restingPeerFills?: ReadonlyMap<string, GeoJsonGeometry>) => ReadonlyMap<string, GeoJsonGeometry>;
114
+ export declare const computeFillTiling: (input: FillTilingInput) => FillTilingResult;
@@ -21,7 +21,7 @@ export type ShapeLabelResolutionContext = Readonly<{
21
21
  zoom: number;
22
22
  /** Number of features from the same layer that intersect the viewport. */
23
23
  shapesInViewport: number;
24
- /** Number of other in-viewport features whose bbox intersects this feature's bbox. */
24
+ /** Number of other in-viewport features whose **geometry shares area** with this feature's polygon (bbox prefilter, then polygon intersection for Polygon/MultiPolygon geometries; bbox-only for Point/Line types). Fully contained inner polygons are not counted — their fill does not obscure the map below. */
25
25
  overlappingShapesCount: number;
26
26
  /** GeoJSON geometry type of the feature. */
27
27
  geometryType: GeoJsonGeometry["type"];
@@ -0,0 +1,39 @@
1
+ import type { GeoJsonFeature } from "@trackunit/geo-json-utils";
2
+ /**
3
+ * Precomputed geometric facts about a single contended shape — a feature whose
4
+ * fill overlaps at least one other feature in the same layer. The library
5
+ * computes these facts; the resolver only orders them.
6
+ */
7
+ export type ContendedShape = Readonly<{
8
+ /** The contended feature. Its `id` (stringified) is what the resolver returns. */
9
+ feature: GeoJsonFeature;
10
+ /** Planar area of the feature's fill, used as the stable tiebreak. */
11
+ area: number;
12
+ /** IDs of other contended shapes that fully contain this one. */
13
+ containedIn: ReadonlyArray<string>;
14
+ }>;
15
+ /**
16
+ * Context for a single resolve pass.
17
+ */
18
+ export type ShapeStackContext = Readonly<{
19
+ /** Current map zoom level. */
20
+ zoom: number;
21
+ }>;
22
+ /**
23
+ * Decides fill ownership for a set of overlapping shapes. Returns the contended
24
+ * feature IDs ordered **top-first** — the first ID owns shared pixels, later
25
+ * IDs are clipped against the winners ahead of them.
26
+ *
27
+ * Pure: same inputs must yield the same ordering. Swappable per-layer via the
28
+ * `overlap.resolveStackOrder` option on `useShapes`.
29
+ */
30
+ export type ResolveShapeStackOrder = (contended: ReadonlyArray<ContendedShape>, ctx: ShapeStackContext) => ReadonlyArray<string>;
31
+ /**
32
+ * Default stack-order policy (ADR-0021 "golden path"). In priority order:
33
+ *
34
+ * 1. **Containment** — a shape contained within another ranks above its
35
+ * container, so the specific inner site beats the broad outer one.
36
+ * 2. **Smaller area on top** — stable tiebreak; inner shapes are typically
37
+ * smaller, so this reinforces containment and keeps nested sites visible.
38
+ */
39
+ export declare const defaultShapeStackOrder: ResolveShapeStackOrder;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Time-based double-click detection for interactive DOM overlays.
3
+ *
4
+ * Call the returned `dispatch` on every `click` event. When two calls arrive
5
+ * within `DOUBLE_CLICK_THRESHOLD_MS`, the second call is treated as a
6
+ * double-click and `onDblClick` is invoked; otherwise `onClick` is invoked.
7
+ *
8
+ * A third rapid click resets the window (triple-click is not treated as a
9
+ * second double-click).
10
+ *
11
+ * @internal
12
+ */
13
+ export declare const useManualDblClick: <TArg>(onClick: ((arg: TArg) => void) | undefined, onDblClick: ((arg: TArg) => void) | undefined) => ((arg: TArg) => void);
@@ -0,0 +1,89 @@
1
+ import type { GeoJsonGeometry, GeoJsonPosition } from "@trackunit/geo-json-utils";
2
+ import type { MapApi } from "../../core/types";
3
+ import type { LayerHandle, ShapesUnderCursorHit } from "../types";
4
+ /**
5
+ * Per-handle, per-feature clipped fill geometries (ADR-0021), keyed
6
+ * `handleId → (featureId → geometry)`. Mirrors the style-overrides channel in
7
+ * `useShapeDecorations`.
8
+ */
9
+ export type FillGeometryOverrides = ReadonlyMap<string, ReadonlyMap<string, GeoJsonGeometry>>;
10
+ /**
11
+ * Per-handle, per-feature z-index overrides for fill promotion (ADR-0021 Path B),
12
+ * keyed `handleId → (featureId → zIndex)`. Higher value = visually on top.
13
+ * Only features in an overlap group receive an entry.
14
+ */
15
+ export type ZIndexOverrides = ReadonlyMap<string, ReadonlyMap<string, number>>;
16
+ type UseShapeFillTilingConfig = Readonly<{
17
+ api: MapApi | undefined;
18
+ handles: ReadonlyArray<LayerHandle>;
19
+ onFillGeometriesChange: (overrides: FillGeometryOverrides) => void;
20
+ onZIndexOverridesChange: (overrides: ZIndexOverrides) => void;
21
+ /**
22
+ * Called once per pointer-settle with the overall top hit (smallest area
23
+ * across all handles/groups). Fires only when at least one candidate hit
24
+ * exists and decoration hover is not active.
25
+ */
26
+ onSettledHover?: (handleId: string, featureId: string) => void;
27
+ /**
28
+ * Currently selected feature id. The matching feature is forced to the front
29
+ * of its overlap group, overriding resting order.
30
+ */
31
+ selectedFeatureId?: string | null;
32
+ /**
33
+ * Per-handle set of feature ids whose fill is suppressed (`fillOpacity === 0`
34
+ * on the resolved context-aware style). Suppressed features are excluded from
35
+ * tiling stack computation and return no fill hit during settle / queryAt.
36
+ * Derived from `viewportStyleOverrides` in `<Layers>` via `isFillSuppressed`.
37
+ */
38
+ suppressedFillIdsByHandle?: ReadonlyMap<string, ReadonlySet<string>>;
39
+ }>;
40
+ /** Return type of `useShapeFillTiling`. */
41
+ export type UseShapeFillTilingReturn = Readonly<{
42
+ /**
43
+ * Imperative hit-test for a given handle and position. Uses the current
44
+ * viewport snapshot captured by the last render. Returns an empty array
45
+ * when the handle id is unknown or has no polygonal features.
46
+ */
47
+ queryAt: (handleId: string, position: GeoJsonPosition) => ReadonlyArray<ShapesUnderCursorHit>;
48
+ /**
49
+ * Promote a feature to the front of its overlap group (sticky until the next
50
+ * hover or selection). Used when the pointer is over a shape decoration
51
+ * (e.g. edge label) that is not on the fill/stroke geometry — same
52
+ * promotion channel as stroke/fill settle promotion.
53
+ */
54
+ promoteFeature: (handleId: string, featureId: string) => void;
55
+ /**
56
+ * Symmetric counterpart to `promoteFeature`. Clears the promotion for
57
+ * `featureId` in its overlap group only when it is the current group winner;
58
+ * no-op otherwise.
59
+ */
60
+ clearPromotion: (handleId: string, featureId: string) => void;
61
+ /**
62
+ * Suppress pointer-settle fill promotion while a shape decoration (edge label,
63
+ * etc.) is hovered — the cursor may sit over another polygon's fill visually.
64
+ */
65
+ setDecorationHoverActive: (active: boolean) => void;
66
+ }>;
67
+ /**
68
+ * `useShapeFillTiling` — clips overlapping polygon fills for shape handles that
69
+ * opted in via `overlap: { mode: "tile" }`, and promotes shapes on stroke-hover
70
+ * settle (sticky, ~120ms debounce), decoration hover (edge labels, etc.), or when
71
+ * a shape is selected (selection always forces front). Emits the result through
72
+ * `onFillGeometriesChange` and `onZIndexOverridesChange`; `<Layers>` merges them
73
+ * into each shape handle's `featureFillGeometries` and `featureZIndexOverrides`.
74
+ *
75
+ * **Path B (ADR-0021):** `computeFillTiling` is promotion-unaware — it only runs on
76
+ * viewport / feature-set / selection changes and produces stable resting fills and
77
+ * resting z-indices. Hover promotion removes the resting clip for each group's
78
+ * current sticky winner (so covered area can paint on top) and raises its z-index.
79
+ * Non-winners in an active group are re-clipped against the winner's full outline
80
+ * only (exclusive rings stay painted). Promotion clears when the pointer leaves
81
+ * all sites on the handle.
82
+ *
83
+ * Resting tiling (viewport + feature set) is deferred while the map is moving
84
+ * and coalesced on `idle` so pan/zoom does not run `computeFillTiling` every frame.
85
+ *
86
+ * @internal
87
+ */
88
+ export declare const useShapeFillTiling: ({ api, handles, onFillGeometriesChange, onZIndexOverridesChange, onSettledHover, selectedFeatureId, suppressedFillIdsByHandle, }: UseShapeFillTilingConfig) => UseShapeFillTilingReturn;
89
+ export {};
@@ -1,6 +1,6 @@
1
- import { type GeoJsonFeature, type GeoJsonFeatureCollection } from "@trackunit/geo-json-utils";
1
+ import { type GeoJsonFeature, type GeoJsonFeatureCollection, type GeoJsonPosition } from "@trackunit/geo-json-utils";
2
2
  import type { CategoryKey, ControlConfig } from "../../controls/types";
3
- import type { FitParticipation, ShapeInteractiveMode, ShapeLayerHandle, ShapeStyle } from "../types";
3
+ import type { FitParticipation, ShapeInteractiveMode, ShapeLayerHandle, ShapeOverlapConfig, ShapeStyle, ShapesUnderCursorHit } from "../types";
4
4
  import type { ShapeDecoration } from "./shapeDecorations";
5
5
  import type { ShapeLabelResolutionContext } from "./shapeLabelResolution";
6
6
  export type UseShapesOptions = Readonly<{
@@ -14,6 +14,13 @@ export type UseShapesOptions = Readonly<{
14
14
  resolveStyle: (feature: GeoJsonFeature, ctx?: ShapeLabelResolutionContext) => ShapeStyle;
15
15
  /** Which parts of the shape respond to interaction. Default: `"stroke"` */
16
16
  interactive?: ShapeInteractiveMode;
17
+ /**
18
+ * Opt-in fill tiling for overlapping polygons (ADR-0021). When set to
19
+ * `{ mode: "tile" }`, overlapping fills are clipped so every pixel is painted
20
+ * by exactly one feature and ownership follows the cursor. Strokes stay full.
21
+ * Orthogonal to `interactive` — neither implies the other.
22
+ */
23
+ overlap?: ShapeOverlapConfig;
17
24
  /**
18
25
  * Controls to contribute to the map UI, keyed by semantic area.
19
26
  * Omit entirely (or omit a key) to contribute nothing to that area.
@@ -31,6 +38,17 @@ export type UseShapesOptions = Readonly<{
31
38
  loading?: boolean;
32
39
  /** Controls whether this layer's bounds are included in fit-to-content operations. Default: `"all"` */
33
40
  fitParticipation?: FitParticipation;
41
+ /**
42
+ * Called by `<Layers>` after the pointer settle debounce fires and the
43
+ * stroke hit-test completes for this layer. Receives the full hit list
44
+ * (fill + stroke flags) sorted by feature id. Useful for custom
45
+ * hover-promotion, tooltips, or analytics.
46
+ *
47
+ * Only fires for layers with `overlap: { mode: "tile" }`.
48
+ */
49
+ onShapesUnderCursor?: (hits: ReadonlyArray<ShapesUnderCursorHit>, ctx: Readonly<{
50
+ position: GeoJsonPosition;
51
+ }>) => void;
34
52
  }>;
35
53
  export type UseShapesReturn = ShapeLayerHandle;
36
54
  /**