Atlas graph: name nodes in full, clamping labels to the room actually there

Fan and focus labels ellipsized at a flat cap (12 cells for a fan node, 16 for
the focus/selection) even when the canvas had ample room beside the marker, so
"YUL-Polytechnique" read "YUL-Polytechniq…" with half the row empty to its
right. Clamp each label to the cells genuinely free between its marker and the
canvas edge instead — a shared _clip helper feeds both the generous cap and
the per-marker room — and widen the east gutter so the fan sits a little west
of the edge, giving even the tightest due-east marker space to name itself.
A long name now loses letters only when it truly won't fit; the list below
still carries the fullest name regardless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 4.8 committed 2026-07-19 21:44:02 -04:00
1 parent bd13b117ce
commit e5aa5810dc
2 files changed
+79 -22

No files matched your search

+49 -21
View File
@@ -68,10 +68,12 @@ if TYPE_CHECKING:
from ..context import AppContext
#: Horizontal / vertical dot-space margins the neighbour fan keeps clear of the canvas
#: edge. The east margin is wide enough to hold a fan node's rightward label (see
#: :meth:`AtlasScreen._label_right`), so even the fan's outermost marker has room to
#: name itself without the label running off the edge.
_PAD_X_DOTS = 28
#: edge. The east margin holds a fan node's rightward label (see
#: :meth:`AtlasScreen._label_right`); it is deliberately roomy — pulling the whole fan a
#: little west of the edge — so even the tightest (due-east) marker has cells to name
#: itself in full rather than clipping a long contact name, which the labels' room-to-edge
#: clamp then spends wherever the fan leaves it.
_PAD_X_DOTS = 44
_PAD_Y_DOTS = 6
#: The canvas's floor in character rows: below this the fan's shape stops reading.
@@ -81,13 +83,17 @@ _CANVAS_MIN_H = 6
#: graph would like to be — a windowed list needs at least a few rows to scroll in.
_LIST_MIN_ROWS = 3
#: The widest the focus/selection label may render on the canvas before it ellipsizes.
_LABEL_W = 16
#: The widest any on-canvas label renders before it ellipsizes — a generous cap that most
#: real node names clear whole. It is only an upper bound: each label is *also* clamped to
#: the cells actually free between its marker and the canvas edge (see :meth:`_label_right`,
#: :meth:`_label_left`, :meth:`_place_label`), so a name loses letters only when it genuinely
#: won't fit, not to a fixed short budget — the fan's placement leaves most markers far more
#: room than the old flat cap allowed.
_LABEL_W = 22
#: The widest a *fan* node's rightward label renders before it ellipsizes. Shorter than
#: the focus/selection cap so a name always fits in the east gutter (:data:`_PAD_X_DOTS`)
#: even on the fan's outermost marker; the list below carries the fuller name.
_FAN_LABEL_W = 12
#: Kept as the *fan* label cap for symmetry with the focus/selection one; both now defer to
#: the per-marker room-to-edge clamp, so the two need no longer differ.
_FAN_LABEL_W = _LABEL_W
#: The fan's angular reach on each side of due east, in radians. The whole fan stays
#: east of the focus — neighbours to the right, labels rightward — and a smaller
@@ -661,13 +667,32 @@ class AtlasScreen(Screen):
placed[node] = (round(x), round(y))
return placed
@staticmethod
def _clip(label: str, room: int) -> str:
"""``label`` fit to ``room`` cells: whole if it fits, else ellipsized (``…`` alone
at one cell, nothing at zero). The cap and the room-to-edge both flow through here,
so a name is only ever shortened as far as it truly must be."""
room = min(room, _LABEL_W)
if room <= 0:
return ""
if len(label) <= room:
return label
return "…" if room == 1 else label[: room - 1] + "…"
def _place_label(
self, canvas: MapCanvas, x: int, y: int, label: str, rgb: RGB
) -> None:
"""Place one marker label (right of the marker when it fits, else left),
retrying a row below then above on collision."""
if len(label) > _LABEL_W:
label = label[: _LABEL_W - 1] + "…"
retrying a row below then above on collision.
The label is clamped to whichever side has more room — the cells free to the
right of the marker, or to its left — so the selection keeps as much of its name
as the canvas allows before ellipsizing."""
cx = x >> 1
room = max(canvas.cell_w - (cx + 2), cx - 1) # the roomier of right / left
label = self._clip(label, room)
if not label:
return
for dy in (0, 4, -4):
if canvas.marker_label(x, y + dy, label, rgb):
return
@@ -682,14 +707,15 @@ class AtlasScreen(Screen):
direction of the walk. The label tries the marker's own row first, then a
row below and above to slip past a crowded neighbour; if every checked row
is blocked it is stamped to the right regardless, so a node is never left a
bare glyph. The fan's east gutter (:data:`_PAD_X_DOTS`) is sized so a
:data:`_FAN_LABEL_W`-capped name lands without running off the edge, even on
the fan's outermost marker.
bare glyph. The name is clamped to the cells actually free between the marker
and the canvas edge, so it keeps its full length wherever the fan leaves room
and only clips on the tightest (due-east) markers.
"""
if len(label) > _FAN_LABEL_W:
label = label[: _FAN_LABEL_W - 1] + "…"
cx, cy = x >> 1, y >> 2
start = cx + 2
label = self._clip(label, canvas.cell_w - start)
if not label:
return
for dy in (0, 1, -1, 2, -2):
if canvas._place_run(start, cy + dy, label, rgb, bold=True, checked=True):
return
@@ -702,11 +728,13 @@ class AtlasScreen(Screen):
The focus is drawn first, so a collision is rare (a trail-back label at
most); after the dodge rows are exhausted the label is stamped anyway —
the focus must always be named.
the focus must always be named. The name is clamped to the cells free to the
marker's left (column 0 onward).
"""
if len(label) > _LABEL_W:
label = label[: _LABEL_W - 1] + "…"
cx, cy = x >> 1, y >> 2
label = self._clip(label, cx - 1)
if not label:
return
start = cx - 1 - len(label)
for dy in (0, 1, -1):
if canvas._place_run(start, cy + dy, label, rgb, bold=True, checked=True):
+30 -1
View File
@@ -108,7 +108,7 @@ def test_atlas_link_row_shrinks_the_key_before_the_name() -> None:
screen.note_viewport(24)
row = next(
line for line in _plain(screen.render_body(68)).split("\n")
if "Repeater-Downtown-01" in line
if "Repeater-Downtown-01" in line and "❯" in line # the list row, not a canvas label
)
assert "Repeater-Downtown-01" in row # the name shows whole, not clipped
assert "…" in row # the key is the lane that gave ground
@@ -116,6 +116,35 @@ def test_atlas_link_row_shrinks_the_key_before_the_name() -> None:
assert "d4c3b2a1f0e9" not in row # …but not the whole id — it was truncated
def test_atlas_graph_labels_names_in_full_when_the_canvas_has_room() -> None:
"""A neighbour's name is drawn whole on the canvas, not clipped to a flat short cap.
The fan pulls west of the edge and each label clamps to the room actually there, so a
long contact name keeps its letters wherever the canvas can hold it.
"""
long_names = [
Contact(name="YUL-Polytechnique", public_key="e8" * 32, node_type=2),
Contact(name="Repeater-Downtown-01", public_key="d4" * 32, node_type=2),
]
topo = MeshTopology(US, contacts=long_names)
when = utcnow()
for c, snr in zip(long_names, (8.0, 1.0)):
topo.add_walk([topo.self_id, topo.canonical(c.public_key)], snrs=[snr], when=when,
source="trace")
screen = AtlasScreen(
session=_FakeSession(),
topo=topo,
contacts={topo.canonical(c.public_key): c for c in long_names},
self_label="Homestead",
)
screen.note_viewport(24)
# The canvas rows are everything above the legend line.
lines = _plain(screen.render_body(80)).split("\n")
canvas = "\n".join(lines[: next(i for i, l in enumerate(lines) if "edge = SNR" in l)])
assert "YUL-Polytechnique" in canvas # 17 chars, drawn whole — no "YUL-Polytechniq…"
assert "Repeater-Downtown-01" in canvas # 20 chars, whole
def test_atlas_empty_graph_renders_guidance() -> None:
"""With no evidence at all the screen explains how the atlas fills up."""
topo = MeshTopology(US, contacts=[])