Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ Unreleased
large atoms swung towards the eye by rotation no longer clip at the
viewport edge, and a cell with no atoms is sized correctly too.

- In the interactive viewer, changing the frame or a display style no
longer resets the zoom level.

- A view opened with a zoom above one now starts zoomed in at that
level, rather than fitting the window regardless of the zoom set.

0.20.0
------

Expand Down
2 changes: 1 addition & 1 deletion src/hofmann/rendering/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def _rotation_z(angle: float) -> np.ndarray:

_KEY_ROTATION_STEP = 0.05 # radians (~3 degrees) per key press
_KEY_ZOOM_FACTOR = 1.1 # multiplicative zoom per key press / scroll step
_KEY_PAN_FRACTION = 0.05 # fraction of scene extent per key press
_KEY_PAN_FRACTION = 0.05 # fraction of the viewport panned per key press
_PERSPECTIVE_STEP = 0.1 # perspective increment per key press
_PERSPECTIVE_FLOOR = 1e-9 # below this, the descent lands on Orthographic
_DISTANCE_FACTOR = 1.05 # viewing distance multiplier per key press
Expand Down
8 changes: 6 additions & 2 deletions src/hofmann/rendering/projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ def _scene_extent(
Returns the radius of a 2D bounding circle centred at the origin
that encloses every atom and unit-cell corner under any rotation:
the largest centre distance plus display radius, widened by the
projection's worst-case magnification, and scaled by zoom.
projection's worst-case magnification.

The extent is in scene units and excludes ``view.zoom``: the
projected coordinates already carry it, so consumers must not
re-apply it.
"""
coords = scene.frames[frame_index].coords
dists = np.linalg.norm(coords - view.centre, axis=1)
Expand Down Expand Up @@ -104,7 +108,7 @@ def _scene_extent(
# not a magic-threshold clamp on the magnification.
max_extent *= view.projection.max_magnification(max_extent)

return float(max_extent * view.zoom)
return float(max_extent)


def _make_wedges(
Expand Down
18 changes: 18 additions & 0 deletions tests/test_rendering/test_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,24 @@ def test_pan_down(self):
expected = old_centre + step * view.rotation[1]
np.testing.assert_allclose(view.centre, expected)

def test_pan_step_halves_when_zoom_doubles(self):
"""The pan step is a fraction of the *visible* extent, so doubling
the zoom halves the world-space step. Guards the ``/ view.zoom``
in the pan step (a ratio, not the production formula restated)."""
v1, s1, st1, iv1 = _key_action_fixtures()
v1.zoom = 1.0
c1 = v1.centre.copy()
_do_key("shift+left", v1, s1, st1, iv1, base_extent=10.0)
step_at_1 = float(np.linalg.norm(v1.centre - c1))

v2, s2, st2, iv2 = _key_action_fixtures()
v2.zoom = 2.0
c2 = v2.centre.copy()
_do_key("shift+left", v2, s2, st2, iv2, base_extent=10.0)
step_at_2 = float(np.linalg.norm(v2.centre - c2))

np.testing.assert_allclose(step_at_2, step_at_1 / 2.0)

# -- Perspective --

def test_perspective_increase(self):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_rendering/test_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,25 @@ def test_empty_scene_perspective_is_magnified(self):
extent = _scene_extent(scene, view, 0, atom_scale=0.5)
assert extent > 1.0 # pre-fix: gated out, returns the 1.0 floor

def test_extent_is_independent_of_zoom(self):
# The extent is in scene units; the coordinates carry the zoom.
# A frame-navigation recompute after zooming therefore returns
# the same viewport, so the interactive zoom is not reset.
scene = StructureScene(
species=["C"],
frames=[Frame(coords=np.array([[0.0, 0.0, 5.0]]))],
atom_styles={"C": AtomStyle(1.0, (0.5, 0.5, 0.5))},
)
e1 = _scene_extent(
scene, ViewState(zoom=1.0, projection=Orthographic()), 0,
atom_scale=0.5,
)
e2 = _scene_extent(
scene, ViewState(zoom=2.0, projection=Orthographic()), 0,
atom_scale=0.5,
)
assert e1 == e2


class TestMakeWedges:
def test_pure_composition_returns_single_full_circle(self):
Expand Down
Loading