Рефакторинг редактора: Model/View decoupling¶
Контекст¶
В проекте две реализации редактора:
- termin/editor/ — Qt6, "золотой стандарт", работает лучше
- termin/editor_tcgui/ — tcgui, активная миграция (см. termin-app tcgui migration), местами расходится с Qt
Цель — вынести бизнес-логику в UI-agnostic слой, чтобы оба view делили одну модель и отличались только рендером/ивентами. Устранить дублирование и причину расхождений поведения между редакторами.
Цели¶
- UI-agnostic модель в
termin/editor_core/ - Qt и tcgui view используют одни и те же Controller/Model/Services
- Удалить дублирование логики между
editor/иeditor_tcgui/
Не-цели¶
- Не переписываем Qt редактор — он остаётся, пока tcgui не догонит
- Не трогаем уже чистые модули:
undo_stack,editor_commands,editor_state_io,file_processors/ - Не меняем C++ core
Архитектура¶
View(Qt) View(tcgui) — widgets, layouts, event→action
\ /
Controller — тонкий: переводит view events в model calls
│
Model ──→ Observers — состояние + бизнес-логика, наблюдаемое
│
Services — DialogService, FileService — интерфейсы
│ с двумя реализациями (Qt/tcgui)
Engine core (C++)
Строительные блоки¶
- Observable/Signal — маленький pub/sub, без Qt, без tcgui
- DialogService (abstract):
show_message / show_confirm / show_input / open_file / save_file - FileService (abstract): watcher, external editor launch
- Model классы — чистый Python, только engine API + наблюдатели
Фазы¶
Фаза 0 — Подготовка (1-2 дня)¶
- Создать
termin/editor_core/— новый UI-agnostic слой - Observable/Signal примитив (взять из tcbase если есть, иначе ~50 строк)
- Интерфейсы
DialogService,FileService(только сигнатуры) - Qt и tcgui реализации интерфейсов (обёртки поверх существующего)
Фаза 1 — Pilot: SceneTree (3-4 дня)¶
editor_core/scene_tree_model.py:EntityOperations(create/delete/rename/reparent/duplicate), наблюдаемый список entitySceneTreeController(Qt) →EntityOperations+DialogServiceSceneTreeControllerTcgui— тот же код операций- Done когда: одинаковое поведение add/delete/rename в обоих редакторах (smoke-test)
- Ценность: отработать паттерн до применения на большом
Фаза 2 — Inspector Model (2 дня)¶
editor_core/inspector_model.py: какой инспектор активен, какая entity/asset редактируется- Qt
InspectorControllerи tcguiInspectorControllerTcguiподписываются на модель - Убираем дублирование выбора "какой инспектор показать"
Фаза 3 — Rendering Model (неделя+, самая рискованная)¶
- Предварительно: портировать
ViewportListWidgetна tcgui (сейчас заглушка_NoOpViewportList) editor_core/rendering_model.py: Display/Viewport state + CRUD (~500 LOC из 1398)- Qt
RenderingControllerусыхает до view: табы, QWindow embed, сигналы - tcgui
RenderingControllerдотягивается до паритета с Qt - Резерв времени 50% — может вылезти SDL embedding, shared device, сигналы
Фаза 4 — Диалоги (параллельно с 1-3 по мере надобности)¶
- Все
QDialog→DialogServiceAPI - Стартуем с view-specific реализаций (каждый диалог пишется дважды, просто)
- К универсальному form-builder переходим только если обнаружится реальная польза
Фаза 5 — Cleanup (2-3 дня)¶
- Сверка паритета Qt vs tcgui (панели, диалоги, поведение)
- Удаление дубликатов из
editor/иeditor_tcgui/ - Короткий doc про новую архитектуру
Оценка и риски¶
| Фаза | Срок | Риск |
|---|---|---|
| 0 | 1-2 д | низкий |
| 1 (pilot) | 3-4 д | средний (отрабатываем паттерн) |
| 2 | 2 д | низкий |
| 3 | 7-10 д | высокий (RenderingController, SDL embed) |
| 4 | 5-7 д | средний (рутина) |
| 5 | 2-3 д | низкий |
Итого: ~4 недели на одного dev + запас 30-50% на неожиданное.
Главные риски¶
- RenderingController — возможно потребуется пересобрать Фазу 3 после аналитики
- Threading — engine тикает в своём цикле, view в своём; Observable должен быть thread-safe или callbacks marshalled в UI thread
- Паритет инспекторов — Qt показывает debug info, которого нет в tcgui; надо решить что считать правдой
Что уже разделено (не трогаем)¶
undo_stack.py,editor_commands.py— чистые, без Qteditor_state_io.py— callback-basedscene_manager.py,project_file_watcher.py,settings.py— UI-agnostic обёрткиfile_processors/— полностью UI-agnosticEntityInspector(базовый) — в tcgui уже callback-basedFieldWidgets— в tcgui уже UI-agnostic версия
Где логика вшита в UI (топ-5)¶
editor/rendering_controller.py(1398 LOC) — QTabWidget + QWindow + SDL embedding + CRUD всё вместеeditor/scene_tree_controller.py— 80% чисто,QMessageBox/QInputDialogвшиты в handlerseditor/inspector_controller.py— логика выбора инспектора прибита кQStackedWidgetиндексам- Диалоги (15+ штук) — каждый
QDialogс внутренней логикой; tcgui часть уже дублирует ViewportListWidget(Qt) vs_NoOpViewportListзаглушка в tcgui — прямая причина расхождений в панелях рендеринга
Статус¶
| Фаза | Статус | Что сделано |
|---|---|---|
| 0 | ✅ | editor_core/ скелет, Signal, DialogService абстрактный + Qt/tcgui реализации |
| 1 | ✅ | EntityOperations (SceneTree pilot) — create/delete/rename/reparent/duplicate + prefab/fbx/glb drops. Оба контроллера делегируют. |
| 2 | ✅ | InspectorModel (kind + target + extras + Signal). Qt/tcgui InspectorController подписываются. |
| 3 | ✅ | RenderingModel: editor_display_ptr, offscreen_context, selected display/viewport, display_input_managers, attach/detach scene, config sync, find_viewport_config, apply_display_input. Селекшн-стейт через Signal. ViewportListWidget портирован на tcgui. |
| 4 | ⏳ opportunistic | Диалоги во view-specific коде (pipeline_inspector, project_browser, scene_manager_viewer) остаются на прямых Qt/tcgui вызовах. Переводим на DialogService по мере касания. |
| 5 | ✅ | Cleanup: мёртвый код удалён (-137 LOC в Qt rendering_controller). Архитектура задокументирована — см. editor architecture. |
Что получено в цифрах¶
- Qt
rendering_controller.py: 1398 → 1063 LOC (−24%) - tcgui
rendering_controller.py: 503 → 473 LOC (−6%) - Общий UI-agnostic код в
editor_core/: ~700 LOC (signal, dialog_service, entity_operations, inspector_model, rendering_model) - Оба редактора делят один и тот же код entity-операций, инспектор-диспатча, scene attach/detach и input-mode routing.
Как поддерживать¶
- Новая scene-операция → метод в
EntityOperations, вызов черезself._opsиз view. - Новый inspector kind → enum +
show_XвInspectorModel+ ветка в_on_model_changedв обоих view. - Новое rendering-состояние → поле + Signal в
RenderingModel, view подписывается. - Новый diagnostic/CRUD диалог → метод в
DialogService(если shared) или напрямую в view (если UI-specific).
Подробности: editor architecture.
Журнал¶
- Начат: 2026-04-21
- Завершён (фазы 0–3, 5): 2026-04-22