Миграция termin.editor с Qt6 на termin-gui¶
Цель¶
Заменить PyQt6 в редакторе на собственный GUI-фреймворк termin-gui (tcgui).
Параллельная реализация в termin/editor_tcgui/ — старый редактор (termin/editor/) не трогаем до завершения.
Переключение через флаг в run_editor.py.
Ключевые архитектурные решения¶
3D Viewport через FBO¶
Display не встраивается как отдельное окно. Вместо этого:
- Display рендерит в OpenGL Framebuffer Object (FBO)
- Виджет Viewport3D в tcgui владеет FBO, показывает его содержимое через glBlitFramebuffer
- Mouse/keyboard события приходят из tcgui и пробрасываются в C++ input manager
Это устраняет необходимость в SDLEmbeddedWindowBackend для редактора и убирает зависимость на Qt для встраивания окон.
Параллельная реализация¶
termin/editor/ ← существующий Qt6 (не трогаем)
termin/editor_tcgui/ ← новая реализация на tcgui
Доменный код используется напрямую из обоих вариантов:
- UndoStack, editor_commands.py
- SceneManager, ResourceManager
- ProjectFileWatcher, SceneFileController, ResourceLoader
- EditorSettings
Фаза 1 — FBO Backend (критический блокер)¶
Всё остальное зависит от этого.
1.1 C++: FBO-вариант tc_render_surface¶
Добавить в C++ (и Python-биндинги):
_render_surface_new_from_fbo(fbo_id: int, w: int, h: int) -> int
_render_surface_resize_fbo(ptr: int, new_fbo_id: int, w: int, h: int)
Display рендерит в этот FBO. Рендер-пайплайн не знает разницы.
1.2 Python: FBOWindowBackend¶
termin/visualization/platform/backends/fbo_backend.py:
- Создаёт FBO через tgfx graphics backend
- Оборачивает в tc_render_surface через _render_surface_new_from_fbo
- Реализует тот же интерфейс что SDLEmbeddedWindowBackend (poll_events, mouse/key dispatch) но без SDL-окна
- Input-события получает извне (от Viewport3D виджета)
1.3 tcgui: виджет Viewport3D¶
Новый виджет в termin-gui/python/tcgui/widgets/viewport3d.py:
- layout(): при изменении размера пересоздаёт FBO, вызывает on_resize(w, h, fbo_id)
- render(): glBlitFramebuffer из FBO в back-buffer (по clip-rect виджета)
- Пробрасывает все mouse/key события в FBOWindowBackend
- Коллбек on_resize → внешний код обновляет Display
Фаза 2 — Точка входа¶
termin/editor_tcgui/run_editor.py:
ui = UI(width=1600, height=900, title="Termin Editor")
win = EditorWindowTcgui(world, scene, engine.scene_manager, graphics)
win.build(ui)
engine.set_poll_events_callback(lambda: ui.poll())
engine.set_should_continue_callback(lambda: not win.should_close())
В корневом run_editor.py добавить флаг:
--ui=tcgui (новый)
--ui=qt (существующий, дефолт пока)
Фаза 3 — Скелет EditorWindowTcgui¶
termin/editor_tcgui/editor_window.py — не наследуется ни от чего UI-специфичного.
Layout:
VStack
MenuBar
Splitter (horizontal, stretch)
TabView [Scene | Rendering] ← левая панель
Viewport3D ← центр, растягивается
ScrollArea > VStack [Inspector] ← правая панель
TabView [Project | Console] ← нижняя панель
StatusBar
Весь инициализационный код (ResourceLoader, ProjectFileWatcher, процессоры, SceneManager, InteractionSystem) копируется без изменений — он Qt-free.
Фаза 4 — MenuBar¶
termin/editor_tcgui/menu_bar_controller.py:
- Та же сигнатура конструктора (~25 коллбеков)
- Вместо QMenuBar.addMenu + QAction.triggered.connect:
python
menu_bar.add_menu("File", file_menu)
menu_bar.register_shortcuts(ui) # уже есть в tcgui
- update_undo_redo_actions() → item.enabled = can_undo()
- update_play_action() → item.label = "Stop" / "Play"
Фаза 5 — Дерево сцены¶
termin/editor_tcgui/scene_tree_controller.py:
- TreeWidget (drag-drop уже есть: draggable=True, on_drop)
- TreeNode.data = entity
- tree.on_select = lambda node: on_object_selected(node.data)
- Инкрементальные операции: add_entity, remove_entity, move_entity
- Контекстное меню: ui.show_overlay(menu, ...) — паттерн из diffusion-editor
SceneTreeModel(QAbstractItemModel) удаляется. Логика обходов дерева переезжает в контроллер.
Фаза 6 — Field Widgets¶
termin/editor_tcgui/widgets/field_widgets.py:
| Qt | tcgui |
|---|---|
FloatFieldWidget (DoubleSpinBox) |
SpinBox (decimals=3) |
Vec3FieldWidget (3× DoubleSpinBox) |
HStack(SpinBox×3) |
BoolFieldWidget (QCheckBox) |
Checkbox |
StringFieldWidget (QLineEdit) |
TextInput |
ColorFieldWidget |
Button → ColorDialog |
EnumFieldWidget (QComboBox) |
ComboBox |
ResourceFieldWidget |
HStack(TextInput, Button) |
IntFieldWidget |
SpinBox (step=1) |
Фаза независима — можно начать параллельно с Фазой 1.
Фаза 7 — TransformInspector и EntityInspector¶
termin/editor_tcgui/transform_inspector.py:
- QFormLayout → VStack(HStack(Label, widget), ...)
- DoubleSpinBox → tcgui SpinBox
- pyqtSignal → коллбек on_transform_changed: Callable
termin/editor_tcgui/entity_inspector.py:
- Список компонентов: QListWidget → tcgui ListWidget
- QScrollArea → tcgui ScrollArea
- Контекстное меню компонента → tcgui Menu overlay
Фаза 8 — InspectorController¶
termin/editor_tcgui/inspector_controller.py:
- Вместо QStackedWidget: каждая панель имеет widget.visible
- Показ нужной панели: остальные visible = False
- 8 инспекторов: Entity, Material, Display, Viewport, Pipeline, Texture, Mesh, GLB
- Mesh и GLB инспекторы используют Viewport3D для preview (зависят от Фазы 1)
Фаза 9 — Project Browser¶
termin/editor_tcgui/project_browser.py:
- Дерево директорий: tcgui TreeWidget
- Список файлов: tcgui ListWidget
- Drag из файлов в сцену: drag-source в ListWidget (нужно добавить в tcgui)
Фаза 10 — Console и Dialogs¶
Console: tcgui TextArea (readonly) + перехват лога через tcbase.log callback.
Dialogs:
- MessageBox — есть в tcgui ✓
- FileDialog — есть в tcgui ✓
- InputDialog (Rename entity и т.п.) — добавить в tcgui: Dialog(Label + TextInput + OK/Cancel)
- Scene Properties, Layers, Shadow Settings → Dialog + форма из field widgets (Фаза 6)
Фаза 11 — Простые диалоги и действия¶
Форм-диалоги на основе field widgets (Фаза 6) + простые действия.
| Диалог/действие | Qt-оригинал | Объём |
|---|---|---|
| Settings (text editor path) | settings_dialog.py (102) |
форма с browse |
| Project Settings (render sync) | project_settings_dialog.py (121) |
1 combo |
| Shadow Settings | shadow_settings_dialog.py (124) |
3 поля + apply |
| Layers & Flags | layers_dialog.py (128) |
2 скролла × 64 поля |
| Fullscreen | editor_mode_controller.py (30) |
show/hide панелей |
| Run Standalone | editor_window.py (20) |
subprocess + валидация |
| Undo Stack Viewer | undo_stack_viewer.py (96) |
2 списка |
Фаза 12 — Game Mode и Scene конфигурация¶
| Диалог/действие | Qt-оригинал | Объём |
|---|---|---|
| Toggle Game Mode (Play/Stop) | editor_mode_controller.py (50+) |
scene mode + UI state |
| Scene Properties | scene_inspector.py (596) |
multi-tab форма |
| Agent Types | agent_types_dialog.py (279) |
список + форма |
| SpaceMouse Settings | spacemouse_settings_dialog.py (207) |
multi-group форма |
Фаза 13 — Debug Viewers (простые)¶
Tree/list viewers для отладки. Паттерн: Dialog + TreeWidget + detail panel.
| Viewer | Qt-оригинал | Объём |
|---|---|---|
| Inspect Registry | inspect_registry_viewer.py (205) |
tree + search + detail |
| NavMesh Registry | navmesh_registry_viewer.py (245) |
tree + detail |
| Audio Debugger | audio_debugger.py (193) |
status + channel list |
| Scene Manager Viewer | scene_manager_viewer.py (569) |
tree + action buttons |
Фаза 14 — Debug Viewers (сложные)¶
| Viewer | Qt-оригинал | Объём |
|---|---|---|
| Resource Manager Viewer | resource_manager_viewer.py (803) |
multi-tab tree |
| Core Registry Viewer | core_registry_viewer.py (913) |
multi-tab C API |
| Profiler Panel | profiler_panel.py (409) |
graph widget + table |
| Modules Panel | modules_panel.py (513) |
module list + compiler log |
Фаза 15 — Сложные инструменты (отложить)¶
Большие standalone-инструменты, можно портировать последними.
| Инструмент | Qt-оригинал | Объём |
|---|---|---|
| Framegraph Debugger | framegraph_debugger.py (1116) |
C++ core + dual modes + SDL |
| Pipeline Editor | termin.nodegraph (1000+) |
полноценный node-graph UI |
Фаза 16 — Избавление от Qt в shared-модулях¶
Четыре модуля из termin/editor/ используются tcgui-редактором, но содержат Qt-зависимости.
Нужно переписать их как чисто Python (без PyQt6).
16.1 ProjectFileWatcher (HIGH)¶
Файл: termin/editor/project_file_watcher.py
Qt-зависимости: QFileSystemWatcher, QTimer
Переписать на watchdog или os.scandir-based polling:
- Обход директорий, отслеживание mtime
- Периодический rescan через таймер (callback из main loop или threading.Timer)
- Сохранить API: watch_directory(path), register_processor(...), rescan(), project_path
16.2 EditorSettings (HIGH)¶
Файл: termin/editor/settings.py
Qt-зависимости: QSettings
Переписать на tcbase.Settings (уже есть в termin-base):
- from tcbase import Settings → JSON-хранилище в ~/.config/{app}/settings.json
- API: get(key, default), set(key, value), contains(key), group(prefix) (context manager)
- Автосохранение при каждом set()
- instance(), init_text_editor_if_empty() — сохранить
16.3 ResourceLoader (HIGH)¶
Файл: termin/editor/resource_loader.py
Qt-зависимости: QWidget (parent), QFileDialog, QMessageBox
- Убрать
parent: QWidgetпараметр - Заменить
QFileDialog/QMessageBoxна callback-интерфейс (tcgui вызывает свои диалоги) - Или: resource_loader уже не использует диалоги напрямую в tcgui пути — проверить и при необходимости вынести диалоговые вызовы в контроллер
16.4 external_editor (MEDIUM)¶
Файл: termin/editor/external_editor.py
Qt-зависимости: QWidget, QMessageBox
open_in_text_editor(path)— ядро (subprocess.Popen) Qt-free- Убрать
QMessageBox— логировать ошибки черезlog.error
Фаза 17 — Переключение¶
- Переключить дефолт
run_editor.pyна--ui=tcgui - Регрессия: открыть проект, сущности, undo/redo, save/load, drag-drop
- Удалить
--ui=qtветку - Удалить
termin/editor/ - Убрать
PyQt6из зависимостей - Убрать Qt-код из
SDLEmbeddedWindowBackend
Что нужно добавить в termin-gui¶
| Компонент | Размер |
|---|---|
Viewport3D виджет |
средний |
InputDialog |
маленький |
drag-source в ListWidget |
маленький |
Граф зависимостей¶
Фаза 1 (FBO Backend) ←── блокирует ──→ Фаза 2 → Фаза 3 → [4,5,8,9,10]
↑
Фаза 6 (Field Widgets) ────────────────────────── блокирует Фазы 7,8
Рекомендуемый старт: Фаза 1 + Фаза 6 параллельно.