Skip to content

feat(glyphs): expose TexturedGlobeGlyph's tilt transform for callers - #323

Merged
MAfarrag merged 6 commits into
mainfrom
feat/globe-tilt-transform
Aug 29, 2026
Merged

feat(glyphs): expose TexturedGlobeGlyph's tilt transform for callers#323
MAfarrag merged 6 commits into
mainfrom
feat/globe-tilt-transform

Conversation

@MAfarrag

@MAfarrag MAfarrag commented Aug 29, 2026

Copy link
Copy Markdown
Member

Description

Exposes the transform TexturedGlobeGlyph applies to place its sphere, so a caller can align their own
scene geometry with the rendered globe instead of reimplementing the tilt (implements #322; found
adopting the glyph in earthlens' eclipse_geometry_3d.ipynb, which draws an eclipse marker, a
geostationary ring, and a Moon orbit plane that must all sit correctly relative to the globe).

  • rotation_matrix(spin=0.0) — the (3, 3) body-to-world matrix the glyph uses: R_tilt(x) @ R_z(spin)
    (spin about the polar axis, then the fixed axial tilt about world x). Computable without a texture/draw.
  • transform(points, spin=0.0) — applies it to a (3,) point or (N, 3) array of body-frame points
    (unit sphere, +z at the north pole, equatorial plane z = 0, (lon, lat)
    [cos(lat)·cos(lon), cos(lat)·sin(lon), sin(lat)]) and returns the world-space positions.
  • Single source of truth: _spun_mesh now goes through rotation_matrix, and the redundant cached
    _tilt_matrix is dropped — so the exposed transform provably lands where the glyph's own mesh does.
  • Backward-compatible: the X-axis tilt default is unchanged; _spun_mesh still yields
    R_x(tilt_deg) @ R_z(spin) @ base for every existing caller. (Per the issue's "Out of Scope", the default
    is untouched — this exposes option B, not a new tilt axis.)

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?

Two /review-rounds passes were run and every finding resolved (a scalar-input ValueError fix, concrete return types clearing the new mypy errors, an ArrayLike annotation, and contract tests for empty/1x3/list input, rotation_matrix orthogonality+freshness, pre-_prepare use, and output independence). SonarCloud quality gate is OK (0 open issues).

New TestTiltTransform cases in tests/test_textured_globe_glyph.py (86 tests total; 100% line + branch
coverage of the module) covering the issue's Definition of Done:

  • Transform lands where the mesh does — pushing the base vertices through transform(..., spin) equals
    _spun_mesh(spin) (the core DoD guarantee).
  • rotation_matrix is tilt-then-spin — equals R_x(tilt) @ R_z(spin); identity at tilt=0, spin=0.
  • transform shapes/values — a (3,) point → (3,) (north pole under a 90° tilt → -y); (N, 3)(N, 3);
    bad shapes ((2,), (4, 2), 3-D) raise ValueError.
  • Default tilt unchanged — the mesh still equals R_x(tilt) @ R_z(spin) @ base (backward-compat regression).

Reproduce (external uv env, worktree on 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 (86 passed, 100% coverage)
  • Test B — full suite pytest tests/ -q → 2558 passed; ruff check + ruff format --check clean;
    module doctests pass

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

Add rotation_matrix(spin) and transform(points, spin) so a caller can place
their own scene geometry consistently with the rendered globe instead of
reimplementing the tilt. The methods return / apply the exact body-to-world
transform the glyph uses -- R_tilt(x) @ R_z(spin), i.e. spin about the polar
axis then the axial tilt about world x -- and _spun_mesh now goes through
rotation_matrix, so the exposed transform provably lands where the mesh does.

- transform() accepts a (3,) point or an (N, 3) array in the body frame (unit
  sphere, +z pole, equatorial plane z=0) and returns the world-space position.
- The X-axis tilt default is unchanged; drop the now-redundant cached
  _tilt_matrix (rotation_matrix is the single source of truth).
- Add tests (transform lands where the mesh does, matrix = tilt then spin,
  shape/validation, default-tilt mesh unchanged) and a docs example.

Closes #322
- transform() checks ndim before indexing shape[-1], so a scalar / 0-d input
  raises the documented ValueError instead of IndexError (M1); add scalar cases
  to the bad-shape test.
- Wrap the rotation_matrix / transform returns in np.asarray so the declared
  ndarray return type is concrete, clearing two new no-any-return mypy errors
  under warn_return_any (M2).
- Annotate transform's points as numpy.typing.ArrayLike (it accepts array-likes
  such as a plain list) (N2) and document that non-finite points propagate (N1).
…ring)

Add the missing 'from cleopatra.basemap.reference import relief' so the snippet
runs as copy-pasted (L2), and widen the axis limits after plotting -- draw()
pins them to the unit sphere, so the radius-1.3 geostationary ring was clipped
outside the [-1, 1] cube (L3).
Assert the behaviours the code supports but nothing pinned down (100% coverage
could not catch a regression in them): empty (0,3) and (1,3) shapes preserved
(not squeezed), list/array-like input, rotation_matrix orthogonality + freshness
(a returned matrix a caller mutates must not corrupt a later call), transform
usable before _prepare/draw, and the returned array not aliasing the input.
Strengthen the array test to assert the transform is applied per row (N3).
Reword the docs prose to describe the transform as spin-then-tilt (R_tilt @ R_z),
matching rotation_matrix's docstring, so the two don't read as opposite orders
(N1); note that inf inputs to transform also emit a numpy RuntimeWarning (N2).
Construct the glyph before the pytest.raises block so only the transform() call
-- the one meant to fail -- can throw inside it.
@sonarqubecloud

Copy link
Copy Markdown

@MAfarrag
MAfarrag merged commit 24f080e into main Aug 29, 2026
10 checks passed
@MAfarrag
MAfarrag deleted the feat/globe-tilt-transform branch August 29, 2026 15:50
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(glyphs): let TexturedGlobeGlyph's tilt be reproducible by callers

1 participant