@energy8platform/golem 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +376 -0
- package/bin/golem.js +2 -0
- package/dist/editor.css +701 -0
- package/dist/editor.js +59202 -0
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.js +5521 -0
- package/dist/lib/cli.js.map +7 -0
- package/dist/lib/editor/api.d.ts +42 -0
- package/dist/lib/editor/app.d.ts +13 -0
- package/dist/lib/editor/atlas-detect.d.ts +28 -0
- package/dist/lib/editor/atlas-panel.d.ts +1 -0
- package/dist/lib/editor/atlas.d.ts +39 -0
- package/dist/lib/editor/gizmo-math.d.ts +36 -0
- package/dist/lib/editor/gizmos.d.ts +185 -0
- package/dist/lib/editor/layers.d.ts +1 -0
- package/dist/lib/editor/library.d.ts +3 -0
- package/dist/lib/editor/panels.d.ts +26 -0
- package/dist/lib/editor/props.d.ts +19 -0
- package/dist/lib/editor/server.d.ts +11 -0
- package/dist/lib/editor/stage.d.ts +222 -0
- package/dist/lib/editor/store.d.ts +1053 -0
- package/dist/lib/editor/timeline-layout.d.ts +64 -0
- package/dist/lib/editor/timeline.d.ts +45 -0
- package/dist/lib/editor-entry.d.ts +2 -0
- package/dist/lib/editor-entry.js +5490 -0
- package/dist/lib/editor-entry.js.map +7 -0
- package/dist/lib/harness.js +51012 -0
- package/dist/lib/ktx2.d.ts +15 -0
- package/dist/lib/mcp-server.d.ts +2 -0
- package/dist/lib/preview/harness.d.ts +16 -0
- package/dist/lib/preview/render-preview.d.ts +27 -0
- package/dist/lib/preview/viewer.d.ts +1 -0
- package/dist/lib/rig-anim.d.ts +109 -0
- package/dist/lib/rig-api.d.ts +26 -0
- package/dist/lib/rig-atlas.d.ts +20 -0
- package/dist/lib/rig-check.d.ts +45 -0
- package/dist/lib/rig-constraints.d.ts +59 -0
- package/dist/lib/rig-deform.d.ts +27 -0
- package/dist/lib/rig-format.d.ts +5036 -0
- package/dist/lib/rig-history.d.ts +21 -0
- package/dist/lib/rig-import-layers.d.ts +32 -0
- package/dist/lib/rig-io.d.ts +9 -0
- package/dist/lib/rig-mesh-image.d.ts +2 -0
- package/dist/lib/rig-mesh.d.ts +143 -0
- package/dist/lib/rig-path.d.ts +101 -0
- package/dist/lib/rig-presets.d.ts +101 -0
- package/dist/lib/rig-queue.d.ts +2 -0
- package/dist/lib/rig-runtime.d.ts +70 -0
- package/dist/lib/rig-state.d.ts +64 -0
- package/dist/lib/rig-symbol.d.ts +60 -0
- package/dist/lib/rig-template-library.d.ts +3 -0
- package/dist/lib/rig-templates.d.ts +46 -0
- package/dist/lib/rig-tools.d.ts +376 -0
- package/dist/lib/runtime.d.ts +9 -0
- package/dist/lib/runtime.js +1584 -0
- package/dist/lib/runtime.js.map +7 -0
- package/dist/lib/spine-import.d.ts +68 -0
- package/dist/lib/tools.d.ts +2 -0
- package/dist/lib/tools.js +5242 -0
- package/dist/lib/tools.js.map +7 -0
- package/editor.html +3 -0
- package/package.json +96 -0
package/README.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# @energy8platform/golem — прототип AI-first риггинга для слотов
|
|
2
|
+
|
|
3
|
+
Документ (`rig.json`, формат v2) — источник истины. Агент работает через MCP, человек — через вьюер и редактор в браузере (`npm run editor`). Рантайм — PixiJS v8.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/rig-format.ts схема документа (zod) + семантическая валидация; v1: deform-треки, asset.frame (атлас)
|
|
7
|
+
src/rig-anim.ts чистая математика: easing, семплирование, Pose, аффинные матрицы костей, контуры слотов
|
|
8
|
+
src/rig-deform.ts 3×3 деформер картинки: именованные параметры → смещения точек, биквадратичный патч, якобиан
|
|
9
|
+
src/rig-constraints.ts ограничения без решателя: кто кем управляет, цепь IK, знак сгиба, досягаемость, кривая по точкам
|
|
10
|
+
src/rig-state.ts AnimationState: base-цикл, очередь с приоритетами, overlay-слои (моргание), crossfade
|
|
11
|
+
src/rig-runtime.ts RigPlayer для PixiJS v8: Sprite или MeshPlane на слот, setPose, createTextures (атлас)
|
|
12
|
+
src/rig-symbol.ts RigSymbol — адаптер под SymbolView из @energy8platform/game-engine
|
|
13
|
+
src/rig-presets.ts пресеты: idle_breathing, weight_shift, win_bounce, anticipation_shake, blink, mouth_talk, dim, symbol_idle, symbol_win
|
|
14
|
+
src/rig-tools.ts операции над документом, импорт слоёв (swap/bone), createSymbol, replaceAsset, layerPrompt
|
|
15
|
+
src/rig-check.ts автокритика без рендера: out_of_canvas, static_track, loop_seam, deform_flip, patch_offset, motion_pop, …
|
|
16
|
+
src/rig-history.ts снимки .rig-history/, undo, структурный diff
|
|
17
|
+
src/rig-atlas.ts упаковка PNG-слоёв в atlas.png с экструзией краёв
|
|
18
|
+
src/rig-templates.ts rig_save_as_preset: шаблон из готовой анимации (треки относительно rest, кости по role, множители)
|
|
19
|
+
src/preview/ renderPreview: headless Chromium → контакт-лист PNG (onion skin) для агента
|
|
20
|
+
src/rig-api.ts реестр инструментов — MCP, CLI и редактор регистрируют один список
|
|
21
|
+
src/mcp-server.ts MCP (stdio) поверх всего этого
|
|
22
|
+
src/editor/ редактор: сервер (HTTP/SSE), сцена, гизмо, таймлайн, панели
|
|
23
|
+
scripts/smoke.ts прогон всей петли без агента + проверки меша/атласа/символа
|
|
24
|
+
scripts/verify-mcp.ts интеграционный маршрут через настоящий stdio MCP
|
|
25
|
+
scripts/verify-engine.mjs RigSymbol внутри настоящей SymbolCell движка (из соседнего проекта игры)
|
|
26
|
+
scripts/rig-cli.ts те же MCP-инструменты из shell: `npm run rig -- <tool> '<json>'`
|
|
27
|
+
examples/hero/ тестовый персонаж из прямоугольников + layers.json
|
|
28
|
+
examples/cashier/ настоящий персонаж (cashier.png) и список его анимаций anim.md
|
|
29
|
+
ross/ настоящий Spine-проект (json + atlas + ktx2) — оракул и демо импорта
|
|
30
|
+
docs/superpowers/specs/ проектные решения этапа 2 (mesh, состояние, атлас, агентский UX)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Установка
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
npx playwright install chromium # один раз
|
|
38
|
+
npm run build:harness # бандл для headless-рендера (после правок runtime/harness — повторить)
|
|
39
|
+
npm run typecheck
|
|
40
|
+
npm test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Шаг 1 — smoke без агента
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run smoke
|
|
47
|
+
```
|
|
48
|
+
Пишет `examples/hero/rig.json`, `examples/hero/out/{rest,idle,win,anticipation,win_onion}.png`,
|
|
49
|
+
экспорт с атласом в `examples/hero/out/export/`, символ из одной картинки в `examples/symbol/`.
|
|
50
|
+
Проверяет: непустой WebGL-кадр, различие кадров, **меш при нулевой деформации = спрайт (0 px)**,
|
|
51
|
+
**рендер из атласа = рендер из файлов (0 px)** в том числе для деформированного торса, onion skin, `rig_check` без ошибок.
|
|
52
|
+
|
|
53
|
+
Что должно быть видно глазами:
|
|
54
|
+
- `rest.png` — персонаж собран, кости (оверлей) стоят в суставах, подписи `id (role)`.
|
|
55
|
+
- `idle.png` — грудь дышит (меш, шея и пояс закреплены, голова не тянется), голова покачивается, руки чуть ходят.
|
|
56
|
+
- `win.png` — два прыжка с затуханием, squash/stretch; `win_onion.png` — то же с призраком предыдущего кадра.
|
|
57
|
+
- `examples/symbol/out/win.png` — pop масштаба и затухающее желе одной картинки.
|
|
58
|
+
|
|
59
|
+
## Шаг 2 — петля с агентом
|
|
60
|
+
|
|
61
|
+
Подключи MCP в Claude Code (`.mcp.json` в корне проекта):
|
|
62
|
+
```json
|
|
63
|
+
{ "mcpServers": { "rig": { "command": "npx", "args": ["tsx", "src/mcp-server.ts"], "cwd": "<абсолютный путь к rig>" } } }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Инструменты:
|
|
67
|
+
|
|
68
|
+
| Группа | Инструменты |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Документ | `rig_create`, `rig_create_symbol` (риг из одной картинки), `rig_set_meta` (имя, холст, notes, референс-подложка `reference`; `null` убирает подложку), `rig_get`, `rig_get_attachment` (одно вложение целиком — массивы `vertices`/`weights` для сырой правки), `rig_rename` (переименование кости/слота/attachment/ассета/ограничения с переписыванием всех ссылок; `presets.json` и `preset.params` анимаций не переписываются — ответ называет, где остался старый id), `rig_validate`, `rig_check` |
|
|
71
|
+
| Ассеты | `rig_set_asset` (ассет из целой картинки или прямоугольника `frame` в ней — файлов-вырезок нет; переразметка кадра держит на месте исходный пиксель под центром картинки; меш при этом сохраняет старые uvs, инструмент называет его в ответе — перетрассируйте его заново (`rig_mesh_from_image`)), `rig_remove_asset` (отказ, пока ассет показан хоть одним attachment), `rig_place_asset` (слот + region + показ одним вызовом, `z = max + 1`) |
|
|
72
|
+
| Слои | `rig_import_layers` (`layers.json`: `swap`, `bone`), `rig_replace_asset`, `rig_layer_prompt` (промпт перегенерации по провенансу) |
|
|
73
|
+
| История | `rig_undo(steps)`, `rig_diff(steps)` — снимки в `.rig-history/` рядом с `rig.json` |
|
|
74
|
+
| Скелет | `rig_add_bone` (`bind` сразу переносит слоты на новую кость, не сдвигая картинку), `rig_update_bone`, `rig_remove_bone` (слоты уходят к родителю тоже без сдвига), `rig_set_slot` (смена `bone` сохраняет мировое положение: regions пересчитываются в новой кости, взвешенные меши не трогаются), `rig_remove_slot`, `rig_set_draw_order` (список слоёв сзади наперёд одним вызовом, `z = index`) |
|
|
75
|
+
| Поза и цель | `rig_get_pose` (обратная связь числами: мировые `x`,`y`,`angle`,`scaleX/Y` и локальные `x`,`y`,`rotation` по костям, показанное вложение и мировой bbox по слотам — без `animation` rest-поза, иначе поза в `t` со всеми применёнными ограничениями), `rig_set_goal` (кость в точку документа; под живым IK правка уходит на кость-цель ограничения, под transform/path — отказ), `rig_copy_tracks` (треки одних костей на другие пара за парой, сдвиг фазы, зеркало по x — знак меняют `x`, `rotation`, `shearX`, `shearY`, и лево-правым оно будет только при зеркальных системах родителей) |
|
|
76
|
+
| Анимация | `rig_apply_preset` (встроенные и шаблоны), `rig_set_keys` (`merge`), `rig_set_tracks`, `rig_adjust_track` (amplitude / offset / timeScale), `rig_remove_animation` |
|
|
77
|
+
| Шаблоны | `rig_save_as_preset` (анимация → шаблон в `presets.json`), `rig_list_presets` |
|
|
78
|
+
| Вывод | `rig_render_preview` (`onionSkin`, `region` для crop/zoom, сводка `rig_check` по анимации), `rig_export` (атлас) |
|
|
79
|
+
|
|
80
|
+
На риге с ключами `drawOrder` слот теперь можно добавить и удалить, не сломав валидацию: `rig_set_slot` дописывает новый
|
|
81
|
+
слот в конец каждого ключа, `rig_remove_slot` вычёркивает из них старый. `rig_mesh_from_image` трассирует и framed-ассеты
|
|
82
|
+
(кадр вырезается из листа в памяти); отказ остаётся только для `rotate`/`trim` экспортных атласов.
|
|
83
|
+
|
|
84
|
+
Каждая мутация отвечает списком «changed:» (структурные операции — ещё и сводкой рига); невалидный результат не
|
|
85
|
+
сохраняется, предыдущая версия уходит в историю. `rig_get verbose=true` показывает pivot/offset слотов и треки с диапазонами ключей.
|
|
86
|
+
|
|
87
|
+
Те же инструменты из shell (для скриптов и для агентов без MCP-конфига):
|
|
88
|
+
```bash
|
|
89
|
+
npm run rig -- list # все инструменты с параметрами
|
|
90
|
+
npm run rig -- rig_check '{"path":"examples/hero/rig.json"}'
|
|
91
|
+
npm run rig -- rig_render_preview '{"path":"…","animation":"idle","scale":0.4,"saveTo":"out/idle.png"}'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`npm run verify:mcp` проходит маршрут через настоящий stdio MCP: три задания README, deform-ключи с заведомым
|
|
95
|
+
переворотом меша (`rig_check` обязан его найти), diff/undo, onion skin, символ с экспортом в атлас, шаблон из ручной
|
|
96
|
+
анимации, `merge`/`verbose`/`region`. Журнал и превью — в `examples/hero/out/mcp/`.
|
|
97
|
+
|
|
98
|
+
Слепая оценка двух агентов на риге cashier (задания из `anim.md` и поиск внесённых дефектов) — [отчёт](docs/agent-eval-2026-09-07.md).
|
|
99
|
+
|
|
100
|
+
### Деформация картинки (mesh)
|
|
101
|
+
|
|
102
|
+
Слот, у которого есть `deform`-трек, рендерится как `MeshPlane` 9×9 поверх 3×3 контрольных точек.
|
|
103
|
+
Параметры безразмерные (доли размера картинки), все складываются:
|
|
104
|
+
|
|
105
|
+
| prop | что делает | закреплено |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `bulge` | средний ряд расходится по горизонтали (дыхание, пульс) | верх и низ |
|
|
108
|
+
| `bend` | средний ряд сдвигается вбок (мягкий наклон, C-изгиб) | верх и низ |
|
|
109
|
+
| `sway` | сдвиг ряда пропорционально расстоянию от pivot (волосы, наклон от опоры) | ряд pivot |
|
|
110
|
+
| `squash` | сжатие к pivot по вертикали с расширением по горизонтали, не задевая детей | pivot |
|
|
111
|
+
| `p<r><c>x`, `p<r><c>y` | прямое смещение точки в пикселях, ряды/колонки 0..2 сверху-слева | — |
|
|
112
|
+
|
|
113
|
+
`|bend|,|sway| ≤ 1`, `|bulge|,|squash| ≤ 0.9`. Переворот треугольников (в том числе из-за overshoot easing между ключами)
|
|
114
|
+
ловит `rig_check` как `deform_flip`. Обоснование и сравнение вариантов — в спеке этапа 2.
|
|
115
|
+
|
|
116
|
+
### Свечение и VFX-слои
|
|
117
|
+
|
|
118
|
+
`slot.blend`: `normal | add | multiply | screen` (в `layers.json` — поле `blend`). Слой свечения рисуется на чёрном и кладётся
|
|
119
|
+
с `add`: чёрное исчезает, никаких «наклеек» с краями кожи; alpha-трек управляет силой свечения. Так делаются светящиеся глаза и
|
|
120
|
+
подсветка пламени у символов. Превью рендерится на непрозрачном фоне внутри WebGL (как в игре), иначе аддитивный слой
|
|
121
|
+
над пустотой давал бы чёрные прямоугольники; призрак onion skin кладётся сверху.
|
|
122
|
+
|
|
123
|
+
### Шаблоны из ручных анимаций
|
|
124
|
+
|
|
125
|
+
`rig_save_as_preset(animation, name, groups?)` сохраняет анимацию как шаблон: треки костей относительно rest и по `role`,
|
|
126
|
+
масштаб как отношение, deform как есть. Параметры шаблона: `amplitude` (все относительные значения), `speed` (время) и
|
|
127
|
+
именованные группы треков (`{"turn": ["bone.root.rotation"]}`) — каждая становится своим множителем. Применение на другом
|
|
128
|
+
риге: `rig_apply_preset(preset: "<имя>", params)` — кости ищутся по роли, затем по id; чего нет в целевом риге, пропускается.
|
|
129
|
+
Библиотека — `presets.json` рядом с рига или любой путь в `library`.
|
|
130
|
+
|
|
131
|
+
### Состояния в игре
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const player = new RigPlayer(doc, await createTextures(doc, src => loadTexture(base + src)), { mix: 0.15 });
|
|
135
|
+
player.state.setBase("idle"); // цикл
|
|
136
|
+
player.state.setOverlay("eyes", "blink"); // независимый цикл поверх
|
|
137
|
+
await player.state.play("win", { priority: 1 }); // прерывает idle, после завершения — обратно; промис всегда резолвится
|
|
138
|
+
player.state.setBase("dim"); // незацикленный base держит последнюю позу
|
|
139
|
+
```
|
|
140
|
+
`RigSymbol` (`src/rig-symbol.ts`) заворачивает это в `playIdle / playWin / showStatic / resize / setDim` — интерфейс
|
|
141
|
+
`SymbolView` из `@energy8platform/game-engine` (≥ 0.43: `resize` принимает и `number`, и `{width, height}`), без
|
|
142
|
+
зависимости от движка.
|
|
143
|
+
|
|
144
|
+
### В игре
|
|
145
|
+
|
|
146
|
+
Пакет публикуемый: `npm run build` собирает `dist/lib/` (esbuild по entry, `.d.ts` от tsc) и клиент редактора.
|
|
147
|
+
Exports: `@energy8platform/golem/runtime` (браузер: `RigPlayer`, `RigSymbol`, `createTextures`, `parse`; `pixi.js` —
|
|
148
|
+
peer, в игре должна быть одна копия), `/tools` (реестр инструментов), `/editor` (`startEditorServer`). Bin:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
golem editor --root public/assets/rigs # редактор над ригами игры; в шаблоне create-slot — `npm run rigs`
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`--root` — папка ригов (список `open`, загрузки, `/rig/*`), сам `editor.html` и `dist/` отдаются из пакета.
|
|
155
|
+
Движок подключает риги через `@energy8platform/game-engine/golem` (`loadRigs`, re-export'ы, compile-time контракт
|
|
156
|
+
`RigSymbol` = `SymbolView`), а `defineGameConfig` вырезает `.rig-history/` из `vite build` — риги лежат прямо в
|
|
157
|
+
`public/`, vite dev перезагружает игру на каждую запись редактора. Спека:
|
|
158
|
+
`docs/superpowers/specs/2026-09-15-engine-integration-design.md`.
|
|
159
|
+
|
|
160
|
+
### Настоящие данные
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npm run verify:engine # RigSymbol внутри SymbolCell из ../kitsune-wrath (KITSUNE_DIR=… для другого пути)
|
|
164
|
+
```
|
|
165
|
+
Символ из одной картинки, как это делает агент:
|
|
166
|
+
```bash
|
|
167
|
+
S=examples/symbol/rig.json
|
|
168
|
+
npm run rig -- rig_create_symbol '{"path":"'$S'","image":"../hero/layers/head.png","name":"gem","margin":0.2}'
|
|
169
|
+
for p in symbol_idle symbol_win dim; do npm run rig -- rig_apply_preset '{"path":"'$S'","preset":"'$p'"}'; done
|
|
170
|
+
npm run rig -- rig_render_preview '{"path":"'$S'","animation":"win","fps":10,"onionSkin":true,"scale":0.5,"saveTo":"examples/symbol/out/win.png"}'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Шаг 3 — слои (риск №1 закрыт)
|
|
174
|
+
|
|
175
|
+
Спайк сегментации на `cashier.png` (артефакты удалены при чистке репозитория): вырезание из плоской картинки + bleed 6 px даёт чистый
|
|
176
|
+
rest и микро-движение; большое движение (стакан ко рту) требует скрытых частей — их выгоднее **генерировать сразу по
|
|
177
|
+
частям**. Маршрут для агента: `rig_layer_prompt(id)` → генерация своим инструментом на `#00FF00` → `rig_replace_asset(id, file)`.
|
|
178
|
+
Патчи выражений (веки, рот) — `swap: true` в `layers.json`; несколько состояний одного места (`mouth_open_small`, `mouth_sip`)
|
|
179
|
+
объединяются полем `slot: "mouth"` в один скрытый слот. Анимируются пресетами `blink` / `mouth_talk` и в игре идут как overlay
|
|
180
|
+
(во вьюере — поле `overlays`).
|
|
181
|
+
|
|
182
|
+
## Посмотреть анимацию живьём / GIF / видео
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm run viewer # → http://localhost:8000/viewer.html
|
|
186
|
+
npm run export -- examples/hero/rig.json idle 24 # mp4 + gif через ffmpeg
|
|
187
|
+
```
|
|
188
|
+
Агенту по-прежнему отдаём контакт-лист (`rig_render_preview`) — видео вижн-модели не читают.
|
|
189
|
+
|
|
190
|
+
## Редактор
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npm run editor # → http://127.0.0.1:8765/editor.html?rig=examples/hero/rig.json
|
|
194
|
+
npm run editor -- --watch # пересборка клиента при правках src/editor
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Редактор и агент делят один `rig.json` и одну историю: каждая правка мышью — вызов того же инструмента из `src/rig-api.ts`
|
|
198
|
+
(`rig_update_bone`, `rig_set_keys merge`, `rig_apply_preset`, …), сервер применяет его к актуальному файлу и делает снимок в
|
|
199
|
+
`.rig-history/`. Правки агента через MCP появляются в редакторе сразу (сервер следит за файлом), `rig_undo` / `rig_redo`
|
|
200
|
+
общие. Режимы как в Spine: **Setup** правит rest (drag сустава, поворот за конец кости, масштаб, рамка картинки), **Animate**
|
|
201
|
+
пишет ключи на времени плейхеда (автоключ). Таймлайн — dopesheet: выбор, перенос и удаление ключей, длительность за правый
|
|
202
|
+
край. Панель свойств: поля с ромбами ключей, easing — четырьмя числами и, у выделенного ключа, ещё и перетаскиваемой
|
|
203
|
+
кривой в единичном квадрате (рисуется той же `ease()`, которой считает рантайм, так что картинка не может разойтись с
|
|
204
|
+
воспроизведением; первое перетаскивание ручки переводит именованный ease в `bezier`, `step` рисуется ступенькой),
|
|
205
|
+
слайдеры параметров пресета и amplitude/offset/timeScale трека с предпросмотром до отпускания.
|
|
206
|
+
Спека: `docs/superpowers/specs/2026-09-08-editor-stage-a-design.md`.
|
|
207
|
+
|
|
208
|
+
### Ручная сборка
|
|
209
|
+
|
|
210
|
+
Риг собирается мышью с нуля, без CLI: `new` и `open` в верхней панели создают риг и переключаются на существующий.
|
|
211
|
+
- `+ png` грузит картинки в папку рига; у каждой в списке файлов — кнопки `atlas`, `ref`, `whole`.
|
|
212
|
+
- с пустым выделением панель свойств показывает форму документа: имя, холст, notes и поля подложки `reference` (`src`, `opacity`, `x`, `y`, `scale`).
|
|
213
|
+
- режим `atlas` (клавиша `A`): `detect` с ползунками `gap`/`min area` предлагает прямоугольники, `snap` поджимает их по альфе, нажатие на пустом месте рисует новый прямоугольник, 8 ручек меняют размер, `Delete` убирает выделенные, `merge` объединяет несколько в один; имя прямоугольника (Enter) — это `rig_set_asset` с `frame`; предложенные прямоугольники живут только в памяти страницы и не переживают перезагрузку.
|
|
214
|
+
- корзина ассетов — миниатюры; drag на холст или на строку кости кладёт ассет, двойной клик — в центр холста; уже размещённые притушены.
|
|
215
|
+
- инструмент `B`: нажатие ставит сустав кости, перетаскивание — направление и длину; с выделенной частью она сама перепривязывается на новую кость.
|
|
216
|
+
- строка слота, перетащенная на кость, и выбор `bone` в форме слота — оба сохраняют картинку на месте.
|
|
217
|
+
- список **layers**: слои идут передними сверху; перетаскивание в Setup — `rig_set_draw_order`, в Animate — ступенчатый ключ `drawOrder` на плейхеде; поле `z` правится только в Setup.
|
|
218
|
+
- по две ручки масштаба у кости и у картинки, Shift — равномерно (оба масштаба по отношению вдоль тянущейся оси), курсор каждой — по направлению её оси.
|
|
219
|
+
- галочка `lock images`: клик по картинке выбирает её кость.
|
|
220
|
+
- поле `id` у кости, слота, attachment, ассета и ограничения — редактируемое; Enter — это `rig_rename`.
|
|
221
|
+
|
|
222
|
+
Спека: `docs/superpowers/specs/2026-09-11-editor-stage-e-authoring-design.md`.
|
|
223
|
+
|
|
224
|
+
Каждый коммит редактора несёт версию документа, с которым он сделан (`mtimeMs:size`); если файл на диске успел
|
|
225
|
+
измениться — правкой агента через MCP, другим процессом, — сервер отвечает 409 и актуальным документом вместо того,
|
|
226
|
+
чтобы выполнить инструмент, а редактор подставляет пришедший документ и предупреждает статусом, не считая это своей
|
|
227
|
+
ошибкой. Без версии в запросе (MCP, CLI, curl) поведение прежнее — агент не затронут. Запись `rig.json` атомарна
|
|
228
|
+
(временный файл рядом, затем rename): раньше `writeFile` мог отдать параллельному читателю усечённый файл.
|
|
229
|
+
|
|
230
|
+
Внутри одного процесса операции над одним путём рига выполняются последовательно; HTTP-проверка версии входит
|
|
231
|
+
в ту же очередь. **Отдельные процессы MCP, CLI и редактора общей блокировки не имеют**: если они одновременно
|
|
232
|
+
редактируют один файл, правки всё ещё могут потеряться. Пока назначайте одному ригу одного активного писателя.
|
|
233
|
+
Результаты проверки и оставшиеся ограничения — в [аудите от 11 сентября 2026](docs/verification-2026-09-11.md).
|
|
234
|
+
Спека: `docs/superpowers/specs/2026-09-09-editor-stage-d-design.md`.
|
|
235
|
+
|
|
236
|
+
### Меши
|
|
237
|
+
|
|
238
|
+
Агент: `rig_mesh_from_image {id, spacing, tolerance}` превращает region-attachment в меш по alpha картинки (контур + сетка +
|
|
239
|
+
Делоне), `rig_auto_weights {id, bones?, radius?, maxInfluences}` раздаёт веса по расстоянию до костей — вершина берёт
|
|
240
|
+
`maxInfluences` ближайших костей с весом `1/(d/radius + 1)²` (`radius` — масштаб спада в мировых пикселях; без параметра
|
|
241
|
+
— `1`, то есть результат побайтово прежний; больше — влияние ровнее и захватывает дальние кости, меньше — жёстче и
|
|
242
|
+
локальнее; жёсткого обрыва нет ни при каком радиусе — именно обрыв давал шов на середине конечности), `rig_set_mesh` /
|
|
243
|
+
`rig_set_weights` пишут массивы целиком, `rig_set_keys` принимает массивы (deform `vertices`, drawOrder, color), `rig_check`
|
|
244
|
+
ловит `mesh_fold` (ошибка, если свёрнуто ≥ 5 % площади меша, иначе предупреждение) и `weights_sum`. `npm run verify:mesh`
|
|
245
|
+
собирает cashier из слоёв этими инструментами с параметрами по умолчанию и кладёт контакт-лист в
|
|
246
|
+
`examples/cashier/out/auto-maxwin.png` рядом с авторским (`maxwin-reference.png`). Фикстуры кассира (`examples/cashier/rig.json`
|
|
247
|
+
и `layers/`) локальные, в репозитории их нет: без них скрипт выходит с кодом 2 и называет недостающий файл
|
|
248
|
+
(`npm run cashier:build` пересобирает их, если исходники на месте). Скрипт держит базовую линию `KNOWN_FOLDS`: на
|
|
249
|
+
полностью сложенном локте в `maxwin` веса по расстоянию складывают меш (`upper_l`, `forearm_l`) — известное ограничение,
|
|
250
|
+
разобрано в разделе 9 спеки, лечится кистью весов, поэтому выход 0; любая НОВАЯ ошибка — код 1, а если известная
|
|
251
|
+
перестала падать, скрипт скажет «improved — update KNOWN_FOLDS».
|
|
252
|
+
|
|
253
|
+
Редактор: у меша видны вершины и контур; drag вершины в Setup пишет `rig_set_mesh`, в Animate — ключ `deform.<id>.vertices`
|
|
254
|
+
(такой трек кладётся на таймлайн как обычный: двойной клик ставит ключ с формой, которая уже видна в этот момент, ключи
|
|
255
|
+
двигаются и удаляются); `W` — режим весов (только в Setup): тепловая карта выбранной кости, кисть (`[` `]` — радиус,
|
|
256
|
+
Shift — убавить), `auto weights`, `to mesh` у региона.
|
|
257
|
+
|
|
258
|
+
Спека: `docs/superpowers/specs/2026-09-08-editor-stage-b-meshes-design.md`.
|
|
259
|
+
|
|
260
|
+
### Пути
|
|
261
|
+
|
|
262
|
+
Агент: `rig_set_path_attachment {path, id, slot, points?, vertices?, weights?, closed?}` создаёт или заменяет PATH
|
|
263
|
+
attachment — ровно одна из двух форм за вызов (минимум 2 опорные точки, 3 — для замкнутого пути). `points` — опорные
|
|
264
|
+
точки в координатах документа: кривая строится заново по Catmull-Rom, поэтому у скинированного пути это рвёт веса и
|
|
265
|
+
авторские касательные — форма для рисования пути с нуля. `vertices` / `weights` — сырые контрольные точки
|
|
266
|
+
`[cPrev, p, cNext]` на узел (в пространстве кости слота, либо скинированные — список влияний на точку, как у
|
|
267
|
+
`rig_set_weights`) — единственная форма, которая сохраняет веса и касательные, поэтому редактор коммитит правку
|
|
268
|
+
существующего пути только ею. `rig_path_anchor {path, id, op: insert|remove, index, t?}` добавляет и убирает один узел:
|
|
269
|
+
`insert` делит сегмент после `index` в параметре `t` (0..1, по умолчанию 0.5) де Кастельжо — кривая не меняет форму НИ В
|
|
270
|
+
ОДНОЙ позе, потому что положение каждой новой контрольной точки в пространстве кости-влияния — это смесь исходных точек
|
|
271
|
+
в ТОЙ ЖЕ кости с теми же коэффициентами де Кастельжо, а это тождество по матрицам костей; `remove` убирает тройку узла
|
|
272
|
+
(форма меняется — это неизбежно). Обе операции пересчитывают `lengths` и отбрасывают deform-ключи, которым не подошла
|
|
273
|
+
новая длина.
|
|
274
|
+
|
|
275
|
+
Редактор правит взвешенные пути наравне с мешами — запрет из C снят. Узел тащится вместе с двумя касательными, ручки
|
|
276
|
+
касательных показываются только у выделенного узла (иначе на пятиузловом `tail_path` их стало бы вдвое больше), клик
|
|
277
|
+
рядом с кривой выбирает путь, `Ctrl`-клик вставляет узел в ближайшей точке параметра, `Delete` на узле — удаляет. Коммит
|
|
278
|
+
идёт сырой формой `rig_set_path_attachment`, поэтому взвешенный путь остаётся взвешенным, а нетронутые касательные —
|
|
279
|
+
авторскими: правка — это дельта в пространстве каждой кости-влияния (та же математика, которой `deformOffsetsFor`
|
|
280
|
+
считает deform-ключ в Animate), а не переигрывание всех влияний из одной мировой точки — это схлопывает разброс между
|
|
281
|
+
образами вершины в разных костях, а этот разброс и несёт анимацию (крошечный, 0.01 px, сдвиг узла в Setup у наивного
|
|
282
|
+
пересчёта даёт в позе смещение до 489 px на меше `n_fur` и до 24 px на самом `tail_path`). В Animate путь пишет
|
|
283
|
+
deform-ключи так же, как меш (`tail_path` в `ross` приходит из Spine с 17 такими треками).
|
|
284
|
+
|
|
285
|
+
Спека: `docs/superpowers/specs/2026-09-09-editor-stage-d-design.md`.
|
|
286
|
+
|
|
287
|
+
### Ограничения
|
|
288
|
+
|
|
289
|
+
Агент: `rig_add_ik_chain {bone}` оснащает конечность одной командой — ставит кость-цель (`role: ik_target`, ребёнком
|
|
290
|
+
корня, а не внутри цепи, иначе решатель тянул бы сам себя) в текущий конец цепи и IK `<bone>_ik` со знаком сгиба из
|
|
291
|
+
текущей позы, так что от появления ограничения риг не шелохнётся; если двухкостная цепь дотянулась бы до самого
|
|
292
|
+
корня, берётся одна кость, и инструмент об этом говорит. На свежеимпортированном риге это ДВА шага: кости из
|
|
293
|
+
`rig_import_layers` нулевой длины и сидят в своих суставах, а цель встаёт в конец цепи — поэтому сначала
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
rig_update_bone {id: "leg_l", length: 60} # столько, сколько кость реально занимает на картинке
|
|
297
|
+
rig_add_ik_chain {bone: "leg_l"} # → кость-цель leg_l_ik и IK; дальше позу задаёт она
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
(без длины одно-костный IK целился бы в собственное начало кости, и инструмент честно отказывается). После импорта
|
|
301
|
+
Spine длины уже есть, и хватает второй строки. `rig_set_ik` / `rig_set_transform` / `rig_set_path` создают
|
|
302
|
+
и правят ограничения (поля сливаются: `{id, mix}` правит только mix), `rig_remove_constraint` удаляет вместе с
|
|
303
|
+
треками, `rig_set_path_attachment` строит и правит кривую пути (`rig_path_anchor` добавляет и убирает узел — раздел
|
|
304
|
+
«Пути» выше). `rig_check` добавляет
|
|
305
|
+
`constraint_dead` (все mix — 0 и ни один ключ их не поднимает), `constraint_cycle` (цель сидит на кости, которую это
|
|
306
|
+
же ограничение пишет), `ik_unreachable` (цель за пределами досягаемости двухкостной цепи — она висит прямой; одинокая
|
|
307
|
+
кость целится, а не тянется, и не считается) и `path_missing` (слот-цель никогда не показывает путь).
|
|
308
|
+
|
|
309
|
+
Редактор: выделенное ограничение подсвечивает свои кости и ведёт пунктир от конца цепи к цели — к ромбу на
|
|
310
|
+
кости-цели, у path — к ближайшей опорной точке кривой, которая тоже обводится. Кость, которой прямо сейчас управляет
|
|
311
|
+
IK, тащить напрямую нельзя: жест перенаправляется на цель ограничения (`Alt` отключает перенаправление, ручка
|
|
312
|
+
масштаба по-прежнему правит саму кость), а кость под transform или path отклоняется со статусом. «Прямо сейчас» —
|
|
313
|
+
по значениям на плейхеде: ограничение, у которого на этом кадре все mix равны 0, решатель пропускает, и кость
|
|
314
|
+
правится как обычная. Кривая пути рисуется у каждого показанного пути, опорные точки видны и тащатся у выделенного —
|
|
315
|
+
как вершины меша, включая взвешенные, импортированные из Spine (раздел «Пути» выше). Числовые поля ограничения
|
|
316
|
+
(`mix`, `softness`, у path ещё
|
|
317
|
+
`position` и `spacing`) в Animate пишут обычные ключи на времени плейхеда. В дереве: «add ik chain» на кости,
|
|
318
|
+
«add ik / transform / path» на заголовке группы (занятый id отклоняется: `rig_set_*` считает известный id патчем и
|
|
319
|
+
переписал бы чужое ограничение), `Delete` удаляет ограничение вместе с его треками (о них спросит).
|
|
320
|
+
|
|
321
|
+
Спека: `docs/superpowers/specs/2026-09-08-editor-stage-c-constraints-design.md`.
|
|
322
|
+
|
|
323
|
+
## Шаг 4 — риг класса Spine: формат v2 и импорт настоящего Spine-проекта (ross)
|
|
324
|
+
|
|
325
|
+
Инструмент больше не «двигает картинки»: документ v2 — это меши с весами костей, регионы, пути, shear и режимы наследования,
|
|
326
|
+
ограничения IK (1–2 кости, softness/stretch/compress), transform (мировой и локальный, relative) и path (4 режима шага,
|
|
327
|
+
3 режима поворота, замкнутые и открытые пути), треки деформации вершин, порядка отрисовки, цвета слота и миксов ограничений.
|
|
328
|
+
Это своя реализация по документированной семантике Spine, не порт рантайма; spine-core используется только как оракул в тестах.
|
|
329
|
+
|
|
330
|
+
Проверка на `ross/` (Spine 4.0: 163 кости, 116 слотов, 110 мешей, из них 49 взвешенных, 2 IK, 17 transform, 7 path,
|
|
331
|
+
17 анимаций): в каждой анимации на 9 моментах времени все кости совпадают с spine-core лучше 0.01 px, вершины мешей и путей
|
|
332
|
+
с deform-ключами лучше 0.05 px, attachments, порядок отрисовки и цвета совпадают точно (`tests/spine-import.test.ts`).
|
|
333
|
+
Кадр (163 кости + 110 мешей + все ограничения) считается ~0.9 мс.
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
npm run rig -- rig_import_spine '{"path":"ross/rig.json","json":"ross.json"}' # атлас и страница находятся сами; .ktx2 → PNG рядом с rig.json
|
|
337
|
+
npm run rig -- rig_render_preview '{"path":"ross/rig.json","animation":"jump","fps":4,"scale":0.25,"saveTo":"ross/out/jump.png"}'
|
|
338
|
+
npm run render:ross # листы simple / jump / sitting в ross/out/
|
|
339
|
+
npm run render:spine -- devil # любой другой экспорт Spine: json + атлас из каталога, все анимации
|
|
340
|
+
npm run spine:sweep # невязки к spine-core по каждой анимации
|
|
341
|
+
npm run ktx2 -- ross/ross.ktx2 ross/ross.png # транскодер из three (devDependency), нужен только для KTX2
|
|
342
|
+
```
|
|
343
|
+
Импорт делает холст по объединению всех кадров всех анимаций (а не по setup-позе, иначе прыжок вылетает за край),
|
|
344
|
+
выбрасывает треки, целиком равные setup-значению (Spine ключует всё подряд, они ничего не меняют), и переводит y-вверх
|
|
345
|
+
в y-вниз сопряжением отражением. `rig_check` на таких ригах шумит меньше: постоянный трек с не-setup значением — это
|
|
346
|
+
удержание позы (info), рывок ровно на stepped-ключе — авторский (info).
|
|
347
|
+
|
|
348
|
+
Что пришлось воспроизвести, чтобы совпасть с оракулом: до первого ключа трека действует setup-значение (импортёр вставляет
|
|
349
|
+
явный ключ); безье-кривые Spine — это 10 линейных отрезков (9 внутренних точек), не точная кривая; deform у взвешенных
|
|
350
|
+
вершин — смещение на каждое влияние; у path-ограничения при `constantSpeed` длина меряется по уже деформированной кривой
|
|
351
|
+
(4 хорды на кривую для суммы, 10 внутри), замкнутый путь заворачивает позицию, открытый продолжает касательные за концами;
|
|
352
|
+
transform- и path-ограничения правят мировую матрицу и раскладываются обратно в локальные значения (иначе при неравномерном
|
|
353
|
+
масштабе родителя поворот «уезжает»); двухкостный IK при отрицательном масштабе добавляет 180° и зеркалит сгиб.
|
|
354
|
+
|
|
355
|
+
Не поддержано или отличается: linked mesh, clipping/bounding box/point attachments (пропускаются с предупреждением), скины
|
|
356
|
+
сливаются в один с префиксом (переключения нет), physics 4.2 и sequence 4.1 нет, смешивание анимаций — наш `AnimationState`
|
|
357
|
+
(кроссфейд поз), а не MixBlend-треки; в локальном режиме transform-ограничения масштаб смешивается линейно (spine-core делит
|
|
358
|
+
на масштаб кости); IK у костей с нестандартным `inherit` приближённый.
|
|
359
|
+
|
|
360
|
+
## Что дальше по коду
|
|
361
|
+
- Редактор: кисть весов с учётом направления сгиба; режимы шага пути в UI сверх трёх имеющихся; кривая ease сразу для
|
|
362
|
+
нескольких выделенных ключей и её правка прямо на таймлайне (сейчас — только в панели свойств одного ключа).
|
|
363
|
+
- Ручная сборка — вне объёма этапа (спека E §7): мультивыбор частей, перетаскивание подложки мышью, переименование
|
|
364
|
+
внутри `presets.json`, ключи `drawOrder` смещениями в духе Spine.
|
|
365
|
+
- Агентский слой поверх v2: агент читает позу числами (`rig_get_pose`) и ставит кость в точку документа
|
|
366
|
+
(`rig_set_goal`, то же правило разрешения цели, что у драга в редакторе), а перенос треков между костями со
|
|
367
|
+
сдвигом фазы и зеркалом (`rig_copy_tracks`) делает вторую ногу цикла из первой. Пресетов движения это **не
|
|
368
|
+
добавляет** — библиотеки походок и параметризованных циклов по-прежнему нет; IK-пресеты (ходьба и прочие
|
|
369
|
+
циклы; `rig_add_ik_chain` — это оснастка, а не движение), пресеты на мешах и экспорт в Spine остаются впереди.
|
|
370
|
+
- Многопользовательский режим: 409 защищает от затирания правки версией, которую клиент уже не держит, но не сливает
|
|
371
|
+
две одновременные правки — совместного редактирования пока нет.
|
|
372
|
+
- Два долга версии записи, которых 409 не закрывает (спека D §7): версию успешного ответа `/api/tool/*` сервер берёт
|
|
373
|
+
`stat`-ом ПОСЛЕ того, как инструмент записал файл, поэтому чужая запись в это субмиллисекундное окно отдаст клиенту
|
|
374
|
+
версию чужих байт — закрывается, только если инструмент сам вернёт версию тех байт, которые записал он сам; и потеря
|
|
375
|
+
обновления при двух одновременных `mutate`, которые оба читают файл до того, как первый из них успел записать — 409
|
|
376
|
+
этого не видит, лечится очередью коммитов на клиенте.
|
package/bin/golem.js
ADDED