Высокопроизводительный браузерный редактор для командной работы (схемы, вайрфреймы, доски). Стек зафиксирован спайками S1–S4: Pixi.js v8 (рендер), Yjs (CRDT-синк), ECS (модель), Node y-websocket + Go API (бэкенд). Контекст и решения — implementation-plan.md, spikes/DECISIONS.md.
M0–M4 закрыты. В M5 — перф-пасс (viewport-culling/LOD), воспроизводимый бенч против реального движка, нагрузочный тест синка и стресс больших документов.
Новое в M5 (оба трека):
- Viewport-culling + LOD. Рендер держит Pixi-узел только для сущностей, чей мировой
AABB пересекает видимую область камеры (+margin) и крупнее ~0.5px на экране; при
сдвиге/зуме множество узлов пересогласуется, пока камера неподвижна — обрабатываются
только дельты. Стоимость кадра ∝ числу видимых объектов, а не размеру документа.
Геометрия (
packages/engine/src/cull.ts) покрыта юнит-тестами.fitсчитает границы по всем сущностям (не по видимым узлам). - Перф-бенч против реального движка —
tools/bench(@ove/perf-bench): перепрогон «облёта» камеры из спайка S1, но через продакшен-рендер с culling, плюс статистика видимо/всего.pnpm --filter @ove/perf-bench bench(headed = реальный GPU). Закрывает риск «перф на интегрированном GPU» (остаётся прогон на Intel UHD/Iris). - Нагрузочный тест синка (10 одновременных) —
server/sync/loadtest.mjs(pnpm --filter @ove/sync-server loadtest): N клиентов на комнату, замер латентности, пропускной способности и сходимости под нагрузкой (все документы идентичны). Проверено: 10×200 правок → сходятся, документы идентичны, латентность sub-ms локально. - Стресс больших документов + перф-HUD. Офлайн
?doc=demo&stress=Nгрузит N сгенерированных объектов и показывает FPS + culling-статистику; E2Em5-perf.spec.tsпроверяет инвариант culling (видимо ≪ всего при зуме) на 1500 объектах.
M0–M3 закрыты (Foundation → однопользовательский канвас → совместное редактирование → фичи редактора и экспорт). В M4 бэкенд получил аутентификацию, права доступа, S3 и воспроизводимый прод-деплой.
Новое в M4 (Track B):
- Аутентификация. Регистрация/вход по email+паролю (bcrypt), stateless access-токены
(HMAC-SHA256, без внешних зависимостей). Фронт хранит токен и шлёт
Authorization: Bearer; протухший токен → автоматический возврат на экран входа. Офлайн-демо (?doc=demo) работает без auth. - Права доступа. Документы scoped по владельцу/участникам: список показывает только свои и расшаренные. Семантика «ссылка = доступ» — открытие документа по ссылке добавляет аутентифицированного пользователя в участники (сохраняет M3-UX шаринга, но уже под auth). Переименование/удаление — только владелец. Join-токен синка привязан к пользователю и требует доступа к документу.
- S3 (Yandex Object Storage). Presigned PUT/GET по AWS SigV4 (реализован на stdlib,
совпадает с эталонным AWS-вектором; path-style для Yandex). Клиент грузит ассеты/экспорты
напрямую в хранилище, API лишь подписывает URL и хранит метаданные (
assets). Без ключей загрузки отключаются (503), остальное работает. - Хардненинг. Rate limiting на IP (токен-бакет), CORS по списку доверенных origin,
request-id в логах,
/metrics(Prometheus text), healthchecks всех сервисов. - Инфра.
docker-compose.prod.yml: nginx раздаёт собранный редактор и проксирует/api→Go и/sync→y-websocket (единый origin). Образ API — distroless с self-health. Окружения dev/stage/prod черезinfra/.env(см.infra/.env.example).
M0–M2 закрыты (Foundation → однопользовательский канвас → совместное редактирование). В M3 редактор получил полный набор инструментов работы с фигурами и экспорт.
Новое в M3 (Track A):
- Группировка с вложенностью. Группа — логический контейнер (рендер её не видит,
порядок задают z дочерних фигур); хранится отдельно от фигур и со-редактируется/
персистится наравне с ними. Выделение группы раскрывается в её листья, поэтому
весь мультитрансформ M1 работает без изменений.
Ctrl+G/Ctrl+Shift+G, двойной клик «проваливается» в группу, рамка захватывает группу целиком. Один шаг undo на всю операцию (UndoManager отслеживает и фигуры, и группы). - Панель свойств выделения: заливка/обводка/толщина/прозрачность, позиция/размер, кегль/шрифт; правка всего выделения одной транзакцией.
- Панель слоёв = дерево групп с раскрытием, drag-reorder верхнего уровня (z), переименованием групп и удалением по строке.
- Контекстное меню (ПКМ без сдвига — меню, right-drag остаётся паном): дублировать
(
Ctrl+D), группировать/разгруппировать, на передний/задний план, удалить. - Экспорт PNG/SVG всей сцены из модели: viewBox по границам, многострочный текст
через
<tspan>с корректной базовой линией, привязкаfont-family; PNG — растеризация того же SVG (кадр не зависит от текущего zoom/pan).
Track B: шаринг доски ссылкой (копирование ?doc=…) на стартовом экране и в шапке
редактора. Аутентификация и права доступа к комнатам — M4.
Канвас (M1, @ove/engine + apps/editor): инструменты rect/ellipse/line/arrow/
text (V/R/O/L/A/T); продакшен-выделение/трансформ (маркеры постоянного экранного
размера, неравномерный resize, rotate, мультивыделение, снэппинг с направляющими);
текст-оверлей с IME; слои/z-order; палитра; nudge.
Совместность (M2):
- Yjs = источник истины. Редактор мутирует через адаптер
SceneStore, который делегирует в@ove/syncSyncEngine; ECS-World— проекция, в которую пишет только наблюдатель (петли структурно невозможны). UI не переписан. - Троттлинг drag: патчи жеста коалесцируются в одну Yjs-транзакцию на кадр (оптимистичный рендер локально).
- Awareness: живые курсоры соавторов с именами, presence и контуры их выделения; индикатор соединения.
- Scoped undo/redo через
Y.UndoManager(отмена правит только свои изменения). - Персистентность Yjs→Postgres (
server/sync): журнал апдейтов + дебаунс-снапшот; документ переживает отключение всех клиентов и рестарт сервера (холодный late-join). - Доски и доступ: стартовый экран со списком досок (создание/переименование/ удаление); открытие доски запрашивает комнату и HMAC join-токен у Go API и подключается к синку по авторизованному WebSocket (сервер синка проверяет токен на ws-upgrade).
- Go API: документы (CRUD), комнаты, выпуск/проверка join-токенов.
Exit M3 достигнут: функционален весь список MVP из брифа — примитивы, co-edit,
слои + группировка (с вложенностью), экспорт PNG/SVG. Проверено автотестами
(node --test в @ove/sync/@ove/engine/@ove/export, go test) и headless-E2E.
apps/editor React-оболочка: тулбар, палитра, панель свойств, дерево слоёв, меню
packages/model Контракт: типы Shape/Component/Group, фабрики (граница треков)
packages/ecs ECS-проекция (типизированные массивы + dirty-tracking) ← seed s2/world
packages/sync Yjs-биндинг (источник истины): фигуры + группы, публичный API ← seed s2/sync
packages/engine Pixi-сцена, transform-tool, группировка, экспорт, viewport-culling/LOD ← seed s1
packages/export SVG из модели (viewBox/tspan/baseline) + PNG-растеризация ← seed s4
server/sync Node y-websocket + персистентность Yjs→Postgres + ws-auth ← seed s2/server
server/api Go REST API: auth/документы/комнаты/ассеты + права + S3 presign ← ref s3
infra docker-compose (dev + prod=nginx) / nginx-прокси / .env-окружения
tools/bench Перф-бенч против реального движка (culling) — перепрогон S1 на целевом GPU (M5)
Зависимости направлены к контракту: ecs → model, sync → ecs, model,
engine → ecs, model, sync, export, export → model, editor → engine, model.
pnpm install
pnpm build # tsc-эмит всех пакетов (topo) + vite-сборка редактора
pnpm lint
pnpm typecheck
pnpm dev # редактор на http://localhost:5173
pnpm sync-server # y-websocket на ws://localhost:1234 (нужен для M1+)Браузерные E2E-тесты M3 (Pixi/WebGL + React в headless Chromium, офлайн-демо без бэкенда):
pnpm --filter @ove/editor exec playwright install chromium # один раз: браузер
pnpm --filter @ove/editor e2e # vite build + playwright testПокрывают: загрузку сцены (init WebGL), панель свойств, группировку/разгруппировку
(тулбар + Ctrl+G/Ctrl+Shift+G), undo, контекстное меню → дублирование, экспорт PNG/SVG.
E2E M5 (m5-perf.spec.ts) — стресс 1500 объектов + инвариант viewport-culling.
Перф и нагрузка (M5):
pnpm --filter @ove/perf-bench bench # FPS-бенч против реального движка (headed = реальный GPU)
pnpm --filter @ove/sync-server loadtest # нагрузочный тест синка: 10 клиентов, сходимость, латентность
# ручной стресс-просмотр: открыть редактор на ?doc=demo&stress=2000 (перф-HUD: FPS + culling)Требуется Node ≥ 22 и pnpm 9.
Бэкенд (Track B) — dev:
docker compose -f infra/docker-compose.yml up -d --build # postgres + sync + api
# API на http://localhost:8080 (healthz/readyz/metrics, /api/v1/auth/*, /api/v1/documents)
# Все /api/v1/* (кроме auth/register|login) требуют Authorization: Bearer <token>.Прод (единый origin: nginx раздаёт редактор и проксирует API/синк):
cp infra/.env.example infra/.env # заполнить секреты (openssl rand -hex 32) и S3
docker compose --env-file infra/.env -f infra/docker-compose.prod.yml up -d --build
# web на :80 → / (редактор), /api → Go, /sync → y-websocketТребуется Go ≥ 1.24 (для запуска API без контейнера) и Docker.
Go-тесты бэкенда (auth-токены, SigV4-presign, rate-limit, CORS/auth-middleware):
cd server/api && go test ./...@ove/model— единственный источник типов сцены:Shape(rect/ellipse/line/ arrow/text) как объединение компонентов (Transform,Geometry,Style,TextContent,ZIndex). M3 добавил опциональныйShapeBase.parentи типGroup(группа без геометрии) — обратносовместимо. Без рантайм-зависимостей.@ove/sync— публичный API:SyncEngine(addShape/patch/moveTo/removeShape/clear/undo+addGroup/patchGroup/removeGroup/allGroups), сериализацияShape ↔ Y.Map. Паттерн «Yjs = источник истины, ECS = проекция; единственный writer-наблюдатель». Undo отслеживает и фигуры, и группы.
Эти две поверхности — граница между треками Front-graph и Sync+Backend; менять их можно только согласованно.