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:
1 parent
bd13b117ce
commit
e5aa5810dc
2 files changed
+79
-22
No files matched your search
+49
-21
@@ -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
@@ -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=[])
|
||||
|
||||
Reference in new issue
Block a user