Skip to content

feat(globe): add area sampling and a face_colors accessor to TexturedGlobeGlyph - #332

Open
MAfarrag wants to merge 6 commits into
mainfrom
feat/textured-globe-area-sampling
Open

feat(globe): add area sampling and a face_colors accessor to TexturedGlobeGlyph#332
MAfarrag wants to merge 6 commits into
mainfrom
feat/textured-globe-area-sampling

Conversation

@MAfarrag

@MAfarrag MAfarrag commented Sep 5, 2026

Copy link
Copy Markdown
Member

Description

TexturedGlobeGlyph point-samples one texture cell per mesh-face centre, which is what makes the
sample-once/rotate-per-frame contract cheap. The cost is that a texture feature narrower than one mesh face
falls between the sample points and disappears entirely — however fine the texture is, only a finer mesh brought
it back. That bites geospatial callers who drape a small regional dataset onto a mostly-transparent global canvas
(found while adopting the glyph in Digital-Earth).

Two additions, each useful on its own, keeping the sample-once contract (the reduction runs once in _prepare):

  • sampling="point" | "area". "point" (default) is unchanged — the cheap per-centre lookup, the right
    choice for a photographic basemap. "area" reduces the whole texture block each face covers, alpha-aware: a
    face's colour is the mean of the opaque cells it spans (or transparent if it spans none), so a small opaque
    feature stays visible rather than fading toward transparent. Faces finer than a single texture cell fall back to
    point sampling, so a coarse texture never gains gaps.
  • face_colors property. A read-only accessor returning the per-face RGBA the globe will paint (before
    per-frame lighting), so a caller can check whether its data survived the sampling — e.g. that a small feature
    landed on at least one face — without a draw() and without reaching into _facecolors.

"point" behaviour is byte-identical to before. No new runtime dependencies.

Issues

Type of change

Check relevant points.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Dev changes (CI/pyproject.toml/docs/examples/testing)

How Has This Been Tested?

New TestAreaSampling (8 tests) and TestFaceColors (4 tests) in tests/test_textured_globe_glyph.py, covering
the issue's Definition of Done:

  • texture-finer-than-mesh — a 3×3 opaque patch narrower than a face: "point" paints 0 faces (the gap),
    "area" paints it, and the reduced face keeps the opaque colour (alpha-aware, not a faded average).
  • mesh-finer-than-texture — a coarse fully-opaque texture on a fine mesh: "area" matches "point" exactly
    (no gaps).
  • fully transparent texture"area" paints nothing.
  • face_colors — correct shape (n_lat-1, n_lon-1, 4), available without a draw(), returns a copy
    (mutation-safe), and reflects the sampling mode.
  • validation — an unknown sampling value raises ValueError.

Reproduce (external uv env, worktree src):

VIRTUAL_ENV=C:/python-environments/uv/cleopatra PYTHONPATH=src \
  C:/python-environments/uv/cleopatra/Scripts/python.exe -m pytest \
  tests/test_textured_globe_glyph.py -q
  • Test A — tests/test_textured_globe_glyph.py → 97 passed (86 pre-existing + 11 new)
  • Test B — pytest --doctest-modules src/cleopatra/glyphs/globe/textured_globe_glyph.py -q → 9 passed

Checklist:

  • updated version number in pyproject.toml
  • added changes to History.rst
  • updated the latest version in README file
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

TexturedGlobeGlyph point-sampled one texture cell per mesh-face centre,
so a feature narrower than one face fell between the sample points and
vanished -- however fine the texture; only a finer mesh brought it back
(#325). Two additions, each useful on its own:

- sampling="point"|"area". "point" (default) is unchanged: the cheap
  sample-once lookup, right for photographic basemaps. "area" reduces
  the texture block each face covers, alpha-aware -- a face's colour is
  the mean of the opaque cells it spans, or transparent if it spans
  none -- so a small opaque feature survives. Faces finer than one
  texture cell fall back to point sampling, so a coarse texture never
  gains gaps. The reduction runs once in _prepare, keeping the
  sample-once/rotate-per-frame contract.
- face_colors: a read-only property returning the per-face RGBA the
  globe will paint (before per-frame lighting), so a caller can check
  whether its data survived the sampling without a draw() or reaching
  into private state.

Closes #325
… mesh-finer fallback

Close the review's area-mode test gaps: exercise a 3-channel RGB texture (the
common relief() basemap input) -- every face stays opaque and block-averages,
differing from centre sampling on a high-frequency pattern -- and a fractional-
alpha cell, which contributes and leaves the face partly transparent. Also give
test_area_matches_point_when_mesh_finer_than_texture a non-uniform texture so it
actually pins that area picks the same cell as point (a uniform fill could not).
…) semantics

The area reduction includes every cell with alpha > 0, not only fully-opaque
ones, and averages RGB un-premultiplied -- so a face over feathered edges keeps
their mean colour and a mean alpha below 1. The docstrings/comments said
'opaque', which reads as fully-opaque. Reword to 'non-transparent (alpha > 0)',
spell out the fractional-alpha behaviour, and rename the internal mask from
'opaque' to 'nontransparent' to match. Behaviour is unchanged.
… docstrings

Cosmetic Round-2 review cleanups, no behaviour change: collapse the one-argument
sampling ValueError onto a single line so the new hunk is ruff-format-canonical
(L1); open face_colors with 'The base per-face RGBA colours...' so the headline
agrees with the pre-lighting hedge (L2); rewrap the sampling docstring to remove
an orphan line (N1); and align its 'fall back to point sampling' clause with the
code's precise empty-block condition (N4).
… cells

The area reduction averages RGB un-premultiplied: a face over two non-transparent
cells of alpha 64 and 192 with distinct colours must reduce to the plain mean
(red 0.5, not the alpha-weighted 0.25) and the mean alpha. Pins the documented
'each cell counts once' contract against a future premultiplied-mean regression.
The 'point' string literal appeared three times (default arg, validation tuple,
mode comparison), which SonarCloud python:S1192 flags as a duplicated literal.
Hoist the modes to module constants SAMPLING_POINT/SAMPLING_AREA/SAMPLING_MODES
and reference them, leaving one definition of each. Behaviour unchanged.
@sonarqubecloud

sonarqubecloud Bot commented Sep 5, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(globe): TexturedGlobeGlyph drops texture detail finer than one mesh face

1 participant