Class LabelDeclutterer
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
sceneVectorSceneThe overlay scene (point + text ops).
viewportViewportThe live viewport whose projection places anchors.
cullBoundsSKRectScreen-space rectangle outside which ops are not drawn.
honorScaleVisibilityboolWhether to apply S-100 Part 9 §11.1 SCAMIN culling.
anchorRotationDegreesdoubleDegrees to rotate each anchor about (
centerX,centerY) — matching the overlay's rotation — before building footprints. Zero for the north-up v1 case.centerXfloatScreen-space X of the rotation centre.
centerYfloatScreen-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()