Skip to content

fix(guide): rysuj ramkę celu pod kursorem, nie nad nim#74

Merged
mpasternak merged 3 commits into
mainfrom
feat/guide-frame-under-cursor
Jul 25, 2026
Merged

fix(guide): rysuj ramkę celu pod kursorem, nie nad nim#74
mpasternak merged 3 commits into
mainfrom
feat/guide-frame-under-cursor

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Problem

Czerwony prostokąt obrysowujący cel kładł się na kursorze. Jego półprzezroczyste wypełnienie (rgba(238,17,17,0.08)) przykrywało grot strzałki — czyli dokładnie to, co czytelnik ma śledzić.

To nie był błąd rysowania, tylko konsekwencja warstw:

PDF:   [ SVG: strzałka, gwiazdka, RAMKA ]   ← zawsze na wierzchu
       [ PNG: strona + kursor            ]   ← płaskie piksele

Kursor powstaje jeszcze w żywej stronie (cursor.js, z-index: 2147483647) i po page.screenshot() jest już tylko grupą pikseli. PNG nie ma warstw, więc nic nie da się wsunąć pod jego fragment po fakcie.

Rozwiązanie

Ramka przenosi się tam, gdzie warstwy jeszcze istnieją — do strony, przed migawkę:

strona:  kursor   z-index 2147483647   ← wygrywa
         RAMKA    z-index 2147483645
   ↓ page.screenshot()
PNG:  poprawnie złożone przez przeglądarkę

cursor_effects.js dostaje trwałe frame()/clearFrame() — obok istniejącego highlight(), które jest filmowym błyskiem i samo znika po ~900 ms. Pudełko jest rozszerzane o połowę grubości obrysu, bo CSS rysuje border do środka, a SVG stroke centruje na ścieżce: bez tego ramka byłaby o 2 px ciaśniejsza niż dotąd.

_framed_screenshot obejmuje migawkę malowaniem i sprzątaniem. Sprzątanie w finally, bo ramka żyje w stronie, nie w tym procesie — wyjątek pomiędzy zostawiłby ją na każdym kolejnym zrzucie, obrysowującą zły element.

Dla select: maluje ją callback ujawnienia listy: tylko on zna pudełko kontrolki, bo zmierzone wcześniej należy do kontrolki zwiniętej, która dla page-enhanced selecta nigdy nie była na ekranie.

Usunięty martwy kod

Adnotacja kind="frame" znika całkiem — z gałęzią w _svg, regułą CSS .frame i polami x/y/w/h w Annotation. Zostawiona byłaby ścieżką, która wygląda na działającą i kusi do „przywrócenia". Geometrię nadal wyznacza target_shape, a FRAMED_ACTIONS nadal mówi które akcje obrysować — polityka została w jednym miejscu, zmienił się tylko malarz.

Weryfikacja

Wygenerowany PDF, wycinek kroku click — kursor przecina dolną krawędź ramki i jest w całości widoczny:

┌──────────────────────────┐
│ ▓▓ (o) LOGOWANIE ▓▓▓▓▓▓ │   ramka: czerwony obrys + jasne wypełnienie
└────────────▲─────────────┘
             ╲  kursor NA ramce, nie pod nią

To samo na kroku type w wyskakującym oknie.

  • tests/unit/guide/ + tests/unit/overlay/: 184 passed
  • ruff check z bramką C901 ≤ 10: czysto

Nowy test_no_frame_is_ever_drawn_over_the_still jest zamkiem na powrót: gdyby ktoś odtworzył rysowanie ramki w _svg, kursor znów wylądowałby pod czerwonym wypełnieniem, a test to złapie.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PEZwp8YivARFrZSjrSUnD5

mpasternak and others added 3 commits July 25, 2026 09:28
Czerwony prostokąt obrysowujący cel był adnotacją SVG doklejaną nad gotowym
zrzutem — a kursor jest w tym zrzucie wypalony. PNG to płaskie piksele, więc
warstwa dodana później przykrywa wszystko pod sobą, łącznie z kursorem, który
czytelnik ma śledzić. Półprzezroczyste wypełnienie ramki kładło się na grocie
strzałki.

Ramka przenosi się więc tam, gdzie warstwy jeszcze istnieją: do żywej strony,
przed migawkę. `cursor_effects.js` dostaje trwałe `frame()`/`clearFrame()` na
z-index 2147483645 — pod kursorem (2147483647), tak jak filmowy błysk
`highlight()`. Przeglądarka składa obie warstwy i dopiero wtedy spłaszcza je
do PNG.

`_framed_screenshot` obejmuje migawkę malowaniem i sprzątaniem; sprzątanie
w `finally`, bo ramka żyje w stronie, nie w tym procesie — wyjątek pomiędzy
zostawiłby ją na każdym kolejnym zrzucie, obrysowującą zły element. Dla
`select:` maluje ją callback ujawnienia, bo tylko on zna pudełko kontrolki:
zmierzone wcześniej należy do kontrolki zwiniętej.

Adnotacja `kind="frame"` znika całkiem — razem z gałęzią w `_svg`, regułą
`.frame` i polami x/y/w/h w `Annotation`. Zostawiona byłaby martwą ścieżką,
która wygląda na działającą. Geometrię nadal wyznacza `target_shape`, więc
polityka „co i gdzie obrysować" została w jednym miejscu.

Zweryfikowane na zrzutach: kursor przecina dolną krawędź ramki i jest w całości
widoczny, zarówno na kroku `click`, jak i `type` w popupie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEZwp8YivARFrZSjrSUnD5
… bez cichych domyślnych

Trzy rzeczy, które wyszły przy przeglądzie własnej zmiany:

1. `cursor_effects.js` nie miał żadnego pokrycia w przeglądarce, mimo że
   testy overlay jeżdżą po prawdziwym Chromium. Zepsuty z-index przeszedłby
   przez wszystkie 184 testy — jedyną weryfikacją były oględziny zrzutu.
   Dochodzi pięć testów, w tym ten pilnujący jedynej własności, o którą w tej
   zmianie chodzi: ramka MUSI mieć niższy z-index niż kursor. Porównanie jest
   relacyjne, nie dosłowną stałą. Sprawdzone mutacją: podniesienie z-indeksu do
   maksimum i usunięcie rozsunięcia o pół obrysu — oba warianty czerwienią się.

2. `getattr(cap.recorder, "overlay", None)` maskowałoby zmianę nazwy atrybutu:
   ramka zniknęłaby ze wszystkich zrzutów, a testy zostałyby zielone. Każdy
   Recorder — prawdziwy i każdy dubler — ten atrybut ma, więc domyślna wartość
   była nieosiągalna. Zwykły dostęp, głośny AttributeError.

3. `frame()` przyjmowało `options`, których nikt nie podaje — cztery gałęzie bez
   pokrycia. Wartości wracają do stałych (tych samych, co dawna reguła `.frame`),
   a grubość obrysu i z-index dostają nazwane stałe z uzasadnieniem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEZwp8YivARFrZSjrSUnD5
CI złapało to, czego nie złapał mój self-review: przejrzałem tylko
`tests/unit/`, a pięć testów integracyjnych asertowało na adnotacji
`kind="frame"`, usuniętej razem z rysowaniem ramki w SVG.

Te testy mierzą realną geometrię w prawdziwej przeglądarce — czy obrysowane
pudełko to faktycznie pudełko kontrolki — więc skasowanie ich byłoby utratą
pokrycia dokładnie tam, gdzie jest najcenniejsze. Zamiast tego `_frame_spy.py`
nagrywa wywołania `Overlay.frame`, a asercje zachowują sens jeden do jednego:
`_only_frame(painted)` w miejsce `_only(annotations, "frame")`.

Szpieg woła oryginał dalej. Kilka z tych testów czyta potem piksele ze zrzutu,
więc połknięcie wywołania po cichu zmieniłoby obraz, który badają.

Pełny `pytest -m "not network"` lokalnie: 1604 passed, 1 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEZwp8YivARFrZSjrSUnD5
@mpasternak
mpasternak merged commit eb60151 into main Jul 25, 2026
3 checks passed
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.

1 participant