@trackunit/react-map 0.2.125 → 0.2.126

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/index.cjs.js CHANGED
@@ -5579,6 +5579,102 @@ const useFitToContent = (api, handles, options) => {
5579
5579
  return react.useMemo(() => ({ hasFitted, isWaiting, fitNow }), [hasFitted, isWaiting, fitNow]);
5580
5580
  };
5581
5581
 
5582
+ const isWithinBounds = (position, bounds) => {
5583
+ const [lng, lat] = position;
5584
+ const [minLng, minLat, maxLng, maxLat] = bounds;
5585
+ const lngInside = minLng > maxLng ? lng >= minLng || lng <= maxLng : lng >= minLng && lng <= maxLng;
5586
+ return lngInside && lat >= minLat && lat <= maxLat;
5587
+ };
5588
+ const overlaps = (a, b, footprint) => {
5589
+ // Shortest angular longitude delta so labels near the antimeridian (e.g. 179.9° and
5590
+ // -179.9°) are correctly seen as ~0.2° apart, not ~359.8°, matching `isWithinBounds`.
5591
+ const rawLngDelta = Math.abs(a[0] - b[0]);
5592
+ const lngDelta = Math.min(rawLngDelta, 360 - rawLngDelta);
5593
+ return lngDelta < footprint.widthDeg && Math.abs(a[1] - b[1]) < footprint.heightDeg;
5594
+ };
5595
+ /**
5596
+ * Chooses which markers render a label so that no two labels overlap.
5597
+ *
5598
+ * Greedy placement: walks `items` in order, skips any outside the view box,
5599
+ * and adds a label only when its nominal footprint does not collide with an
5600
+ * already-placed one, stopping at `ceiling`. The budget is emergent — however
5601
+ * many non-overlapping labels fit, bounded by the ceiling.
5602
+ *
5603
+ * Deterministic: identical input yields an identical set. Phase 1 places in the
5604
+ * caller-provided order; priority ordering, dispersion, and pin-on-a-stick
5605
+ * displacement layer on top in later phases.
5606
+ */
5607
+ const buildLabelPlacement = ({ items, getId, getPosition, bounds, footprint, ceiling, }) => {
5608
+ const shown = new Set();
5609
+ const placed = [];
5610
+ for (const item of items) {
5611
+ if (shown.size >= ceiling)
5612
+ break;
5613
+ const position = getPosition(item);
5614
+ if (bounds !== null && !isWithinBounds(position, bounds))
5615
+ continue;
5616
+ if (placed.some(other => overlaps(other, position, footprint)))
5617
+ continue;
5618
+ shown.add(getId(item));
5619
+ placed.push(position);
5620
+ }
5621
+ return shown;
5622
+ };
5623
+
5624
+ const WEB_MERCATOR_TILE_SIZE_PX = 256;
5625
+ /**
5626
+ * Degrees of longitude/latitude spanned by a single screen pixel at `zoom`,
5627
+ * near `latitudeDeg`, under Web Mercator.
5628
+ *
5629
+ * Longitude degrees-per-pixel is constant everywhere; latitude degrees-per-pixel
5630
+ * scales by `cos(latitude)` because Mercator stretches higher latitudes. Used to
5631
+ * convert a pixel label footprint into a geo bounding box so collision runs in
5632
+ * the same lng/lat space the marker positions already live in.
5633
+ */
5634
+ const degreesPerPixel$1 = (zoom, latitudeDeg) => {
5635
+ const worldWidthPx = WEB_MERCATOR_TILE_SIZE_PX * 2 ** zoom;
5636
+ const lng = 360 / worldWidthPx;
5637
+ return { lng, lat: lng * Math.cos((latitudeDeg * Math.PI) / 180) };
5638
+ };
5639
+ /**
5640
+ * Upper-bound label footprint as a geo-space width/height, for collision testing.
5641
+ *
5642
+ * A rendered pill's size does NOT follow the zoom circle-size tier: `MapMarker`
5643
+ * lays every pill out at the fixed {@link MARKER_PILL_CONTENT_LAYOUT_SIZE}, and the
5644
+ * label text is capped at `maxLabelWidthPx` (ellipsis beyond). We size the collision
5645
+ * box to that upper bound — widest pill that can render — so the box is never smaller
5646
+ * than what paints, which is what keeps the "no two labels overlap" guarantee honest.
5647
+ * Density is tuned via `paddingPx`, not by shrinking the box.
5648
+ */
5649
+ const labelFootprintDeg = ({ zoom, latitudeDeg, paddingPx }) => {
5650
+ const { circle, pillPaddingY } = MARKER_SIZE_MAP[MARKER_PILL_CONTENT_LAYOUT_SIZE];
5651
+ const { maxLabelWidthPx, gapPx, paddingRightPx } = MARKER_TUNING.pill;
5652
+ // Left pad (== pillPaddingY) + icon disc + gap + max label text + right pad. Assume the icon
5653
+ // disc is present (the wider case) so the box stays an upper bound.
5654
+ const widthPx = pillPaddingY + circle + gapPx + maxLabelWidthPx + paddingRightPx + 2 * paddingPx;
5655
+ const heightPx = markerPillOuterHeightPx(MARKER_PILL_CONTENT_LAYOUT_SIZE) + 2 * paddingPx;
5656
+ const perPixel = degreesPerPixel$1(zoom, latitudeDeg);
5657
+ return { widthDeg: widthPx * perPixel.lng, heightDeg: heightPx * perPixel.lat };
5658
+ };
5659
+
5660
+ const EMPTY_LABEL_IDS = new Set();
5661
+ /**
5662
+ * Viewport-driven label placement: returns the ids of the markers that should
5663
+ * render a label such that no two labels overlap, bounded by `ceiling`.
5664
+ *
5665
+ * Derives an upper-bound pill footprint (`labelFootprintDeg`) and delegates
5666
+ * selection to the pure `buildLabelPlacement`. Exposed as a hook so the placement
5667
+ * participates in React's stability model and later phases can hold incumbency
5668
+ * state here without changing the public surface.
5669
+ */
5670
+ const useLabelPlacement = ({ enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling, }) => react.useMemo(() => {
5671
+ if (!enabled)
5672
+ return EMPTY_LABEL_IDS;
5673
+ const latitudeDeg = bounds === null ? 0 : (bounds[1] + bounds[3]) / 2;
5674
+ const footprint = labelFootprintDeg({ zoom, latitudeDeg, paddingPx });
5675
+ return buildLabelPlacement({ items, getId, getPosition, bounds, footprint, ceiling });
5676
+ }, [enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling]);
5677
+
5582
5678
  // ============================================================================
5583
5679
  // Helpers
5584
5680
  // ============================================================================
@@ -12208,6 +12304,7 @@ exports.useExpandedIds = useExpandedIds;
12208
12304
  exports.useFitFeatureBounds = useFitFeatureBounds;
12209
12305
  exports.useFitToContent = useFitToContent;
12210
12306
  exports.useImageOverlay = useImageOverlay;
12307
+ exports.useLabelPlacement = useLabelPlacement;
12211
12308
  exports.useLayers = useLayers;
12212
12309
  exports.useMap = useMap;
12213
12310
  exports.useMapAnnotation = useMapAnnotation;
package/index.esm.js CHANGED
@@ -5578,6 +5578,102 @@ const useFitToContent = (api, handles, options) => {
5578
5578
  return useMemo(() => ({ hasFitted, isWaiting, fitNow }), [hasFitted, isWaiting, fitNow]);
5579
5579
  };
5580
5580
 
5581
+ const isWithinBounds = (position, bounds) => {
5582
+ const [lng, lat] = position;
5583
+ const [minLng, minLat, maxLng, maxLat] = bounds;
5584
+ const lngInside = minLng > maxLng ? lng >= minLng || lng <= maxLng : lng >= minLng && lng <= maxLng;
5585
+ return lngInside && lat >= minLat && lat <= maxLat;
5586
+ };
5587
+ const overlaps = (a, b, footprint) => {
5588
+ // Shortest angular longitude delta so labels near the antimeridian (e.g. 179.9° and
5589
+ // -179.9°) are correctly seen as ~0.2° apart, not ~359.8°, matching `isWithinBounds`.
5590
+ const rawLngDelta = Math.abs(a[0] - b[0]);
5591
+ const lngDelta = Math.min(rawLngDelta, 360 - rawLngDelta);
5592
+ return lngDelta < footprint.widthDeg && Math.abs(a[1] - b[1]) < footprint.heightDeg;
5593
+ };
5594
+ /**
5595
+ * Chooses which markers render a label so that no two labels overlap.
5596
+ *
5597
+ * Greedy placement: walks `items` in order, skips any outside the view box,
5598
+ * and adds a label only when its nominal footprint does not collide with an
5599
+ * already-placed one, stopping at `ceiling`. The budget is emergent — however
5600
+ * many non-overlapping labels fit, bounded by the ceiling.
5601
+ *
5602
+ * Deterministic: identical input yields an identical set. Phase 1 places in the
5603
+ * caller-provided order; priority ordering, dispersion, and pin-on-a-stick
5604
+ * displacement layer on top in later phases.
5605
+ */
5606
+ const buildLabelPlacement = ({ items, getId, getPosition, bounds, footprint, ceiling, }) => {
5607
+ const shown = new Set();
5608
+ const placed = [];
5609
+ for (const item of items) {
5610
+ if (shown.size >= ceiling)
5611
+ break;
5612
+ const position = getPosition(item);
5613
+ if (bounds !== null && !isWithinBounds(position, bounds))
5614
+ continue;
5615
+ if (placed.some(other => overlaps(other, position, footprint)))
5616
+ continue;
5617
+ shown.add(getId(item));
5618
+ placed.push(position);
5619
+ }
5620
+ return shown;
5621
+ };
5622
+
5623
+ const WEB_MERCATOR_TILE_SIZE_PX = 256;
5624
+ /**
5625
+ * Degrees of longitude/latitude spanned by a single screen pixel at `zoom`,
5626
+ * near `latitudeDeg`, under Web Mercator.
5627
+ *
5628
+ * Longitude degrees-per-pixel is constant everywhere; latitude degrees-per-pixel
5629
+ * scales by `cos(latitude)` because Mercator stretches higher latitudes. Used to
5630
+ * convert a pixel label footprint into a geo bounding box so collision runs in
5631
+ * the same lng/lat space the marker positions already live in.
5632
+ */
5633
+ const degreesPerPixel$1 = (zoom, latitudeDeg) => {
5634
+ const worldWidthPx = WEB_MERCATOR_TILE_SIZE_PX * 2 ** zoom;
5635
+ const lng = 360 / worldWidthPx;
5636
+ return { lng, lat: lng * Math.cos((latitudeDeg * Math.PI) / 180) };
5637
+ };
5638
+ /**
5639
+ * Upper-bound label footprint as a geo-space width/height, for collision testing.
5640
+ *
5641
+ * A rendered pill's size does NOT follow the zoom circle-size tier: `MapMarker`
5642
+ * lays every pill out at the fixed {@link MARKER_PILL_CONTENT_LAYOUT_SIZE}, and the
5643
+ * label text is capped at `maxLabelWidthPx` (ellipsis beyond). We size the collision
5644
+ * box to that upper bound — widest pill that can render — so the box is never smaller
5645
+ * than what paints, which is what keeps the "no two labels overlap" guarantee honest.
5646
+ * Density is tuned via `paddingPx`, not by shrinking the box.
5647
+ */
5648
+ const labelFootprintDeg = ({ zoom, latitudeDeg, paddingPx }) => {
5649
+ const { circle, pillPaddingY } = MARKER_SIZE_MAP[MARKER_PILL_CONTENT_LAYOUT_SIZE];
5650
+ const { maxLabelWidthPx, gapPx, paddingRightPx } = MARKER_TUNING.pill;
5651
+ // Left pad (== pillPaddingY) + icon disc + gap + max label text + right pad. Assume the icon
5652
+ // disc is present (the wider case) so the box stays an upper bound.
5653
+ const widthPx = pillPaddingY + circle + gapPx + maxLabelWidthPx + paddingRightPx + 2 * paddingPx;
5654
+ const heightPx = markerPillOuterHeightPx(MARKER_PILL_CONTENT_LAYOUT_SIZE) + 2 * paddingPx;
5655
+ const perPixel = degreesPerPixel$1(zoom, latitudeDeg);
5656
+ return { widthDeg: widthPx * perPixel.lng, heightDeg: heightPx * perPixel.lat };
5657
+ };
5658
+
5659
+ const EMPTY_LABEL_IDS = new Set();
5660
+ /**
5661
+ * Viewport-driven label placement: returns the ids of the markers that should
5662
+ * render a label such that no two labels overlap, bounded by `ceiling`.
5663
+ *
5664
+ * Derives an upper-bound pill footprint (`labelFootprintDeg`) and delegates
5665
+ * selection to the pure `buildLabelPlacement`. Exposed as a hook so the placement
5666
+ * participates in React's stability model and later phases can hold incumbency
5667
+ * state here without changing the public surface.
5668
+ */
5669
+ const useLabelPlacement = ({ enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling, }) => useMemo(() => {
5670
+ if (!enabled)
5671
+ return EMPTY_LABEL_IDS;
5672
+ const latitudeDeg = bounds === null ? 0 : (bounds[1] + bounds[3]) / 2;
5673
+ const footprint = labelFootprintDeg({ zoom, latitudeDeg, paddingPx });
5674
+ return buildLabelPlacement({ items, getId, getPosition, bounds, footprint, ceiling });
5675
+ }, [enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling]);
5676
+
5581
5677
  // ============================================================================
5582
5678
  // Helpers
5583
5679
  // ============================================================================
@@ -12120,4 +12216,4 @@ const mockMapApi = (overrides) => {
12120
12216
  */
12121
12217
  setupLibraryTranslations();
12122
12218
 
12123
- export { ClusterMarker, ClusterStick, Controls, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, PanelIconButton, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useEntitiesNearCursor, useExpandedIds, useFitFeatureBounds, useFitToContent, useImageOverlay, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
12219
+ export { ClusterMarker, ClusterStick, Controls, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, PanelIconButton, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useEntitiesNearCursor, useExpandedIds, useFitFeatureBounds, useFitToContent, useImageOverlay, useLabelPlacement, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
package/package.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "name": "@trackunit/react-map",
3
- "version": "0.2.125",
3
+ "version": "0.2.126",
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.12.0",
11
- "@trackunit/css-class-variance-utilities": "1.14.28",
12
- "@trackunit/react-form-components": "2.6.47",
13
- "@trackunit/react-core-hooks": "1.21.40",
14
- "@trackunit/geo-json-utils": "1.15.30",
15
- "@trackunit/i18n-library-translation": "2.4.40",
10
+ "@trackunit/react-components": "2.13.0",
11
+ "@trackunit/css-class-variance-utilities": "1.14.29",
12
+ "@trackunit/react-form-components": "2.6.48",
13
+ "@trackunit/react-core-hooks": "1.21.41",
14
+ "@trackunit/geo-json-utils": "1.15.31",
15
+ "@trackunit/i18n-library-translation": "2.4.41",
16
16
  "react-minimal-pie-chart": "^8.4.0",
17
- "@trackunit/react-map-adapter-shared": "0.0.111",
18
- "@trackunit/react-map-color-utils": "0.0.91",
19
- "@trackunit/ui-design-tokens": "1.15.18",
17
+ "@trackunit/react-map-adapter-shared": "0.0.112",
18
+ "@trackunit/react-map-color-utils": "0.0.92",
19
+ "@trackunit/ui-design-tokens": "1.15.19",
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
@@ -53,6 +53,7 @@ export { useImageOverlay, type UseImageOverlayOptions, type UseImageOverlayRetur
53
53
  export { buildExpandedIds, useExpandedIds, type MapFocus, type MapFocusTier, type MapFocusTierDisplay, } from "./layers/mapFocus";
54
54
  export { useRoute, type UseRouteOptions, type UseRouteReturn } from "./layers/routes/useRoute";
55
55
  export { useFitToContent, type FitToContentOptions, type FitToContentResult } from "./layers/useFitToContent";
56
+ export { useLabelPlacement, type UseLabelPlacementParams } from "./layers/useLabelPlacement";
56
57
  export { useLayers, type UseLayersReturn } from "./layers/useLayers";
57
58
  export { DEFAULT_MARKER_SIZE_BREAKPOINTS, MARKER_DISC_BORDER_WIDTH_PX, type MarkerSizeBreakpoint, type PickMarkerSizeOptions, type PickRenderMediumOptions, type PickSymbolDiameterOptions, } from "./layers/useMarkers/adaptiveHelpers";
58
59
  export { useMarkers, type UseMarkersOptions, type UseMarkersReturn } from "./layers/useMarkers/useMarkers";
@@ -0,0 +1,29 @@
1
+ import type { GeoJsonBbox } from "@trackunit/geo-json-utils";
2
+ import type { LabelFootprintDeg } from "./internal/labelFootprint";
3
+ type Position = readonly [number, number, ...Array<number>];
4
+ export type BuildLabelPlacementParams<TAsset> = Readonly<{
5
+ /** Candidate markers, in the order they should be considered for a label. */
6
+ items: ReadonlyArray<TAsset>;
7
+ getId: (item: TAsset) => string;
8
+ getPosition: (item: TAsset) => Position;
9
+ /** Current camera view box; candidates outside it are skipped. `null` disables clipping. */
10
+ bounds: Readonly<GeoJsonBbox> | null;
11
+ /** Nominal geo footprint every label is assumed to occupy, for collision testing. */
12
+ footprint: LabelFootprintDeg;
13
+ /** Maximum number of labels to place (DOM-node ceiling). */
14
+ ceiling: number;
15
+ }>;
16
+ /**
17
+ * Chooses which markers render a label so that no two labels overlap.
18
+ *
19
+ * Greedy placement: walks `items` in order, skips any outside the view box,
20
+ * and adds a label only when its nominal footprint does not collide with an
21
+ * already-placed one, stopping at `ceiling`. The budget is emergent — however
22
+ * many non-overlapping labels fit, bounded by the ceiling.
23
+ *
24
+ * Deterministic: identical input yields an identical set. Phase 1 places in the
25
+ * caller-provided order; priority ordering, dispersion, and pin-on-a-stick
26
+ * displacement layer on top in later phases.
27
+ */
28
+ export declare const buildLabelPlacement: <TAsset>({ items, getId, getPosition, bounds, footprint, ceiling, }: BuildLabelPlacementParams<TAsset>) => ReadonlySet<string>;
29
+ export {};
@@ -0,0 +1,37 @@
1
+ export type DegreesPerPixel = Readonly<{
2
+ lng: number;
3
+ lat: number;
4
+ }>;
5
+ /**
6
+ * Degrees of longitude/latitude spanned by a single screen pixel at `zoom`,
7
+ * near `latitudeDeg`, under Web Mercator.
8
+ *
9
+ * Longitude degrees-per-pixel is constant everywhere; latitude degrees-per-pixel
10
+ * scales by `cos(latitude)` because Mercator stretches higher latitudes. Used to
11
+ * convert a pixel label footprint into a geo bounding box so collision runs in
12
+ * the same lng/lat space the marker positions already live in.
13
+ */
14
+ export declare const degreesPerPixel: (zoom: number, latitudeDeg: number) => DegreesPerPixel;
15
+ export type LabelFootprintDeg = Readonly<{
16
+ widthDeg: number;
17
+ heightDeg: number;
18
+ }>;
19
+ export type LabelFootprintParams = Readonly<{
20
+ /** Current camera zoom. */
21
+ zoom: number;
22
+ /** Reference latitude (typically the viewport centre) for the lat-per-pixel scale. */
23
+ latitudeDeg: number;
24
+ /** Extra spacing added around every label — the density dial. */
25
+ paddingPx: number;
26
+ }>;
27
+ /**
28
+ * Upper-bound label footprint as a geo-space width/height, for collision testing.
29
+ *
30
+ * A rendered pill's size does NOT follow the zoom circle-size tier: `MapMarker`
31
+ * lays every pill out at the fixed {@link MARKER_PILL_CONTENT_LAYOUT_SIZE}, and the
32
+ * label text is capped at `maxLabelWidthPx` (ellipsis beyond). We size the collision
33
+ * box to that upper bound — widest pill that can render — so the box is never smaller
34
+ * than what paints, which is what keeps the "no two labels overlap" guarantee honest.
35
+ * Density is tuned via `paddingPx`, not by shrinking the box.
36
+ */
37
+ export declare const labelFootprintDeg: ({ zoom, latitudeDeg, paddingPx }: LabelFootprintParams) => LabelFootprintDeg;
@@ -0,0 +1,31 @@
1
+ import type { GeoJsonBbox } from "@trackunit/geo-json-utils";
2
+ type Position = readonly [number, number, ...Array<number>];
3
+ export type UseLabelPlacementParams<TAsset> = Readonly<{
4
+ /** When false the hook returns a stable empty set without computing placement. */
5
+ enabled: boolean;
6
+ /** Candidate markers, in the order they should be considered for a label. */
7
+ items: ReadonlyArray<TAsset>;
8
+ /** Stable id accessor — pass a referentially stable function (module const or `useCallback`). */
9
+ getId: (item: TAsset) => string;
10
+ /** Stable position accessor — pass a referentially stable function. */
11
+ getPosition: (item: TAsset) => Position;
12
+ /** Current camera view box; candidates outside it are skipped. `null` disables clipping. */
13
+ bounds: Readonly<GeoJsonBbox> | null;
14
+ /** Current camera zoom; drives the px→geo footprint scale. */
15
+ zoom: number;
16
+ /** Spacing added around every label footprint — the density dial. */
17
+ paddingPx: number;
18
+ /** Maximum number of labels to place (DOM-node ceiling). */
19
+ ceiling: number;
20
+ }>;
21
+ /**
22
+ * Viewport-driven label placement: returns the ids of the markers that should
23
+ * render a label such that no two labels overlap, bounded by `ceiling`.
24
+ *
25
+ * Derives an upper-bound pill footprint (`labelFootprintDeg`) and delegates
26
+ * selection to the pure `buildLabelPlacement`. Exposed as a hook so the placement
27
+ * participates in React's stability model and later phases can hold incumbency
28
+ * state here without changing the public surface.
29
+ */
30
+ export declare const useLabelPlacement: <TAsset>({ enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling, }: UseLabelPlacementParams<TAsset>) => ReadonlySet<string>;
31
+ export {};
@@ -1 +0,0 @@
1
- {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/map/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}