Skip to content

Commit 1317ee5

Browse files
beetlebugorgclaude
andcommitted
feat(scamin): honor SCAMIN exactly for light sectors
The LIGHTS06 sector figure (arcs/legs) was the one discrete light mark not getting exact SCAMIN: its flare (point_symbols) and characteristic text already bucket, but the baker dumped the sector geometry into the shared `lines` source-layer with NO scamin tag, gated only to integer floor(z*) by tile content — so sectors popped in up to ~1 zoom too early when zooming out. Baker: tag each sector with its parent LIGHTS' SCAMIN and emit it into its OWN `sector_lines` source-layer (not the shared `lines`), so it can be bucketed without fanning every coastline/contour into per-SCAMIN variants. Also stop clamping a sector's tile-availability floor to bandMin, matching the flare prim, so a SCAMIN-below-band sector stays available down to its SCAMIN scale. Client: add `sector_lines` to SCAMIN_BUCKET_LAYERS and two thin style layers (solid/dashed) mirroring lines-solid/lines-dashed. Sectors now cut at the exact fractional scale, both directions, like the flare/text. General `lines`/`areas` deliberately left unbucketed: they're continuous / best-available gap-fill, where SCAMIN-removal is a separate (hole-punching) concern. Requires a re-bake (the `scamin` tag + `sector_lines` layer are new baked data). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 70130db commit 1317ee5

2 files changed

Lines changed: 26 additions & 5 deletions

File tree

internal/engine/bake/bake.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ type sectorPrim struct {
262262
cat int
263263
zMin uint32
264264
natMax uint32
265+
scamin uint32 // SCAMIN denominator of the parent LIGHTS (0 = none); emitted as `scamin` so the client's per-SCAMIN bucket layer gates the exact display cutoff, same as point symbols/text
265266
// legNorm is the full-length leg reach (VALNMR nominal range) as a fraction
266267
// of the normalized world — a fixed GROUND distance, so zoom-independent
267268
// (unlike the 25 mm short legs / ring, which are screen-px). Drives the tile
@@ -878,6 +879,7 @@ func (b *Baker) route(p portrayal.Primitive, class string, drawPrio, cat int, zr
878879
b.sectors = append(b.sectors, sectorPrim{
879880
anchor: v.Anchor, params: v.Sector, class: class, cell: b.curCell,
880881
drawPrio: drawPrio, cat: cat, zMin: zMin, natMax: zr.Max,
882+
scamin: b.curScamin,
881883
legNorm: sectorLegFullNorm(v.Anchor.Lat, v.Sector.RadiusNM),
882884
})
883885
}
@@ -1098,10 +1100,11 @@ func (b *Baker) TileCoordsBand(extent, bandMin, bandMax uint32) []tile.TileCoord
10981100
continue
10991101
}
11001102
ax, ay := normX(sp.anchor.Lon), normY(sp.anchor.Lat)
1103+
// Like the flare prim (see lo := r.zMin above), a SCAMIN-bearing sector may
1104+
// sit BELOW bandMin — keep it AVAILABLE down to its SCAMIN scale so the
1105+
// client's per-SCAMIN bucket gates the exact cutoff. Non-SCAMIN sectors have
1106+
// zMin == bandMin (bandZMin floors them), so this is a no-op for them.
11011107
lo := sp.zMin
1102-
if lo < bandMin {
1103-
lo = bandMin
1104-
}
11051108
for z := lo; z <= b.clampZMax(sp.natMax); z++ {
11061109
r := math.Max(sectorRadiusNorm(z), sp.legNorm)
11071110
bb := geo.BoundingBox{
@@ -1490,7 +1493,14 @@ func (b *Baker) emitTileInto(coord tile.TileCoord, extent uint32, buffer float64
14901493
if st.sleg >= 0 {
14911494
attrs = append(attrs, mvt.KeyValue{Key: "sleg", Value: mvt.IntVal(int64(st.sleg))})
14921495
}
1493-
tb.Layer("lines").AddLines(paths, attrs)
1496+
// SCAMIN of the parent LIGHTS — so the client's per-SCAMIN bucket layer
1497+
// gates the sector figure at the EXACT display scale, in both directions,
1498+
// matching the light's flare (point_symbols) and characteristic text. Its
1499+
// own `sector_lines` layer keeps the bucket fan-out off the shared `lines`.
1500+
if sp.scamin != 0 {
1501+
attrs = append(attrs, mvt.KeyValue{Key: "scamin", Value: mvt.IntVal(int64(sp.scamin))})
1502+
}
1503+
tb.Layer("sector_lines").AddLines(paths, attrs)
14941504
}
14951505
}
14961506

web/chartplotter.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,11 @@ const BAND_DISPLAY_MIN = { overview: 0, general: 0, coastal: 10, approach: 12, h
117117
// track the display scale of the object it annotates — keyed on the SOURCE-LAYER,
118118
// not the style-layer id, because the text labels fan out into many style layers
119119
// (text-<halign>-<valign>, light-text) that all read the one `text` source-layer.
120-
const SCAMIN_BUCKET_LAYERS = new Set(["point_symbols", "soundings", "text"]);
120+
// `sector_lines` is the LIGHTS06 sector figure (arcs/legs), baked into its own
121+
// source-layer (not the shared `lines`) precisely so it can be bucketed here
122+
// without fanning every coastline/contour into per-SCAMIN variants — the sector
123+
// then cuts at the same exact scale as its light's flare + characteristic text.
124+
const SCAMIN_BUCKET_LAYERS = new Set(["point_symbols", "soundings", "text", "sector_lines"]);
121125

122126
// The display zoom at which a 1:N (scamin) feature first becomes visible at the
123127
// given latitude: the zoom whose display-scale denominator equals scamin. FRACTIONAL
@@ -1403,6 +1407,13 @@ export class ChartPlotter extends HTMLElement {
14031407
{ id: "lines-solid", type: "line", source: "chart", "source-layer": "lines", filter: ["==", ["coalesce", ["get", "dash"], "solid"], "solid"], paint: { "line-color": this.colorExpr("color_token"), "line-width": ["coalesce", ["get", "width_px"], 1] } },
14041408
{ id: "lines-dashed", type: "line", source: "chart", "source-layer": "lines", filter: ["==", ["get", "dash"], "dashed"], paint: { "line-color": this.colorExpr("color_token"), "line-width": ["coalesce", ["get", "width_px"], 1], "line-dasharray": [4, 3] } },
14051409
{ id: "lines-dotted", type: "line", source: "chart", "source-layer": "lines", filter: ["all", ["==", ["get", "dash"], "dotted"], ["!", ["has", "danger_depth"]]], paint: { "line-color": this.colorExpr("color_token"), "line-width": ["coalesce", ["get", "width_px"], 1], "line-dasharray": [1, 2] } },
1410+
// LIGHTS06 sector figure (coloured arcs / OUTLW backing / dashed legs) — its
1411+
// OWN source-layer so it can be SCAMIN-bucketed (see SCAMIN_BUCKET_LAYERS)
1412+
// without dragging every coastline/contour into per-SCAMIN variants. Styling
1413+
// mirrors lines-solid/lines-dashed (the sector tessellation emits only solid
1414+
// and dashed runs); sleg/category/boundary gating rides combineFilters as before.
1415+
{ id: "sector-lines-solid", type: "line", source: "chart", "source-layer": "sector_lines", filter: ["==", ["coalesce", ["get", "dash"], "solid"], "solid"], paint: { "line-color": this.colorExpr("color_token"), "line-width": ["coalesce", ["get", "width_px"], 1] } },
1416+
{ id: "sector-lines-dashed", type: "line", source: "chart", "source-layer": "sector_lines", filter: ["==", ["get", "dash"], "dashed"], paint: { "line-color": this.colorExpr("color_token"), "line-width": ["coalesce", ["get", "width_px"], 1], "line-dasharray": [4, 3] } },
14061417
// OBSTRN/WRECKS dotted foul boundary (client-side): shown only when the
14071418
// feature's VALSOU is ≤ the live safety contour. Filter updates on
14081419
// safetyContour — no re-bake. Excluded from lines-dotted above.

0 commit comments

Comments
 (0)