@chrtco/sdk 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -2600,20 +2600,37 @@ declare function worldMinusBboxPolygon(bbox: BBox): GeoJSON.Polygon;
2600
2600
  * `band-collapse.test.ts` re-proves it on every CI run by differentially
2601
2601
  * evaluating legacy-vs-collapsed through MapLibre's real three gates.
2602
2602
  *
2603
- * **Scope.** This is axis 1 ONLY: the per-band layers survive, with identical
2604
- * ids and identical positions, so the band-major draw order is untouched.
2605
- * "Axis 2" (collapsing the per-band copies into one layer per class) was
2606
- * evaluated and **KILLED** — it is unsound for the opaque-fill classes
2603
+ * **Scope.** For FEATURE layers (fills, lines, feature symbols) this is
2604
+ * axis 1 ONLY: the per-band layers survive, with identical ids and identical
2605
+ * positions, so the band-major draw order is untouched. "Axis 2" (collapsing
2606
+ * the per-band copies into one layer per class) was evaluated and **KILLED**
2607
+ * for feature layers — it is unsound for the opaque-fill classes
2607
2608
  * (`DOMINANCE_EXEMPT_FILL_CLASSES` ≡ `OPAQUE_FILL_LAYERS`), where band-major
2608
2609
  * order is the ONLY thing separating the bands, and 15/15 class pairs are
2609
- * provably unsatisfiable with one layer each. Do not reintroduce it.
2610
+ * provably unsatisfiable with one layer each. Do not reintroduce it there.
2610
2611
  * Evidence: `.chrt/perf/2026-07-14-p8-d4-spike.md`.
2611
2612
  *
2612
- * **Load-bearing invariant.** The collapsed layer MUST keep the `-band{B}`
2613
- * id segment: `custom-style.ts` `bandFloor()` parses `/-band(\d)/` to clamp
2614
- * customer minzoom/scamin overrides to the band's data floor, on the live
2615
- * public Style API path. Renaming the id silently unclamps those overrides.
2616
- * Asserted in `band-collapse.test.ts`.
2613
+ * LABEL layers are different (CHRTsail #121 — MapLibre Native iOS silently
2614
+ * collapses its whole render pass above 482 symbol layers, and the served
2615
+ * style carried 575): `makeCollapsedCrossBandLabel` folds a label's ×6 band
2616
+ * fan into ONE layer per class, and `collapsedOrientationGate` folds the
2617
+ * orientation fan's per-dominator variants into its per-cell layers. That is
2618
+ * NOT the killed axis 2, because what the fill argument protects — paint
2619
+ * order as the sole band separator — does not apply to labels: their
2620
+ * load-bearing order is the cross-class P4 priority ladder (each class keeps
2621
+ * exactly one layer in its ladder slot), and within-class placement priority
2622
+ * rides the `rank*1000 + within-key` symbol-sort-key that labels.ts
2623
+ * documents as designed to survive precisely this merge.
2624
+ *
2625
+ * **Load-bearing invariant.** A collapsed per-band FEATURE layer MUST keep
2626
+ * the `-band{B}` id segment: `custom-style.ts` `bandFloor()` parses
2627
+ * `/-band(\d)/` to clamp customer minzoom/scamin overrides to the band's
2628
+ * data floor, on the live public Style API path. Renaming the id silently
2629
+ * unclamps those overrides. Cross-band LABEL layers are the deliberate
2630
+ * exception: they carry every band floor INSIDE the filter (tile-zoom
2631
+ * clauses a customer override cannot touch), which supersedes the clamp —
2632
+ * `bandFloor()` returning 0 for a bare label id is correct, not a gap.
2633
+ * Both halves asserted in `band-collapse.test.ts`.
2617
2634
  */
2618
2635
 
2619
2636
  /**
package/dist/index.d.ts CHANGED
@@ -2600,20 +2600,37 @@ declare function worldMinusBboxPolygon(bbox: BBox): GeoJSON.Polygon;
2600
2600
  * `band-collapse.test.ts` re-proves it on every CI run by differentially
2601
2601
  * evaluating legacy-vs-collapsed through MapLibre's real three gates.
2602
2602
  *
2603
- * **Scope.** This is axis 1 ONLY: the per-band layers survive, with identical
2604
- * ids and identical positions, so the band-major draw order is untouched.
2605
- * "Axis 2" (collapsing the per-band copies into one layer per class) was
2606
- * evaluated and **KILLED** — it is unsound for the opaque-fill classes
2603
+ * **Scope.** For FEATURE layers (fills, lines, feature symbols) this is
2604
+ * axis 1 ONLY: the per-band layers survive, with identical ids and identical
2605
+ * positions, so the band-major draw order is untouched. "Axis 2" (collapsing
2606
+ * the per-band copies into one layer per class) was evaluated and **KILLED**
2607
+ * for feature layers — it is unsound for the opaque-fill classes
2607
2608
  * (`DOMINANCE_EXEMPT_FILL_CLASSES` ≡ `OPAQUE_FILL_LAYERS`), where band-major
2608
2609
  * order is the ONLY thing separating the bands, and 15/15 class pairs are
2609
- * provably unsatisfiable with one layer each. Do not reintroduce it.
2610
+ * provably unsatisfiable with one layer each. Do not reintroduce it there.
2610
2611
  * Evidence: `.chrt/perf/2026-07-14-p8-d4-spike.md`.
2611
2612
  *
2612
- * **Load-bearing invariant.** The collapsed layer MUST keep the `-band{B}`
2613
- * id segment: `custom-style.ts` `bandFloor()` parses `/-band(\d)/` to clamp
2614
- * customer minzoom/scamin overrides to the band's data floor, on the live
2615
- * public Style API path. Renaming the id silently unclamps those overrides.
2616
- * Asserted in `band-collapse.test.ts`.
2613
+ * LABEL layers are different (CHRTsail #121 — MapLibre Native iOS silently
2614
+ * collapses its whole render pass above 482 symbol layers, and the served
2615
+ * style carried 575): `makeCollapsedCrossBandLabel` folds a label's ×6 band
2616
+ * fan into ONE layer per class, and `collapsedOrientationGate` folds the
2617
+ * orientation fan's per-dominator variants into its per-cell layers. That is
2618
+ * NOT the killed axis 2, because what the fill argument protects — paint
2619
+ * order as the sole band separator — does not apply to labels: their
2620
+ * load-bearing order is the cross-class P4 priority ladder (each class keeps
2621
+ * exactly one layer in its ladder slot), and within-class placement priority
2622
+ * rides the `rank*1000 + within-key` symbol-sort-key that labels.ts
2623
+ * documents as designed to survive precisely this merge.
2624
+ *
2625
+ * **Load-bearing invariant.** A collapsed per-band FEATURE layer MUST keep
2626
+ * the `-band{B}` id segment: `custom-style.ts` `bandFloor()` parses
2627
+ * `/-band(\d)/` to clamp customer minzoom/scamin overrides to the band's
2628
+ * data floor, on the live public Style API path. Renaming the id silently
2629
+ * unclamps those overrides. Cross-band LABEL layers are the deliberate
2630
+ * exception: they carry every band floor INSIDE the filter (tile-zoom
2631
+ * clauses a customer override cannot touch), which supersedes the clamp —
2632
+ * `bandFloor()` returning 0 for a bare label id is correct, not a gap.
2633
+ * Both halves asserted in `band-collapse.test.ts`.
2617
2634
  */
2618
2635
 
2619
2636
  /**
package/dist/index.js CHANGED
@@ -27282,6 +27282,34 @@ function makeCollapsedBandVariant(layer, B, isExempt) {
27282
27282
  delete collapsed.maxzoom;
27283
27283
  return [collapsed];
27284
27284
  }
27285
+ function collapsedOrientationGate(minzoom) {
27286
+ const clauses = [NOT_DOMINATED_FILTER];
27287
+ for (const D of BAND_DRAW_ORDER) {
27288
+ const dMaxzoom = dominatedVariantMaxzoom(D);
27289
+ if (dMaxzoom <= minzoom) continue;
27290
+ clauses.push(["all", dominatedByMinFilter(D), ["<", ["zoom"], dMaxzoom]]);
27291
+ }
27292
+ return ["any", ...clauses];
27293
+ }
27294
+ function makeCollapsedCrossBandLabel(label, isExempt) {
27295
+ const classMinzoom = label.minzoom ?? 0;
27296
+ const clauses = [];
27297
+ for (const B of BAND_DRAW_ORDER) {
27298
+ const floor = BAND_PRIORITY_RANGES[B].minzoom;
27299
+ const parts = [bandFilterExpression(B)];
27300
+ if (floor > 0) parts.push([">=", ["zoom"], floor]);
27301
+ if (!isExempt) {
27302
+ parts.push(collapsedDominanceGate(B, Math.max(classMinzoom, floor)));
27303
+ }
27304
+ clauses.push(parts.length === 1 ? parts[0] : ["all", ...parts]);
27305
+ }
27306
+ const collapsed = {
27307
+ ...label,
27308
+ filter: combineWithBandFilter(label.filter, ["any", ...clauses])
27309
+ };
27310
+ delete collapsed.maxzoom;
27311
+ return collapsed;
27312
+ }
27285
27313
 
27286
27314
  // src/atlas/style-generator.ts
27287
27315
  var META_LAYERS = /* @__PURE__ */ new Set([
@@ -27433,7 +27461,7 @@ var CELL_BAND_OPACITY_CURVE = {
27433
27461
  "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
27434
27462
  "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
27435
27463
  };
27436
- function fanOrientationLabelByCellBand(label) {
27464
+ function fanOrientationLabelByCellBand(label, bandFan) {
27437
27465
  const cellBandExpr = [
27438
27466
  "slice",
27439
27467
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -27458,6 +27486,16 @@ function fanOrientationLabelByCellBand(label) {
27458
27486
  ...label.paint ?? {},
27459
27487
  "text-opacity": opacityExpr
27460
27488
  };
27489
+ if (bandFan === "collapsed") {
27490
+ variants.push({
27491
+ ...label,
27492
+ id: `${label.id}-cell${code}-notdom`,
27493
+ filter: combineWithBandFilter(cellFiltered, collapsedOrientationGate(minzoom)),
27494
+ minzoom,
27495
+ paint
27496
+ });
27497
+ continue;
27498
+ }
27461
27499
  variants.push({
27462
27500
  ...label,
27463
27501
  id: `${label.id}-cell${code}-notdom`,
@@ -27487,18 +27525,50 @@ function byLabelPriorityBottomUp(bundles) {
27487
27525
  (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
27488
27526
  );
27489
27527
  }
27490
- function chartModeFilterForBand(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27491
- let filter = combineWithBandFilter(layer.filter, bandFilterExpression(B));
27528
+ function chartModeBandClauses(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27529
+ const parts = [bandFilterExpression(B)];
27492
27530
  if (!isDominanceExempt(layer)) {
27493
27531
  for (const D of higherCascadeBands) {
27494
- filter = combineWithBandFilter(filter, ["!", dominatedByMinFilter(D)]);
27532
+ parts.push(["!", dominatedByMinFilter(D)]);
27495
27533
  }
27496
27534
  }
27497
27535
  if (lockedBounds && isRequestedBand) {
27498
- filter = combineWithBandFilter(filter, withinBboxFilter(lockedBounds));
27536
+ parts.push(withinBboxFilter(lockedBounds));
27537
+ }
27538
+ return parts;
27539
+ }
27540
+ function chartModeFilterForBand(layer, B, higherCascadeBands, lockedBounds, isRequestedBand) {
27541
+ const [first, ...rest] = chartModeBandClauses(
27542
+ layer,
27543
+ B,
27544
+ higherCascadeBands,
27545
+ lockedBounds,
27546
+ isRequestedBand
27547
+ );
27548
+ let filter = combineWithBandFilter(layer.filter, first);
27549
+ for (const part of rest) {
27550
+ filter = combineWithBandFilter(filter, part);
27499
27551
  }
27500
27552
  return filter;
27501
27553
  }
27554
+ function makeCollapsedChartLabel(label, cascade, requestedSet, lockedBounds) {
27555
+ const clauses = cascade.map((B, i) => {
27556
+ const parts = chartModeBandClauses(
27557
+ label,
27558
+ B,
27559
+ cascade.slice(i + 1),
27560
+ lockedBounds,
27561
+ requestedSet.has(B)
27562
+ );
27563
+ return parts.length === 1 ? parts[0] : ["all", ...parts];
27564
+ });
27565
+ const collapsed = {
27566
+ ...label,
27567
+ filter: combineWithBandFilter(label.filter, ["any", ...clauses])
27568
+ };
27569
+ delete collapsed.maxzoom;
27570
+ return collapsed;
27571
+ }
27502
27572
  function generateStyle(palette = "DAY", opts = {}) {
27503
27573
  const sources = buildSources(opts);
27504
27574
  const sourceId = opts.region ?? DEFAULT_REGION;
@@ -27514,6 +27584,7 @@ function generateStyle(palette = "DAY", opts = {}) {
27514
27584
  const labelLayers = [];
27515
27585
  const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
27516
27586
  const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
27587
+ const bandFan = opts.bandFan ?? "collapsed";
27517
27588
  const lockedBounds = opts.chartMode?.lockedBounds;
27518
27589
  const bundles = [];
27519
27590
  for (const objectClass of orderedClasses) {
@@ -27567,6 +27638,12 @@ function generateStyle(palette = "DAY", opts = {}) {
27567
27638
  )
27568
27639
  );
27569
27640
  for (const { label } of chartLabelBundles) {
27641
+ if (bandFan === "collapsed") {
27642
+ labelLayers.push(
27643
+ makeCollapsedChartLabel(label, chartCascade, requestedSet, lockedBounds)
27644
+ );
27645
+ continue;
27646
+ }
27570
27647
  for (let i = 0; i < chartCascade.length; i++) {
27571
27648
  const B = chartCascade[i];
27572
27649
  const higher = chartCascade.slice(i + 1);
@@ -27589,7 +27666,6 @@ function generateStyle(palette = "DAY", opts = {}) {
27589
27666
  }
27590
27667
  layers.push(...labelLayers);
27591
27668
  } else {
27592
- const bandFan = opts.bandFan ?? "collapsed";
27593
27669
  const emitBand = (base, code) => bandFan === "legacy" ? makeBandVariants(base, code) : makeCollapsedBandVariant(base, code, isDominanceExempt(base));
27594
27670
  for (const code of BAND_DRAW_ORDER) {
27595
27671
  for (const { featureLayers } of regularBundles) {
@@ -27608,7 +27684,7 @@ function generateStyle(palette = "DAY", opts = {}) {
27608
27684
  for (const { objectClass, label } of bundles) {
27609
27685
  if (!label) continue;
27610
27686
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
27611
- labelLayers.push(...fanOrientationLabelByCellBand(label));
27687
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan));
27612
27688
  }
27613
27689
  }
27614
27690
  const featureLabelBundles = byLabelPriorityBottomUp(
@@ -27617,6 +27693,10 @@ function generateStyle(palette = "DAY", opts = {}) {
27617
27693
  )
27618
27694
  );
27619
27695
  for (const { label } of featureLabelBundles) {
27696
+ if (bandFan === "collapsed") {
27697
+ labelLayers.push(makeCollapsedCrossBandLabel(label, isDominanceExempt(label)));
27698
+ continue;
27699
+ }
27620
27700
  for (const code of BAND_DRAW_ORDER) {
27621
27701
  labelLayers.push(...emitBand(label, code));
27622
27702
  }