Files
houseplan-card/legacy/specs/209-vacuum-trail-smoothing.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в
`legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код,
тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ —
на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`),
DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README
каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди
перенесённых нет. Относительные ссылки перенесённых файлов переписаны
(`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) —
все 26 резолвятся. Попутно: битая ссылка в
`089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` —
теперь команда `git show` по истории. Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00

21 KiB
Raw Permalink Blame History

Issue #209 — плавный след пылесоса

  • Дата: 2026-08-28
  • Тип: feature · приоритет P3
  • Оценка: пользовательская ценность 4/10 · ценность для разработки 2/10 · сложность 3/10 · риск 5/10
  • Issue: #209
  • Ветка: issue/209-vacuum-trail-smoothing

Канонические документы: docs/SCOPE.md, docs/VACUUM.md, docs/USER-GUIDE.ru.md, docs/ARCHITECTURE.md, docs/TESTING.md.

1. Сценарий и персона

Домочадец либо пользователь настенного киоска смотрит на текущую или последнюю уборку в View. Телеметрия приходит дискретными точками, поэтому существующая линия заметно ломается на каждой точке и выглядит менее естественно, чем фактическое движение робота.

Администратор видит тот же результат в обычном View после настройки источника и калибровки. Редакторы не показывают след и этой задачей не меняются.

2. Что человек увидит до и после

До: текущий и предыдущий следы состоят из прямых отрезков с резкими углами в телеметрических точках.

После: те же маршруты показаны плавными кривыми с мягкими поворотами. Кривая не отходит от исходной ломаной более чем на 17,5 см, не соединяет разрывы и не опережает движущийся значок пылесоса.

3. Проблема

_renderVacuums() строит текущий маршрут командами SVG M/L, а предыдущий — отдельным polyline. Это создаёт две реализации одной визуальной геометрии и делает каждый дискретный поворот резким. Наивный Catmull–Rom или другой интерполирующий сплайн неприемлем: на неравномерно прореженных данных он способен далеко выйти за фактический маршрут, пересечь стену либо визуально отметить не убранную область.

Изменение должно оставаться представлением существующих точек. Источник данных, их хранение, нормализация, приоритет, прореживание и lifecycle уборки не меняются.

4. Scope

  • сгладить текущий видимый след на всех поверхностях, где он уже рисуется;
  • тем же способом сгладить предыдущий run в режиме always;
  • заменить отдельную polyline-ветку previous на общий pure builder SVG-path;
  • ограничить сглаживание фиксированным радиусом 17,5 см в координатах плана;
  • сохранить точные endpoints каждого подпути и буквальные разрывы между ними;
  • сохранить casing/core, current/previous opacity и все trail modes;
  • покрыть геометрию pure unit-тестами, production-bundle smoke и новой golden-сценой с детерминированным сохранённым маршрутом;
  • документировать новый визуальный контракт.

5. Non-scope

  • изменение VacPath, backend Store, trails.py, калибровки или матрицы;
  • добавление/удаление/перестановка сохранённых телеметрических точек;
  • изменение лимитов 64 подпути / 4000 точек / 600 локальных точек;
  • изменение resolveCurrentVacPath(), trimVacPathTarget(), trail modes, 30-минутного resume grace или приоритета integration → server → local;
  • UI-настройка степени сглаживания либо возможность его выключить;
  • сглаживание движения puck, изменение live tip, толщины, цветов или opacity;
  • добавление следа в редакторы или на поверхности, которые его сейчас не имеют;
  • реконструкция маршрута, устранение телепортов, collision-aware обход стен;
  • изменение room-cleaning highlight и прочих vacuum overlays.

6. Контракт поведения

6.1 Единицы и порядок преобразований

Ограничение 17,5 см переводится в единицы текущего пространства единственным существующим масштабом gridPitch / cellCm. Сначала каждая vacuum-точка проходит существующую affine-калибровку в плоские координаты плана, затем строится сглаженная геометрия. Только после этого все её точки проходят существующую проекцию _scenePoint() для flat/iso-рендера.

Так предел измеряется в физическом плане, а не в экранных пикселях и не меняется от zoom, размера карточки, DPR, темы или проекции. Для изометрии сохраняется проекция той же плоской кривой; отдельного изометрического коэффициента нет.

6.2 Ограниченное скругление угла

Для подпути A…B…C каждая валидная внутренняя вершина B скругляется локально:

  1. r = min(17,5 см в единицах плана, |AB| / 2, |BC| / 2).
  2. На AB выбирается P на расстоянии r от B, на BC — Q на расстоянии r от B.
  3. До P идёт прямая; участок P → Q рисуется quadratic Bézier с control B; затем продолжается следующий участок.
  4. Первая и последняя исходные точки остаются точными endpoints. Две точки дают обычную прямую без curve-команды.

Кривая лежит в convex hull P–B–Q; каждая точка этого hull находится не далее r от исходной вершины B, которая принадлежит исходной ломаной. Поэтому расстояние от новой кривой до исходного маршрута не превышает 17,5 см. Ограничение через половины соседних отрезков не позволяет скруглениям соседних вершин поменяться местами на коротком сегменте.

Нулевая/неfinite длина, совпадающие точки и разворот примерно на 180° не сглаживаются в этой вершине и безопасно остаются прямыми. Почти прямой ход может быть представлен curve-командой, но визуально обязан оставаться на той же прямой. Builder не выдаёт NaN/Infinity и не создаёт петли.

6.3 Подпути, current/previous и live tip

  • Каждый нормализованный подпуть начинается отдельной M; между подпутями нет L, Q, C или иной синтетической связи.
  • trimVacPathTarget() выполняется до сглаживания ровно в существующих случаях. После trim последняя оставшаяся точка остаётся точным endpoint кривой.
  • Live tip остаётся отдельной нулевой линией и стартует из этого endpoint, как сейчас; кривая не может уйти к текущей target-точке раньше puck.
  • Previous run проходит normalizeVacPath() и тот же builder. Его <g class="prev">, opacity и пара case/core сохраняются; меняется только polyline на <path>.
  • Case и core получают буквально одинаковый d.

6.4 Состояния отображения

Сглаживание включено всегда и не добавляет настройки. Матрица видимости прежняя:

Условие Результат
trail_mode: never Нет current и previous.
cleaning + moving Сглаженный current согласно текущему source authority.
cleaning + not moving След скрыт.
always Сглаженный доступный current и сглаженный previous.
hidden/deleted/HA-disabled/static icon Следа нет.
Нет telemetry/calibration/drawable path Следа нет, без ошибки.

7. UX, accessibility, touch и i18n

Новых controls, текстов, фокуса, keyboard/touch-жестов и screen-reader сущностей нет. View и киоск получают один результат; hover и pointer modality не влияют на форму линии. prefers-reduced-motion не относится к задаче: это статическая форма, а не анимация.

Новых i18n-ключей нет. Поведение одинаково для RU/EN/DE и всех тем. Existing case/core обязаны сохранить читаемость на светлой, тёмной и цветной заливке.

8. Модель данных, совместимость и миграция

Config, Store и HA payload не меняются; новых полей и schema migration нет. Любой старый план автоматически получает сглаженный визуал для существующих точек. Экспорт/импорт и downgrade совместимы: старая версия покажет ту же геометрию снова ломаной, не потеряв данные.

9. Архитектура и производительность

src/vacuum.ts получает один pure, renderer-independent builder представления пути. Он принимает уже откалиброванные плоские VacPath и предел в единицах плана, возвращает типизированные команды move | line | quadratic, не знает о Lit, SVG DOM, cellCm, flat/iso и CSS. src/houseplan-card.ts остаётся владельцем scale/projection/serialization и использует builder для current и previous.

Обработка — один линейный проход O(points), память O(points). Dense resampling запрещён. Для N входных точек builder создаёт не более 2N рисуемых команд; сохранённые точки не копятся между рендерами. Нормализация и cap выполняются до builder, поэтому верхняя граница остаётся 4000 точек.

Рендер не добавляет timer, animation frame или network/backend work. Existing Full Performance остаётся release-gate; отдельный runtime feature flag не нужен.

Security/privacy boundary не меняется: новых данных, сервисов, permissions, логов и внешних ресурсов нет.

10. Acceptance criteria

AC Критерий Доказательство
AC1 У подпути с поворотами внутренние углы представлены quadratic-командами; первая и последняя исходные точки совпадают с endpoints результата. Pure unit.
AC2 Ни одна выборочная точка кривой не дальше 17,5 см от исходной ломаной; правило сохраняется при коротких и резко неравномерных отрезках. Табличный unit с численным sampling и переводом cellCm/gridPitch.
AC3 Несколько подпутей сохраняют ровно столько M, не получают межсегментного bridge и независимо сохраняют endpoints. Pure unit + production-bundle smoke.
AC4 0/1-точечные, 2-точечные, duplicate, non-finite и 180° cases не дают петли, NaN или Infinity; drawable straight path остаётся drawable. Pure edge-case matrix.
AC5 Current проходит прежнюю arbitration/normalization/trim цепочку; движущийся server/integration trail заканчивается на прежней предпоследней target-точке, а tip остаётся приклеен к puck. Existing + расширенный smoke_vacuum.mjs.
AC6 Previous в always использует <path> и тот же builder/предел; current и previous case/core имеют попарно одинаковый d, прежние classes/opacity сохраняются. Production-bundle smoke.
AC7 Матрица never/cleaning/always, hidden, static icon, unknown map и absent telemetry не меняется. Existing targeted vacuum smokes.
AC8 Flat View и iso View показывают проекции одной сглаженной плоской геометрии; zoom/DPR не меняют физический предел. Unit projection invariant + smoke_isometric_live_touch.mjs.
AC9 Детерминированный сохранённый маршрут с прямыми, 90°/острыми поворотами и разрывом визуально плавный, не соединяет разрыв и читается в case/core. Новая golden-сцена desktop dark; baseline принимается только из reviewed Linux CI.
AC10 64 подпути / 4000 точек обрабатываются линейно, finite, без resampling и с не более 2N командами; release performance gate остаётся зелёным. Structural/max-budget unit + Full Performance перед стабильным релизом.
AC11 Рабочие гейты зелёные и tracked bundle синхронизирован. typecheck, unit, build+bundle compare, selected smokes, no-new-any, check-docs, golden CI.

11. План автотестов и реализации

  1. В src/vacuum.ts добавить тип команд и pure bounded-corner builder.
  2. В test/vacuum.test.mjs проверить endpoints, 17,5-см bound численным sampling quadratic, gaps, short/duplicate/reversal/non-finite cases и budget.
  3. В _renderVacuums() сначала affine-преобразовать подпуть, вызвать builder с this._cmToUnits(17.5), затем проецировать и сериализовать typed commands.
  4. Перевести previous с polyline на общий path pipeline; сохранить classes.
  5. Актуализировать demo/smoke_vacuum.mjs: не считать M/L равными числу точек, а проверять semantics (M, Q, endpoints, gaps, trim, previous path, paired d, opacity, tip). Сохранить проверки source authority и modes.
  6. Расширить изометрический smoke проверкой curved path/projection.
  7. Добавить отдельную golden matrix scene с сохранённым real-shape fixture; baseline локально не принимать — получить и review в Linux CI.
  8. Запустить node scripts/smoke-select.mjs --base origin/dev --head HEAD и все перечисленные им обязательные smokes, плюс smoke_vacuum.mjs и smoke_isometric_live_touch.mjs независимо от selector.

Мутационный guard должен заменить bounded builder прямыми L либо убрать quadratic branch; focused unit/smoke обязан стать красным. Если существующий mutation framework не поддерживает локальную мутацию без ложного diff, отдельный guard script допускается в скоупе задачи.

12. Риски и меры

Риск Мера
Кривая отмечает место, где робот не был Доказанный предел 17,5 см в plan coordinates, без unconstrained spline.
Срез угла пересекает стену Предел равен половине типичного диаметра робота; collision-aware reconstruction вне scope.
Разрыв превращается в линию Typed per-subpath commands и AC3.
След опережает puck Trim до builder, точный endpoint и существующий tip, AC5.
Previous и current выглядят по-разному Один builder и один serializer, AC6.
Короткие/повторные точки дают петли Degenerate/reversal fallback и AC4.
4000 точек замедляют View Линейный builder без resampling, command bound и performance gate.
Iso нарушает физический предел Сглаживание до affine scene projection, AC8.

13. Release-артефакты и rollback

Изменение пользовательское. Implementation-коммит имеет User-Visible: yes и включает:

  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md со ссылкой на #209;
  • docs/VACUUM.md и vacuum-раздел docs/USER-GUIDE.ru.md;
  • docs/ARCHITECTURE.md — единый bounded path builder;
  • docs/TESTING.md — pure/smoke/golden coverage;
  • docs/STATUS.md — фактическую release-линию;
  • unit, targeted smokes, mutation coverage и golden scenario;
  • синхронные tracked bundle copies после npm run build/bundle:sync;
  • docs screenshots согласно обязательному check-docs fingerprint-процессу.

Новых config/schema/i18n/backend/security artifacts нет. Golden baseline принимается только отдельным reviewed Linux CI artifact по канону.

Rollback — revert implementation-коммита: данные и config не требуют обратной миграции, след снова станет ломаным. Если regression обнаружен до revert, изолированно вернуть renderer к существующей M/L сериализации можно без изменения сохранённого пути.

14. Принятые решения и технические предположения

  1. Владелец принял defaults: предел ровно 17,5 см; сглаживание всегда включено без UI-настройки; previous сглаживается тем же способом.
  2. Радиус считается после vacuum affine в плоских координатах плана и до _scenePoint(); это техническое следствие физического контракта.
  3. Разворот около 180° не скругляется, чтобы не создавать loop/cusp; точный epsilon определяет реализация и фиксирует unit-тестом.
  4. Previous проходит normalizeVacPath() перед builder, даже если текущий backend отдаёт flat points: это сохраняет единый drawable/cap контракт.
  5. Сцены, которые сегодня не рисуют vacuum trail, не получают его в #209.