feat(photos): добавить пресеты кривой и починить переключение фото
- PCHIP для предпросмотра и серверного рендера - пресеты всех каналов в профиле, импорт и экспорт - мерж настроек auth без потери разделов - сохранение вкладки и правок текущего фото при переключении
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-23
|
||||
@@ -0,0 +1,148 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
Сейчас кривая хранится в `light.curve` как точки на канал, `normalizeCurve`
|
||||
сортирует/зажимает/дедуплицирует их, а по умолчанию массив пуст. UI рисует
|
||||
статичные углы и прямые сегменты; `buildCurveLUT` (TS) и `curveLUT` (Go)
|
||||
интерполируют линейно. Пресетов нет. Настройки профиля лежат в
|
||||
`users.preferences` (JSON-блоб); `PATCH /auth/me/preferences` сейчас **заменяет**
|
||||
весь объект и знает только `photo_info`, поэтому добавление пресетов без мержа
|
||||
затирало бы другие разделы.
|
||||
|
||||
См. `proposal.md` — Why и `specs/photos/spec.md`, `specs/auth/spec.md` — требования.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Крайние узлы как обычные точки; первый клик вставляет между ними; узлы двигаются
|
||||
по x/y; вне них кривая плоская.
|
||||
- Плавная монотонная интерполяция в SVG-пути и в LUT; Go и TS считают одинаково.
|
||||
- Встроенные пресеты контраста и пользовательские пресеты (кривые всех четырёх
|
||||
каналов) в профиле; сохранение/применение/переименование/удаление;
|
||||
импорт/экспорт JSON.
|
||||
- Серверный мерж разделов настроек с валидацией.
|
||||
|
||||
**Non-Goals:**
|
||||
- Пресеты как «look» из четырёх кривых; Android-клиент; шаринг пресетов между
|
||||
пользователями (только файл); undo/redo.
|
||||
|
||||
## Decisions
|
||||
|
||||
**Крайние точки — обычные узлы.**
|
||||
- Первая и последняя точки массива являются крайними; по умолчанию это `(0,0)` и
|
||||
`(1,1)`. Пустой канал в UI показывает эти две точки; первое взаимодействие
|
||||
записывает их в `light.curve`.
|
||||
- Вне крайних точек значение постоянно (текущее поведение `curveEval`), поэтому
|
||||
сдвинутый по x узел даёт плоскую «полку», как на скрине.
|
||||
- При перетаскивании точки остаются отсортированными по x; UI не даёт точке
|
||||
пересечь соседнюю (x ограничивается между соседями), на коммите — `normalizeCurve`.
|
||||
- Альтернатива: фиксировать x крайних на 0/1 — отвергнуто, пользователь хочет
|
||||
двигать и по x.
|
||||
|
||||
**Интерполяция — монотонный кубический Эрмит (PCHIP).**
|
||||
- Наклоны отрезков `d_i`, касательные по Fritsch–Carlson (нулевые при смене
|
||||
знака/нулевом наклоне, ограничение против выбросов). Участок между точками
|
||||
считается по кубической формуле Эрмита.
|
||||
- SVG-путь: те же касательные переводятся в кубические Безье
|
||||
(`P1 = P0 + (h/3, m_i*h/3)`, `P2 = P3 - (h/3, m_{i+1}*h/3)`), плюс плоские
|
||||
сегменты от x=0 до первой и от последней до x=1.
|
||||
- LUT 256: те же значения; `buildCurveLUT` (TS) и `curveLUT` (Go) обязаны
|
||||
совпадать. WASM и WebGL получают готовый LUT из TS — их менять не нужно.
|
||||
- Альтернативы: Catmull-Rom (выбросы/гало на тоновой кривой), линейная
|
||||
интерполяция (текущая, отвергнута).
|
||||
|
||||
**Пресет — кривые всех четырёх каналов.**
|
||||
- `CurvePreset { name: string; channels: { rgb: CurvePoint[]; red: CurvePoint[];
|
||||
green: CurvePoint[]; blue: CurvePoint[] } }`.
|
||||
- Каждый канал содержит 2..16 точек; пустых каналов в пресете нет — линейный
|
||||
канал хранится как `(0,0),(1,1)`.
|
||||
- Встроенные `Линейный`, `Средний контраст`, `Сильный контраст` — клиентские
|
||||
константы. Контраст задаётся кривой RGB, каналы R/G/B линейные. Предлагаемые
|
||||
точки RGB:
|
||||
- Средний: `(0,0),(0.25,0.2),(0.5,0.5),(0.75,0.8),(1,1)`
|
||||
- Сильный: `(0,0),(0.2,0.12),(0.5,0.5),(0.8,0.88),(1,1)`
|
||||
- Сохранение берёт `effectiveCurvePoints` каждого канала, поэтому в пресет
|
||||
всегда попадают все четыре кривые.
|
||||
- Применение заменяет кривые всех четырёх каналов; текущий выбранный канал не
|
||||
меняется.
|
||||
- Подпись: имя пресета, если кривые всех четырёх каналов совпадают с пресетом
|
||||
(с допуском), иначе `Пользовательский`.
|
||||
|
||||
**Хранение и мерж настроек.**
|
||||
- `preferencesPayload` расширяется `curve_presets`; поля-указатели позволяют
|
||||
отличить «не передано» от «пусто».
|
||||
- Handler читает текущие `users.preferences`, мержит переданные разделы
|
||||
(`photo_info`, `curve_presets`), валидирует, пишет и возвращает полный объект.
|
||||
Сохранение «Сведений» с `{photo_info}` сохраняет пресеты; сохранение пресетов
|
||||
сохраняет `photo_info`.
|
||||
- Валидация: не больше 100 пресетов, имя 1..64, все четыре канала присутствуют
|
||||
и содержат 2..16 точек, `x,y` конечные в 0..1. Некорректный раздел
|
||||
отклоняется целиком.
|
||||
- Альтернативы: отдельный endpoint (больше поверхности), клиент шлёт весь объект
|
||||
(устаревшие вкладки затирают чужие изменения).
|
||||
|
||||
**Переключение фото.**
|
||||
- Вкладка правой панели хранится в `AlbumPanel` и передаётся в `PhotoPanel`
|
||||
через `bind:tab`, поэтому переживает перемонтирование панели при загрузке
|
||||
следующего фото.
|
||||
- Cleanup эффекта `PhotoEditPanel` регистрируется на каждом запуске; при смене
|
||||
`photo.id` накопленные правки отправляются до инициализации нового фото.
|
||||
- Ответы сохранений проверяются по id фото и номеру операции: устаревшее
|
||||
сохранение не подменяет текущее фото и его параметры. Панель рендерит только
|
||||
фото, совпадающее с текущим маршрутом.
|
||||
|
||||
**Пользовательские действия и файлы.**
|
||||
- Сохранить: диалог имени, кривые всех четырёх каналов; при совпадении имени —
|
||||
подтверждение перезаписи.
|
||||
- Переименовать/удалить: действия в меню пользовательского пресета.
|
||||
- Экспорт: JSON `{ version: 1, presets: CurvePreset[] }` с кривыми всех каналов,
|
||||
только пользовательские пресеты (встроенные всегда доступны).
|
||||
- Импорт: файл-пикер, валидация, upsert по имени, сохранение через preferences;
|
||||
при некорректном файле профиль не меняется.
|
||||
|
||||
**Совместимость формата.**
|
||||
- Старый формат `{ name, channel, points }` читается из профиля и импорта:
|
||||
точки попадают в указанный канал, остальные становятся линейными.
|
||||
- При следующем сохранении список перезаписывается новым форматом со всеми
|
||||
четырьмя каналами.
|
||||
|
||||
**UI.**
|
||||
- Статичный текст «Линейный» заменяется кнопкой, открывающей popover-меню:
|
||||
встроенные, пользовательские, «Сохранить…», «Импортировать…», «Экспортировать…».
|
||||
- В SVG крайние точки — такие же перетаскиваемые кружки, как остальные; первый
|
||||
клик по свободному полю вставляет точку между крайними; двойной клик сбрасывает.
|
||||
- Сохраняется существующая клавиатурная правка точек.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
[Старые кривые начнут выглядеть иначе (плавно вместо линейно)] → это осознанное
|
||||
изменение рендера; точки не мигрируются, плоские участки вне крайних сохраняются.
|
||||
|
||||
[Расхождение PCHIP в Go и TS] → одна формула, тесты с захардкоженными значениями
|
||||
LUT в обоих языках; WASM/WebGL используют готовый LUT.
|
||||
|
||||
[Гонка при мерже preferences] → мерж на сервере по разделам: разные разделы не
|
||||
затирают друг друга; одновременная запись одного раздела — last-write-wins,
|
||||
приемлемо для настроек одного пользователя.
|
||||
|
||||
[Рост списка пресетов / большой импорт] → лимиты количества, размера имени и
|
||||
точек; handler уже ограничивает тело 1 МБ.
|
||||
|
||||
[Старые пресеты хранятся в формате одного канала] → клиент конвертирует их при
|
||||
чтении/импорте, а первое сохранение пресетов перезаписывает профиль новым
|
||||
форматом.
|
||||
|
||||
[Пересечение точек при перетаскивании] → ограничение x соседями и нормализация
|
||||
на коммите.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Схема не меняется: `curve_presets` — ключ в JSON-блобе `users.preferences`.
|
||||
Старый формат пресетов конвертируется на клиенте при чтении. Откат: удалить
|
||||
ключ; старые кривые остаются в `light.curve` без изменений данных.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Нет. Точные точки «Среднего/Сильного контраста» — настраиваемый дефолт, не влияет
|
||||
на контракт.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Тоновая кривая сейчас ведёт себя не как инструмент из Lightroom: крайние узлы не
|
||||
существуют до первых кликов, линия строится по прямым сегментам, а готовых
|
||||
пресетов контраста и пользовательских пресетов нет. Пользователь не может быстро
|
||||
применить типовой контраст или сохранить свою кривую. Дополнительно
|
||||
`PATCH /auth/me/preferences` заменяет весь объект настроек, поэтому хранение
|
||||
пресетов в профиле без мержа затирало бы другие разделы.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Кривая.** Крайние узлы `(0,0)` и `(1,1)` существуют сразу и являются обычными
|
||||
точками; первый клик добавляет точку между ними. Крайние узлы двигаются по x и
|
||||
y, вне них кривая постоянна («полки», как на скрине). Линия и рендер используют
|
||||
плавную монотонную кубическую интерполяцию (PCHIP): SVG-путь и LUT в Go/TS.
|
||||
«Сбросить» возвращает линейную кривую.
|
||||
- **Встроенные пресеты.** `Линейный`, `Средний контраст`, `Сильный контраст` —
|
||||
каждый описывает все четыре канала: контраст задаётся кривой RGB, каналы
|
||||
R/G/B остаются линейными. Подпись показывает активный пресет; при ручной
|
||||
правке любого канала — `Пользовательский`.
|
||||
- **Пользовательские пресеты.** Пресет описывает кривые всех каналов
|
||||
(`name`, `channels: { rgb, red, green, blue }`). Сохранение текущих кривых
|
||||
всех каналов, применение заменяет все четыре кривые, переименование и
|
||||
удаление. Хранение в `users.preferences.curve_presets` (профиль пользователя),
|
||||
приватно для пользователя.
|
||||
- **Мерж настроек.** `PATCH /auth/me/preferences` принимает `photo_info` и
|
||||
`curve_presets` по отдельности, мержит переданные разделы с сохранёнными,
|
||||
валидирует и возвращает полный merged-объект. Сохранение «Сведений» больше не
|
||||
затирает пресеты, и наоборот.
|
||||
- **Импорт/экспорт.** JSON-файл `{ version: 1, presets: [...] }`: экспорт
|
||||
пользовательских пресетов, импорт с добавлением в профиль (при совпадении имени
|
||||
— перезапись).
|
||||
- **Совместимость.** Миграций схемы нет: пресеты лежат в JSON-блобе
|
||||
`preferences`. Старые пресеты формата `{ channel, points }` конвертируются в
|
||||
четыре канала при чтении и перезаписываются при следующем сохранении.
|
||||
Существующие сохранённые кривые начнут интерполироваться плавно, поэтому их
|
||||
визуальный результат может немного измениться.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
Нет — поведение входит в существующие способности `photos` и `auth`.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `photos`: добавляются требования «Тоновая кривая» (крайние узлы, плавная
|
||||
интерполяция, встроенные пресеты, сброс) и «Пользовательские пресеты кривой»
|
||||
(пресет из четырёх каналов, сохранение/применение/переименование/удаление,
|
||||
импорт/экспорт).
|
||||
- `auth`: требование «Хранение пользовательских настроек интерфейса»
|
||||
расширяется мержем разделов и хранением пользовательских пресетов кривой;
|
||||
добавляется сценарий «сохранение одного раздела не затирает другие».
|
||||
|
||||
## Impact
|
||||
|
||||
- `backend/internal/auth/handlers.go` — `preferencesPayload` с `curve_presets`,
|
||||
мерж разделов, валидация имени и кривых всех четырёх каналов.
|
||||
- `backend/internal/auth/store.go` / `sqlite_store.go` — без изменений схемы;
|
||||
запись merged-JSON через существующий `UpdatePreferences`.
|
||||
- `backend/internal/photos/edit.go` — `curveEval`/`curveLUT` переходят на PCHIP;
|
||||
тесты Go на гладкость/LUT.
|
||||
- `frontend/src/lib/edit.ts` — PCHIP в `buildCurveLUT`, типы и нормализация
|
||||
пресетов, точки кривой.
|
||||
- `frontend/src/lib/components/PhotoEditPanel.svelte` — узлы-углы, плавный SVG-путь,
|
||||
меню пресетов, сохранение/переименование/удаление, импорт/экспорт.
|
||||
- `frontend/src/lib/types.ts`, `stores/auth.svelte.ts`, `api.ts` — тип
|
||||
`Preferences.curve_presets`, сохранение через существующий endpoint.
|
||||
- Новые вспомогательные модули: меню/диалоги пресетов, импорт/экспорт JSON.
|
||||
- Тесты: `edit_test.go`, `edit.test.ts`, тесты мержа/валидации preferences,
|
||||
e2e сохранения пресетов в профиле.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Хранение пользовательских настроек интерфейса
|
||||
Система SHALL хранить пользовательские настройки (отображение интерфейса и пресеты кривой) в профиле пользователя и SHALL возвращать их при получении текущего пользователя. Настройки SHALL быть приватными для пользователя. Отсутствие настроек SHALL трактоваться как значения по умолчанию. Сохранение одного раздела настроек SHALL не затирать остальные разделы.
|
||||
|
||||
#### Scenario: Получение настроек
|
||||
- **WHEN** аутентифицированный пользователь запрашивает свой профиль
|
||||
- **THEN** ответ содержит его настройки отображения и пресеты кривой (или пустой объект по умолчанию)
|
||||
|
||||
#### Scenario: Сохранение настроек
|
||||
- **WHEN** пользователь сохраняет настройки
|
||||
- **THEN** система сохраняет их в профиле и возвращает сохранённое состояние
|
||||
|
||||
#### Scenario: Сохранение раздела не затирает другие
|
||||
- **WHEN** пользователь сохраняет только один раздел настроек (например, сведения о фото или пресеты кривой)
|
||||
- **THEN** остальные разделы профиля сохраняются без изменений
|
||||
|
||||
#### Scenario: Изоляция настроек
|
||||
- **WHEN** другой пользователь запрашивает свой профиль
|
||||
- **THEN** он не получает настройки первого пользователя
|
||||
|
||||
#### Scenario: Некорректные настройки
|
||||
- **WHEN** пользователь присылает настройки недопустимой структуры или размера
|
||||
- **THEN** система отклоняет запрос с ошибкой и не меняет сохранённые настройки
|
||||
@@ -0,0 +1,84 @@
|
||||
# Spec Delta
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Тоновая кривая
|
||||
Тоновая кривая SHALL задаваться контрольными точками для каналов RGB, R, G и B. Крайние точки SHALL существовать по умолчанию, SHALL двигаться по обеим осям и SHALL задавать плоские участки вне себя. Кривая SHALL быть плавной и монотонной между точками; первый клик по полю SHALL добавлять точку между крайними. Сброс SHALL возвращать линейную кривую.
|
||||
|
||||
#### Scenario: Крайние узлы существуют с самого начала
|
||||
- **WHEN** пользователь открывает инструмент «Кривая»
|
||||
- **THEN** у каждого канала уже есть крайние точки, и их можно перетаскивать
|
||||
|
||||
#### Scenario: Добавление точки
|
||||
- **WHEN** пользователь кликает по свободному полю кривой
|
||||
- **THEN** между крайними точками добавляется новая точка, а крайние остаются на месте
|
||||
|
||||
#### Scenario: Подвижные крайние узлы
|
||||
- **WHEN** пользователь двигает крайнюю точку по x или y
|
||||
- **THEN** вне крайних точек кривая остаётся плоской, а между точками — плавной
|
||||
|
||||
#### Scenario: Плавная интерполяция
|
||||
- **WHEN** кривая сохранена и отрендерена
|
||||
- **THEN** предпросмотр и итоговое фото используют одну и ту же плавную монотонную кривую
|
||||
|
||||
#### Scenario: Встроенные пресеты
|
||||
- **WHEN** пользователь выбирает «Линейный», «Средний контраст» или «Сильный контраст»
|
||||
- **THEN** кривые всех четырёх каналов заменяются кривыми пресета (контраст — в RGB, R/G/B — линейные), а подпись показывает выбранный пресет
|
||||
|
||||
#### Scenario: Пользовательская правка
|
||||
- **WHEN** пользователь вручную меняет точки кривой
|
||||
- **THEN** подпись показывает «Пользовательский»
|
||||
|
||||
#### Scenario: Сброс
|
||||
- **WHEN** пользователь сбрасывает кривую
|
||||
- **THEN** текущий канал возвращается к линейной кривой с крайними точками
|
||||
|
||||
#### Scenario: Независимые каналы
|
||||
- **WHEN** пользователь выбирает канал R, G или B
|
||||
- **THEN** он меняет только выбранный канал, остальные каналы остаются прежними
|
||||
|
||||
### Requirement: Переключение фото в редакторе
|
||||
При переключении фото правая панель SHALL сохранять выбранную вкладку. Несохранённые правки SHALL сохраняться тому фото, для которого они сделаны, в том числе если пользователь уже переключился на другое фото. Ответы фоновых запросов SHALL NOT подменять текущее фото или его параметры.
|
||||
|
||||
#### Scenario: Вкладка не меняется
|
||||
- **WHEN** пользователь переключает фото
|
||||
- **THEN** выбранная вкладка правой панели остаётся прежней
|
||||
|
||||
#### Scenario: Правки уходят текущему фото
|
||||
- **WHEN** пользователь меняет настройки после переключения
|
||||
- **THEN** изменения сохраняются текущему фото, а не предыдущему
|
||||
|
||||
#### Scenario: Фоновое сохранение не подменяет фото
|
||||
- **WHEN** сохранение предыдущего фото завершается после переключения
|
||||
- **THEN** панель продолжает показывать текущее фото, а его параметры не меняются
|
||||
|
||||
### Requirement: Пользовательские пресеты кривой
|
||||
Пользователь SHALL сохранять текущие кривые всех каналов (RGB, R, G и B) как именованный пресет, применять, переименовывать и удалять его. Пресет SHALL содержать кривую для каждого из четырёх каналов. Пресеты SHALL храниться в профиле пользователя и SHALL быть приватными. Импорт и экспорт SHALL выполняться JSON-файлом; импорт SHALL добавлять пресеты в профиль.
|
||||
|
||||
#### Scenario: Сохранение пресета
|
||||
- **WHEN** пользователь сохраняет текущие кривые под именем
|
||||
- **THEN** пресет появляется в списке и сохраняется в профиле вместе с кривыми RGB, R, G и B
|
||||
|
||||
#### Scenario: Применение пресета
|
||||
- **WHEN** пользователь выбирает пользовательский пресет
|
||||
- **THEN** кривые всех четырёх каналов заменяются сохранёнными, а текущий канал не меняется
|
||||
|
||||
#### Scenario: Переименование и удаление
|
||||
- **WHEN** пользователь переименовывает или удаляет пресет
|
||||
- **THEN** список пресетов и профиль обновляются
|
||||
|
||||
#### Scenario: Приватность пресетов
|
||||
- **WHEN** другой пользователь открывает свои пресеты
|
||||
- **THEN** он не видит пресеты первого пользователя
|
||||
|
||||
#### Scenario: Экспорт пресетов
|
||||
- **WHEN** пользователь экспортирует пресеты
|
||||
- **THEN** он получает JSON-файл с пользовательскими пресетами и кривыми всех четырёх каналов
|
||||
|
||||
#### Scenario: Импорт пресетов
|
||||
- **WHEN** пользователь импортирует корректный JSON-файл
|
||||
- **THEN** пресеты добавляются или обновляются в его профиле
|
||||
|
||||
#### Scenario: Некорректный файл или пресет
|
||||
- **WHEN** имя или кривая хотя бы одного канала недопустимы
|
||||
- **THEN** система отклоняет пресет и не меняет профиль
|
||||
@@ -0,0 +1,49 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Общая математика кривой (TS + Go)
|
||||
|
||||
- [x] 1.1 Заменить линейную интерполяцию в `frontend/src/lib/edit.ts` на монотонный кубический Эрмит (PCHIP): наклоны, касательные, оценка между точками, плоские участки вне крайних — проверить `npm run test` (`edit.test.ts`, значения LUT и монотонность)
|
||||
- [x] 1.2 Повторить ту же формулу в `backend/internal/photos/edit.go` (`curveEval`/`curveLUT`) — проверить `go test ./internal/photos/ -run TestCurve -v`
|
||||
- [x] 1.3 Сверить Go и TS на нескольких кривых (линейная, средний/сильный контраст, кривая с крайними внутри) захардкоженными значениями LUT — проверить оба набора тестов
|
||||
|
||||
## 2. Backend: мерж настроек и пресеты
|
||||
|
||||
- [x] 2.1 Расширить `preferencesPayload` в `backend/internal/auth/handlers.go` полем `curve_presets` и валидацией (лимит количества, имя 1..64, 2..16 точек на канал, x/y в 0..1) — проверить `go test ./internal/auth/ -v`
|
||||
- [x] 2.2 Сделать серверный мерж разделов: читать текущие `preferences`, мержить `photo_info`/`curve_presets`, писать и возвращать merged-объект — проверить тестом «сохранение photo_info не затирает пресеты и наоборот»
|
||||
- [x] 2.3 Добавить e2e-проверку сохранения пресетов в профиле и их приватности — проверить `go test ./internal/e2e/ -run TestCurvePresets -v`
|
||||
|
||||
## 3. UI: узлы и плавная кривая
|
||||
|
||||
- [x] 3.1 Показывать крайние точки `(0,0)` и `(1,1)` по умолчанию; первый клик вставляет точку между ними; крайние двигаются по x/y, вне них — плоские участки — проверить вручную и `npm run check`
|
||||
- [x] 3.2 Рисовать SVG-путь кубическими Безье по тем же касательным (плоские сегменты до/после крайних) — проверить визуально, что линия плавная
|
||||
- [x] 3.3 Сохранить клавиатурную правку точек, двойной клик-сброс и ограничение x между соседями — проверить `npm run check` и ручным сценарием
|
||||
|
||||
## 4. UI: пресеты и файлы
|
||||
|
||||
- [x] 4.1 Заменить статичный «Линейный» на меню встроенных пресетов (`Линейный`, `Средний контраст`, `Сильный контраст`) с активной подписью/«Пользовательский» — проверить `npm run check`
|
||||
- [x] 4.2 Добавить тип `Preferences.curve_presets` и сохранение/применение/переименование/удаление пользовательских пресетов через существующий endpoint — проверить `npm run test` и сохранение в профиле
|
||||
- [x] 4.3 Реализовать импорт/экспорт JSON `{ version: 1, presets: [...] }` с валидацией и upsert по имени — проверить ручным импортом/экспортом и `npm run check`
|
||||
|
||||
## 5. Интеграция и проверка
|
||||
|
||||
- [x] 5.1 Прогнать `npm run check`, `npm run test`, `go test ./...`, `make build` — всё зелёное
|
||||
- [x] 5.2 Проверить, что WebGL и WASM используют новый LUT, а старые сохранённые кривые рендерятся плавно без миграции данных — визуально и тестами
|
||||
- [x] 5.3 Проверить приватность и лимиты пресетов (чужой профиль, слишком длинное имя, >16 точек) — тестами `internal/auth`/`internal/e2e`
|
||||
|
||||
## 6. Правка: пресет описывает все каналы
|
||||
|
||||
- [x] 6.1 Обновить `CurvePreset` в `frontend/src/lib/types.ts` и `curve-presets.ts`: `channels` для RGB/R/G/B, встроенные пресеты и поиск активного пресета по всем каналам — проверить `npm run test`
|
||||
- [x] 6.2 Обновить серверную модель и валидацию `curve_presets` в `backend/internal/auth/handlers.go`: объект `channels` со всеми четырьмя каналами — проверить `go test ./internal/auth/ -v`
|
||||
- [x] 6.3 Обновить сохранение и применение в `PhotoEditPanel.svelte` и `CurvePresetMenu.svelte`: сохранять и заменять все четыре канала, не переключать текущий канал — проверить `npm run check`
|
||||
- [x] 6.4 Обновить импорт/экспорт и совместимость со старым форматом `{name, channel, points}`, тесты и e2e — проверить `npm run test` и `go test ./internal/e2e/ -run TestCurvePresets -v`
|
||||
|
||||
## 7. Переключение фото
|
||||
|
||||
- [x] 7.1 Сохранять вкладку правой панели при смене фото, вынеся её состояние в `AlbumPanel` — проверить в браузере
|
||||
- [x] 7.2 Исправить регистрацию cleanup в `PhotoEditPanel`, чтобы накопленные правки отправлялись при переключении — проверить в браузере
|
||||
- [x] 7.3 Защитить панель от устаревших ответов сохранения/загрузки и кратковременного `photo = null` — проверить `npm run check`
|
||||
|
||||
## Workflow follow-up
|
||||
|
||||
- Архивировать изменение после удовлетворения требований ревью проекта.
|
||||
- Проверить архивированный результат.
|
||||
Reference in New Issue
Block a user