@trackunit/react-map 0.2.86 → 0.2.88

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
@@ -7757,9 +7757,16 @@ const shapesUnderCursor = (position, features, options) => {
7757
7757
  /**
7758
7758
  * Connected components of features that share fill area. Bbox prefilter then
7759
7759
  * exact polygon intersection, mirroring `countOverlappingShapes`.
7760
+ *
7761
+ * `getGeoJsonPolygonIntersection` delegates to `polygon-clipping`, which can throw
7762
+ * `"Unable to complete output ring"` for degenerate but schema-valid geometry
7763
+ * (SAGA-743). A throw here is treated as "no overlap" — the safe degradation for
7764
+ * grouping — and recorded in `intersectionFailures` so the hook can report it
7765
+ * (mirrors how `droppedInvalidFeatures` surfaces the sibling failure mode).
7760
7766
  */
7761
7767
  const buildOverlapGroups = (records) => {
7762
7768
  const parent = records.map((_, index) => index);
7769
+ const intersectionFailures = [];
7763
7770
  const find = (index) => {
7764
7771
  let root = index;
7765
7772
  while (parent[root] !== root)
@@ -7783,7 +7790,17 @@ const buildOverlapGroups = (records) => {
7783
7790
  continue;
7784
7791
  if (!bboxesIntersect$1(a.bbox, b.bbox))
7785
7792
  continue;
7786
- if (geoJsonUtils.getGeoJsonPolygonIntersection(a.geometry, b.geometry) !== null) {
7793
+ let overlaps;
7794
+ try {
7795
+ overlaps = geoJsonUtils.getGeoJsonPolygonIntersection(a.geometry, b.geometry) !== null;
7796
+ }
7797
+ catch (error) {
7798
+ // polygon-clipping failed on this pair. Treat as no overlap (do not union)
7799
+ // so tiling degrades to full fills rather than crashing the map.
7800
+ intersectionFailures.push({ featureIdA: a.id, featureIdB: b.id, error });
7801
+ continue;
7802
+ }
7803
+ if (overlaps) {
7787
7804
  union(i, j);
7788
7805
  }
7789
7806
  }
@@ -7799,7 +7816,7 @@ const buildOverlapGroups = (records) => {
7799
7816
  group.push(record);
7800
7817
  }
7801
7818
  });
7802
- return [...groups.values()].filter(group => group.length >= 2);
7819
+ return { groups: [...groups.values()].filter(group => group.length >= 2), intersectionFailures };
7803
7820
  };
7804
7821
  const buildContendedShapes = (group) => group.map(record => {
7805
7822
  const containedIn = [];
@@ -7943,7 +7960,12 @@ const computeFillTiling = (input) => {
7943
7960
  const fillGeometries = new Map();
7944
7961
  const featureToGroupKey = new Map();
7945
7962
  const featureZIndex = new Map();
7946
- const overlapGroups = buildOverlapGroups(records);
7963
+ const { groups: overlapGroups, intersectionFailures } = buildOverlapGroups(records);
7964
+ if (intersectionFailures.length > 0) {
7965
+ const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
7966
+ // eslint-disable-next-line no-console -- Intentional: warn devs when polygon-clipping fails on schema-valid geometry (SAGA-743); the pair is treated as non-overlapping and full fills still render
7967
+ console.warn(`[computeFillTiling] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping (full fill still renders): ${pairList}`);
7968
+ }
7947
7969
  for (const group of overlapGroups) {
7948
7970
  const contended = buildContendedShapes(group);
7949
7971
  const recordById = new Map(group.map(record => [record.id, record]));
@@ -8018,6 +8040,7 @@ const computeFillTiling = (input) => {
8018
8040
  featureToGroupKey,
8019
8041
  featureZIndex,
8020
8042
  droppedInvalidFeatures,
8043
+ intersectionFailures,
8021
8044
  };
8022
8045
  };
8023
8046
 
@@ -8323,6 +8346,31 @@ const buildShapeLabelResolutionContext = (feature, args) => {
8323
8346
  };
8324
8347
  };
8325
8348
 
8349
+ /** Stable, order-independent key so the pair (a,b) dedupes to the same entry as (b,a). */
8350
+ const resolveIntersectionFailureDedupeKey = (layerHandleId, failure) => {
8351
+ const [first, second] = [failure.featureIdA, failure.featureIdB].sort();
8352
+ return `${layerHandleId}:${first}|${second}`;
8353
+ };
8354
+ /**
8355
+ * Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
8356
+ * (SAGA-743), at most once per handle/pair per session.
8357
+ * Note: `computeFillTiling` emits `console.warn` separately.
8358
+ */
8359
+ const captureFillTilingIntersectionFailures = (intersectionFailures, layerHandleId, sessionReportedDedupeKeys, report) => {
8360
+ for (const failure of intersectionFailures) {
8361
+ const key = resolveIntersectionFailureDedupeKey(layerHandleId, failure);
8362
+ if (sessionReportedDedupeKeys.has(key))
8363
+ continue;
8364
+ sessionReportedDedupeKeys.add(key);
8365
+ report({
8366
+ layerHandleId,
8367
+ featureIdA: failure.featureIdA,
8368
+ featureIdB: failure.featureIdB,
8369
+ message: failure.error instanceof Error ? failure.error.message : String(failure.error),
8370
+ });
8371
+ }
8372
+ };
8373
+
8326
8374
  /**
8327
8375
  * Extract ALL vertex coordinates from a geometry, walking every part of
8328
8376
  * multi-geometries. Returns a flat array of [lng, lat] positions.
@@ -9577,6 +9625,7 @@ const bboxesIntersect = (a, b) => a[2] >= b[0] && a[0] <= b[2] && a[3] >= b[1] &
9577
9625
  */
9578
9626
  const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds) => {
9579
9627
  const counts = new Map();
9628
+ const intersectionFailures = [];
9580
9629
  for (const feature of features) {
9581
9630
  counts.set(feature, 0);
9582
9631
  }
@@ -9605,7 +9654,21 @@ const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds)
9605
9654
  const geomB = featureB.geometry;
9606
9655
  const isPolygonalB = geomB !== null && (geomB.type === "Polygon" || geomB.type === "MultiPolygon");
9607
9656
  if (isPolygonalA && isPolygonalB) {
9608
- if (geoJsonUtils.getGeoJsonPolygonIntersection(geomA, geomB) !== null) {
9657
+ let overlaps;
9658
+ try {
9659
+ overlaps = geoJsonUtils.getGeoJsonPolygonIntersection(geomA, geomB) !== null;
9660
+ }
9661
+ catch (error) {
9662
+ // polygon-clipping failed on this pair (SAGA-743). Treat as no overlap so
9663
+ // decoration counts degrade gracefully instead of crashing the map.
9664
+ intersectionFailures.push({
9665
+ featureIdA: featureA.id !== undefined ? String(featureA.id) : `index:${i}`,
9666
+ featureIdB: featureB.id !== undefined ? String(featureB.id) : `index:${j}`,
9667
+ error,
9668
+ });
9669
+ continue;
9670
+ }
9671
+ if (overlaps) {
9609
9672
  if (geoJsonUtils.isFullyContainedInGeoJsonGeometry(geomA, geomB) !== true) {
9610
9673
  counts.set(featureA, (counts.get(featureA) ?? 0) + 1);
9611
9674
  }
@@ -9620,7 +9683,7 @@ const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds)
9620
9683
  }
9621
9684
  }
9622
9685
  }
9623
- return counts;
9686
+ return { counts, intersectionFailures };
9624
9687
  };
9625
9688
  /**
9626
9689
  * Value-based equality for the nested style overrides maps. Used to skip
@@ -9706,6 +9769,25 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
9706
9769
  const previousLayoutSides = react.useRef(new Map());
9707
9770
  const prevStyleOverridesRef = react.useRef(new Map());
9708
9771
  const prevDecorationLayersRef = react.useRef([]);
9772
+ const errorHandler = reactCoreHooks.useErrorHandlerOrNull();
9773
+ // Session-level dedupe so a recurring polygon-clipping failure (SAGA-743) reports
9774
+ // to Sentry at most once per handle/pair per session, not on every pan frame.
9775
+ const sessionReportedIntersectionKeysRef = react.useRef(new Set());
9776
+ const reportOverlapIntersectionFailures = react.useCallback((intersectionFailures, layerHandleId) => {
9777
+ if (intersectionFailures.length === 0)
9778
+ return;
9779
+ const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
9780
+ // eslint-disable-next-line no-console -- Intentional: warn devs when polygon-clipping fails on schema-valid geometry (SAGA-743); the pair is treated as non-overlapping and decorations still render
9781
+ console.warn(`[useShapeDecorations] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping: ${pairList}`);
9782
+ if (errorHandler === null)
9783
+ return;
9784
+ captureFillTilingIntersectionFailures(intersectionFailures, layerHandleId, sessionReportedIntersectionKeysRef.current, payload => {
9785
+ errorHandler.captureException(new Error(`polygon-clipping failed during decoration overlap counting: ${JSON.stringify(payload)}`), {
9786
+ level: "warning",
9787
+ fingerprint: ["react-map", "fill-tiling", "intersection-failure"],
9788
+ });
9789
+ });
9790
+ }, [errorHandler]);
9709
9791
  /**
9710
9792
  * Labels that just transitioned from absent (hidden) to placed. While in
9711
9793
  * this set, edge identity is NOT stored — the algorithm gets one extra
@@ -9765,7 +9847,9 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
9765
9847
  let overlappingCountsCache = null;
9766
9848
  const getOverlappingCount = (feature) => {
9767
9849
  if (overlappingCountsCache === null) {
9768
- overlappingCountsCache = buildOverlappingShapesCountMap(handle.features.features, featureBboxes, bounds);
9850
+ const { counts, intersectionFailures } = buildOverlappingShapesCountMap(handle.features.features, featureBboxes, bounds);
9851
+ overlappingCountsCache = counts;
9852
+ reportOverlapIntersectionFailures(intersectionFailures, handle.id);
9769
9853
  }
9770
9854
  return overlappingCountsCache.get(feature) ?? 0;
9771
9855
  };
@@ -10026,6 +10110,7 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
10026
10110
  hasSourceReadiness,
10027
10111
  renderedSourceIds,
10028
10112
  isReady,
10113
+ reportOverlapIntersectionFailures,
10029
10114
  ]);
10030
10115
  const updateDecorationsRef = react.useRef(updateDecorations);
10031
10116
  react.useEffect(() => {
@@ -10558,7 +10643,7 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
10558
10643
  const nextRestingFills = new Map();
10559
10644
  const nextRestingZIndices = new Map();
10560
10645
  for (const handle of currentHandles) {
10561
- const { fillGeometries, featureToGroupKey, featureZIndex, droppedInvalidFeatures } = computeFillTiling({
10646
+ const { fillGeometries, featureToGroupKey, featureZIndex, droppedInvalidFeatures, intersectionFailures } = computeFillTiling({
10562
10647
  features: handle.features.features,
10563
10648
  viewportBounds: GLOBAL_BOUNDS,
10564
10649
  resolveStackOrder: handle.overlap?.resolveStackOrder ?? defaultShapeStackOrder,
@@ -10574,6 +10659,14 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
10574
10659
  });
10575
10660
  });
10576
10661
  }
10662
+ if (errorHandler !== null && intersectionFailures.length > 0) {
10663
+ captureFillTilingIntersectionFailures(intersectionFailures, handle.id, sessionReportedDedupeKeysRef.current, payload => {
10664
+ errorHandler.captureException(new Error(`polygon-clipping failed during fill-tiling overlap grouping: ${JSON.stringify(payload)}`), {
10665
+ level: "warning",
10666
+ fingerprint: ["react-map", "fill-tiling", "intersection-failure"],
10667
+ });
10668
+ });
10669
+ }
10577
10670
  featureToGroupKeyByHandleRef.current.set(handle.id, new Map(featureToGroupKey));
10578
10671
  if (fillGeometries.size > 0) {
10579
10672
  nextRestingFills.set(handle.id, fillGeometries);
package/index.esm.js CHANGED
@@ -7756,9 +7756,16 @@ const shapesUnderCursor = (position, features, options) => {
7756
7756
  /**
7757
7757
  * Connected components of features that share fill area. Bbox prefilter then
7758
7758
  * exact polygon intersection, mirroring `countOverlappingShapes`.
7759
+ *
7760
+ * `getGeoJsonPolygonIntersection` delegates to `polygon-clipping`, which can throw
7761
+ * `"Unable to complete output ring"` for degenerate but schema-valid geometry
7762
+ * (SAGA-743). A throw here is treated as "no overlap" — the safe degradation for
7763
+ * grouping — and recorded in `intersectionFailures` so the hook can report it
7764
+ * (mirrors how `droppedInvalidFeatures` surfaces the sibling failure mode).
7759
7765
  */
7760
7766
  const buildOverlapGroups = (records) => {
7761
7767
  const parent = records.map((_, index) => index);
7768
+ const intersectionFailures = [];
7762
7769
  const find = (index) => {
7763
7770
  let root = index;
7764
7771
  while (parent[root] !== root)
@@ -7782,7 +7789,17 @@ const buildOverlapGroups = (records) => {
7782
7789
  continue;
7783
7790
  if (!bboxesIntersect$1(a.bbox, b.bbox))
7784
7791
  continue;
7785
- if (getGeoJsonPolygonIntersection(a.geometry, b.geometry) !== null) {
7792
+ let overlaps;
7793
+ try {
7794
+ overlaps = getGeoJsonPolygonIntersection(a.geometry, b.geometry) !== null;
7795
+ }
7796
+ catch (error) {
7797
+ // polygon-clipping failed on this pair. Treat as no overlap (do not union)
7798
+ // so tiling degrades to full fills rather than crashing the map.
7799
+ intersectionFailures.push({ featureIdA: a.id, featureIdB: b.id, error });
7800
+ continue;
7801
+ }
7802
+ if (overlaps) {
7786
7803
  union(i, j);
7787
7804
  }
7788
7805
  }
@@ -7798,7 +7815,7 @@ const buildOverlapGroups = (records) => {
7798
7815
  group.push(record);
7799
7816
  }
7800
7817
  });
7801
- return [...groups.values()].filter(group => group.length >= 2);
7818
+ return { groups: [...groups.values()].filter(group => group.length >= 2), intersectionFailures };
7802
7819
  };
7803
7820
  const buildContendedShapes = (group) => group.map(record => {
7804
7821
  const containedIn = [];
@@ -7942,7 +7959,12 @@ const computeFillTiling = (input) => {
7942
7959
  const fillGeometries = new Map();
7943
7960
  const featureToGroupKey = new Map();
7944
7961
  const featureZIndex = new Map();
7945
- const overlapGroups = buildOverlapGroups(records);
7962
+ const { groups: overlapGroups, intersectionFailures } = buildOverlapGroups(records);
7963
+ if (intersectionFailures.length > 0) {
7964
+ const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
7965
+ // eslint-disable-next-line no-console -- Intentional: warn devs when polygon-clipping fails on schema-valid geometry (SAGA-743); the pair is treated as non-overlapping and full fills still render
7966
+ console.warn(`[computeFillTiling] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping (full fill still renders): ${pairList}`);
7967
+ }
7946
7968
  for (const group of overlapGroups) {
7947
7969
  const contended = buildContendedShapes(group);
7948
7970
  const recordById = new Map(group.map(record => [record.id, record]));
@@ -8017,6 +8039,7 @@ const computeFillTiling = (input) => {
8017
8039
  featureToGroupKey,
8018
8040
  featureZIndex,
8019
8041
  droppedInvalidFeatures,
8042
+ intersectionFailures,
8020
8043
  };
8021
8044
  };
8022
8045
 
@@ -8322,6 +8345,31 @@ const buildShapeLabelResolutionContext = (feature, args) => {
8322
8345
  };
8323
8346
  };
8324
8347
 
8348
+ /** Stable, order-independent key so the pair (a,b) dedupes to the same entry as (b,a). */
8349
+ const resolveIntersectionFailureDedupeKey = (layerHandleId, failure) => {
8350
+ const [first, second] = [failure.featureIdA, failure.featureIdB].sort();
8351
+ return `${layerHandleId}:${first}|${second}`;
8352
+ };
8353
+ /**
8354
+ * Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
8355
+ * (SAGA-743), at most once per handle/pair per session.
8356
+ * Note: `computeFillTiling` emits `console.warn` separately.
8357
+ */
8358
+ const captureFillTilingIntersectionFailures = (intersectionFailures, layerHandleId, sessionReportedDedupeKeys, report) => {
8359
+ for (const failure of intersectionFailures) {
8360
+ const key = resolveIntersectionFailureDedupeKey(layerHandleId, failure);
8361
+ if (sessionReportedDedupeKeys.has(key))
8362
+ continue;
8363
+ sessionReportedDedupeKeys.add(key);
8364
+ report({
8365
+ layerHandleId,
8366
+ featureIdA: failure.featureIdA,
8367
+ featureIdB: failure.featureIdB,
8368
+ message: failure.error instanceof Error ? failure.error.message : String(failure.error),
8369
+ });
8370
+ }
8371
+ };
8372
+
8325
8373
  /**
8326
8374
  * Extract ALL vertex coordinates from a geometry, walking every part of
8327
8375
  * multi-geometries. Returns a flat array of [lng, lat] positions.
@@ -9576,6 +9624,7 @@ const bboxesIntersect = (a, b) => a[2] >= b[0] && a[0] <= b[2] && a[3] >= b[1] &
9576
9624
  */
9577
9625
  const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds) => {
9578
9626
  const counts = new Map();
9627
+ const intersectionFailures = [];
9579
9628
  for (const feature of features) {
9580
9629
  counts.set(feature, 0);
9581
9630
  }
@@ -9604,7 +9653,21 @@ const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds)
9604
9653
  const geomB = featureB.geometry;
9605
9654
  const isPolygonalB = geomB !== null && (geomB.type === "Polygon" || geomB.type === "MultiPolygon");
9606
9655
  if (isPolygonalA && isPolygonalB) {
9607
- if (getGeoJsonPolygonIntersection(geomA, geomB) !== null) {
9656
+ let overlaps;
9657
+ try {
9658
+ overlaps = getGeoJsonPolygonIntersection(geomA, geomB) !== null;
9659
+ }
9660
+ catch (error) {
9661
+ // polygon-clipping failed on this pair (SAGA-743). Treat as no overlap so
9662
+ // decoration counts degrade gracefully instead of crashing the map.
9663
+ intersectionFailures.push({
9664
+ featureIdA: featureA.id !== undefined ? String(featureA.id) : `index:${i}`,
9665
+ featureIdB: featureB.id !== undefined ? String(featureB.id) : `index:${j}`,
9666
+ error,
9667
+ });
9668
+ continue;
9669
+ }
9670
+ if (overlaps) {
9608
9671
  if (isFullyContainedInGeoJsonGeometry(geomA, geomB) !== true) {
9609
9672
  counts.set(featureA, (counts.get(featureA) ?? 0) + 1);
9610
9673
  }
@@ -9619,7 +9682,7 @@ const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds)
9619
9682
  }
9620
9683
  }
9621
9684
  }
9622
- return counts;
9685
+ return { counts, intersectionFailures };
9623
9686
  };
9624
9687
  /**
9625
9688
  * Value-based equality for the nested style overrides maps. Used to skip
@@ -9705,6 +9768,25 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
9705
9768
  const previousLayoutSides = useRef(new Map());
9706
9769
  const prevStyleOverridesRef = useRef(new Map());
9707
9770
  const prevDecorationLayersRef = useRef([]);
9771
+ const errorHandler = useErrorHandlerOrNull();
9772
+ // Session-level dedupe so a recurring polygon-clipping failure (SAGA-743) reports
9773
+ // to Sentry at most once per handle/pair per session, not on every pan frame.
9774
+ const sessionReportedIntersectionKeysRef = useRef(new Set());
9775
+ const reportOverlapIntersectionFailures = useCallback((intersectionFailures, layerHandleId) => {
9776
+ if (intersectionFailures.length === 0)
9777
+ return;
9778
+ const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
9779
+ // eslint-disable-next-line no-console -- Intentional: warn devs when polygon-clipping fails on schema-valid geometry (SAGA-743); the pair is treated as non-overlapping and decorations still render
9780
+ console.warn(`[useShapeDecorations] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping: ${pairList}`);
9781
+ if (errorHandler === null)
9782
+ return;
9783
+ captureFillTilingIntersectionFailures(intersectionFailures, layerHandleId, sessionReportedIntersectionKeysRef.current, payload => {
9784
+ errorHandler.captureException(new Error(`polygon-clipping failed during decoration overlap counting: ${JSON.stringify(payload)}`), {
9785
+ level: "warning",
9786
+ fingerprint: ["react-map", "fill-tiling", "intersection-failure"],
9787
+ });
9788
+ });
9789
+ }, [errorHandler]);
9708
9790
  /**
9709
9791
  * Labels that just transitioned from absent (hidden) to placed. While in
9710
9792
  * this set, edge identity is NOT stored — the algorithm gets one extra
@@ -9764,7 +9846,9 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
9764
9846
  let overlappingCountsCache = null;
9765
9847
  const getOverlappingCount = (feature) => {
9766
9848
  if (overlappingCountsCache === null) {
9767
- overlappingCountsCache = buildOverlappingShapesCountMap(handle.features.features, featureBboxes, bounds);
9849
+ const { counts, intersectionFailures } = buildOverlappingShapesCountMap(handle.features.features, featureBboxes, bounds);
9850
+ overlappingCountsCache = counts;
9851
+ reportOverlapIntersectionFailures(intersectionFailures, handle.id);
9768
9852
  }
9769
9853
  return overlappingCountsCache.get(feature) ?? 0;
9770
9854
  };
@@ -10025,6 +10109,7 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
10025
10109
  hasSourceReadiness,
10026
10110
  renderedSourceIds,
10027
10111
  isReady,
10112
+ reportOverlapIntersectionFailures,
10028
10113
  ]);
10029
10114
  const updateDecorationsRef = useRef(updateDecorations);
10030
10115
  useEffect(() => {
@@ -10557,7 +10642,7 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
10557
10642
  const nextRestingFills = new Map();
10558
10643
  const nextRestingZIndices = new Map();
10559
10644
  for (const handle of currentHandles) {
10560
- const { fillGeometries, featureToGroupKey, featureZIndex, droppedInvalidFeatures } = computeFillTiling({
10645
+ const { fillGeometries, featureToGroupKey, featureZIndex, droppedInvalidFeatures, intersectionFailures } = computeFillTiling({
10561
10646
  features: handle.features.features,
10562
10647
  viewportBounds: GLOBAL_BOUNDS,
10563
10648
  resolveStackOrder: handle.overlap?.resolveStackOrder ?? defaultShapeStackOrder,
@@ -10573,6 +10658,14 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
10573
10658
  });
10574
10659
  });
10575
10660
  }
10661
+ if (errorHandler !== null && intersectionFailures.length > 0) {
10662
+ captureFillTilingIntersectionFailures(intersectionFailures, handle.id, sessionReportedDedupeKeysRef.current, payload => {
10663
+ errorHandler.captureException(new Error(`polygon-clipping failed during fill-tiling overlap grouping: ${JSON.stringify(payload)}`), {
10664
+ level: "warning",
10665
+ fingerprint: ["react-map", "fill-tiling", "intersection-failure"],
10666
+ });
10667
+ });
10668
+ }
10576
10669
  featureToGroupKeyByHandleRef.current.set(handle.id, new Map(featureToGroupKey));
10577
10670
  if (fillGeometries.size > 0) {
10578
10671
  nextRestingFills.set(handle.id, fillGeometries);
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@trackunit/react-map",
3
- "version": "0.2.86",
3
+ "version": "0.2.88",
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.10.7",
10
+ "@trackunit/react-components": "2.10.9",
11
11
  "@trackunit/css-class-variance-utilities": "1.14.17",
12
- "@trackunit/react-form-components": "2.6.12",
13
- "@trackunit/react-core-hooks": "1.21.14",
14
- "@trackunit/geo-json-utils": "1.15.18",
15
- "@trackunit/i18n-library-translation": "2.4.14",
12
+ "@trackunit/react-form-components": "2.6.14",
13
+ "@trackunit/react-core-hooks": "1.21.16",
14
+ "@trackunit/geo-json-utils": "1.15.19",
15
+ "@trackunit/i18n-library-translation": "2.4.16",
16
16
  "react-minimal-pie-chart": "^8.4.0",
17
- "@trackunit/react-map-adapter-shared": "0.0.98",
17
+ "@trackunit/react-map-adapter-shared": "0.0.99",
18
18
  "@trackunit/react-map-color-utils": "0.0.80",
19
19
  "@trackunit/ui-design-tokens": "1.15.7",
20
20
  "@floating-ui/react": "^0.26.25",
@@ -0,0 +1,9 @@
1
+ import type { FillTilingIntersectionFailure } from "./shapeFillTiling";
2
+ type ReportFillTilingIntersectionFailureEvent = (payload: Record<string, unknown>) => void;
3
+ /**
4
+ * Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
5
+ * (SAGA-743), at most once per handle/pair per session.
6
+ * Note: `computeFillTiling` emits `console.warn` separately.
7
+ */
8
+ export declare const captureFillTilingIntersectionFailures: (intersectionFailures: ReadonlyArray<FillTilingIntersectionFailure>, layerHandleId: string, sessionReportedDedupeKeys: Set<string>, report: ReportFillTilingIntersectionFailureEvent) => void;
9
+ export {};
@@ -70,12 +70,25 @@ export type FillTilingResult = Readonly<{
70
70
  featureZIndex: ReadonlyMap<string, number>;
71
71
  /** Features excluded from fill tiling when boundary schema validation fails. */
72
72
  droppedInvalidFeatures: ReadonlyArray<DroppedInvalidFillTilingFeature>;
73
+ /**
74
+ * Feature pairs whose overlap test threw inside `polygon-clipping` (SAGA-743).
75
+ * The pair was treated as non-overlapping; surfaced here so the hook can report
76
+ * it to Sentry, since the geometry is schema-valid and never hits
77
+ * `droppedInvalidFeatures`.
78
+ */
79
+ intersectionFailures: ReadonlyArray<FillTilingIntersectionFailure>;
73
80
  }>;
74
81
  export type DroppedInvalidFillTilingFeature = Readonly<{
75
82
  feature: GeoJsonFeature;
76
83
  /** `safeParse` failure details (`parsed.error.format()`). */
77
84
  validationError: unknown;
78
85
  }>;
86
+ export type FillTilingIntersectionFailure = Readonly<{
87
+ featureIdA: string;
88
+ featureIdB: string;
89
+ /** The error thrown by `polygon-clipping` (typically "Unable to complete output ring"). */
90
+ error: unknown;
91
+ }>;
79
92
  /**
80
93
  * Planar area of any GeoJSON geometry (outer rings minus holes). Returns 0 for
81
94
  * non-polygonal types (Point, LineString, …). Relative magnitude only — not