-
Nikitos создал
§4.2 назвал дыру прямо: «grep scissor/clipRect = 0 — нет ни scissor, ни clip». Из-за этого не работали прокрутка списка, портрет в рамке и миникарта. В SDL3 GPU оказался SDL_SetGPUScissor (с 3.2.0), так что stencil для этого не понадобился — взял scissor, он закрывает ровно названную дыру. ЧТО СДЕЛАНО: * C: таблица обрезок кадра (R2D_MAX_CLIPS 256), r2d_render_set_clip/clear_clip/ get_clip/clip_count; каждая команда помнит СВОЮ обрезку (R2DDrawCmd.clip), а проход рвёт участок по клипу и ставит scissor перед рисованием. Повтор той же обрезки не занимает новый слот — иначе скролл переполнил бы таблицу за секунду; * engine.setClip / clearClip / getClip / clipCount; * $.gfx.clip(x, y, w, h) | ({rect}) | (узел), clipOff(), clipRect(), clipCount(); * .clip(true | {x,y,w,h} | false) у узла — обрезка по своей коробке или по прямоугольнику. КЛЮЧЕВОЕ: обрезка действует на КОМАНДУ, а не на кадр. Батч кадра один, но каждый спрайт помнит свой прямоугольник, и отправка рвётся по клипу. Поэтому РАЗНЫЕ УЗЛЫ ОДНОГО КАДРА обрезаются по-разному — это и проверено тестом (два узла, два клипа, 3600 пикселей каждый). ЕЩЁ ОДНА ТОНКОСТЬ, найденная по дороге: сброс обрезок стоял в начале _render, а игра ставит клип в $.render, который идёт РАНЬШЕ — клип стирался перед самой отрисовкой. Сброс перенесён в начало кадра отрисовки (setRender). ПРОВЕРКА (tests/agent/highlevel_clip_test.py, по пикселям): без обрезки узел виден весь; клип 600,500,100,80 даёт ровно 600..699 x 500..579; clipOff возвращает целый узел; обрезка узла НЕ трогает соседа (не «растекается»); два узла одного кадра обрезаются каждый своим; нулевой прямоугольник снимает обрезку. ДОКИ: render.md §5.1 (как работает, где вызывать, таблица вызовов, ограничения — обрезка узла не наследуется детьми), API.md (engine.setClip и родня), §1.6 TASKS переписан из «нет scissor/clip» в «сделано». Полный qjs, агентские тесты, doc_coverage, doc_claims, duplicate_keys, быстрый набор — зелёные.f714982d