@trackunit/react-map 0.2.227 → 0.2.234-alpha-4632fb37afe.0

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.
Files changed (48) hide show
  1. package/index.cjs.js +110 -52
  2. package/index.esm.js +110 -52
  3. package/package.json +11 -11
  4. package/src/draw/useDraw.d.ts +2 -1
  5. package/src/layers/shared/consoleWarnMap.d.ts +7 -0
  6. package/src/layers/shared/useDevWarnings.d.ts +14 -0
  7. package/src/layers/useHeatmap/useHeatmap.d.ts +2 -1
  8. package/src/layers/useHeatmap/utils/normalizeHeatmapData.d.ts +4 -1
  9. package/src/layers/useHeatmap/utils/resolveHeatmapStyle.d.ts +8 -2
  10. package/src/layers/useShapes/captureDroppedInvalidFillTilingFeatures.d.ts +2 -1
  11. package/src/layers/useShapes/captureFillTilingIntersectionFailures.d.ts +2 -1
  12. package/src/layers/useShapes/utils/formatDroppedInvalidFeatureIds.d.ts +3 -0
  13. package/src/layers/useShapes/utils/formatIntersectionFailurePairs.d.ts +3 -0
  14. package/translation.cjs.js +2 -0
  15. package/translation.esm.js +2 -0
  16. package/translation10.cjs.js +2 -0
  17. package/translation10.esm.js +2 -0
  18. package/translation11.cjs.js +2 -0
  19. package/translation11.esm.js +2 -0
  20. package/translation12.cjs.js +2 -0
  21. package/translation12.esm.js +2 -0
  22. package/translation13.cjs.js +2 -0
  23. package/translation13.esm.js +2 -0
  24. package/translation14.cjs.js +2 -0
  25. package/translation14.esm.js +2 -0
  26. package/translation15.cjs.js +2 -0
  27. package/translation15.esm.js +2 -0
  28. package/translation16.cjs.js +2 -0
  29. package/translation16.esm.js +2 -0
  30. package/translation17.cjs.js +2 -0
  31. package/translation17.esm.js +2 -0
  32. package/translation2.cjs.js +2 -0
  33. package/translation2.esm.js +2 -0
  34. package/translation3.cjs.js +2 -0
  35. package/translation3.esm.js +2 -0
  36. package/translation4.cjs.js +2 -0
  37. package/translation4.esm.js +2 -0
  38. package/translation5.cjs.js +2 -0
  39. package/translation5.esm.js +2 -0
  40. package/translation6.cjs.js +2 -0
  41. package/translation6.esm.js +2 -0
  42. package/translation7.cjs.js +2 -0
  43. package/translation7.esm.js +2 -0
  44. package/translation8.cjs.js +2 -0
  45. package/translation8.esm.js +2 -0
  46. package/translation9.cjs.js +2 -0
  47. package/translation9.esm.js +2 -0
  48. package/migrations/entry.js.map +0 -1
package/index.cjs.js CHANGED
@@ -3455,8 +3455,17 @@ const useControls = (base, areas) => {
3455
3455
  /**
3456
3456
  * Single dev-warning channel for react-map hooks (invalid data, invalid style, unhandled render errors).
3457
3457
  * Messages are prefixed with the emitting hook, e.g. `[useHeatmap]`.
3458
+ *
3459
+ * Skipped in production builds: the warnings describe consumer mistakes a developer fixes
3460
+ * once, and with polled data they would otherwise log on every data change. Vitest runs
3461
+ * with `NODE_ENV` set to `test`, so specs can still spy on `console.warn`.
3462
+ *
3463
+ * Call it from effects, handlers and callbacks, never during render: pure normalizers
3464
+ * return their warnings and `useDevWarnings` emits them once per result.
3458
3465
  */
3459
3466
  const consoleWarnMap = (source, message, ...details) => {
3467
+ if (process.env.NODE_ENV === "production")
3468
+ return;
3460
3469
  // eslint-disable-next-line no-console -- Intentional: the one react-map warning channel; invalid input is dropped or defaulted and render errors without onError must stay visible
3461
3470
  console.warn(`[${source}] ${message}`, ...details);
3462
3471
  };
@@ -5913,17 +5922,39 @@ const createLazyGetter = (compute) => {
5913
5922
  };
5914
5923
  };
5915
5924
 
5925
+ /** Shared empty result so hooks with nothing to report keep a stable reference. */
5926
+ const NO_DEV_WARNINGS = [];
5927
+ /**
5928
+ * Emits a normalizer's warnings through the react-map dev-warning channel, from an
5929
+ * effect rather than during render.
5930
+ *
5931
+ * Pure normalizers (`filterPositioned`, `normalizeHeatmapData`, ...) run inside render
5932
+ * and must not log: StrictMode and discarded concurrent renders would log them twice.
5933
+ * They return their warnings instead, and this hook emits each list once. The list's
5934
+ * identity is the dedupe key, so StrictMode's double effect run and re-renders that
5935
+ * keep the same normalized result add nothing; a new normalization pass (a new list)
5936
+ * warns again, which is what a developer feeding new invalid data expects.
5937
+ */
5938
+ const useDevWarnings = (source, warnings) => {
5939
+ const emittedRef = react.useRef(null);
5940
+ react.useEffect(() => {
5941
+ if (warnings.length === 0 || emittedRef.current === warnings)
5942
+ return;
5943
+ emittedRef.current = warnings;
5944
+ for (const message of warnings) {
5945
+ consoleWarnMap(source, message);
5946
+ }
5947
+ }, [source, warnings]);
5948
+ };
5949
+
5916
5950
  // ============================================================================
5917
5951
  // Constants
5918
5952
  // ============================================================================
5919
5953
  const EMPTY_CONTROLS$3 = {};
5920
5954
  const EMPTY_FEATURES$1 = { type: "FeatureCollection", features: [] };
5921
- // ============================================================================
5922
- // Internal helpers
5923
- // ============================================================================
5924
5955
  /**
5925
5956
  * Filter waypoints to only valid GeoJSON positions. Invalid entries are dropped.
5926
- * Logs console.warn once when any waypoints are filtered.
5957
+ * Pure: it runs during render, so it returns one grouped warning instead of logging.
5927
5958
  */
5928
5959
  const filterValidWaypoints = (waypoints) => {
5929
5960
  const valid = [];
@@ -5933,12 +5964,11 @@ const filterValidWaypoints = (waypoints) => {
5933
5964
  valid.push(result.data);
5934
5965
  }
5935
5966
  }
5936
- if (valid.length < waypoints.length) {
5937
- const filtered = waypoints.length - valid.length;
5938
- // eslint-disable-next-line no-console -- Intentional: warn devs when invalid waypoints are passed; invalid entries are filtered out
5939
- console.warn(`[useRoute] Filtered ${filtered} invalid waypoint(s). Expected [longitude, latitude] with latitude between -90 and 90.`);
5940
- }
5941
- return valid;
5967
+ const filtered = waypoints.length - valid.length;
5968
+ const warnings = filtered > 0
5969
+ ? [`Filtered ${filtered} invalid waypoint(s). Expected [longitude, latitude] with latitude between -90 and 90.`]
5970
+ : NO_DEV_WARNINGS;
5971
+ return { waypoints: valid, warnings };
5942
5972
  };
5943
5973
  /**
5944
5974
  * Convert waypoints to a GeoJSON FeatureCollection with a single LineString.
@@ -5986,7 +6016,9 @@ const useRoute = (options) => {
5986
6016
  // ---- Data readiness tracking ----
5987
6017
  const layerReady = useLayerReady(loading);
5988
6018
  // ---- Filter to valid waypoints only ----
5989
- const waypoints = react.useMemo(() => filterValidWaypoints(rawWaypoints), [rawWaypoints]);
6019
+ const filtered = react.useMemo(() => filterValidWaypoints(rawWaypoints), [rawWaypoints]);
6020
+ const waypoints = filtered.waypoints;
6021
+ useDevWarnings("useRoute", filtered.warnings);
5990
6022
  // ---- Convert to GeoJSON ----
5991
6023
  const features = react.useMemo(() => toLineStringFeatureCollection(id, waypoints), [id, waypoints]);
5992
6024
  // ---- Lazy getBounds with cache ----
@@ -6101,8 +6133,7 @@ const useFitToContent = (api, handles, options) => {
6101
6133
  const stillPending = filterHandles(handlesRef.current, "initial").filter(h => h.loadsData && h.loading);
6102
6134
  if (stillPending.length > 0) {
6103
6135
  const names = stillPending.map(h => h.name).join(", ");
6104
- // eslint-disable-next-line no-console -- Intentional: warn devs when layers are too slow to load
6105
- console.warn(`[useFitToContent] Timeout (${timeout}ms) reached. Fitting with available data. Still loading: ${names}`);
6136
+ consoleWarnMap("useFitToContent", `Timeout (${timeout}ms) reached. Fitting with available data. Still loading: ${names}`);
6106
6137
  }
6107
6138
  performFit();
6108
6139
  }, timeout);
@@ -6134,7 +6165,8 @@ const useFitToContent = (api, handles, options) => {
6134
6165
  * Converts consumer items into weighted GeoJSON `Point` features.
6135
6166
  *
6136
6167
  * Null positions and zero weights are dropped silently. Invalid positions and
6137
- * negative or non-finite weights are dropped with one grouped warning per pass.
6168
+ * negative or non-finite weights are dropped and reported as one grouped warning
6169
+ * per pass in `warnings`; the hook emits it from an effect, never during render.
6138
6170
  */
6139
6171
  const normalizeHeatmapData = ({ data, getPosition, getWeight, }) => {
6140
6172
  const features = [];
@@ -6164,12 +6196,15 @@ const normalizeHeatmapData = ({ data, getPosition, getWeight, }) => {
6164
6196
  properties: { weight },
6165
6197
  });
6166
6198
  }
6167
- if (invalidPositionCount > 0 || invalidWeightCount > 0) {
6168
- consoleWarnMap("useHeatmap", `Filtered ${invalidPositionCount} item(s) with invalid position and ${invalidWeightCount} item(s) with invalid weight. Expected [longitude, latitude] with longitude between -180 and 180, latitude between -90 and 90, and a finite weight >= 0.`);
6169
- }
6199
+ const warnings = invalidPositionCount > 0 || invalidWeightCount > 0
6200
+ ? [
6201
+ `Filtered ${invalidPositionCount} item(s) with invalid position and ${invalidWeightCount} item(s) with invalid weight. Expected [longitude, latitude] with longitude between -180 and 180, latitude between -90 and 90, and a finite weight >= 0.`,
6202
+ ]
6203
+ : NO_DEV_WARNINGS;
6170
6204
  return {
6171
6205
  features: { type: "FeatureCollection", features },
6172
6206
  positions,
6207
+ warnings,
6173
6208
  };
6174
6209
  };
6175
6210
 
@@ -6223,13 +6258,15 @@ const DEFAULT_HEATMAP_COLOR_RANGE = [
6223
6258
  const DEFAULT_PARSED_COLOR_RANGE = DEFAULT_HEATMAP_COLOR_RANGE.map(parseHeatmapColor).filter(sharedUtils.nonNullable);
6224
6259
  const resolveColorRange = (colorRange) => {
6225
6260
  if (colorRange === undefined)
6226
- return DEFAULT_PARSED_COLOR_RANGE;
6261
+ return { colorRange: DEFAULT_PARSED_COLOR_RANGE, warning: null };
6227
6262
  const parsed = colorRange.map(parseHeatmapColor);
6228
6263
  const valid = parsed.filter(sharedUtils.nonNullable);
6229
6264
  if (colorRange.length >= 2 && valid.length === parsed.length)
6230
- return valid;
6231
- consoleWarnMap("useHeatmap", "colorRange needs at least two supported colours (#rgb, #rrggbb, #rrggbbaa, rgb(), rgba()); using the default.", colorRange);
6232
- return DEFAULT_PARSED_COLOR_RANGE;
6265
+ return { colorRange: valid, warning: null };
6266
+ return {
6267
+ colorRange: DEFAULT_PARSED_COLOR_RANGE,
6268
+ warning: `colorRange needs at least two supported colours (#rgb, #rrggbb, #rrggbbaa, rgb(), rgba()); using the default. Received: ${JSON.stringify(colorRange)}`,
6269
+ };
6233
6270
  };
6234
6271
  const HEATMAP_STYLE_RULES = {
6235
6272
  radiusPixels: { defaultValue: 30, min: 1, max: 100 },
@@ -6239,7 +6276,8 @@ const HEATMAP_STYLE_RULES = {
6239
6276
  /**
6240
6277
  * Applies defaults and supported ranges so both adapters receive identical style values.
6241
6278
  *
6242
- * Finite values are clamped; non-finite values fall back to the default with one warning.
6279
+ * Finite values are clamped; non-finite values fall back to the default with one grouped
6280
+ * warning. Pure: it runs inside a render-time `useMemo`, so it returns its warnings.
6243
6281
  */
6244
6282
  const resolveHeatmapStyle = (style) => {
6245
6283
  const nonFiniteKeys = [];
@@ -6254,16 +6292,20 @@ const resolveHeatmapStyle = (style) => {
6254
6292
  }
6255
6293
  return Math.min(rule.max, Math.max(rule.min, value));
6256
6294
  };
6295
+ const colorRange = resolveColorRange(style?.colorRange);
6257
6296
  const resolved = {
6258
6297
  radiusPixels: resolveValue("radiusPixels"),
6259
6298
  intensity: resolveValue("intensity"),
6260
6299
  opacity: resolveValue("opacity"),
6261
- colorRange: resolveColorRange(style?.colorRange),
6300
+ colorRange: colorRange.colorRange,
6262
6301
  };
6302
+ const warnings = [];
6263
6303
  if (nonFiniteKeys.length > 0) {
6264
- consoleWarnMap("useHeatmap", `Non-finite style value(s) for ${nonFiniteKeys.join(", ")} replaced with defaults.`);
6304
+ warnings.push(`Non-finite style value(s) for ${nonFiniteKeys.join(", ")} replaced with defaults.`);
6265
6305
  }
6266
- return resolved;
6306
+ if (colorRange.warning !== null)
6307
+ warnings.push(colorRange.warning);
6308
+ return { style: resolved, warnings: warnings.length === 0 ? NO_DEV_WARNINGS : warnings };
6267
6309
  };
6268
6310
 
6269
6311
  const EMPTY_CONTROLS$2 = {};
@@ -6325,12 +6367,17 @@ const useHeatmap = (options) => {
6325
6367
  if (style?.colorRange !== colorRange && !esToolkit.isEqual(style?.colorRange, colorRange)) {
6326
6368
  setColorRange(style?.colorRange);
6327
6369
  }
6328
- const resolvedStyle = react.useMemo(() => resolveHeatmapStyle({
6370
+ const resolved = react.useMemo(() => resolveHeatmapStyle({
6329
6371
  radiusPixels: style?.radiusPixels,
6330
6372
  intensity: style?.intensity,
6331
6373
  opacity: style?.opacity,
6332
6374
  colorRange,
6333
6375
  }), [style?.radiusPixels, style?.intensity, style?.opacity, colorRange]);
6376
+ const resolvedStyle = resolved.style;
6377
+ // Normalization and style resolution run during render and stay pure; their
6378
+ // warnings are emitted once per pass from an effect (SAGA-907).
6379
+ useDevWarnings("useHeatmap", normalized.warnings);
6380
+ useDevWarnings("useHeatmap", resolved.warnings);
6334
6381
  const getBounds = react.useMemo(() => createLazyGetter(() => computeBoundsFromPositions(normalized.positions)), [normalized.positions]);
6335
6382
  const counts = react.useMemo(() => ({ total: data.length, points: normalized.features.features.length }), [data.length, normalized.features.features.length]);
6336
6383
  return react.useMemo(() => ({
@@ -7396,7 +7443,8 @@ const EMPTY_FEATURES = { type: "FeatureCollection", features: [] };
7396
7443
  const EMPTY_CONTROLS$1 = {};
7397
7444
  /**
7398
7445
  * Filter items to only those with valid positions and extract id + position.
7399
- * Invalid positions (e.g. latitude out of range) are skipped. Logs once when any are filtered.
7446
+ * Invalid positions (e.g. latitude out of range) are skipped. Pure: it runs during
7447
+ * render, so it returns one grouped warning instead of logging.
7400
7448
  */
7401
7449
  const filterPositioned = (data, getPosition, getId) => {
7402
7450
  const result = [];
@@ -7413,11 +7461,12 @@ const filterPositioned = (data, getPosition, getId) => {
7413
7461
  }
7414
7462
  }
7415
7463
  }
7416
- if (invalidCount > 0) {
7417
- // eslint-disable-next-line no-console -- Intentional: warn devs when getPosition returns invalid GeoJSON; items are filtered out
7418
- console.warn(`[useMarkers] Filtered ${invalidCount} item(s) with invalid position. Expected [longitude, latitude] with latitude between -90 and 90.`);
7419
- }
7420
- return result;
7464
+ const warnings = invalidCount > 0
7465
+ ? [
7466
+ `Filtered ${invalidCount} item(s) with invalid position. Expected [longitude, latitude] with latitude between -90 and 90.`,
7467
+ ]
7468
+ : NO_DEV_WARNINGS;
7469
+ return { items: result, warnings };
7421
7470
  };
7422
7471
  /**
7423
7472
  * Convert positioned items to a GeoJSON FeatureCollection of Points.
@@ -7521,11 +7570,13 @@ const useMarkers = (options) => {
7521
7570
  // structurally-identical inline arrays do NOT trigger infinite re-renders.
7522
7571
  // The reference check short-circuits isEqual for the common stable-ref case.
7523
7572
  const [prevData, setPrevData] = react.useState(data);
7524
- const [positionedItems, setPositionedItems] = react.useState(() => filterPositioned(data, getPosition, getId));
7573
+ const [positioned, setPositioned] = react.useState(() => filterPositioned(data, getPosition, getId));
7525
7574
  if (data !== prevData && !esToolkit.isEqual(data, prevData)) {
7526
7575
  setPrevData(data);
7527
- setPositionedItems(filterPositioned(data, getPosition, getId));
7576
+ setPositioned(filterPositioned(data, getPosition, getId));
7528
7577
  }
7578
+ const positionedItems = positioned.items;
7579
+ useDevWarnings("useMarkers", positioned.warnings);
7529
7580
  // ---- Convert to GeoJSON FeatureCollection ----
7530
7581
  const features = react.useMemo(() => toPointFeatureCollection(positionedItems), [positionedItems]);
7531
7582
  // ---- Resolved server clusters (after resolveGroups entity-shape decisions) ----
@@ -9379,14 +9430,8 @@ const computeFillTiling = (input) => {
9379
9430
  }
9380
9431
  validFeatures.push(feature);
9381
9432
  }
9382
- if (droppedInvalidFeatures.length > 0) {
9383
- const idList = droppedInvalidFeatures
9384
- .map(entry => (entry.feature.id !== undefined ? String(entry.feature.id) : undefined))
9385
- .filter((id) => id !== undefined)
9386
- .join(", ") || "(no feature ids)";
9387
- // eslint-disable-next-line no-console -- Intentional: warn devs when schema-invalid polygonal geometry is excluded from fill tiling
9388
- console.warn(`[computeFillTiling] Excluded ${droppedInvalidFeatures.length} feature(s) with invalid polygonal geometry from fill tiling (full fill still renders; overlap clipping is skipped): ${idList}`);
9389
- }
9433
+ // Dropped features are returned, not logged: this function runs inside the hook's
9434
+ // recompute path and stays pure; the hook emits one dev warning per pass.
9390
9435
  // Densify before any polygon/boundary math so clip geometry follows great-circle
9391
9436
  // arcs on large polygons (mirrors the ADR-0024 precedent for edge labels). Always
9392
9437
  // defer to densifyGeodesicFeatures' per-feature resolution (geodesic override >
@@ -9419,11 +9464,7 @@ const computeFillTiling = (input) => {
9419
9464
  const featureToGroupKey = new Map();
9420
9465
  const featureZIndex = new Map();
9421
9466
  const { groups: overlapGroups, intersectionFailures } = buildOverlapGroups(records);
9422
- if (intersectionFailures.length > 0) {
9423
- const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
9424
- // 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
9425
- console.warn(`[computeFillTiling] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping (full fill still renders): ${pairList}`);
9426
- }
9467
+ // Failures are returned for the hook to warn about and report (SAGA-743).
9427
9468
  for (const group of overlapGroups) {
9428
9469
  const contended = buildContendedShapes(group);
9429
9470
  const recordById = new Map(group.map(record => [record.id, record]));
@@ -9502,6 +9543,9 @@ const computeFillTiling = (input) => {
9502
9543
  };
9503
9544
  };
9504
9545
 
9546
+ /** Renders failing feature pairs as `a↔b, c↔d` for dev warnings and error reports. */
9547
+ const formatIntersectionFailurePairs = (failures) => failures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
9548
+
9505
9549
  const DEFAULT_ZOOM = 0;
9506
9550
  const DEFAULT_TILE_SIZE = 256;
9507
9551
  /**
@@ -9812,7 +9856,8 @@ const resolveIntersectionFailureDedupeKey = (layerHandleId, failure) => {
9812
9856
  /**
9813
9857
  * Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
9814
9858
  * (SAGA-743), at most once per handle/pair per session.
9815
- * Note: `computeFillTiling` emits `console.warn` separately.
9859
+ * Note: the dev warning for this case is emitted by `useShapeFillTiling` from its recompute
9860
+ * path (`computeFillTiling` itself is pure and logs nothing).
9816
9861
  */
9817
9862
  const captureFillTilingIntersectionFailures = (intersectionFailures, layerHandleId, sessionReportedDedupeKeys, report) => {
9818
9863
  for (const failure of intersectionFailures) {
@@ -11234,9 +11279,7 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
11234
11279
  const reportOverlapIntersectionFailures = react.useCallback((intersectionFailures, layerHandleId) => {
11235
11280
  if (intersectionFailures.length === 0)
11236
11281
  return;
11237
- const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
11238
- // 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
11239
- console.warn(`[useShapeDecorations] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping: ${pairList}`);
11282
+ consoleWarnMap("useShapeDecorations", `polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping: ${formatIntersectionFailurePairs(intersectionFailures)}`);
11240
11283
  if (errorHandler === null)
11241
11284
  return;
11242
11285
  captureFillTilingIntersectionFailures(intersectionFailures, layerHandleId, sessionReportedIntersectionKeysRef.current, payload => {
@@ -11659,7 +11702,8 @@ const captureOnce = (key, report, sessionReportedDedupeKeys, payload) => {
11659
11702
  };
11660
11703
  /**
11661
11704
  * Report to Sentry each dropped invalid feature via `report`, at most once per handle/feature per session.
11662
- * Note: `computeFillTiling` emits `console.warn` separately.
11705
+ * Note: the dev warning for this case is emitted by `useShapeFillTiling` from its recompute
11706
+ * path (`computeFillTiling` itself is pure and logs nothing).
11663
11707
  */
11664
11708
  const captureDroppedInvalidFillTilingFeatures = (droppedInvalidFeatures, layerHandleId, sessionReportedDedupeKeys, report) => {
11665
11709
  for (const entry of droppedInvalidFeatures) {
@@ -11673,6 +11717,12 @@ const captureDroppedInvalidFillTilingFeatures = (droppedInvalidFeatures, layerHa
11673
11717
  }
11674
11718
  };
11675
11719
 
11720
+ /** Lists the ids of features dropped from fill tiling, or a placeholder when none carry an id. */
11721
+ const formatDroppedInvalidFeatureIds = (dropped) => dropped
11722
+ .map(entry => (entry.feature.id !== undefined ? String(entry.feature.id) : undefined))
11723
+ .filter(sharedUtils.nonNullable)
11724
+ .join(", ") || "(no feature ids)";
11725
+
11676
11726
  // ============================================================================
11677
11727
  // Default resolver
11678
11728
  // ============================================================================
@@ -12109,6 +12159,14 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
12109
12159
  layerGeodesic: handle.style.geodesic,
12110
12160
  featureStyles: handle.featureStyles,
12111
12161
  });
12162
+ // The dev warnings live here, not in computeFillTiling: recompute runs from
12163
+ // effects and idle callbacks, so the pure function stays silent (SAGA-907).
12164
+ if (droppedInvalidFeatures.length > 0) {
12165
+ consoleWarnMap("useShapes", `Excluded ${droppedInvalidFeatures.length} feature(s) with invalid polygonal geometry from fill tiling (full fill still renders; overlap clipping is skipped): ${formatDroppedInvalidFeatureIds(droppedInvalidFeatures)}`);
12166
+ }
12167
+ if (intersectionFailures.length > 0) {
12168
+ consoleWarnMap("useShapes", `polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping (full fill still renders): ${formatIntersectionFailurePairs(intersectionFailures)}`);
12169
+ }
12112
12170
  if (errorHandler !== null && droppedInvalidFeatures.length > 0) {
12113
12171
  captureDroppedInvalidFillTilingFeatures(droppedInvalidFeatures, handle.id, sessionReportedDedupeKeysRef.current, payload => {
12114
12172
  errorHandler.captureException(new Error(`Schema-invalid polygonal geometry excluded from fill tiling: ${JSON.stringify(payload)}`), {