Плагины редактора — registerEditorWindow / registerEditorTool
Расширяй редактор сцен le.codes из кода проекта: оверлейные панели и инструменты вьюпорта,
автоматизирующие рутинную сборку сцен (рассыпание пропов, связывание узлов, измерения).
Регистрации живут в файлах *.editor.ts — они компилируются и выполняются только в бандлах
редактора (файлы сцен), поэтому плагины стоят
ноль байт в поставке.
// scatter.editor.ts — только бандлы редактора
const state = { model: "", count: 5 }
registerEditorWindow("Scatter", (ui, editor) => {
state.model = ui.asset("model")
state.count = ui.slider("count", { min: 1, max: 20, step: 1, value: 5 })
ui.toolButton("Paint", "scatter")
ui.info(`${editor.nodes().length} nodes in the scene`)
})
registerEditorTool("scatter", {
cursor: "copy",
onViewportClick(hit, editor) {
editor.transact(() => { // вся покраска = ОДИН шаг undo
for (let i = 0; i < state.count; i++) {
editor.addNode(editor.uniqueName("prop"), {
model: state.model,
position: [ hit.point[0] + Math.random(), hit.point[1], hit.point[2] + Math.random() ],
eulerAngles: [ 0, Math.random() * 360, 0 ],
})
}
})
},
})Плагины пишут в документ, никогда — в живое состояние
Каждая запись через editor попадает в файл сцены через обычный путь коммита редактора —
поэтому действие плагина отменяемо (один стек doc-патчей для людей и плагинов), выглядит обычным
git-диффом и применяется к запущенному вьюпорту на самом дешёвом живом уровне. Багованный плагин
в худшем случае запишет плохие данные; испортить живое состояние движка он не может.
Окна
registerEditorWindow(title, (ui, editor) => …) добавляет сворачиваемую панель, пришвартованную
поверх вьюпорта (рейка инспектора остаётся привязанной к выделению — окна глобальны для
редактора). Окно, эмитящее ui.toolButton для инструмента, — панель настроек этого
инструмента: активация инструмента раскрывает и подсвечивает её, так что настройки лежат ровно
там, где инструмент используется. Функция рендера — immediate-mode, тот же протокол
InspectorUI, что у своих карточек
инспектора: она перезапускается при каждом
взаимодействии (и при каждом изменении документа) и описывает панель вызовами виджетов. Все ключи
полей окна — временное состояние редактора (у окон нет записи в документе): размеры кистей,
выбранные ассеты; они переживают перерендеры, но никогда не пишутся в файл. К словарю добавляются
два оконных виджета:
| Виджет | Что делает |
|---|---|
ui.asset(key) |
Дропдаун ассетов проекта (GLB-пути проекта). Возвращает путь, "" = ничего. |
ui.toolButton(label, tool) |
Переключает именованный инструмент вьюпорта — рисуется нажатым, пока тот активен. |
Чтобы поделиться значениями виджетов с инструментом, копируй их в состояние модуля в рендере (пример выше) — immediate-mode возвращает текущее значение на каждом прогоне.
Инструменты вьюпорта
registerEditorTool(name, hooks) добавляет запись в тулбар (также активируется через
ui.toolButton, деактивируется Esc). Пока инструмент активен, каждый клик по вьюпорту вызывает
onViewportClick(hit, editor):
type EditorRayHit = {
point: [number, number, number] // точка попадания в мировых координатах
normal: [number, number, number]
node: string | null // абсолютный путь узла, в который попали; null = плоскость земли
}На сборках с физикой попадание берётся из точного рейкаста по мешам сцены; иначе (и при промахах)
идёт фолбэк на плоскость земли (y = 0). hooks.cursor задаёт CSS-курсор вьюпорта (по умолчанию
"crosshair"); hooks.icon задаёт глиф кнопки в тулбаре (один символ / эмодзи — инструменты без
него делят общую «волшебную палочку», так что задавай его всякий раз, когда редактор регистрирует
два и больше инструментов). Бросающий хук логируется и пропускается — уронить редактор он не
может.
API editor
Передаётся рендерам окон и хукам инструментов. Узлы адресуются абсолютным путём
('city/in1/pt1'; у корневого узла путь — это его голое имя):
| Член | Что делает |
|---|---|
selection / select(path) |
Путь выделенного узла (чтение / смена); может быть ключом вида path::part для внутренностей ассета. |
nodes() |
Узлы документа: { path, name, kind }[] (path = идентичность, name = уникальное среди соседей отображаемое имя). |
uniqueName(base, parentPath?) |
Первое незанятое имя base, base2, … среди соседей под parentPath (корень сцены, если не задан). |
addNode(name, def) |
Добавить КОРНЕВОЙ узел из простых данных дефа (грамматика файла сцены; строковое значение model — путь ассета). В именах нельзя / и :. |
setProp(path, key, value) |
Записать один проп дефа — трансформы применяются вживую, остальное патчится. |
removeNode(path) / duplicate(path) |
Удалить / дублировать (возвращает путь копии). |
raycast(x, y) |
Тот же рейкаст вьюпорта, который получают клики инструментов. |
transact(fn) |
Сгруппировать все правки документа внутри fn в ОДИН шаг undo. |
Дефы с model-ассетом перезапускают компиляцию сцены (бандл должен завендорить URL ассета); дефы
mesh/light/group применяются живыми патчами без компиляции.
Реестр __editorPlugins, который питают регистрации, — внутренний: его читает обвязка редактора
сцен; никогда не трогай его напрямую.