LeCodesdocs

Плагины редактора — registerEditorWindow / registerEditorTool

Расширяй редактор сцен le.codes из кода проекта: оверлейные панели и инструменты вьюпорта, автоматизирующие рутинную сборку сцен (рассыпание пропов, связывание узлов, измерения). Регистрации живут в файлах *.editor.ts — они компилируются и выполняются только в бандлах редактора (файлы сцен), поэтому плагины стоят ноль байт в поставке.

TypeScript
// 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) добавляет запись в тулбар рядом с move/rotate/scale (также активируется через ui.toolButton, деактивируется Esc или W/E/R). Пока инструмент активен, гизмо трансформа скрыто, клики выделения приостановлены, и каждый клик по вьюпорту вызывает onViewportClick(hit, editor):

TypeScript
type EditorRayHit = {
  point: [number, number, number]    // точка попадания в мировых координатах
  normal: [number, number, number]
  node: string | null                // узел файла сцены, в который попали; null = плоскость земли
}

На сборках с физикой попадание берётся из точного рейкаста по мешам сцены; иначе (и при промахах) идёт фолбэк на плоскость земли (y = 0). hooks.cursor задаёт CSS-курсор вьюпорта (по умолчанию "crosshair"); hooks.icon задаёт глиф кнопки в тулбаре (один символ / эмодзи — инструменты без него делят общую «волшебную палочку», так что задавай его всякий раз, когда редактор регистрирует два и больше инструментов). Бросающий хук логируется и пропускается — уронить редактор он не может.

API editor

Передаётся рендерам окон и хукам инструментов:

Член Что делает
selection / select(name) Имя выделенного узла (чтение / смена).
nodes() Узлы документа: { name, kind }[] (mesh / model / light / group).
uniqueName(base) Первое незанятое имя base, base2, ….
addNode(name, def) Добавить узел из простых данных дефа (грамматика файла сцены; строковое значение model — путь ассета).
setProp(name, key, value) Записать один проп дефа — трансформы применяются вживую, остальное патчится.
removeNode(name) / duplicate(name) Удалить / дублировать (возвращает имя копии).
raycast(x, y) Тот же рейкаст вьюпорта, который получают клики инструментов.
transact(fn) Сгруппировать все правки документа внутри fn в ОДИН шаг undo.

Дефы с model-ассетом перезапускают компиляцию сцены (бандл должен завендорить URL ассета); дефы mesh/light/group применяются живыми патчами без компиляции.

Реестр __editorPlugins, который питают регистрации, — внутренний: его читает обвязка редактора сцен; никогда не трогай его напрямую.