@volter/editor-blender 0.1.11 → 0.1.12
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/contributions/blender-icon-trace.mjs +1 -1
- package/contributions/blender-icons.source.mjs +5 -5
- package/contributions/blender-node-editor.document.tsx +14 -14
- package/contributions/blender-node-panels.source.mjs +1 -1
- package/contributions/blender-outliner-model.ts +2 -2
- package/contributions/blender-properties-view.tsx +1 -1
- package/contributions/blender-runtime.document.tsx +1 -1
- package/contributions/blender-timeline.document.tsx +7 -7
- package/contributions/blender-uv-editor.document.tsx +4 -4
- package/contributions/blender-version.status.tsx +6 -6
- package/contributions/blender.command.ts +2 -2
- package/contributions/blender.status.tsx +8 -8
- package/contributions/models.finder.ts +2 -2
- package/contributions/shading.layout.ts +3 -3
- package/contributions/uv-editing.layout.ts +1 -1
- package/dist-node/serving.mjs +4 -4
- package/host/blender-runtime-host.ts +15 -15
- package/node/blender-mcp.ts +5 -5
- package/package.json +7 -7
- package/serving/blender-routes.ts +4 -4
- package/serving/blender-wasm-artifact.ts +3 -3
- package/serving/index.ts +1 -1
- package/src/layouts.tsx +1 -1
- package/src/looks.ts +2 -2
- package/src/node-view-state.ts +2 -2
- package/src/timeline-view-state.ts +2 -2
- package/src/uv-view-state.ts +2 -2
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* ## Licence
|
|
20
20
|
*
|
|
21
21
|
* Blender's icon sources are GPL-2.0-or-later, like the rest of the Blender
|
|
22
|
-
* tree, and a trace of one is a derivative of it. `@
|
|
22
|
+
* tree, and a trace of one is a derivative of it. `@volter/blender` therefore
|
|
23
23
|
* carries BOTH licences — its SPDX expression is
|
|
24
24
|
* `AGPL-3.0-only AND GPL-3.0-or-later` (`packages/blender/LICENSE`): our code
|
|
25
25
|
* AGPL-3.0-only, Blender's traced artwork conveyed under GPL-3.0-or-later on
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* sources and are GPL-2.0-or-later artwork (see "GPL PATHS, DELIBERATELY"
|
|
5
5
|
* below, `blender-icon-trace.mjs`, and the provenance file
|
|
6
6
|
* `blender.icons.traced.json`); the other 149 are drawn HERE in Blender's
|
|
7
|
-
* IDIOM. That mixture is why `@
|
|
7
|
+
* IDIOM. That mixture is why `@volter/blender` is
|
|
8
8
|
* `AGPL-3.0-only AND GPL-3.0-or-later` (`packages/blender/LICENSE`).
|
|
9
9
|
* The idiom is what a screenshot conveys: a 16-unit grid, monochrome filled
|
|
10
10
|
* silhouettes, ~1.3-unit strokes rendered as filled capsules with round ends,
|
|
@@ -2464,7 +2464,7 @@ g(
|
|
|
2464
2464
|
* and the outliner tints a row's type glyph the same way. Each name below is
|
|
2465
2465
|
* a category the palette inks (`EditorTheme.color.category`, where every
|
|
2466
2466
|
* value's source coordinates are recorded); a glyph paints
|
|
2467
|
-
* `var(--
|
|
2467
|
+
* `var(--volter-category-<tone>, currentColor)`, so under a palette that names
|
|
2468
2468
|
* no category group it paints exactly as it did monochrome.
|
|
2469
2469
|
*
|
|
2470
2470
|
* A NAME, NOT A SITE. A tone here tints EVERY site that asks for this icon
|
|
@@ -2507,7 +2507,7 @@ g(
|
|
|
2507
2507
|
* glyph — and an explicit site tone wins by contract. A tone there would
|
|
2508
2508
|
* never paint.
|
|
2509
2509
|
*/
|
|
2510
|
-
/** The union `IconCategoryTone` declares (`@
|
|
2510
|
+
/** The union `IconCategoryTone` declares (`@volter/editor-sdk/looks`), and the
|
|
2511
2511
|
* reason `blender.style.ts` may narrow this JSON with an assertion: a tone
|
|
2512
2512
|
* outside it never reaches the artifact. */
|
|
2513
2513
|
const TONES = ['object', 'modifier', 'material', 'tool', 'operator', 'data', 'scene', 'collection', 'select'];
|
|
@@ -2752,7 +2752,7 @@ g(
|
|
|
2752
2752
|
dataVertex(10, 20),
|
|
2753
2753
|
);
|
|
2754
2754
|
|
|
2755
|
-
// The Outliner's DATABLOCK row (`@
|
|
2755
|
+
// The Outliner's DATABLOCK row (`@volter/blender`'s datablock hierarchy) —
|
|
2756
2756
|
// the same MESH_DATA mark the Properties rail's Data tab draws, under its own
|
|
2757
2757
|
// name because the two sites composite differently: `outliner.png`'s data
|
|
2758
2758
|
// glyph is #07b189 over the row's #272727 and `properties-object.png`'s tab is
|
|
@@ -2783,7 +2783,7 @@ g('outliner-data', glyphs['properties-data'].path);
|
|
|
2783
2783
|
* justify one shape.
|
|
2784
2784
|
*
|
|
2785
2785
|
* GPL PATHS, DELIBERATELY — AND THE PACKAGE'S SPDX SAYS SO. Blender's icon
|
|
2786
|
-
* sources are GPL-2.0-or-later, and `@
|
|
2786
|
+
* sources are GPL-2.0-or-later, and `@volter/blender` is
|
|
2787
2787
|
* `AGPL-3.0-only AND GPL-3.0-or-later` (`packages/blender/LICENSE`, whose
|
|
2788
2788
|
* notice block names this set, its provenance file and the §13 combination):
|
|
2789
2789
|
* our code is AGPL, these 197 paths are Blender's artwork conveyed as GPL-3.0,
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*
|
|
32
32
|
* WHY IT IS THE DRAWER AND NOT A SECOND DOCUMENT. Blender's Shading workspace
|
|
33
33
|
* puts the Shader Editor in the AREA BELOW the 3D viewport, and this host's
|
|
34
|
-
* bottom group (`
|
|
34
|
+
* bottom group (`volter:bottom-center`) is that area: the Model workspace hides
|
|
35
35
|
* it for exactly this reason, recorded in `workspace-regions.ts` — "Blender's
|
|
36
36
|
* modeling workspace has no timeline strip". The Shading workspace shows it
|
|
37
37
|
* and puts this in it.
|
|
@@ -109,8 +109,8 @@ export const title = 'Shader Editor';
|
|
|
109
109
|
|
|
110
110
|
// THE VIEW'S PRODUCT DOOR (WORK.md §The core is Code-OSS U8, ruling 1). This
|
|
111
111
|
// view's verbs are published ONCE, here, where the view itself is contributed:
|
|
112
|
-
// under the Code-OSS frame each becomes a `
|
|
113
|
-
// command the bridge dispatches into the view, and standalone `
|
|
112
|
+
// under the Code-OSS frame each becomes a `volter.blender-node-view.<verb>`
|
|
113
|
+
// command the bridge dispatches into the view, and standalone `volter-model-editor edit` —
|
|
114
114
|
// which has no command service — reaches the SAME table through the session's
|
|
115
115
|
// `blender-node-view` verb. One table, two doors, which is why the remaining
|
|
116
116
|
// read-only editors (UV Editing, Animation, Texture Paint) add no session verb
|
|
@@ -1171,9 +1171,9 @@ function NodeEditorHeader({
|
|
|
1171
1171
|
style={{
|
|
1172
1172
|
display: 'flex',
|
|
1173
1173
|
alignItems: 'center',
|
|
1174
|
-
gap: 'var(--
|
|
1174
|
+
gap: 'var(--volter-space-2)',
|
|
1175
1175
|
height: HEADER_HEIGHT,
|
|
1176
|
-
padding: '0 var(--
|
|
1176
|
+
padding: '0 var(--volter-space-2)',
|
|
1177
1177
|
background: NODE_THEME.background,
|
|
1178
1178
|
color: NODE_THEME.text,
|
|
1179
1179
|
font: `${UI_TEXT_POINTS}px inherit`,
|
|
@@ -1189,7 +1189,7 @@ function NodeEditorHeader({
|
|
|
1189
1189
|
<span style={{ opacity: 0.6 }}>{Math.round(zoom * 100)}%</span>
|
|
1190
1190
|
<button
|
|
1191
1191
|
type="button"
|
|
1192
|
-
className="
|
|
1192
|
+
className="volter-btn"
|
|
1193
1193
|
data-variant="ghost"
|
|
1194
1194
|
data-size="sm"
|
|
1195
1195
|
onClick={onViewAll}
|
|
@@ -1199,7 +1199,7 @@ function NodeEditorHeader({
|
|
|
1199
1199
|
</button>
|
|
1200
1200
|
<button
|
|
1201
1201
|
type="button"
|
|
1202
|
-
className="
|
|
1202
|
+
className="volter-btn"
|
|
1203
1203
|
data-variant="ghost"
|
|
1204
1204
|
data-size="sm"
|
|
1205
1205
|
data-testid="node-editor-use-nodes"
|
|
@@ -1258,7 +1258,7 @@ function StatusLine({
|
|
|
1258
1258
|
height: STATUS_HEIGHT,
|
|
1259
1259
|
display: 'flex',
|
|
1260
1260
|
alignItems: 'center',
|
|
1261
|
-
padding: '0 var(--
|
|
1261
|
+
padding: '0 var(--volter-space-2)',
|
|
1262
1262
|
background: NODE_THEME.background,
|
|
1263
1263
|
color: error || refusal ? '#ffa028' : '#888888',
|
|
1264
1264
|
font: `${UI_TEXT_POINTS}px inherit`,
|
|
@@ -1302,14 +1302,14 @@ function NodePanel({
|
|
|
1302
1302
|
overflow: 'auto',
|
|
1303
1303
|
// THE SIDEBAR IS PART OF THE NODE EDITOR'S AREA, so it wears the
|
|
1304
1304
|
// space's own `TH_BACK` like the header and the canvas do. It used to
|
|
1305
|
-
// paint `--
|
|
1305
|
+
// paint `--volter-color-surface-panel`, which is the DOCK's grey
|
|
1306
1306
|
// (#303030 under the Blender look): Blender fills every region of a
|
|
1307
1307
|
// node editor area with `TH_BACK` and draws panels ON it
|
|
1308
1308
|
// (`ED_region_panels`), so the grey was this view wearing its
|
|
1309
1309
|
// container's colour. MEASURED 2026-09-19 (the frame walk, beat 5):
|
|
1310
1310
|
// at (600,500) — inside the Shader Editor's pane, which a 320px
|
|
1311
1311
|
// sidebar had squeezed the canvas out of — the read was #303030 where
|
|
1312
|
-
// the beat named #1a1a1a. The frame's `
|
|
1312
|
+
// the beat named #1a1a1a. The frame's `volter.nodeEditor.background`
|
|
1313
1313
|
// theme colour carries the same traced value for the workbench's own
|
|
1314
1314
|
// painting; this is the same number, from the same trace, for the
|
|
1315
1315
|
// pixels we paint ourselves.
|
|
@@ -1317,10 +1317,10 @@ function NodePanel({
|
|
|
1317
1317
|
color: NODE_THEME.text,
|
|
1318
1318
|
font: `${UI_TEXT_POINTS}px inherit`,
|
|
1319
1319
|
borderLeft: '1px solid #161616',
|
|
1320
|
-
padding: 'var(--
|
|
1320
|
+
padding: 'var(--volter-space-2)',
|
|
1321
1321
|
}}
|
|
1322
1322
|
>
|
|
1323
|
-
<div style={{ opacity: 0.6, marginBottom: 'var(--
|
|
1323
|
+
<div style={{ opacity: 0.6, marginBottom: 'var(--volter-space-2)' }}>Node</div>
|
|
1324
1324
|
{!node ? (
|
|
1325
1325
|
// Blender's panel `poll` is `context.active_node is not None`
|
|
1326
1326
|
// (`space_node.py:806-808`): with no active node the tab draws nothing.
|
|
@@ -1339,7 +1339,7 @@ function NodePanel({
|
|
|
1339
1339
|
<div
|
|
1340
1340
|
style={{
|
|
1341
1341
|
opacity: 0.6,
|
|
1342
|
-
margin: 'var(--
|
|
1342
|
+
margin: 'var(--volter-space-3) 0 var(--volter-space-1)',
|
|
1343
1343
|
}}
|
|
1344
1344
|
>
|
|
1345
1345
|
Properties
|
|
@@ -1371,7 +1371,7 @@ function Row({
|
|
|
1371
1371
|
}) {
|
|
1372
1372
|
return (
|
|
1373
1373
|
<div
|
|
1374
|
-
style={{ display: 'flex', alignItems: 'center', gap: 'var(--
|
|
1374
|
+
style={{ display: 'flex', alignItems: 'center', gap: 'var(--volter-space-2)', height: 20 }}
|
|
1375
1375
|
onPointerDown={() =>
|
|
1376
1376
|
onRefuse(`"${label}" is read here and written nowhere — inspection parity.`)
|
|
1377
1377
|
}
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
* ## Licence
|
|
75
75
|
*
|
|
76
76
|
* What lands in the JSON is a table of PANEL NAMES and SOCKET NAMES read out
|
|
77
|
-
* of GPL-2.0-or-later sources. `@
|
|
77
|
+
* of GPL-2.0-or-later sources. `@volter/blender` is AGPL-3.0-only, which that
|
|
78
78
|
* is compatible with (relicense to GPL-3.0, then §13), and the package
|
|
79
79
|
* already carries Blender itself — the same standing this file's sibling
|
|
80
80
|
* `blender-icon-trace.mjs` records for the icon paths. Nothing traced here
|
|
@@ -41,7 +41,7 @@ interface PresentedView {
|
|
|
41
41
|
* `BlenderRuntimeView.blenderSelection`. The engine is the truth and this
|
|
42
42
|
* is the read; {@link blenderEngineSelection} is the door. */
|
|
43
43
|
blenderSelection(): { readonly selected: readonly string[]; readonly active: string | null };
|
|
44
|
-
/** Where `
|
|
44
|
+
/** Where `volter.stage.mode` comes from — see `BlenderRuntimeView.stageMode`. */
|
|
45
45
|
setStageMode(mode: string | null): void;
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -203,7 +203,7 @@ async function read(selected: readonly string[]): Promise<void> {
|
|
|
203
203
|
if (seq !== readSeq) return;
|
|
204
204
|
// THE MODE RIDES WITH THE TREE, because the tree is what needs it (pose
|
|
205
205
|
// rows exist only in pose mode) and because the read happens anyway.
|
|
206
|
-
// `
|
|
206
|
+
// `volter.stage.mode` is then the document's own published context — see
|
|
207
207
|
// `BlenderRuntimeView.stageMode`.
|
|
208
208
|
blenderPresentedView()?.setStageMode(tree?.mode ?? null);
|
|
209
209
|
publish({ tree, ...index(tree), loading: false, error: null });
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* widget text #e6e6e6 :98 number slider #4772b3 :97
|
|
40
40
|
* checkbox mark #ffffff :77 (wcol_option.item)
|
|
41
41
|
*
|
|
42
|
-
* The values are literal rather than `var(--
|
|
42
|
+
* The values are literal rather than `var(--volter-…)` because the palette is
|
|
43
43
|
* the FRAME's grading (U8/U9) and eyedropped: `blender.palette.json` carries
|
|
44
44
|
* `surface.panel` #2f2f2f where the source says `.space_properties.back` is
|
|
45
45
|
* #303030, and `content.primary` #e5e5e5 against `.text` #e6e6e6. Where a
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* TypeScript geometry module. The bpy scripts that authored a `.blend` are
|
|
17
17
|
* ordinary project files beside it (`src/models/<name>.py`); this document
|
|
18
18
|
* does not run them, a person or an agent does, through the session's Blender
|
|
19
|
-
* (`
|
|
19
|
+
* (`volter blender-mcp`).
|
|
20
20
|
*
|
|
21
21
|
* WITHOUT AN ENTRY it is still the document `blender-start` presents into: a
|
|
22
22
|
* project that lists no `.blend` of its own gets one Model document at the
|
|
@@ -117,12 +117,12 @@ const REFUSALS = {
|
|
|
117
117
|
* sentence, with ONE author: the `play` verb throws it and the header's
|
|
118
118
|
* controls wear it as their title while they are disabled.
|
|
119
119
|
*
|
|
120
|
-
* It exists because the two halves disagreed. `
|
|
120
|
+
* It exists because the two halves disagreed. `volter.timeline.play` refused by
|
|
121
121
|
* name while the BUTTON called `transport()?.play()` straight through — the
|
|
122
122
|
* clock went to `playing`, both glyphs flipped to Pause, and the playhead
|
|
123
123
|
* stayed on frame 1 forever, because `#seek`/the play tick return early with
|
|
124
124
|
* no mixer to move (`blender-runtime-skin.ts`). Measured on U8's walk 3
|
|
125
|
-
* (2026-09-20) over a `--template models` scaffold: `
|
|
125
|
+
* (2026-09-20) over a `--template models` scaffold: `volter.timeline.state`
|
|
126
126
|
* answered `action: null` with that warning while the header showed a running
|
|
127
127
|
* transport. A control whose success can be invisible must say so.
|
|
128
128
|
*/
|
|
@@ -240,8 +240,8 @@ function report(): unknown {
|
|
|
240
240
|
* no mixer (`blender-runtime-skin.ts`), so the seek was a no-op and
|
|
241
241
|
* `report()` then answered `frame: 1` — the playhead's honest position and a
|
|
242
242
|
* complete lie about the gesture. Measured on walk 5 over a fresh
|
|
243
|
-
* `model-editor create` scaffold: `
|
|
244
|
-
* `
|
|
243
|
+
* `model-editor create` scaffold: `volter.timeline.frame {frame:120}` and
|
|
244
|
+
* `volter.timeline.jump-end` (frame 250) both answered `frame: 1`,
|
|
245
245
|
* `refusal: null`. This is the half of walk 4's W5 (#7740) that the header
|
|
246
246
|
* got and the VERBS did not — there the buttons were disabled wearing this
|
|
247
247
|
* sentence as their title while `play` threw it, and the five verbs that
|
|
@@ -822,9 +822,9 @@ function TimelineViewMenu() {
|
|
|
822
822
|
style={{
|
|
823
823
|
display: 'flex',
|
|
824
824
|
alignItems: 'center',
|
|
825
|
-
gap: 'var(--
|
|
825
|
+
gap: 'var(--volter-space-2)',
|
|
826
826
|
width: '100%',
|
|
827
|
-
padding: 'var(--
|
|
827
|
+
padding: 'var(--volter-space-1) var(--volter-space-3)',
|
|
828
828
|
border: 'none',
|
|
829
829
|
background: 'transparent',
|
|
830
830
|
color: TIMELINE_CHROME.widgetText,
|
|
@@ -851,7 +851,7 @@ function TimelineViewMenu() {
|
|
|
851
851
|
background: 'transparent',
|
|
852
852
|
color: TIMELINE_CHROME.text,
|
|
853
853
|
font: 'inherit',
|
|
854
|
-
padding: '0 var(--
|
|
854
|
+
padding: '0 var(--volter-space-2)',
|
|
855
855
|
cursor: 'pointer',
|
|
856
856
|
}}
|
|
857
857
|
>
|
|
@@ -93,8 +93,8 @@ const REFUSALS = {
|
|
|
93
93
|
} as const;
|
|
94
94
|
|
|
95
95
|
/** THE VIEW'S VERBS, published once, here — U8's ruling 1 (2026-09-19). Under
|
|
96
|
-
* the Code-OSS frame each becomes a `
|
|
97
|
-
* bridge dispatches into the view, and standalone `
|
|
96
|
+
* the Code-OSS frame each becomes a `volter.blender-uv-view.<verb>` command the
|
|
97
|
+
* bridge dispatches into the view, and standalone `volter-model-editor edit` reaches the
|
|
98
98
|
* SAME table through the session. **This view adds no `blender-*` session
|
|
99
99
|
* verb at all**, which is exactly what the ruling asked the remaining I5
|
|
100
100
|
* views to stop paying for. */
|
|
@@ -381,8 +381,8 @@ export default function BlenderUvEditor() {
|
|
|
381
381
|
// MEASURED difference between a drawer panel and an EDITOR GROUP. In
|
|
382
382
|
// the drawer this view's `height: '100%'` worked, because the drawer
|
|
383
383
|
// group imposes a height on its panel. As a `workspace.document` in
|
|
384
|
-
// `
|
|
385
|
-
// box — measured live 2026-09-19, `
|
|
384
|
+
// `volter:area:uv` it resolved to ZERO against an auto-height content
|
|
385
|
+
// box — measured live 2026-09-19, `volter.blender-uv-view.state`
|
|
386
386
|
// answered `size: {w: 782, h: 0}` with a fully READ subject beneath
|
|
387
387
|
// it (24 corners, 6 faces, the ProbeChecker backdrop found), which is
|
|
388
388
|
// a view that has everything to draw and nowhere to draw it. The node
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* It is read through the SDK's host door and NOT through `@editor/*`, and
|
|
17
17
|
* its two inline styles spell the theme tokens directly
|
|
18
|
-
* (`var(--
|
|
18
|
+
* (`var(--volter-space-2)`, `var(--volter-font-md)`) rather than importing the
|
|
19
19
|
* host's `fontSizeVar`/`spaceVar` handles: `@volter/editor-blender` is pinned at ZERO
|
|
20
20
|
* host imports (`validate-editor-closure.mjs`), the first package to reach
|
|
21
21
|
* that destination, and a token NAME is the published contract while the
|
|
@@ -53,7 +53,7 @@ export default function BlenderVersionStatus() {
|
|
|
53
53
|
if (version === null) return null;
|
|
54
54
|
return (
|
|
55
55
|
<span
|
|
56
|
-
className="
|
|
56
|
+
className="volter-status-copy"
|
|
57
57
|
data-testid="status-blender-version"
|
|
58
58
|
data-engine-version={version}
|
|
59
59
|
// NO COLOUR AND NO `sm`. Measured on the native-2x frames, Blender's
|
|
@@ -67,8 +67,8 @@ export default function BlenderVersionStatus() {
|
|
|
67
67
|
style={{
|
|
68
68
|
display: 'inline-flex',
|
|
69
69
|
alignItems: 'center',
|
|
70
|
-
gap: 'var(--
|
|
71
|
-
fontSize: 'var(--
|
|
70
|
+
gap: 'var(--volter-space-2)',
|
|
71
|
+
fontSize: 'var(--volter-font-md)',
|
|
72
72
|
}}
|
|
73
73
|
title={`This project is built against engine ${version}`}
|
|
74
74
|
>
|
|
@@ -82,8 +82,8 @@ export default function BlenderVersionStatus() {
|
|
|
82
82
|
<span
|
|
83
83
|
aria-hidden="true"
|
|
84
84
|
style={{
|
|
85
|
-
width: 'var(--
|
|
86
|
-
height: 'var(--
|
|
85
|
+
width: 'var(--volter-stroke-resting)',
|
|
86
|
+
height: 'var(--volter-icon-md)',
|
|
87
87
|
background: 'currentColor',
|
|
88
88
|
}}
|
|
89
89
|
/>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* These left `command-listener.ts`'s switch (and their rows left
|
|
11
11
|
* `command-table.ts`) when Blender's editor half left the host — WORK.md
|
|
12
|
-
* §The workbench, item D. `
|
|
12
|
+
* §The workbench, item D. `volter blender-mcp` is transport only: every
|
|
13
13
|
* `execute_blender_code`, `get_scene_info`, `get_object_info` and
|
|
14
14
|
* `get_viewport_screenshot` still arrives as the same `blender-*` command
|
|
15
15
|
* through the same relay, and is answered here instead of there.
|
|
@@ -93,7 +93,7 @@ export const commands: CommandContribution['commands'] = {
|
|
|
93
93
|
'blender-rig': verb('none', 60_000),
|
|
94
94
|
'blender-action-clip': verb('none', 120_000),
|
|
95
95
|
// THE NODE VIEW'S OWN ACTIONS as a session verb — the keyboard ruling's
|
|
96
|
-
// `
|
|
96
|
+
// `volter.*`-command-per-action pattern, and the only way the drawer's view
|
|
97
97
|
// can be read or driven at all: `editor.document.*` is scoped to the active
|
|
98
98
|
// CENTER document by its own contract, and a utility is not one. Read-only
|
|
99
99
|
// over the MODEL: `look` and the view transform are inspection state, and a
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
* a surface ever makes a button mean something else, it publishes that and
|
|
39
39
|
* this reads it; it does not get hardcoded here.
|
|
40
40
|
*
|
|
41
|
-
* The inline styles spell the theme tokens directly (`var(--
|
|
42
|
-
* `var(--
|
|
41
|
+
* The inline styles spell the theme tokens directly (`var(--volter-space-5)`,
|
|
42
|
+
* `var(--volter-font-md)`) instead of importing the host's `spaceVar` /
|
|
43
43
|
* `fontSizeVar` handles: `@volter/editor-blender` is pinned at ZERO host imports
|
|
44
44
|
* (`validate-editor-closure.mjs`). See `blender-version.status.tsx`.
|
|
45
45
|
*/
|
|
@@ -140,7 +140,7 @@ const RADIUS = 3.6;
|
|
|
140
140
|
const BOX = { width: 12, height: 16 } as const;
|
|
141
141
|
|
|
142
142
|
function MouseGlyph({ button }: { button: MouseButton }) {
|
|
143
|
-
const id = `
|
|
143
|
+
const id = `volter-mouse-${button}`;
|
|
144
144
|
const rightX = BOX.width - BUTTON_W;
|
|
145
145
|
const fillX = button === 'right' ? rightX : 0;
|
|
146
146
|
// The stroke's own rect runs along the centre line of a 1-unit stroke, so it
|
|
@@ -156,7 +156,7 @@ function MouseGlyph({ button }: { button: MouseButton }) {
|
|
|
156
156
|
// 16 CSS px, which is the `lg` icon rung — and Blender's glyph measures
|
|
157
157
|
// exactly 16 CSS tall in the frame. `width: auto` then resolves to 12
|
|
158
158
|
// from the viewBox, the measured width, with no second number to drift.
|
|
159
|
-
style={{ height: 'var(--
|
|
159
|
+
style={{ height: 'var(--volter-icon-lg)', width: 'auto', display: 'block', flex: '0 0 auto' }}
|
|
160
160
|
>
|
|
161
161
|
<title>{button} mouse button</title>
|
|
162
162
|
{/* The outer silhouette: what the fill is intersected with, so the lit
|
|
@@ -209,11 +209,11 @@ function MouseGlyph({ button }: { button: MouseButton }) {
|
|
|
209
209
|
export default function BlenderMouseHintsStatus() {
|
|
210
210
|
return (
|
|
211
211
|
<span
|
|
212
|
-
className="
|
|
212
|
+
className="volter-status-copy"
|
|
213
213
|
data-testid="status-blender-mouse-hints"
|
|
214
214
|
// Blender's hint-to-hint gap is 9.5 CSS (glyph 1 ends at 51.5, glyph 2
|
|
215
215
|
// starts at 61; the second gap repeats it to a tenth). `space-5` is 10.
|
|
216
|
-
style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--
|
|
216
|
+
style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--volter-space-5)' }}
|
|
217
217
|
>
|
|
218
218
|
{HINTS.map((hint) => (
|
|
219
219
|
<span
|
|
@@ -231,8 +231,8 @@ export default function BlenderMouseHintsStatus() {
|
|
|
231
231
|
style={{
|
|
232
232
|
display: 'inline-flex',
|
|
233
233
|
alignItems: 'center',
|
|
234
|
-
gap: 'var(--
|
|
235
|
-
fontSize: 'var(--
|
|
234
|
+
gap: 'var(--volter-space-1)',
|
|
235
|
+
fontSize: 'var(--volter-font-md)',
|
|
236
236
|
}}
|
|
237
237
|
title={`${hint.label} — the ${hint.button} mouse button in the viewport`}
|
|
238
238
|
>
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* file the engine in the tab opens, edits and saves. The bpy scripts that
|
|
6
6
|
* authored it are ordinary project files beside it (`src/models/<name>.py`);
|
|
7
7
|
* this document does not run them, a person or an agent does, through the
|
|
8
|
-
* session's Blender (`
|
|
8
|
+
* session's Blender (`volter blender-mcp`).
|
|
9
9
|
*
|
|
10
10
|
* It replaced `modelsFromMeshModules`, which listed every module exporting
|
|
11
11
|
* `build(): THREE.BufferGeometry` (owner ruling 2026-09-19, "do the delete":
|
|
12
12
|
* the TypeScript mesh kit was the modeling engine while Blender could not run
|
|
13
13
|
* in the tab; it now does, and a model is Blender data).
|
|
14
14
|
*
|
|
15
|
-
* Select it in `
|
|
15
|
+
* Select it in `volter.adapter.ts`:
|
|
16
16
|
*
|
|
17
17
|
* documents: { find: [{ finder: 'modelsFromBlendFiles', include: ['src/models/**\/*.blend'] }] }
|
|
18
18
|
*
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
* is why this arrangement is Model's with one change; and the centre column is
|
|
29
29
|
* split EXACTLY 50/50, 514 over 514 of the 1031 px between the top bar and the
|
|
30
30
|
* status bar. On the captured 928-px grid that is 464/464, which is the size
|
|
31
|
-
* `
|
|
31
|
+
* `volter:bottom-center` was already carrying at `"visible": false`.
|
|
32
32
|
*
|
|
33
33
|
* WHICH AREA THE SHADER EDITOR IS, AND WHAT IT IS MADE OF. Blender's node
|
|
34
34
|
* editor is the area BELOW the viewport — and an AREA IS AN EDITOR GROUP
|
|
35
35
|
* (orchestrator ruling 2026-09-19), not the bottom utility drawer. So Shading
|
|
36
36
|
* declares it as an `areas` entry holding the `blender-node-editor.document`
|
|
37
|
-
* contribution, the dock puts it in `
|
|
37
|
+
* contribution, the dock puts it in `volter:area:shader` below the centre at
|
|
38
38
|
* Blender's own 50/50, and the frame's layout host puts the same document in
|
|
39
39
|
* a second VS Code editor group. The drawer is `hidden` here, as it is in
|
|
40
40
|
* Model: Blender's Shading screen has no utility strip.
|
|
@@ -86,7 +86,7 @@ export const layout: WorkspaceLayoutContribution = {
|
|
|
86
86
|
// `x294 y23 w1282 h514`, measured through bpy on the engine — and both the
|
|
87
87
|
// dock and the Code-OSS frame have exactly that shape in editor groups. So
|
|
88
88
|
// the Shader Editor is a `workspace.document` this workspace opens into
|
|
89
|
-
// `
|
|
89
|
+
// `volter:area:shader`, not a `workspace.utility` in the drawer.
|
|
90
90
|
//
|
|
91
91
|
// AND THAT IS WHAT RETIRED THE DRAWER RACE BY CONSTRUCTION. Switching to UV
|
|
92
92
|
// Editing kept opening the Shader Editor — and the cause was NOT the dock
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* its width. **RULED 2026-09-19 (orchestrator): a Blender editor AREA is an
|
|
30
30
|
* editor group; the drawer holds utilities.** The UV editor is not a utility
|
|
31
31
|
* at all now — it is a `workspace.document` this workspace opens into
|
|
32
|
-
* `
|
|
32
|
+
* `volter:area:uv`, LEFT of the model document at Blender's own 786/1571, and
|
|
33
33
|
* under the frame the layout host puts the same document in a second VS Code
|
|
34
34
|
* editor group. Nothing is deviating and nothing is owed to U10 here.
|
|
35
35
|
*
|
package/dist-node/serving.mjs
CHANGED
|
@@ -58,7 +58,7 @@ var BLENDER_WALI_SUBSTRATE = [
|
|
|
58
58
|
{ segment: "browser-wali", specifier: "@volter/browser-wali" },
|
|
59
59
|
{ segment: "browser-runtime", specifier: "@volter/browser-runtime" }
|
|
60
60
|
];
|
|
61
|
-
var BLENDER_WASM_DIR_VARIABLE = "
|
|
61
|
+
var BLENDER_WASM_DIR_VARIABLE = "VOLTER_BLENDER_WASM_DIR";
|
|
62
62
|
var SHIPPED_BLENDER_WASM_DIR = (() => {
|
|
63
63
|
const candidate = fileURLToPath(new URL("../../blender-engine/wasm/", import.meta.url));
|
|
64
64
|
return existsSync(candidate) ? candidate.replace(/\/$/, "") : null;
|
|
@@ -277,7 +277,7 @@ async function readBody(req) {
|
|
|
277
277
|
return Buffer.concat(chunks);
|
|
278
278
|
}
|
|
279
279
|
function blenderRoutesPlugin(services) {
|
|
280
|
-
const transferRoot = join2(tmpdir(), "
|
|
280
|
+
const transferRoot = join2(tmpdir(), "volter-blender-transfer");
|
|
281
281
|
const transferPath = (id) => join2(transferRoot, id);
|
|
282
282
|
void rm(transferRoot, { recursive: true, force: true });
|
|
283
283
|
const transferId = (value) => value !== null && /^[0-9a-f-]{36}$/.test(value) ? value : null;
|
|
@@ -463,7 +463,7 @@ function blenderRoutesPlugin(services) {
|
|
|
463
463
|
match: /^\/__editor\/blender-project-file$/,
|
|
464
464
|
handle: async (_req, res, url) => {
|
|
465
465
|
const relPath = url.searchParams.get("path") ?? "";
|
|
466
|
-
const scratchDocument = isContainedRelativePath(relPath) && relPath.startsWith(".
|
|
466
|
+
const scratchDocument = isContainedRelativePath(relPath) && relPath.startsWith(".volter/tmp/") && relPath.endsWith(".blend");
|
|
467
467
|
if (!services.isProjectOwnedPath(relPath) && !scratchDocument) {
|
|
468
468
|
json(res, { error: "Not a project-owned path." }, 400);
|
|
469
469
|
return;
|
|
@@ -601,7 +601,7 @@ function blenderRoutesPlugin(services) {
|
|
|
601
601
|
}
|
|
602
602
|
];
|
|
603
603
|
return {
|
|
604
|
-
name: "
|
|
604
|
+
name: "volter-blender-routes",
|
|
605
605
|
configureServer(server) {
|
|
606
606
|
server.middlewares.use((req, res, next) => {
|
|
607
607
|
const url = new URL(req.url ?? "/", "http://local");
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* and other packages' exports), which is what lets this lane ship against a
|
|
11
11
|
* workbench it was not built in.
|
|
12
12
|
*
|
|
13
|
-
* `
|
|
13
|
+
* `volter blender-mcp` is transport only: every `execute_blender_code`,
|
|
14
14
|
* `get_scene_info`, `get_object_info` and `get_viewport_screenshot` arrives
|
|
15
15
|
* here as a `blender-*` command and is answered by Blender running in this
|
|
16
16
|
* tab's worker (`@volter/blender-engine/browser`). The model exists in that worker
|
|
@@ -296,7 +296,7 @@ export async function openModelDocumentBlend(
|
|
|
296
296
|
* (`../contributions/blender-properties-*`) call these three rather than the
|
|
297
297
|
* `blender-rna*` commands: same session, same `session.py` functions, one
|
|
298
298
|
* fewer hop, and no `EditorCommandResult` envelope to unwrap in a render.
|
|
299
|
-
* The COMMANDS remain the wire's door onto the same calls, so a `
|
|
299
|
+
* The COMMANDS remain the wire's door onto the same calls, so a `volter-model-editor eval`
|
|
300
300
|
* and the panel read one thing.
|
|
301
301
|
*
|
|
302
302
|
* THEY NEVER START THE ENGINE. A panel asking what the engine holds must not
|
|
@@ -327,7 +327,7 @@ export function blenderSessionStarted(): boolean {
|
|
|
327
327
|
* So the version is minted HERE, at the door every RNA write goes through,
|
|
328
328
|
* and it is bumped by the write rather than by the picture. Both halves of
|
|
329
329
|
* each door bump it once: the in-page function is what the wire's
|
|
330
|
-
* `blender-rna-set` / `blender-outliner-set` case calls, so a `
|
|
330
|
+
* `blender-rna-set` / `blender-outliner-set` case calls, so a `volter-model-editor eval` and
|
|
331
331
|
* a panel click are one path. `blender-execute` bumps it too — arbitrary bpy
|
|
332
332
|
* can change anything RNA answers, and a view that went stale under a probe's
|
|
333
333
|
* own script would be the same defect one layer out.
|
|
@@ -531,8 +531,8 @@ export async function blenderOutlinerSet(
|
|
|
531
531
|
|
|
532
532
|
/**
|
|
533
533
|
* THE NODE EDITOR'S VERBS, published ONCE and reached two ways (U8's ruling 1,
|
|
534
|
-
* 2026-09-19). Under the Code-OSS frame each is a `
|
|
535
|
-
* command the bridge dispatches into the view; standalone `
|
|
534
|
+
* 2026-09-19). Under the Code-OSS frame each is a `volter.blender-node-view.<verb>`
|
|
535
|
+
* command the bridge dispatches into the view; standalone `volter-model-editor edit`, which has
|
|
536
536
|
* no command service, reaches the SAME table through the session's
|
|
537
537
|
* `blender-node-view` verb below. One table, two doors — the shape
|
|
538
538
|
* `key-actions.ts`'s action table already has, and the reason the next
|
|
@@ -757,7 +757,7 @@ async function runtimeView(): Promise<RuntimeView> {
|
|
|
757
757
|
`The Blender Model document is not open, or the open one is not a Model this engine can present ` +
|
|
758
758
|
`to (it must answer applyFrame, captureSnapshot, recordPresentation and recordPhotograph): nothing ` +
|
|
759
759
|
`published a presentable view as ${id} within 15 s (bound model: ${boundModel?.documentId ?? 'none'}). ` +
|
|
760
|
-
'Open the Model document first (`
|
|
760
|
+
'Open the Model document first (`volter blender-mcp` opens it before its first call).',
|
|
761
761
|
);
|
|
762
762
|
}
|
|
763
763
|
|
|
@@ -1030,7 +1030,7 @@ export function blenderRuntime(): BlenderRuntime {
|
|
|
1030
1030
|
// how long it has been stuck — the loop that would send the number is the loop
|
|
1031
1031
|
// that is stuck — so this side keeps the clock and the host carries it out on
|
|
1032
1032
|
// the heartbeat, the one channel that still beats through a blocked main
|
|
1033
|
-
// thread. `
|
|
1033
|
+
// thread. `volter-model-editor status` prints the block. Published as a READ of the live
|
|
1034
1034
|
// meter rather than a snapshot, so the host always asks the running session:
|
|
1035
1035
|
// an in-flight call's age has to be computed at the moment it is reported.
|
|
1036
1036
|
// The page's own long-task half is the HOST's, behind this same door.
|
|
@@ -1050,7 +1050,7 @@ export function lastBlenderCapture(): CaptureRequest | null {
|
|
|
1050
1050
|
* not lose what a script modelled: before this, closing the tab lost it, with
|
|
1051
1051
|
* no `.blend` anywhere. `models/` rather than `public/` is what says a
|
|
1052
1052
|
* document is the SOURCE a shipped artifact is exported from, not the artifact
|
|
1053
|
-
* — which is also why it carries no provenance record (`
|
|
1053
|
+
* — which is also why it carries no provenance record (`volter blender-mcp`'s
|
|
1054
1054
|
* Mirror owns that line, for `public/`).
|
|
1055
1055
|
*
|
|
1056
1056
|
* It is the fallback only. A project that declares the `model` finder lists
|
|
@@ -1193,7 +1193,7 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1193
1193
|
// RNA version is minted at the door (ruling 3, 2026-09-19), and
|
|
1194
1194
|
// ARBITRARY bpy can change anything RNA answers, so a script that
|
|
1195
1195
|
// reached the engine around it would leave every view drawing the tree
|
|
1196
|
-
// it had. One path for a `
|
|
1196
|
+
// it had. One path for a `volter-model-editor eval`, an MCP call and a panel's own
|
|
1197
1197
|
// operator.
|
|
1198
1198
|
// THE DOOR'S TEXT, verbatim — `execute_blender_code`'s MCP contract is
|
|
1199
1199
|
// that one string, so the wire keeps answering it while the in-page
|
|
@@ -1240,7 +1240,7 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1240
1240
|
}
|
|
1241
1241
|
// THE TREE DOOR on the wire, beside the RNA one: `blender-outliner`
|
|
1242
1242
|
// answers Blender's View Layer tree and `blender-outliner-set` writes one
|
|
1243
|
-
// restriction column, so a `
|
|
1243
|
+
// restriction column, so a `volter-model-editor eval` reads exactly what the hierarchy
|
|
1244
1244
|
// panel draws.
|
|
1245
1245
|
case 'blender-outliner': {
|
|
1246
1246
|
const selected = Array.isArray(cmd['selected'])
|
|
@@ -1251,7 +1251,7 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1251
1251
|
return { ok: true, data: { result: await session.outliner(selected) } };
|
|
1252
1252
|
}
|
|
1253
1253
|
// THE NODE-TREE DOOR on the wire, beside the other two: one material's
|
|
1254
|
-
// shader node tree, whole, so a `
|
|
1254
|
+
// shader node tree, whole, so a `volter-model-editor eval` reads exactly what the node
|
|
1255
1255
|
// view draws.
|
|
1256
1256
|
case 'blender-node-tree':
|
|
1257
1257
|
return {
|
|
@@ -1264,7 +1264,7 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1264
1264
|
},
|
|
1265
1265
|
};
|
|
1266
1266
|
// THE UV DOOR on the wire, beside the node one: one mesh's UV layout,
|
|
1267
|
-
// so a `
|
|
1267
|
+
// so a `volter-model-editor eval` reads exactly what the UV view draws.
|
|
1268
1268
|
case 'blender-uv-layout':
|
|
1269
1269
|
return {
|
|
1270
1270
|
ok: true,
|
|
@@ -1276,7 +1276,7 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1276
1276
|
},
|
|
1277
1277
|
};
|
|
1278
1278
|
// THE RIG AND CLIP DOORS on the wire, beside the UV one: the skin
|
|
1279
|
-
// binding and the action as three.js tracks, so a `
|
|
1279
|
+
// binding and the action as three.js tracks, so a `volter-model-editor eval` reads
|
|
1280
1280
|
// exactly what the presenter bound and what the Timeline plays.
|
|
1281
1281
|
case 'blender-rig':
|
|
1282
1282
|
return {
|
|
@@ -1303,7 +1303,7 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1303
1303
|
//
|
|
1304
1304
|
// THE SESSION IS THE STANDALONE DOOR ONTO `NODE_VIEW_VERBS`, not a second
|
|
1305
1305
|
// implementation (U8's ruling 1). Under the Code-OSS frame each verb is a
|
|
1306
|
-
// `
|
|
1306
|
+
// `volter.blender-node-view.<verb>` command; standalone `volter-model-editor edit` has no
|
|
1307
1307
|
// command service, so this verb routes the SAME table. `invokeViewVerb`
|
|
1308
1308
|
// throws the view's own refusal, which is the sentence this door already
|
|
1309
1309
|
// answered with.
|
|
@@ -1368,7 +1368,7 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1368
1368
|
}
|
|
1369
1369
|
case 'blender-list-files': {
|
|
1370
1370
|
// WHO to attribute these files to, answered in the same breath as the
|
|
1371
|
-
// listing. The write-back door (`
|
|
1371
|
+
// listing. The write-back door (`volter blender-mcp`'s Mirror) records a
|
|
1372
1372
|
// file it lands under `public/` in the project's provenance ledger, and
|
|
1373
1373
|
// what it can honestly name is this session and the model state it had
|
|
1374
1374
|
// reached — taken here, at the instant of the listing, rather than
|
package/node/blender-mcp.ts
CHANGED
|
@@ -108,7 +108,7 @@ function projectRelative(project: string, absolute: string): string | null {
|
|
|
108
108
|
/**
|
|
109
109
|
* The worker's filesystem is the only place a script's outputs exist; this
|
|
110
110
|
* mirrors them to disk. Roots: the project (a script's `public/models/x.glb`
|
|
111
|
-
* lands in the project) plus any `
|
|
111
|
+
* lands in the project) plus any `VOLTER_BLENDER_MIRROR_ROOTS` (colon-separated
|
|
112
112
|
* absolute paths — the replay harness names its run directory). Only files
|
|
113
113
|
* whose size or mtime changed since the last mirror are read back, and only
|
|
114
114
|
* files the SESSION owns are listed at all — a project file it merely read
|
|
@@ -117,7 +117,7 @@ function projectRelative(project: string, absolute: string): string | null {
|
|
|
117
117
|
* THIS IS THE DOOR THAT RECORDS, because it is the door a session's bytes
|
|
118
118
|
* enter the project through. MEASURED 2026-09-18: a script's
|
|
119
119
|
* `export_scene.gltf` to `<project>/public/models/lantern.glb` landed here and
|
|
120
|
-
* `.
|
|
120
|
+
* `.volter/provenance.json` was never created — the one state
|
|
121
121
|
* `scripts/validate-project-provenance.mjs` calls fatal, and the opposite of
|
|
122
122
|
* the doctrine that a generated artifact enters `public/` through a door that
|
|
123
123
|
* records it atomically. So a file this lands under `public/` is POSTed to the
|
|
@@ -131,7 +131,7 @@ function projectRelative(project: string, absolute: string): string | null {
|
|
|
131
131
|
* transport uses that same session boundary as the other product commands;
|
|
132
132
|
* it never creates a second ledger writer in the MCP process.
|
|
133
133
|
*
|
|
134
|
-
* Everything else the session owns — `.
|
|
134
|
+
* Everything else the session owns — `.volter/tmp/*.png`, a `.blend`, a replay
|
|
135
135
|
* harness's run directory outside the project — is mirrored with a plain write
|
|
136
136
|
* and deliberately NOT recorded: the ledger is about what the project SHIPS,
|
|
137
137
|
* and `public/` is what ships.
|
|
@@ -232,7 +232,7 @@ class TabSession {
|
|
|
232
232
|
#origin: string | null = null;
|
|
233
233
|
#port: number | null = null;
|
|
234
234
|
/** A replay starts with a fresh model once; ordinary agent sessions persist. */
|
|
235
|
-
#freshSessionPending = Boolean(process.env['
|
|
235
|
+
#freshSessionPending = Boolean(process.env['VOLTER_BLENDER_FRESH_SESSION']);
|
|
236
236
|
constructor(
|
|
237
237
|
private readonly project: string,
|
|
238
238
|
private readonly ensureEditor: () => Promise<void>,
|
|
@@ -308,7 +308,7 @@ export async function serveBlenderMcp(
|
|
|
308
308
|
const defaults = blenderDefaults as Record<string, string>;
|
|
309
309
|
const mirror = new Mirror(project, [
|
|
310
310
|
project,
|
|
311
|
-
...(process.env['
|
|
311
|
+
...(process.env['VOLTER_BLENDER_MIRROR_ROOTS'] ?? '')
|
|
312
312
|
.split(':')
|
|
313
313
|
.filter((root) => root.startsWith('/')),
|
|
314
314
|
], `${command} blender-mcp`);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@volter/editor-blender",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "AGPL-3.0-only AND GPL-3.0-or-later",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.12",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"./mcp": "./node/blender-mcp.ts",
|
|
31
31
|
"./package.json": "./package.json"
|
|
32
32
|
},
|
|
33
|
-
"
|
|
33
|
+
"volter": {
|
|
34
34
|
"contributions": [
|
|
35
35
|
"./contributions/blender-node-editor.document.tsx",
|
|
36
36
|
"./contributions/blender-timeline.document.tsx",
|
|
@@ -69,14 +69,14 @@
|
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
72
|
-
"@volter/blender-engine": "0.1.
|
|
73
|
-
"@volter/editor-threejs": "0.5.
|
|
72
|
+
"@volter/blender-engine": "0.1.10",
|
|
73
|
+
"@volter/editor-threejs": "0.5.69",
|
|
74
74
|
"zod": "^4.3.6"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@volter/editor-sdk": "0.5.
|
|
78
|
-
"@volter/editor-live": "0.5.
|
|
79
|
-
"@volter/editor-project": "0.5.
|
|
77
|
+
"@volter/editor-sdk": "0.5.69",
|
|
78
|
+
"@volter/editor-live": "0.5.69",
|
|
79
|
+
"@volter/editor-project": "0.5.69",
|
|
80
80
|
"react": "^19.0.0",
|
|
81
81
|
"three": "^0.180.0",
|
|
82
82
|
"vite": "^6.4.3"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@volter/editor-blender`'s server half (`package.json#
|
|
2
|
+
* `@volter/editor-blender`'s server half (`package.json#volter.serving`): the routes the Blender
|
|
3
3
|
* in the tab needs from the session — the engine's WebAssembly build, the file spool its
|
|
4
4
|
* transports move bytes through, the project's files as Blender reads them, and the two ways
|
|
5
5
|
* bytes it produced reach the project (a shipped output, the session's `.blend`). They were
|
|
@@ -55,7 +55,7 @@ export function blenderRoutesPlugin(services: ProjectServingServices): Plugin {
|
|
|
55
55
|
// octet-stream; the requester GETs it once and it is gone. The spool keeps PATH AUTHORITY
|
|
56
56
|
// WITH THE REQUESTER: the page never names a destination on disk, only an id the caller
|
|
57
57
|
// minted.
|
|
58
|
-
const transferRoot = join(tmpdir(), '
|
|
58
|
+
const transferRoot = join(tmpdir(), 'volter-blender-transfer');
|
|
59
59
|
const transferPath = (id: string): string => join(transferRoot, id);
|
|
60
60
|
// Every id is a uuid minted by a caller in a PREVIOUS process, so nothing live is here yet:
|
|
61
61
|
// clearing once at startup bounds what a failed transfer can leave.
|
|
@@ -261,7 +261,7 @@ export function blenderRoutesPlugin(services: ProjectServingServices): Plugin {
|
|
|
261
261
|
handle: async (_req, res, url) => {
|
|
262
262
|
const relPath = url.searchParams.get('path') ?? '';
|
|
263
263
|
const scratchDocument =
|
|
264
|
-
isContainedRelativePath(relPath) && relPath.startsWith('.
|
|
264
|
+
isContainedRelativePath(relPath) && relPath.startsWith('.volter/tmp/') && relPath.endsWith('.blend');
|
|
265
265
|
if (!services.isProjectOwnedPath(relPath) && !scratchDocument) {
|
|
266
266
|
json(res, { error: 'Not a project-owned path.' }, 400);
|
|
267
267
|
return;
|
|
@@ -410,7 +410,7 @@ export function blenderRoutesPlugin(services: ProjectServingServices): Plugin {
|
|
|
410
410
|
];
|
|
411
411
|
|
|
412
412
|
return {
|
|
413
|
-
name: '
|
|
413
|
+
name: 'volter-blender-routes',
|
|
414
414
|
configureServer(server) {
|
|
415
415
|
server.middlewares.use((req, res, next) => {
|
|
416
416
|
const url = new URL(req.url ?? '/', 'http://local');
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* setting and no flag, because nothing a user does may depend on the skew —
|
|
24
24
|
* the ruling's "a skew may refuse a capability by name, never answer the same
|
|
25
25
|
* question differently" leaves nothing for a chooser to choose.
|
|
26
|
-
* `
|
|
26
|
+
* `VOLTER_BLENDER_WASM_DIR` is the existing door for pointing the editor at a
|
|
27
27
|
* different build, and pointing it at a WALI pack is how the substrate skew is
|
|
28
28
|
* reached in development.
|
|
29
29
|
*
|
|
@@ -110,14 +110,14 @@ export const BLENDER_WALI_SUBSTRATE = [
|
|
|
110
110
|
{ segment: 'browser-runtime', specifier: '@volter/browser-runtime' },
|
|
111
111
|
] as const;
|
|
112
112
|
|
|
113
|
-
export const BLENDER_WASM_DIR_VARIABLE = '
|
|
113
|
+
export const BLENDER_WASM_DIR_VARIABLE = 'VOLTER_BLENDER_WASM_DIR';
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* The bundle `@volter/blender-engine` ships — Blender itself, so its own GPL
|
|
117
117
|
* package (ARCHITECTURE-CORE §Licensing, "Blender's licence stops at the
|
|
118
118
|
* wire"). This resolution is a PATH, not an import: the editor serves those
|
|
119
119
|
* bytes and never imports the engine. `import.meta.url` is THIS FILE's esbuild bundle
|
|
120
|
-
* (`<pkg>/dist-node/serving.mjs`, the `
|
|
120
|
+
* (`<pkg>/dist-node/serving.mjs`, the `volter.serving` module); from it, two
|
|
121
121
|
* directories up and across is the sibling package -- `packages/blender-engine/wasm`
|
|
122
122
|
* in a checkout, `node_modules/@volter/blender-engine/wasm` installed. Existence is
|
|
123
123
|
* checked once so a realm that lacks it gets a named absence, not a path.
|
package/serving/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@volter/editor-blender`'s server half (`package.json#
|
|
2
|
+
* `@volter/editor-blender`'s server half (`package.json#volter.serving`, the project-serving door in
|
|
3
3
|
* `@volter/editor-sdk/session/project-serving`): the routes the Blender in the tab reads and
|
|
4
4
|
* writes through.
|
|
5
5
|
*/
|
package/src/layouts.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The BLENDER layout — the composition a models project's adapter declares
|
|
3
|
-
* (`
|
|
3
|
+
* (`volter.adapter.ts`: `editor: { Layout: ModelLayout }`, the door
|
|
4
4
|
* `@volter/editor-sdk/layouts` documents). `ModelArrangement` names the Model
|
|
5
5
|
* workspace and the chrome regions it shows; Sculpt and Texture open on it too
|
|
6
6
|
* — their reserved panels (the brush rail, the layer stack) ship WITH their
|
package/src/looks.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Blender skew's LOOK and BINDINGS as importable values — `@volter/editor-blender/looks`,
|
|
3
|
-
* the public surface a project's `
|
|
3
|
+
* the public surface a project's `volter.adapter.ts` declares them from
|
|
4
4
|
* (ARCHITECTURE-CORE §Adapters and contributions are code, not configs):
|
|
5
5
|
*
|
|
6
6
|
* editor: { Layout: ModelLayout, style: blenderStyle, keymap: blenderKeymap }
|
|
7
7
|
*
|
|
8
8
|
* The same two objects the package's `workspace.style` / `workspace.keymap`
|
|
9
|
-
* contributions register (`package.json#
|
|
9
|
+
* contributions register (`package.json#volter.contributions`). Registration
|
|
10
10
|
* publishes them to every project that declares this package; the adapter's
|
|
11
11
|
* declaration is what CHOOSES them for one.
|
|
12
12
|
*/
|
package/src/node-view-state.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* own header says so: "a selector resolving outside that document's container
|
|
7
7
|
* is refused by a message naming the scope". The node editor is a DRAWER
|
|
8
8
|
* utility, so nothing in the product could read or drive it: the sighted door
|
|
9
|
-
* (`editor.captureEditorChrome`, `
|
|
9
|
+
* (`editor.captureEditorChrome`, `volter-model-editor screenshot editor`) photographs it, and
|
|
10
10
|
* a photograph cannot click.
|
|
11
11
|
*
|
|
12
12
|
* So the view's actions are SESSION VERBS, which is the pattern the keyboard
|
|
13
|
-
* ruling already asks for — a `
|
|
13
|
+
* ruling already asks for — a `volter.*` command per action rather than a raw
|
|
14
14
|
* listener — and this module is the one place their state lives. The component
|
|
15
15
|
* subscribes to it; `blender-node-view` (the command) reads and writes it. One
|
|
16
16
|
* store, two readers, no second copy.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* is about the DRAWING.
|
|
11
11
|
*
|
|
12
12
|
* WHAT DRIVES IT IS NOT A SESSION VERB. U8's ruling 1 (2026-09-19): every view
|
|
13
|
-
* publishes `
|
|
14
|
-
* table behind the frame's command service and standalone `
|
|
13
|
+
* publishes `volter.<view>.<verb>` commands through `@volter/editor-sdk/views`, one
|
|
14
|
+
* table behind the frame's command service and standalone `volter-model-editor edit`'s
|
|
15
15
|
* session verb — so this view adds no `blender-*` verb of its own.
|
|
16
16
|
*
|
|
17
17
|
* It imports NOTHING, for the reason the node and UV stores do: the view and
|
package/src/uv-view-state.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* surface nothing in the product could read or drive.
|
|
7
7
|
*
|
|
8
8
|
* WHAT DRIVES IT IS NOT A SESSION VERB. U8's ruling 1 (2026-09-19): every view
|
|
9
|
-
* publishes `
|
|
10
|
-
* one table behind the frame's command service and standalone `
|
|
9
|
+
* publishes `volter.<view>.<verb>` commands through `@volter/editor-sdk/views`,
|
|
10
|
+
* one table behind the frame's command service and standalone `volter-model-editor edit`'s
|
|
11
11
|
* session verb — so this view adds NO `blender-*` verb of its own, which is
|
|
12
12
|
* exactly what the ruling asked the remaining I5 views to stop paying for.
|
|
13
13
|
*
|