Table of Contents

Class LabelDeclutterer

Namespace
EncDotNet.S100.Renderers.Skia.Scene
Assembly
EncDotNet.S100.Renderers.Skia.dll

Deterministic, priority-driven label declutter for the tiled subsystem's live label plane. S-100 Part 9 makes overlap avoidance the portrayal engine's responsibility (there is no per-label collision rule in a product's Portrayal Catalogue), so the render subsystem resolves it here from the drawing-priority and SCAMIN already carried on each PaintOp.

public sealed class LabelDeclutterer : IDisposable
Inheritance
LabelDeclutterer
Implements
Inherited Members

Remarks

Point symbols always draw and are never suppressed. Matching the Mapsui "A" arm (the fidelity baseline for issue #347), they do not displace labels either: decluttering operates among labels only. Labels (feature-name text and soundings, both TextPaintOp) yield only to higher-priority labels. The op list is already ordered by ascending S-100 Part 9 drawing priority (later = drawn on top = more important), so labels are placed highest-priority first; a label whose on-screen footprint overlaps an already-placed label footprint is suppressed.

Composite labels. Text instructions that one feature emits at the same anchor (same FeatureReference and world position) are a single annotation whose parts the portrayal placed relative to each other with offsets — e.g. the S-411 WMO egg code's total concentration over its verbose partial-concentration line. Such labels never suppress one another (the "A" arm drew both); each still declutters against every other feature's labels.

Letting a point symbol suppress an overlapping label would drop annotation text the "A" arm always draws — e.g. S-421 route action-point / leg labels anchored on (or beside) a co-located waypoint symbol, which the coarse perceptual gate does not catch. Point symbols are therefore obstacles to nothing; only label-vs-label overlap is resolved here.

All footprints are computed in final on-screen space — each anchor is rotated about the screen centre by the same angle the overlay's point pass uses — so collision is correct under a rotated viewport. The result is deterministic (a stable, priority-ordered greedy placement); unlike the Mapsui "A" arm's non-deterministic declutter, the same scene and viewport always suppress the same labels.

Threading. The reusable per-frame buffers (the suppressed set, the screen-rect index and its bucket lists, and the text-layout scratch) are render-thread-confined: a single instance is reused across frames and Declutter(VectorScene, Viewport, SKRect, bool, double, float, float) EncDotNet.S100.Renderers.Skia.Scene.LabelDeclutterer.ScreenRectIndex.Clear()s them at entry rather than reallocating, eliminating the per-frame allocation that scaled with op count (~180 KB at 1k ops, ~6 MB at 50k). This makes the type non-reentrant — it must only be called from the one render thread (the tiled subsystem's DrawOverlay), never concurrently. The tile-raster path uses its own per-call renderer/scratch and is unaffected.

Methods

Declutter(VectorScene, Viewport, SKRect, bool, double, float, float)

Returns the set of TextPaintOps to suppress this frame (by reference identity) so the live overlay draws a decluttered label plane. Point symbols are never suppressed and never displace a label (parity with the Mapsui arm) — only label-vs-label overlap is resolved. Ops that are scale-culled or fall outside cullBounds are ignored (they are not drawn, so they neither occupy space nor need suppressing).

public IReadOnlySet<TextPaintOp> Declutter(VectorScene scene, Viewport viewport, SKRect cullBounds, bool honorScaleVisibility, double anchorRotationDegrees, float centerX, float centerY)

Parameters

scene VectorScene

The overlay scene (point + text ops).

viewport Viewport

The live viewport whose projection places anchors.

cullBounds SKRect

Screen-space rectangle outside which ops are not drawn.

honorScaleVisibility bool

Whether to apply S-100 Part 9 §11.1 SCAMIN culling.

anchorRotationDegrees double

Degrees to rotate each anchor about (centerX, centerY) — matching the overlay's rotation — before building footprints. Zero for the north-up v1 case.

centerX float

Screen-space X of the rotation centre.

centerY float

Screen-space Y of the rotation centre.

Returns

IReadOnlySet<TextPaintOp>

The text ops to skip; empty when nothing collides. The returned set is a render-thread-confined reusable buffer — valid only until the next Declutter(VectorScene, Viewport, SKRect, bool, double, float, float) call on this instance. Callers that must retain it across frames should copy it.

Dispose()

Disposes the held render-thread text-layout scratch (native paint/fonts). The tiled subsystem's shared instance lives for the process lifetime; this exists so unit tests (and any future per-layer owner) can release the native handles deterministically.

public void Dispose()