Files
Faset_Engine/docs/studies/10-blender-editor-patterns.md

25 KiB
Raw Permalink Blame History

Blender: четыре архитектурных приёма для удобного редактора собственного движка

Синхронизация Faset, 18.09.2026. Принятые решения — ARCHITECTURE.md, этапы до/после MVP — PLAN.md. Ниже сохранено исследование чужих исходников; это не отчёт о реализованных возможностях Faset. Рекомендации, помеченные superseded, остаются только историей рассмотренных вариантов.

Для Faset приняты общие authoring-команды UI/MCP, explicit C++ metadata с TypeId/FieldId, JSON source со стабильными IDs и собственный retained C++ editor UI. MCP работает только с редактором, импортом, build, Play/Stop и editor logs; runtime world и Player через MCP недоступны. Blender не модифицируется: используется официальная программа, стандартный glTF/GLB importer Faset самодостаточен. Optional Python add-on может сохранять IDs и добавлять кнопку экспорта; он не является условием обычного импорта. Границы roundtrip.

Blender здесь рассматривается как дополнительный источник идей для инструментов, а не как четвёртый игровой движок. Самые полезные находки связаны с тем, как одна реализация действия получает интерактивный инструмент, параметры, отмену, поиск и доступ из скрипта. Для собственного редактора это может оказаться важнее внешнего сходства окон и панелей.

Что именно исследовано

17 сентября 2026 загружен официальный репозиторий blender/blender в ../../../blender-source. Зафиксирован commit 28d47268bddcb9dc69143f0e2d9410969da16311; файл версии объявляет 5.3.0 alpha, то есть это снимок разработки, не стабильный релиз. Версия в исходниках.

Checkout намеренно неполный: shallow depth 1, partial clone blob:none, sparse directories windowmanager, makesrna, editors/interface, editors/undo, asset_system, blenkernel, blenloader, scripts/modules/bpy. Git status чистый. Учтены локальные инструкции AGENTS.md; в дереве данного commit нет AGENTS.md и graphify-графа. Ни сборка, ни запуск Blender не выполнялись. Ниже факты из конкретных тел функций отделены от предлагаемой адаптации; требования и ожидаемый эффект прототипов ещё не являются измеренными результатами.

1. Параметризованная команда как основа любого инструмента

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

В исходниках. WM_operator_poll проверяет доступность оператора в текущем контексте, включая составные операции и Python callback. wm_operator_invoke создаёт экземпляр с параметрами, выбирает интерактивный invoke, когда есть событие, или прямой exec, затем обрабатывает результат как Finished/RunningModal и другие состояния. Для интерактивного вызова могут восстанавливаться предыдущие параметры. Проверка доступности, диспетчеризация.

Регистрация создаёт RNA-описание параметров оператора, присваивает идентификатор и UI-метаданные, помещает тип в общий реестр, уведомляет keymap о регистрации. В итоге расширение получает общую инфраструктуру, а не обязано самостоятельно встраивать каждую кнопку и сочетание клавиш. Регистрация оператора. Разделение poll, invoke, execute и modal подтверждается официальным описанием операторов.

Минимум для своего движка. Реестр CommandType со стабильным ID, схемой аргументов, can_execute(context), execute(targets,args) и необязательным интерактивным сеансом. Сеанс получает события, показывает preview и завершается commit/cancel. Кнопка, hotkey, command palette, editor extension и MCP вызывают одинаковую authoring-команду. Игровой C++/будущий Lua работают через отдельный runtime API, без editor Undo. invoke собирает недостающие аргументы; изменение модели делает отдельный слой, доступный без UI. Для размещения фонаря это asset_id, transform, parent/entity IDs и параметры света; mouse picking нужен только интерактивному входу.

Контекст своего редактора лучше описывать явно: документ, набор выбранных IDs, активный viewport, режим инструмента. На старте сеанса сохранить цель, а перед commit повторно проверить её существование и доступность. Не передавать всей бизнес-логике глобальную «текущую область интерфейса»: зависимость команды от случайного фокуса затрудняет тесты и автоматизацию. Modal-инструмент обязан откатывать preview при Escape и корректно завершаться при закрытии документа.

Проверка и приоритет. Один сценарий создания/перемещения должен выдавать эквивалентную модель из меню, hotkey и скрипта. Недопустимый контекст возвращает причину, отменённый preview не оставляет изменений, удаление цели не вызывает use-after-free. Это P0 для редакторного каркаса, реализуемое раньше сложных dock/workspace систем.

2. Отмена целого жеста и изменение параметров уже выполненного действия

Сценарий. Пользователь долго крутит slider радиуса источника света, затем нажимает Undo один раз. Или создаёт кольцо из объектов и после завершения меняет количество экземпляров с 12 на 20. Это две связанные, но разные функции: удобные границы undo и повторное выполнение операции с новыми аргументами.

В исходниках. wm_operator_finished централизованно выбирает обычный или grouped undo push. Счётчик op_undo_depth предотвращает отдельные шаги внутренних операторов, когда внешняя операция уже отвечает за undo. Поэтому составной инструмент не обязан засорять историю промежуточными действиями. Граница завершения, глубина вложенного вызова.

ED_undo_grouped_push при совпадении имени группы очищает активный шаг перед новым push; название группы берётся из undo_group или имени операции. ED_undo_push применяет ограничения количества шагов и памяти. Это конкретная реализация слияния последовательных действий, а не универсальное доказательство, что любые одинаково названные операции безопасно объединять. Grouped undo, выбор группы, бюджет истории.

Самая интересная функция — ED_undo_operator_repeat. Она проверяет возможность повторения, восстанавливает подходящий регион контекста, откатывает прежний результат, проверяет параметры и снова запускает оператор. Если повторение не завершилось успешно, делает redo предыдущего результата. Это основа пользовательской возможности поправить параметры последнего действия, описанной в руководстве Adjust Last Operation. Реализация повторения.

Минимум для своего движка. Ввести EditTransaction с начальным и конечным состоянием затронутых сущностей. Один drag — одна транзакция, вложенные команды присоединяются к ней. Для slider хранить initial value и последний preview; для структурных изменений — список созданных/удалённых сущностей с полными данными восстановления. Слияние ограничить transaction token, document ID и набором target IDs, а не одним названием команды.

После MVP можно добавить панель последней параметризованной операции; это исследовательский backlog, не условие первого экспорта. Хранить checkpoint до операции и её аргументы; каждое изменение параметров пересчитывать от checkpoint, не поверх предыдущего результата. Начать только с детерминированных операций, например массива объектов. Внешние записи файлов, импорты с побочными эффектами и команды, зависящие от новых случайных чисел, потребуют отдельного контракта. Не следует трактовать Blender operator как объект с обязательным inverse(): фактическое хранение undo отделено, тип выбирается по контексту и сериализует шаг через свои callbacks. Выбор UndoType, кодирование шага.

Проверка и приоритет. Сто движений slider дают один шаг; cancel возвращает исходную модель; undo/redo восстанавливают удалённые связи; ошибка пересчёта оставляет предыдущий корректный результат; смена selection не меняет цель незавершённой операции. P0 — транзакции, P1 — панель параметров последнего действия.

3. Описание свойства одновременно обслуживает Inspector и обновление модели

Сценарий. Новый компонент получает поле «дальность света». Нужно число с единицами и диапазоном, tooltip, запрет редактирования read-only ресурса, вызов через скрипт и обновление освещения после изменения. Ручное описание этого поведения в каждом окне быстро расходится.

В исходниках. RNA задаёт тип, семантический подтип, UI-название, диапазон, проверку редактирования и update callback. Например, location имеет translation subtype, привязку к данным объекта, функцию editable-array, UI range и transform notification. Схема transform-свойств. Layout::prop читает метаданные, выбирает подпись/иконку и учитывает доступность поля. Создание UI из свойства, read-only UI.

После редактирования rna_property_update вызывает callback, отправляет уведомление, публикует событие RNA в message bus и помечает данные для dependency graph согласно флагам свойства. При этом исходник явно учитывает опасность бесконечного redraw loop, когда значения операторских параметров меняются во время построения UI. Это полезное предупреждение непосредственно из реальной реализации. Обновление свойства, защита от цикла redraw.

Минимум для своего движка. PropertyDescriptor: стабильный ID, value type, units, default, hard/soft ranges, enum labels, help, getter/setter, editability и категория invalidation. Один setter-путь проходит через validation и транзакцию, затем выдаёт typed change event. Inspector использует готовые widgets; автор компонента может переопределить только layout. Первый набор типов: bool, number, enum, vector, color и asset reference. Скриптовый API и panel plugins должны обращаться к той же схеме.

Сначала менять модель, затем один раз отправлять уведомления после commit; дорогие пересчёты объединять по кадру или транзакции. Draw-функция читает данные и строит интерфейс, но не запускает безусловную мутацию. Для preview допустим отдельный лёгкий update, а mesh rebuild или shader compile можно отложить до подтверждения.

Не путать reflection и формат проекта: RNA — высокоуровневое описание и доступ к свойствам, тогда как Blender DNA описывает низкоуровневые сохраняемые структуры. Это прямо разъясняется в официальном FAQ DNA/RNA и документации RNA. Для своего движка формат сцен, версии и миграции всё равно надо проектировать отдельно.

Проверка и приоритет. Изменение через Inspector и script даёт одинаковую validation/invalidation; один commit вызывает один необходимый rebuild; locked asset объясняет запрет; новое поле появляется без написания нового widget; schema ID переживает переименование подписи. Это P0, но достаточно небольшой reflection-системы — весь makesrna/DNA pipeline не нужен.

4. Поиск команд, который учит интерфейсу и объясняет недоступность

Сценарий. Пользователь помнит действие, но не помнит меню. Плагин добавил новый инструмент, а отдельный индекс поиска никто не обновил. Или кнопка серая без объяснения, какой объект необходимо выбрать.

В исходниках. operator_search_update_fn перебирает общий реестр операторов, фильтрует internal-операторы, сопоставляет слова, проверяет poll, добавляет клавиатурную подсказку. Выбранный результат вызывает тот же оператор. Поиск по реестру.

Отдельный menu search строит UI меню, обходит получившиеся кнопки, сохраняет оператор вместе с аргументами и контекстом, рекурсивно собирает подменю и их путь. Это позволяет искать конкретный пункт, например действие с заранее выставленным enum, а не только абстрактную команду. Захват параметров/контекста пункта, обход меню. Tooltip disabled-кнопки заново получает причину из operator poll и показывает её пользователю. Причина недоступности.

Минимум для своего движка. Command palette из того же реестра: label, keywords, breadcrumb, shortcut и текущая доступность. Пункты меню хранить как данные «command + args + optional context», чтобы индексировать их без выполнения UI draw. На выборе обязательно повторять validation: между поиском и Enter сцена может измениться. Для неудовлетворённого precondition возвращать короткое объяснение вроде «Выберите хотя бы два объекта»; можно показывать такой результат disabled, вместо полного исчезновения команды. Последнее — предлагаемая UX-политика, а не поведение рассмотренного operator search Blender.

Проверка и приоритет. Новая зарегистрированная команда появляется без ручной регистрации в поиске; shortcut меняется вместе с keymap; одинаковые имена различаются breadcrumb; действие использует параметры найденного пункта; переход в другой документ не применяет устаревший context. P1, небольшой объём после готовности command registry.

Практический порядок: сначала команды, транзакции и небольшой слой метаданных; затем Inspector и command palette поверх них; затем интерактивные сеансы и изменение последней операции. Дополнение Blender ценно именно этой взаимосвязью: extensibility и удобство пользователя выходят из общей модели действий и данных, а не требуют четырёх несвязанных подсистем.

В Faset декларативные layout/styles описывают собственные retained widgets; заимствование RNA/operator идей не означает перенос Blender UI-кода или собственного fork Blender. Lua поддерживается позднее обязательным модулем и необязателен для конкретной игры; Python add-on Blender — отдельная внешняя интеграция, а не выбор Python для gameplay Faset.