Как написать плагин
Соберите свой плагин для Studio: создайте папку, напишите панель, поработайте со сценой и опубликуйте плагин.
Плагин — это папка на вашем компьютере. Вы правите её в своём редакторе кода, Studio запускает её без сборки, а когда всё готово, вы публикуете плагин.
Код, который работает в опубликованном проекте, описан в разделе Код в проекте.
Создайте плагин
- Откройте панель Плагины.
- Нажмите Создать плагин.
- Выберите папку. Studio запишет в неё стартовый шаблон:
my-plugin/
meta.json манифест
main.html разметка вашей панели
main.js ваш код — подойдут и .ts, .tsx, .jsx
plugin.d.ts сгенерированные типы для всего API
tsconfig.json чтобы IDE находила эти типы
.wasignore что не загружать при публикации
Чтобы увидеть изменения, сохраните файлы и нажмите Перечитать. Ничего собирать или устанавливать не нужно.
Файлы .ts, .tsx и .jsx можно писать напрямую. Сгенерированный plugin.d.ts даёт вашему
редактору кода автодополнение по всему API.
Манифест
meta.json описывает ваш плагин:
{
"id": "acme.shape-spawner",
"name": "Shape Spawner",
"version": "1.0.0",
"entry": "main.tsx",
"permissions": ["scene:read", "scene:write"],
"panels": [{ "id": "main", "title": "Shapes", "entry": "main.tsx" }],
"icon": "data:image/webp;base64,…"
}
icon — это data URL, а не путь к файлу. Так у локальной папки и у опубликованного плагина
везде одна и та же иконка: на панели инструментов, в заголовке панели и на карточке в
библиотеке. Форма публикации может сделать её за вас из любой картинки.
В манифест можно добавить и другое:
| Поле | Что добавляет |
|---|---|
components | компонент для объектов → Свои компоненты |
runtime | код для опубликованного проекта → Код в проекте |
triggers, steps | пункты в панели событий → Триггеры и шаги |
settings | раздел в Настройках проекта → Настройки проекта |
locales | переводы → Переведите плагин |
actions | пункты в контекстном меню слоёв и ассетов → Пункты меню и горячие клавиши |
commands | команды, которые могут вызывать другие плагины и ассистент → Команды |
skills | руководства, которые ассистент читает в проектах с этим плагином, — каждое в своём файле .md из папки |
Ваша панель
Самая простая панель — это HTML-файл со скриптом:
<div id="root">Loading…</div>
<script type="module">
init(async () => {
// здесь editor, world и meta уже готовы
document.getElementById('root').textContent = `${meta.name} — ${world.getEntities().length} entities`;
});
</script>
HTML можно и не писать, а указать в entry файл .tsx. Studio смонтирует его экспорт по
умолчанию, и панель можно собрать из компонентов самого редактора:
// main.tsx
import { useState } from 'react';
import { Button } from '@was/ui';
export default function Panel() {
const [clicks, setClicks] = useState(0);
return <Button onClick={() => setClicks(clicks + 1)}>{clicks}</Button>;
}
init() ждёт, пока всё будет готовоinit() запускается, когда редактор и сцена готовы. Проверять это самим не нужно.
Что доступно вашему коду
Доступны такие глобальные переменные:
| Переменная | Что это |
|---|---|
editor | API редактора |
world | сцена в реальном времени — сущности и компоненты; null, если в манифесте нет scene:read, и всегда null в слое поверх сцены |
tags | собственные метки вашего плагина на объектах (см. ниже); null, когда world тоже null |
meta | ваш манифест |
init | запускает ваш код, когда редактор и сцена готовы |
Импортировать можно @was/ecs, @was/signals, @was/engine, @was/editor-api, @was/ui,
@was/icons, react и react-dom/client. Любой другой импорт завершится ошибкой, так что вы
сразу это заметите.
Панели на @was/ui используют готовую таблицу стилей, поэтому работают только её классы.
Произвольные значения вроде text-[13px] не работают. Если нужен размер вне шкалы, задайте его
через встроенный style.
Работа со сценой
Сущности можно собирать вручную через world. Но сцены и пространства лучше создавать через
редактор: вы получите те же значения по умолчанию, нумерацию и проверки, что и при работе в
Studio.
await editor.scenes.list();
await editor.scenes.create({ name: 'Chapter 2', trigger: { type: 'image', imageId } });
await editor.scenes.setTrigger(sceneId, { type: 'surface', bindingType: 'wall' });
await editor.scenes.delete(sceneId);
await editor.spaces.create({ name: 'Lobby' });
trigger — это данные якоря. Они проверяются по той же схеме, что и в редакторе, поэтому новые
типы триггеров заработают в вашем плагине без изменений в API плагинов. Если для триггера по
картинке не указать физический размер, он будет вычислен по самой картинке.
Как потом найти свои объекты
Плагину, который генерирует сцены, нужно уметь находить их снова. Для этого есть tags:
tags.set(entity, { kind: 'scene', card: '2' });
tags.find({ kind: 'scene' });
tags.remove(entity, 'card');
Теги хранятся отдельно для каждого плагина, поэтому два плагина никогда не перезапишут теги друг друга. Редактор их не показывает и не меняет.
Комната проекта
Часть состояния относится к текущей сессии, а не к проекту: запущенный таймер, открытое голосование, поднятая рука. Такое состояние не нужно отменять, публиковать или сохранять вместе с проектом.
Комната проекта — небольшое общее хранилище ключей и значений. Его видят все, кто сейчас находится в проекте.
const { now, entries } = await editor.room.get();
await editor.room.set('timer', { running: true, endsAt: now + 60_000 });
editor.on('room.changed', (state) => render(state.entries.timer));
await editor.room.delete('timer');
- Оно переживает перезагрузку страницы.
- Оно очищается через 12 часов после последней записи.
- В нём до 64 ключей. Имя ключа — до 128 символов, значение — до 8 КБ в виде JSON. Запись сверх лимита игнорируется.
- Читать его могут все, кто в проекте. Писать — только те, кто может редактировать проект.
Часы на двух компьютерах могут расходиться на минуты, поэтому now возвращает время сервера.
Сохраняйте время окончания, рассчитанное от now, а обратный отсчёт пусть каждый редактор
ведёт сам. Не храните «сколько секунд осталось»: тогда таймер писал бы в комнату каждую секунду,
у всех сразу.
Сервер запоминает, кто записал каждый ключ, и плагин не может это подделать. Благодаря этому
можно устроить честное голосование, но проверять автора должен ваш плагин: сервер разрешает
любому, кто может редактировать проект, записать любой ключ. Пример Session voting
засчитывает ключ vote/<userId>, только если его записал тот же самый пользователь.
Слой поверх сцены
Панель видите только вы, и её можно закрыть. Если что-то должны видеть все, объявите панель с
"surface": "hud". Она рисуется небольшим слоем поверх сцены:
editor.hud.set({ visible: true, width: 240, height: 96 });
editor.panels.open('main'); // слой может открыть свою панель
Сначала слой скрыт. Показывайте его, только когда ему есть что показать: даже пустой слой перехватывает клики, предназначенные для сцены. Максимальный размер — 640×400.
В слое world равен null. Всё, что ему нужно показать, записывайте из своей панели в комнату
проекта.
Переведите плагин
Держите словарь для каждого языка в одной папке и укажите эту папку в манифесте:
"name": "${meta.name}",
"locales": "locale",
"panels": [{ "id": "main", "title": "${panel.title}", "entry": "main.tsx" }]
my-plugin/
locale/
en.json { "meta": { "name": "Shape Spawner" }, "panel": { "title": "Shapes" } }
ru.json { "meta": { "name": "Генератор фигур" }, "panel": { "title": "Фигуры" } }
name,descriptionи заголовки панелей и компонентов могут быть шаблонами${…}: значение берётся из словаря.- В панели
useEditorLocale()из@was/editor-apiилиeditor.i18n()даётt(key, vars)на языке редактора. Когда человек меняет язык, текст обновляется. - Если нужного языка нет, берётся английский, а если нет и его — любой язык, который есть у плагина.
- После публикации библиотека показывает название и описание на каждом языке.
Запомните настройки
У панели нет localStorage. Чтобы запомнить что-то для человека между визитами, используйте
editor.storage:
await editor.storage.set('lastShape', 'sphere');
const shape = await editor.storage.get('lastShape');
editor.storage.subscribe((values) => render(values));
Ещё есть getAll(), delete(key) и clear(). Значения хранятся как JSON, только для этого
человека и этого плагина: до 256 КБ на плагин и до 1 МБ на все плагины человека вместе. Значения,
которые должны видеть все в проекте, храните в комнате проекта.
Уведомления и вопросы
editor.notify('Scene generated');
editor.notify({ message: 'Could not load the model', kind: 'error' });
const ok = await editor.confirm({ title: 'Delete 12 objects?', danger: true });
- У уведомления могут быть ещё
detail,kind(success,warningилиerror) иduration. confirmотвечаетtrueилиfalse. Клик мимо окна считается ответом «нет».- Оба показывают название вашего плагина. На экране остаётся до трёх уведомлений, а ответа может ждать только один вопрос.
- Используйте их вместо
alert()иconfirm(): панель не может их показать.
Камера
const view = await editor.viewport.camera.get();
await editor.viewport.camera.set({ position: [0, 2, 5], target: [0, 0, 0] });
await editor.viewport.camera.focus([entityId]);
setплавно переводит камеру к близкому виду и перескакивает к далёкому. Передайтеanimate: false, чтобы всегда перескакивать.focusпоказывает в кадре один или несколько объектов. Он отвечаетfalse, когда показывать нечего.- В 2D
setтолько сдвигает и масштабирует вид. - Для этого нужно право
editor:viewport.
Отмена
Изменения, которые ваш плагин вносит в сцену, можно отменить, как собственные правки человека. Изменения с перерывом меньше 0,3 секунды становятся одним шагом.
editor.history.transaction(fn)делает одним шагом всё, что меняетfn, даже еслиfnасинхронная.editor.history.skip(fn)не записывает изменения в историю. Используйте это для данных, которые плагин вычисляет по сцене. Иначе отмена удалит эти данные, плагин запишет их снова, и человек так и не вернётся к своей правке.
Пункты меню и горячие клавиши
actions добавляют пункты в контекстное меню строк в Слоях и в списке ассетов:
"actions": [
{ "id": "check", "title": "Check the scene", "panel": "main",
"targets": ["entity"], "shortcut": "Mod+Alt+KeyD" }
]
editor.actions.on('check', ({ target, ids }) => {
// то, на чём кликнули правой кнопкой, или выделение
});
targets—entityдля строк слоёв иresourceдля строк ассетов. У плагина может быть до 20 действий.- Клик открывает вашу панель
panel, если она закрыта, и вызывает обработчик в ней. shortcutсодержитMod,Altили оба в порядкеMod+Alt+Shiftи код клавиши, напримерKeyD. Сочетание действует на текущее выделение. Собственные клавиши редактора важнее, а сочетания, которые оставляет себе браузер, напримерMod+KeyW, не принимаются.
Команды
commands позволяют другим плагинам и ассистенту вызывать ваш плагин:
"commands": [
{ "name": "diagnose", "description": "Lists the problems in the scene", "panel": "main" }
]
editor.commands.handle('diagnose', async (input, { caller }) => {
return { problems: await findProblems() };
});
- Команда называется
plugin.<plugin id>.<name>. Другие плагины находят её черезeditor.commands.list(). У плагина может быть до 20 команд, имена — в camelCase. - Возвращайте JSON или бросайте ошибку. Проверяйте входные данные сами: за вас их никто не проверит.
- Если панель закрыта, она сначала откроется, но только у плагина, который человек уже разрешил.
permissionsперечисляет, какие права нужны вызывающему плагину. Без этого поля ему нужны все права вашего плагина.- С
destructive: trueчеловек подтверждает каждый вызов, кто бы его ни делал.
Публикация
Форма публикации просит заполнить:
| Поле | Ограничение |
|---|---|
| Иконка | делается из любой выбранной вами картинки и обрезается в квадрат 256×256 |
| Название | — |
| Теги | до 12 |
| Описание | до 500 символов |
| Скриншоты | до 4 |
| Доступ | Бесплатно, Премиум или Только подписчикам — см. Оценки и платные плагины |
Если у плагина есть словари, название и описание берутся из них, на каждом языке.
Другие ограничения: до 64 файлов на плагин, 512 КБ на файл, 50 плагинов на аккаунт. Картинки и другие двоичные файлы должны быть меньше примерно 384 КБ каждый.
Повторная публикация обновляет тот же плагин. Чтобы отредактировать опубликованный плагин, скачайте его: Studio запишет его файлы в выбранную вами папку.
Файлы опубликованного плагина отдаются как обычно, поэтому путь вроде ./icon.png работает.
Локальная папка загружает только файлы .ts, .tsx, .js, .jsx и .json из своего верхнего
уровня, а также файлы .html, которые ваш манифест называет точками входа. Картинки, стили и
файлы из подпапок появятся только после публикации.
Права
Перечислите в манифесте, что нужно плагину: scene:read, scene:write, resources:write,
spaces:write, collaboration:read, collaboration:write, editor:panels и другие. Неизвестные
права сервер отклоняет.
Studio не ограничивает большую часть этих прав: каждый плагин получает их. Исключений два.
Панель получает world, только если в манифесте есть scene:read. Данные проекта
(editor.data) отвечают только плагину, в манифесте которого есть data:read или data:write.
Проверяющие читают список прав, и люди судят о плагине по нему. Просите минимальный набор, которого хватает для работы.
Устанавливая чужой плагин, исходите из того, что ему доступен весь проект. Ставьте только те плагины, которым есть причины доверять. См. Доверие и проверка.
Дальше: Свои компоненты — добавьте объектам собственный компонент.