A blurred seam is the chip's own fill shaded, and "blurred" is measured by eye in OKLab

The thin chevron between two chips the reader can't tell apart is now drawn in the
previous chip's own fill stepped SEAM_SHADE darker (lighter for a dark fill) on the
fill ahead, so nothing on the ribbon is a colour that isn't a chip's. The step is
measured from whichever fill is further along, so the mark clears the fill it sits
on by the full shade however close the two are.

Whether two fills blur is now a perceptual distance, not a hue gap: ui/oklab.py is
the reference OKLab transform (from_hex, to_hex, distance, shaded), and the seam
interlocks only where the fills sit SEAM_BLUR (0.10) or more apart. The hue rule
mis-sized the wheel badly: sixteen first-byte steps measure 0.026 across the greens
and 0.165 across the cyans, so it blurred too little on one side and too much on the
other, and an sRGB distance would only have restated the hue gap, a fixed byte step
being a near-constant 55-60 of 8-bit RGB all the way round. 0.10 is five noticeable
differences, the typical 0x10 pair with a little to spare, erring toward the thin
chevron because a wrong blur still draws a legible seam and a wrong interlock draws
none. The greys need no exemption any more: the keyless grey and the faded slate sit
0.18 apart and keep their interlock on the metric alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5.1 committed 2026-09-16 00:30:50 -04:00
1 parent 282334c761
commit 1838ee3768
5 files changed
+282 -104

No files matched your search

+8 -6
View File
@@ -311,12 +311,14 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
that differ from row to row. As a chip it is the map's yellow star on neutral dark
grey, padded like every other chip.
- Chip seams are **one** interlocked chevron (previous fill on next). Two chips of the
same fill are the exception the interlock can't draw — and so are two hues too close to
tell apart, first key bytes under `pathline.SEAM_HUE_GAP` (0x10) apart on the wheel,
circularly; the greys blend only with their exact selves. There the seam is the **thin**
chevron (`POWERLINE_THIN`) in the chip's ink on the shared fill, so the ribbon runs on
unbroken and the join is a line drawn on it, never a wedge of page cut out of the
route. An elision breaks the ribbon rather than joining it — bare `⋯`
same fill are the exception the interlock can't draw — and so are two fills the eye
can't tell apart: under `pathline.SEAM_BLUR` apart in OKLab (`ui/oklab.py`, THE
perceptual colour distance — never a hue gap, never sRGB, both of which mis-size the
greens against the cyans). There the seam is the **thin** chevron (`POWERLINE_THIN`)
in the previous chip's own fill shaded `SEAM_SHADE` darker (lighter for a dark fill),
drawn on the next, so the ribbon runs on unbroken and the join is a line the chip draws
on itself, never a wedge of page cut out of the route and never a third colour. An
elision breaks the ribbon rather than joining it — bare `⋯`
on the page between a closing point and the next chip's notch, no fill, no padding.
- The ribbon's **outer ends**: the chevron means *the route continues*, so an end that is
the route's own never wears one. It ends **square** — nothing appended, the last chip's
+110
View File
@@ -0,0 +1,110 @@
# SPDX-License-Identifier: Apache-2.0
"""OKLab — the perceptual colour space the UI measures and shades ``#rrggbb`` values in.
Where the app has to ask a question about two colours *as the eye sees them* — are these
two chip fills the same to a reader, what is a slightly darker version of this one — it
asks here, not in RGB and not by hue. sRGB is a device encoding: a fixed step in it is a
different size to the eye in every region of the wheel (a step across the greens is a
whisper, the same step across the cyans a shout), and a hue-only comparison is worse,
blind to lightness and saturation altogether. OKLab (Björn Ottosson, 2020) is built so
that Euclidean distance approximates perceived difference and so that changing ``L``
alone changes lightness and nothing else, which is exactly the two operations wanted.
Everything is the reference transform, no dependencies: linear sRGB → LMS → cube root →
Lab. ``L`` runs 0 (black) to 1 (white); ``a``/``b`` are the two chroma axes, roughly
±0.4 at the sRGB gamut's edge. A distance of about 0.02 is a just-noticeable difference
between two large patches.
"""
from __future__ import annotations
import math
#: An OKLab triple ``(L, a, b)``.
Lab = tuple[float, float, float]
def _linear(channel: float) -> float:
"""The sRGB transfer function, 0–1 encoded → 0–1 linear."""
return channel / 12.92 if channel <= 0.04045 else ((channel + 0.055) / 1.055) ** 2.4
def _encoded(channel: float) -> float:
"""The inverse: 0–1 linear → 0–1 sRGB encoded."""
return 12.92 * channel if channel <= 0.0031308 else 1.055 * channel ** (1 / 2.4) - 0.055
def from_hex(colour: str) -> Lab:
"""``#rrggbb`` → OKLab.
Args:
colour: A six-digit hex colour, ``#`` optional, either case.
Raises:
ValueError: If the string is not six hex digits.
"""
raw = colour.removeprefix("#")
if len(raw) != 6:
raise ValueError(f"not a #rrggbb colour: {colour!r}")
r, g, b = (_linear(int(raw[i : i + 2], 16) / 255) for i in (0, 2, 4))
l_ = (0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b) ** (1 / 3)
m_ = (0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b) ** (1 / 3)
s_ = (0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b) ** (1 / 3)
return (
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
)
def to_hex(lab: Lab) -> str:
"""OKLab → ``#rrggbb``, each channel clipped to the sRGB gamut.
Clipping is the honest answer for the one caller that can leave the gamut — a
saturated fill darkened at constant chroma pushes a channel below zero — because the
clipped colour is the nearest the terminal can show, and it stays the right hue to
the eye at the shifts the app asks for.
"""
lightness, a, b = lab
l_ = lightness + 0.3963377774 * a + 0.2158037573 * b
m_ = lightness - 0.1055613458 * a - 0.0638541728 * b
s_ = lightness - 0.0894841775 * a - 1.2914855480 * b
lms = (l_**3, m_**3, s_**3)
linear = (
4.0767416621 * lms[0] - 3.3077115913 * lms[1] + 0.2309699292 * lms[2],
-1.2684380046 * lms[0] + 2.6097574011 * lms[1] - 0.3413193965 * lms[2],
-0.0041960863 * lms[0] - 0.7034186147 * lms[1] + 1.7076147010 * lms[2],
)
return "#" + "".join(f"{round(_encoded(min(1.0, max(0.0, c))) * 255):02x}" for c in linear)
def distance(first: str, second: str) -> float:
"""Perceived difference between two ``#rrggbb`` colours: Euclidean distance in OKLab.
``0`` for the same colour; about ``0.02`` at the threshold of noticing between two
large patches; ``1`` is black to white.
"""
return math.dist(from_hex(first), from_hex(second))
def shaded(colour: str, by: float, *, floor: float | None = None) -> str:
"""``colour`` moved ``by`` in lightness, away from whichever end it is nearer.
A light colour comes back darker and a dark one lighter, at the same chroma, so the
result is recognisably *the same colour, shaded* rather than a different one — the
version of a fill that draws a line on itself.
Args:
colour: The ``#rrggbb`` to shade.
by: The lightness shift, in OKLab ``L`` (``0.1`` is a clear step, ``0.25`` bold).
floor: If given, the shift starts from this lightness rather than the colour's
own where that is further along — a caller guaranteeing the result clears a
*second* colour's lightness by ``by`` as well as this one's passes that
colour's ``L``. Read as "at least this far from both".
"""
lightness, a, b = from_hex(colour)
if lightness >= 0.5:
start = lightness if floor is None else min(lightness, floor)
return to_hex((start - by, a, b))
start = lightness if floor is None else max(lightness, floor)
return to_hex((start + by, a, b))
+56 -65
View File
@@ -16,10 +16,12 @@ drawn*, so every surface the survey found can eventually route through it:
hash-derived hue (:func:`~meshterm.ui.theme.node_style`), our own node the map's yellow
``★`` on a neutral dark grey, a faded hop dark slate, a keyless hop grey. Two chips that
land on the *same* fill — a mirrored return leg, a stretch of keyless greys — or on two
hues too close to tell apart (first key bytes under :data:`SEAM_HUE_GAP` apart on the
wheel) take the one exception the interlock can't draw: the seam is the *thin* chevron
instead, ink on the shared fill, so the ribbon runs on unbroken and the join is a line
drawn on it (:meth:`PathLine._seam`). Hops elided out of the middle break the
fills too close to tell apart (under :data:`SEAM_BLUR` apart in OKLab, the perceptual
distance :mod:`~meshterm.ui.oklab` measures) take the one exception the interlock can't
draw: the seam is the *thin* chevron instead, the previous fill shaded a step darker
(lighter, for a dark fill) and drawn on the next, so the ribbon runs on unbroken and the
join is a line the chip draws on itself (:meth:`PathLine._seam`). Hops elided out of
the middle break the
ribbon instead of joining it: the mark sits bare on the page between a closing point and
the next chip's notch (:func:`elision_hop`), because a filled ``⋯`` chip would read as a
node by that name. A chip caught by a *cut* — a lane that ran out, a line scrolled past
@@ -77,7 +79,6 @@ earned, and a cursor can never be stranded on a line break.
from __future__ import annotations
import colorsys
import re
from collections.abc import Callable, Sequence
from dataclasses import dataclass
@@ -88,6 +89,7 @@ from rich.measure import Measurement
from rich.text import Text
from ..core.models import LOCAL_DEVICE_LABEL
from . import oklab
from .marks import SELF_MARK
from .termfont import powerline_enabled, powerline_full
from .theme import active_theme, node_style
@@ -101,8 +103,8 @@ POWERLINE_SEP = "\ue0b0"
#: The thin right-pointing chevron (U+E0B1), the solid point's outline sibling in the
#: same core set — powerline's own mark for a join *inside* one colour. It draws the seam
#: between two chips whose fills the eye can't tell apart (:meth:`PathLine._seam`): a
#: line of ink on the fill they share, where a solid point would have vanished into it.
#: between two chips whose fills the eye can't tell apart (:meth:`PathLine._seam`): the
#: previous fill, shaded, drawn on the next, where a solid point would have vanished.
POWERLINE_THIN = "\ue0b1"
#: The rounded caps (U+E0B6 opening, U+E0B4 closing) that finish a path's two outer
@@ -258,49 +260,40 @@ def _style_hex(style: str) -> str | None:
return None
#: How far apart two chip hues must sit on the 256-step wheel for a seam to interlock
#: them: closer than this and the seam is the thin chevron in ink instead, as it is for
#: two fills that are identical (:meth:`PathLine._seam`). The wheel *is* the node's first key
#: byte (:func:`~meshterm.ui.theme.node_style` maps ``0x00``–``0xff`` straight round it),
#: so this is a difference in first bytes — under ``0x10`` and two neighbours are one
#: ribbon to the eye, a chevron of one shade of green laid on the next (JP, 2026-09-15).
#: The gap is circular: ``0xf8`` and ``0x04`` are twelve apart, not two hundred and forty.
SEAM_HUE_GAP = 0x10
#: How far apart two chip fills must sit, as the eye measures it, for the seam between
#: them to interlock: closer than this and the solid point would be laid on a field it
#: can't be told from, so the seam is the thin chevron instead (:meth:`PathLine._seam`).
#: The unit is OKLab distance (:func:`~meshterm.ui.oklab.distance`), where ``0.02`` is a
#: just-noticeable difference between two large patches and ``1`` is black to white.
#:
#: Calibrated against the rule it replaced — first key bytes under ``0x10`` apart on the
#: hue wheel (JP, 2026-09-15) — which measures ``0.087`` at the median hue but ``0.026``
#: across the greens and ``0.165`` across the cyans: the wheel is not uniform to the eye,
#: so a hue gap blurred too little there and too much here, and an sRGB distance would
#: only have restated the hue gap (a fixed byte step is a near-constant ``55``–``60`` of
#: 8-bit RGB all the way round). ``0.10`` is five noticeable differences, the typical
#: ``0x10`` pair with a little to spare, and it errs toward the thin chevron on purpose:
#: a wrong blur still draws a legible seam, a wrong interlock draws none (JP, 2026-09-16).
SEAM_BLUR = 0.10
#: Below this saturation a fill is a grey — a keyless hop, the dim slate, our own
#: neutral — whose hue is noise, so it blends with nothing but its exact self. The
#: spectrum fills and the console's six slots all sit well above it.
_HUE_SAT_FLOOR = 0.5
def _hue_byte(fill: str) -> int | None:
"""Where a ``#rrggbb`` fill sits on the 256-step hue wheel, or ``None`` for a grey.
The inverse of :func:`~meshterm.ui.theme._node_style_spectrum`: every one of its 256
fills recovers the exact first key byte that minted it (the round trip through 8-bit
RGB is lossless at the spectrum's saturation), so two chips are compared by the very
bytes that coloured them — and an override fill or a console slot, which no byte
minted, is still placed on the same wheel by the hue it actually shows.
"""
try:
r, g, b = (int(fill[i : i + 2], 16) / 255 for i in (1, 3, 5))
except (ValueError, IndexError):
return None
hue, sat, _ = colorsys.rgb_to_hsv(r, g, b)
if sat < _HUE_SAT_FLOOR:
return None
return round(hue * 256) % 256
#: How far the thin chevron's colour steps from the fill it is drawn in, in OKLab ``L``
#: — a lightness shift alone, so it reads as *that chip's colour, shaded* rather than as
#: a third colour on the line. The step is taken from the previous fill but measured from
#: whichever of the two fills is further along (:func:`~meshterm.ui.oklab.shaded`'s
#: ``floor``), so the mark clears the fill it actually sits on by this much even when the
#: two differ by everything :data:`SEAM_BLUR` allows. Above the blur on its own, so the
#: line it draws is always one the eye can find (JP, 2026-09-16).
SEAM_SHADE = 0.15
def _fills_blur(before: str, after: str) -> bool:
"""Whether two fills are the same to the eye — identical, or hues under the gap."""
if before == after:
return True
a, b = _hue_byte(before), _hue_byte(after)
if a is None or b is None:
return False
apart = abs(a - b)
return min(apart, 256 - apart) < SEAM_HUE_GAP
"""Whether two fills are the same to the eye: identical, or under :data:`SEAM_BLUR`."""
return before == after or oklab.distance(before, after) < SEAM_BLUR
def _seam_ink(before: str, after: str) -> str:
"""The thin chevron's colour: ``before`` shaded :data:`SEAM_SHADE` past both fills."""
return oklab.shaded(before, SEAM_SHADE, floor=oklab.from_hex(after)[0])
#: A chip's fill, as it appears in a rendered span's style — the ``on #rrggbb`` half.
@@ -990,7 +983,7 @@ class PathLine:
elif hop.gap:
text.append(POWERLINE_SEP, style=fills[i - 1]) # …and stops here
else:
text.append_text(self._seam(fills[i - 1], fills[i], hop))
text.append_text(self._seam(fills[i - 1], fills[i]))
if hop.gap:
text.append(hop.label, style="faint" if hop.dim else "muted")
else:
@@ -1004,7 +997,7 @@ class PathLine:
return text
@staticmethod
def _seam(before: str, after: str, ahead: PathHop) -> Text:
def _seam(before: str, after: str) -> Text:
"""The one cell between two chips: the previous fill's point, laid on the next.
The classic interlock — foreground the chip behind, background the chip ahead —
@@ -1014,35 +1007,33 @@ class PathLine:
to spend two cells leaving a sliver of page between every pair, insurance against
a collision that the palette makes rare).
Two fills that *do* land the same are the exception the interlock cannot draw —
Two fills the eye can't tell apart are the exception the interlock cannot draw —
a chevron in its own background is no chevron — and they are not always bad luck:
a mirrored return leg is a run of identically faded hops by construction, and so
is a stretch of keyless greys. *Nearly* the same is the same exception: two hues a
few steps apart on the wheel draw a chevron the eye can't find either, one shade of
green on the next, so two first bytes under :data:`SEAM_HUE_GAP` apart count too
(:func:`_fills_blur`, JP, 2026-09-15). The greys are exempt: a keyless hop beside a
dimmed one shows two distinct fills and keeps its interlock.
is a stretch of keyless greys. Nor is "can't tell apart" the same as "identical":
two hues a few steps apart on the wheel draw a point the eye can't find either,
one shade of green on the next, so the test is a perceptual distance under
:data:`SEAM_BLUR` (:func:`_fills_blur`), not equality and not a hue gap.
There the seam is the **thin** chevron, :data:`POWERLINE_THIN`, in the chip's ink
on the fill ahead — powerline's own mark for a join inside one colour (JP,
2026-09-16). The ribbon runs on unbroken and the join is a line drawn on it,
where the solid point drawn bare used to cut a wedge of page out of the route: a
run of near hues read as dashes, and on a light terminal the wedge was a hole. It
wears no fill of its own, so it can't be mistaken for a chip, and the ink is the
one the chip ahead uses for its label — the dim ink on a faded leg — so the mark
recedes with what it joins. One cell either way.
There the seam is the **thin** chevron, :data:`POWERLINE_THIN`, in the previous
chip's own fill shaded a step darker — lighter, for a dark fill — and drawn on
the fill ahead (:func:`_seam_ink`, JP, 2026-09-16): powerline's own mark for a
join inside one colour, and the chip it belongs to drawing a line on itself, so
the ribbon runs on unbroken and nothing on it is a colour that isn't a chip's.
The solid point drawn bare used to cut a wedge of page out of the route here (a
run of near hues read as dashes, and on a light terminal the wedge was a hole),
and a chevron in the dark chip ink read as a mark laid on the route rather than a
seam in it. One cell either way.
Args:
before: The fill the point is drawn in (the previous chip's).
after: The fill it is laid on (the next chip's).
ahead: The hop the seam leads into — its ink is the thin chevron's.
Returns:
The seam's single styled cell.
"""
if _fills_blur(before, after):
ink = _DIM_FG if ahead.dim else _CHIP_FG
return Text(POWERLINE_THIN, style=f"{ink} on {after}")
return Text(POWERLINE_THIN, style=f"{_seam_ink(before, after)} on {after}")
return Text(POWERLINE_SEP, style=f"{before} on {after}")
@staticmethod
+68
View File
@@ -0,0 +1,68 @@
# SPDX-License-Identifier: Apache-2.0
"""``ui/oklab.py`` — the perceptual colour space the seam and its shade are measured in."""
from __future__ import annotations
import pytest
from meshterm.ui import oklab
from meshterm.ui.theme import _node_style_spectrum
def _fill(key: str) -> str:
return _node_style_spectrum(key).split()[-1]
def test_every_node_fill_round_trips_through_oklab_exactly() -> None:
"""Hex → Lab → hex is lossless on all 256 spectrum fills and the fixed greys."""
for byte in range(256):
colour = _fill(f"{byte:02x}")
assert oklab.to_hex(oklab.from_hex(colour)) == colour
for grey in ("#000000", "#ffffff", "#64748b", "#334155", "#3f3f46"):
assert oklab.to_hex(oklab.from_hex(grey)) == grey
def test_lightness_runs_black_to_white_and_distance_is_a_metric() -> None:
"""``L`` spans 0–1, black to white is distance 1, and distance is symmetric."""
black, white = oklab.from_hex("#000000"), oklab.from_hex("#ffffff")
assert black[0] == pytest.approx(0.0, abs=1e-6) and white[0] == pytest.approx(1.0, abs=1e-3)
assert oklab.distance("#000000", "#ffffff") == pytest.approx(1.0, abs=1e-3)
assert oklab.distance("#f25555", "#f25555") == 0.0
assert oklab.distance("#f25555", "#5557f2") == oklab.distance("#5557f2", "#f25555")
def test_the_wheel_is_not_uniform_to_the_eye() -> None:
"""The reason this module exists: one hue step is a whisper in green, a shout in cyan."""
green = oklab.distance(_fill("4c"), _fill("5c"))
cyan = oklab.distance(_fill("80"), _fill("90"))
assert green < 0.03 < 0.15 < cyan
def test_shaded_moves_lightness_alone_away_from_the_nearer_end() -> None:
"""A light colour comes back darker, a dark one lighter, chroma untouched."""
light, dark = "#55f255", "#334155"
for colour, sign in ((light, -1), (dark, +1)):
before, after = oklab.from_hex(colour), oklab.from_hex(oklab.shaded(colour, 0.1))
assert (after[0] - before[0]) * sign == pytest.approx(0.1, abs=0.01)
assert after[1] == pytest.approx(before[1], abs=0.02) # in-gamut: chroma kept
assert after[2] == pytest.approx(before[2], abs=0.02)
def test_shaded_floor_clears_a_second_colour_too() -> None:
"""With a floor, the shift starts from whichever lightness is further along."""
a, b = _fill("4c"), _fill("5c") # b is the darker of two near greens
plain = oklab.shaded(a, 0.15)
floored = oklab.shaded(a, 0.15, floor=oklab.from_hex(b)[0])
assert oklab.from_hex(floored)[0] <= oklab.from_hex(plain)[0]
assert oklab.from_hex(b)[0] - oklab.from_hex(floored)[0] == pytest.approx(0.15, abs=0.01)
slate, lighter_slate = "#334155", "#3f3f46" # dark: the floor pushes *up* instead
floored = oklab.shaded(slate, 0.15, floor=oklab.from_hex(lighter_slate)[0])
assert oklab.from_hex(floored)[0] >= oklab.from_hex(oklab.shaded(slate, 0.15))[0]
def test_from_hex_rejects_anything_but_six_digits() -> None:
"""Six hex digits, ``#`` and case optional; anything else is a ``ValueError``."""
with pytest.raises(ValueError):
oklab.from_hex("#fff")
assert oklab.from_hex("F25555") == oklab.from_hex("#f25555")
+40 -33
View File
@@ -14,10 +14,9 @@ import random
import pytest
import meshterm.ui.pathline as pathline
from meshterm.ui import oklab
from meshterm.ui.pathline import (
_CHIP_FG,
_DIM_BG,
_DIM_FG,
_SELF_INK,
_YOU_BG,
CRACK_HEAD,
@@ -30,10 +29,13 @@ from meshterm.ui.pathline import (
POWERLINE_ROUND_OPEN,
POWERLINE_SEP,
POWERLINE_THIN,
SEAM_BLUR,
SEAM_SHADE,
SELF_GLYPH,
WRAP_OFFSET,
PathHop,
PathLine,
_seam_ink,
_style_hex,
cut_mark,
cut_to,
@@ -653,54 +655,59 @@ def _seams(*hops: PathHop) -> list[tuple[str, str]]:
]
def test_a_seam_between_two_of_one_colour_is_the_thin_chevron_in_ink() -> None:
"""Where two chips land on the same fill, the seam is the thin chevron in ink.
def test_a_seam_between_two_of_one_colour_is_the_thin_chevron_shaded() -> None:
"""Where two chips land on the same fill, the seam is the thin chevron in that fill, shaded.
The interlock can't draw a solid point in its own background, so the join is drawn
*on* the shared fill instead — powerline's own mark for a join inside one colour —
and the ribbon runs on unbroken. It wears the chip's ink, the dim ink on a faded leg,
and no fill of its own, so it can never be taken for a chip.
and the ribbon runs on unbroken. Its colour is the previous chip's own, a lightness
step darker, so nothing on the line is a colour that isn't a chip's; a dark fill (the
faded slate) steps lighter instead.
Not a rare accident, either: a mirrored return leg is a run of identically faded hops
by construction, and a stretch of keyless hops shares one grey. Either way it stays a
single cell — the fallback trades the interlock for the outline, not for width.
"""
hue = _style_hex(node_style("aa"))
assert _seams(PathHop("A", key="aa"), PathHop("B", key="aa"))[0] == (
POWERLINE_THIN,
f"{_CHIP_FG} on {hue}",
)
glyph, style = _seams(PathHop("A", key="aa"), PathHop("B", key="aa"))[0]
assert glyph == POWERLINE_THIN and style == f"{_seam_ink(hue, hue)} on {hue}"
shade = _seam_ink(hue, hue)
assert oklab.from_hex(shade)[0] < oklab.from_hex(hue)[0] # darker…
assert oklab.distance(shade, hue) > SEAM_BLUR # …by more than the blur, so it reads
assert oklab.distance(shade, hue) < oklab.distance(hue, _style_hex(node_style("77")))
assert _seams(PathHop("A", key="aa"), PathHop("B", key="77"))[0] == (
POWERLINE_SEP,
f"{hue} on {_style_hex(node_style('77'))}",
) # blended, one cell
assert _seams(PathHop("A", dim=True), PathHop("B", dim=True))[0] == (
POWERLINE_THIN,
f"{_DIM_FG} on {_DIM_BG}",
) # a dimmed return leg reads as hops, not one bar, and recedes with them
glyph, style = _seams(PathHop("A", dim=True), PathHop("B", dim=True))[0]
assert glyph == POWERLINE_THIN and style.endswith(f" on {_DIM_BG}")
lighter = style.split()[0]
assert oklab.from_hex(lighter)[0] > oklab.from_hex(_DIM_BG)[0] # a dark fill steps up
def test_a_seam_between_two_near_hues_is_the_thin_chevron_too() -> None:
"""Two hues under ``SEAM_HUE_GAP`` apart on the wheel blend like an exact match.
def test_a_seam_blurs_by_perceived_distance_not_by_hue_gap() -> None:
"""Two fills under ``SEAM_BLUR`` apart to the eye blur like an exact match.
The wheel is the first key byte, so the gap is a byte difference — and a circular
one, so ``f8`` and ``04`` are neighbours. At the gap itself the interlock is back.
The greys are exempt: a keyless hop beside a dimmed one keeps its blend, because two
distinct greys are two distinct fills and the chevron between them still reads.
The eye, not the wheel: sixteen first-byte steps across the greens are one colour to
a reader and sixteen across the cyans are two, so the same hue gap blurs on one side
and interlocks on the other. The greys work the same way with no exemption needed:
the keyless grey and the faded slate sit far apart in lightness and keep their seam.
"""
hue = _style_hex(node_style("a0"))
thin = lambda key: (POWERLINE_THIN, f"{_CHIP_FG} on {_style_hex(node_style(key))}") # noqa: E731
assert _seams(PathHop("A", key="a0"), PathHop("B", key="af"))[0] == thin("af")
assert _seams(PathHop("A", key="af"), PathHop("B", key="a0"))[0] == thin("a0")
assert _seams(PathHop("A", key="f8"), PathHop("B", key="04"))[0] == thin("04") # wraps
assert _seams(PathHop("A", key="a0"), PathHop("B", key="b0"))[0] == (
POWERLINE_SEP,
f"{hue} on {_style_hex(node_style('b0'))}",
) # exactly the gap apart: distinct
assert _seams(PathHop("A", key="a0"), PathHop("B", key="20"))[0] == (
POWERLINE_SEP,
f"{hue} on {_style_hex(node_style('20'))}",
) # opposite sides of the wheel
green = _seams(PathHop("A", key="4c"), PathHop("B", key="5c"))[0]
cyan = _seams(PathHop("A", key="80"), PathHop("B", key="90"))[0]
assert green[0] == POWERLINE_THIN and cyan[0] == POWERLINE_SEP
assert oklab.distance(_style_hex(node_style("4c")), _style_hex(node_style("5c"))) < SEAM_BLUR
assert oklab.distance(_style_hex(node_style("80")), _style_hex(node_style("90"))) > SEAM_BLUR
# The shade is taken from the chip behind but clears the chip ahead as well.
before, after = _style_hex(node_style("4c")), _style_hex(node_style("5c"))
assert green[1] == f"{_seam_ink(before, after)} on {after}"
assert oklab.distance(_seam_ink(before, after), after) >= SEAM_SHADE - 1e-6
wrapped = _seams(PathHop("A", key="f8"), PathHop("B", key="04"))[0]
assert wrapped[0] == POWERLINE_THIN # neighbours across the wheel's seam, too
glyph, style = _seams(PathHop("A"), PathHop("B", dim=True))[0]
assert glyph == POWERLINE_SEP and " on " in style # two greys, still interlocked