@volter/blender-engine 0.1.9 → 0.1.11
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/browser/blender-emscripten-engine.mts +4 -4
- package/browser/blender-engine.mts +5 -5
- package/browser/blender-wali-engine.mts +7 -7
- package/browser/rna.ts +2 -2
- package/browser/runtime.ts +2 -2
- package/browser/session.py +35 -35
- package/browser/three/attach-presenter.ts +1 -1
- package/browser/three/blender-runtime-armature.ts +1 -1
- package/browser/three/blender-runtime-extras.ts +7 -7
- package/browser/three/blender-runtime-lighting.ts +1 -1
- package/browser/three/blender-runtime-view.ts +7 -7
- package/browser/three/blender-workbench-material.ts +4 -4
- package/browser/three/presenter.ts +1 -1
- package/browser/three/release.ts +1 -1
- package/browser/{vgai_three.py → volter_three.py} +19 -19
- package/browser/worker.ts +3 -3
- package/package.json +1 -1
|
@@ -193,9 +193,9 @@ export async function startEmscriptenBlenderEngine(
|
|
|
193
193
|
// editor's console door is for conditions somebody must resolve, so only the
|
|
194
194
|
// session's OWN named conditions go there.
|
|
195
195
|
const say = (_level: 'log' | 'error', text: string) => {
|
|
196
|
-
if (text.startsWith('@@
|
|
196
|
+
if (text.startsWith('@@VOLTER-READY ')) readyLine = text.slice('@@VOLTER-READY '.length);
|
|
197
197
|
options.log(
|
|
198
|
-
text.startsWith('@@
|
|
198
|
+
text.startsWith('@@VOLTER-WARN') || text.startsWith('@@VOLTER-ERROR') ? 'error' : 'log',
|
|
199
199
|
text,
|
|
200
200
|
);
|
|
201
201
|
};
|
|
@@ -224,11 +224,11 @@ export async function startEmscriptenBlenderEngine(
|
|
|
224
224
|
BLENDER_SYSTEM_PYTHON: '/bw/python',
|
|
225
225
|
BLENDER_SYSTEM_SCRIPTS: '/bw/scripts',
|
|
226
226
|
BLENDER_SYSTEM_DATAFILES: '/bw/datafiles',
|
|
227
|
-
|
|
227
|
+
VOLTER_SESSION_ROOT: SESSION_ROOT,
|
|
228
228
|
HOME: '/root',
|
|
229
229
|
TMPDIR: '/tmp',
|
|
230
230
|
});
|
|
231
|
-
//
|
|
231
|
+
// VOLTER_EXPORT_BUFFER_PATH IS DELIBERATELY UNSET. This engine reads the
|
|
232
232
|
// arena off `HEAPU8`; asking the door to also write it to a file would
|
|
233
233
|
// cost this skew a megabyte-scale write per present for bytes it
|
|
234
234
|
// already has (`session.py`, EXPORT_BUFFER_PATH).
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* and that IS the answer. There is no manifest field, no setting and no flag,
|
|
30
30
|
* because nothing a user does should depend on the skew -- if it did, the
|
|
31
31
|
* skews would be answering differently, which the ruling forbids.
|
|
32
|
-
* `
|
|
32
|
+
* `VOLTER_BLENDER_WASM_DIR` is the existing door for pointing the editor at a
|
|
33
33
|
* different build and is how the substrate skew is reached in development.
|
|
34
34
|
*
|
|
35
35
|
* WHAT DIFFERS, and it is four things, all below this line:
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* implementation over {@link BlenderFiles}.
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
|
-
export const SESSION_ROOT = '/work/.
|
|
46
|
+
export const SESSION_ROOT = '/work/.volter-session';
|
|
47
47
|
export const SESSION_SCRIPT = `${SESSION_ROOT}/session.py`;
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -137,7 +137,7 @@ export interface BlenderEngine {
|
|
|
137
137
|
readonly bootMs: number;
|
|
138
138
|
/** The engine's memory in bytes, now. Linear memory on the standalone skew;
|
|
139
139
|
* null where the skew cannot see the module's memory from the host, which
|
|
140
|
-
* is the WALI case and is why `
|
|
140
|
+
* is the WALI case and is why `volter-model-editor status` prints "unreported" rather than
|
|
141
141
|
* a zero. */
|
|
142
142
|
memoryBytes(): number | null;
|
|
143
143
|
/** Bytes of packed `.data` payload handed back after boot, or null when the
|
|
@@ -303,7 +303,7 @@ export function openSessionChannel(
|
|
|
303
303
|
*
|
|
304
304
|
* The loop below waits for `out/<id>.done` and nothing else. When the
|
|
305
305
|
* program behind the directory DIES, that file is never written, so the
|
|
306
|
-
* call waits forever: `
|
|
306
|
+
* call waits forever: `volter-model-editor status` reports it IN FLIGHT for as long as
|
|
307
307
|
* anyone looks, `blender-start` keeps answering from the host's cached
|
|
308
308
|
* banner, and the harness has no answer to time out against. MEASURED
|
|
309
309
|
* 2026-09-19 three times on the WALI skew -- 507 s, 621 s and 964 s of a
|
|
@@ -404,7 +404,7 @@ export function openSessionChannel(
|
|
|
404
404
|
`Blender is gone, so this call will never be answered: ${ended}. ` +
|
|
405
405
|
'The session keeps its directory and its cached banner, so every later call would ' +
|
|
406
406
|
'wait on the same missing answer; start a new program with `blender-start {fresh: true}` ' +
|
|
407
|
-
'(`
|
|
407
|
+
'(`VOLTER_BLENDER_FRESH_SESSION=1` for the battery harness).',
|
|
408
408
|
);
|
|
409
409
|
await sleep(pollDelay(performance.now() - began));
|
|
410
410
|
}
|
|
@@ -245,9 +245,9 @@ export async function startWaliBlenderEngine(
|
|
|
245
245
|
// `--python-use-system-env` below is what makes Blender read the
|
|
246
246
|
// variable at all.
|
|
247
247
|
PYTHONPATH: `${RESOURCES}/python/lib/numpy313.zip:${RESOURCES}/python/lib/wheels313.zip`,
|
|
248
|
-
|
|
248
|
+
VOLTER_SESSION_ROOT: SESSION_ROOT,
|
|
249
249
|
// THE ARENA'S DOOR ON THIS SKEW. See `readArena`.
|
|
250
|
-
|
|
250
|
+
VOLTER_EXPORT_BUFFER_PATH: ARENA_PATH,
|
|
251
251
|
},
|
|
252
252
|
threadPoolSize: workers.pool,
|
|
253
253
|
// Blender talks to nothing. The editor's own routes are this worker's, not
|
|
@@ -260,13 +260,13 @@ export async function startWaliBlenderEngine(
|
|
|
260
260
|
const say = (text: string) => {
|
|
261
261
|
for (const line of text.split('\n')) {
|
|
262
262
|
if (line === '') continue;
|
|
263
|
-
if (line.startsWith('@@
|
|
263
|
+
if (line.startsWith('@@VOLTER-READY ')) readyLine = line.slice('@@VOLTER-READY '.length);
|
|
264
264
|
// BLENDER'S OWN STREAMS ARE PAGE OUTPUT, NOT EDITOR-CONSOLE CONDITIONS
|
|
265
265
|
// -- the same rule as the standalone engine, and for the same measured
|
|
266
266
|
// reason: only the session's own named conditions belong in the set an
|
|
267
267
|
// agent must drive to zero.
|
|
268
268
|
options.log(
|
|
269
|
-
line.startsWith('@@
|
|
269
|
+
line.startsWith('@@VOLTER-WARN') || line.startsWith('@@VOLTER-ERROR') ? 'error' : 'log',
|
|
270
270
|
line,
|
|
271
271
|
);
|
|
272
272
|
}
|
|
@@ -304,7 +304,7 @@ export async function startWaliBlenderEngine(
|
|
|
304
304
|
// only defensible because the CALLER can degrade VISIBLY; without this
|
|
305
305
|
// the degrade is silent, and before it existed the notice reached a
|
|
306
306
|
// nested worker's devtools console and nothing else.
|
|
307
|
-
onDiagnostic: (message: string) => options.log('error', `@@
|
|
307
|
+
onDiagnostic: (message: string) => options.log('error', `@@VOLTER-ERROR ${message}`),
|
|
308
308
|
},
|
|
309
309
|
)
|
|
310
310
|
.then(
|
|
@@ -316,7 +316,7 @@ export async function startWaliBlenderEngine(
|
|
|
316
316
|
},
|
|
317
317
|
)
|
|
318
318
|
.finally(() => {
|
|
319
|
-
if (ended !== null) options.log('error', `@@
|
|
319
|
+
if (ended !== null) options.log('error', `@@VOLTER-ERROR the Blender program ended: ${ended}`);
|
|
320
320
|
});
|
|
321
321
|
|
|
322
322
|
while (readyLine === null) {
|
|
@@ -352,7 +352,7 @@ export async function startWaliBlenderEngine(
|
|
|
352
352
|
readArena: () => filesystem.readFile(ARENA_PATH),
|
|
353
353
|
bootMs,
|
|
354
354
|
// The module's memory is the program worker's and no door reports it.
|
|
355
|
-
// Null rather than zero: `
|
|
355
|
+
// Null rather than zero: `volter-model-editor status` prints "unreported", which is true,
|
|
356
356
|
// where a zero would be a measurement that was never taken.
|
|
357
357
|
memoryBytes: () => null,
|
|
358
358
|
// There is no packed payload on this skew: Blender's runtime tree is
|
package/browser/rna.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (`bl_rna.properties`) — the ruling's "everything Blender SHOWS about a
|
|
9
9
|
* datablock, we show, in OUR panels, read from the engine through bpy and RNA"
|
|
10
10
|
* (ARCHITECTURE-CORE §Blender north star, "Inspection parity, not editing
|
|
11
|
-
* parity"). Nothing in this file is a
|
|
11
|
+
* parity"). Nothing in this file is a volter invention over RNA: every field is
|
|
12
12
|
* a `PropertyRNA` member, a `bpy.types` name, or an address `path_from_id()`
|
|
13
13
|
* produced.
|
|
14
14
|
*/
|
|
@@ -241,7 +241,7 @@ export interface BlenderRnaWrite {
|
|
|
241
241
|
// session's scene, as rows (WORK.md §Blender in the tab is Blender,
|
|
242
242
|
// "Inspection parity", I3). Every field below is Blender's own — a `TSE_*`
|
|
243
243
|
// element type, an `ICON_*` name `tree_element_get_icon` picks, a restriction
|
|
244
|
-
// column `outliner_draw_restrictbuts` draws — and nothing here is a
|
|
244
|
+
// column `outliner_draw_restrictbuts` draws — and nothing here is a volter
|
|
245
245
|
// invention over the Outliner.
|
|
246
246
|
|
|
247
247
|
/**
|
package/browser/runtime.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface PresentAnswer {
|
|
|
69
69
|
* Read by anything that has to say WHERE a byte came from rather than merely
|
|
70
70
|
* display it — `blender-list-files` answers with it, so the CLI's write-back
|
|
71
71
|
* door can record which session's model a mirrored file was exported from
|
|
72
|
-
* (`packages/
|
|
72
|
+
* (`packages/editor-blender/node/blender-mcp.ts`, class Mirror). Null until the
|
|
73
73
|
* session's first present.
|
|
74
74
|
*/
|
|
75
75
|
export interface PresentedState {
|
|
@@ -186,7 +186,7 @@ export class BlenderRuntime {
|
|
|
186
186
|
// The page console is the editor's ledger (`installEditorConsoleReporting`
|
|
187
187
|
// captures it, source-blind), and this package may import nothing of the
|
|
188
188
|
// editor to say it any other way. Without this line the failure reached
|
|
189
|
-
// a toast and `
|
|
189
|
+
// a toast and `volter-model-editor console` read 0/0 while no Model document could open
|
|
190
190
|
// (measured 2026-09-20 from a registry install).
|
|
191
191
|
console.error(error.message);
|
|
192
192
|
for (const id of [...this.#pending.keys()]) this.#settled(id);
|
package/browser/session.py
CHANGED
|
@@ -117,12 +117,12 @@ import mathutils
|
|
|
117
117
|
import _blender_web
|
|
118
118
|
|
|
119
119
|
# The presenter's refusals, spelled in
|
|
120
|
-
# `packages/
|
|
120
|
+
# `packages/blender-engine/browser/three/blender-runtime-frame.ts`. This is the only
|
|
121
121
|
# place Python reads them, and `Session.present` is what answers them.
|
|
122
122
|
_UNKNOWN_GEOMETRY = "RUNTIME_FRAME_UNKNOWN_GEOMETRY"
|
|
123
123
|
_UNKNOWN_IMAGE = "RUNTIME_FRAME_UNKNOWN_IMAGE"
|
|
124
124
|
|
|
125
|
-
ROOT = os.environ.get("
|
|
125
|
+
ROOT = os.environ.get("VOLTER_SESSION_ROOT", "/work/.volter-session")
|
|
126
126
|
IN = os.path.join(ROOT, "in")
|
|
127
127
|
OUT = os.path.join(ROOT, "out")
|
|
128
128
|
ASK = os.path.join(ROOT, "ask")
|
|
@@ -142,7 +142,7 @@ REPLY = os.path.join(ROOT, "reply")
|
|
|
142
142
|
# whose host cannot reach the module's memory at all
|
|
143
143
|
# (`blender-wali-engine.mts`), where the file is not a fallback but the
|
|
144
144
|
# native door onto the same bytes.
|
|
145
|
-
EXPORT_BUFFER_PATH = os.environ.get("
|
|
145
|
+
EXPORT_BUFFER_PATH = os.environ.get("VOLTER_EXPORT_BUFFER_PATH", "")
|
|
146
146
|
_real_stderr = sys.stderr
|
|
147
147
|
|
|
148
148
|
|
|
@@ -172,7 +172,7 @@ def _prepare_directories():
|
|
|
172
172
|
except FileExistsError:
|
|
173
173
|
pass
|
|
174
174
|
except OSError as error:
|
|
175
|
-
_say("@@
|
|
175
|
+
_say("@@VOLTER-ERROR cannot create %s: %r" % (directory, error))
|
|
176
176
|
try:
|
|
177
177
|
os.chmod(directory, 0o777)
|
|
178
178
|
except OSError:
|
|
@@ -215,7 +215,7 @@ def warn(what):
|
|
|
215
215
|
return
|
|
216
216
|
_WARNED.add(what)
|
|
217
217
|
_WARNINGS.append(what)
|
|
218
|
-
_say("@@
|
|
218
|
+
_say("@@VOLTER-WARN " + what)
|
|
219
219
|
|
|
220
220
|
|
|
221
221
|
# ---------------------------------------------------------------- the world
|
|
@@ -1820,11 +1820,11 @@ _ASK_SEQUENCE = [0]
|
|
|
1820
1820
|
def _drop(path):
|
|
1821
1821
|
"""Remove one of THIS side's own channel files. A failure here means the
|
|
1822
1822
|
ownership rule was broken by the other side, so it is named rather than
|
|
1823
|
-
swallowed -- and named where `
|
|
1823
|
+
swallowed -- and named where `volter-model-editor console` reads it, not into a log."""
|
|
1824
1824
|
try:
|
|
1825
1825
|
os.unlink(path)
|
|
1826
1826
|
except OSError as error:
|
|
1827
|
-
_say("@@
|
|
1827
|
+
_say("@@VOLTER-WARN the session could not remove its own %s: %r" % (path, error))
|
|
1828
1828
|
|
|
1829
1829
|
|
|
1830
1830
|
def ask(payload):
|
|
@@ -1979,7 +1979,7 @@ def _assert_photographed_from(reported, position, target, up):
|
|
|
1979
1979
|
)
|
|
1980
1980
|
|
|
1981
1981
|
|
|
1982
|
-
class
|
|
1982
|
+
class VolterRenderEngine(bpy.types.RenderEngine):
|
|
1983
1983
|
"""The scene's renderer, so `write_still`, `save_render` and Render Result
|
|
1984
1984
|
behave as Blender's own.
|
|
1985
1985
|
|
|
@@ -1989,7 +1989,7 @@ class VgaiRenderEngine(bpy.types.RenderEngine):
|
|
|
1989
1989
|
starts a path trace nobody asked for inside the tab.
|
|
1990
1990
|
"""
|
|
1991
1991
|
|
|
1992
|
-
bl_idname = "
|
|
1992
|
+
bl_idname = "VOLTER_THREE"
|
|
1993
1993
|
bl_label = "three.js"
|
|
1994
1994
|
bl_use_preview = False
|
|
1995
1995
|
|
|
@@ -2087,7 +2087,7 @@ def _register_engine():
|
|
|
2087
2087
|
("BLENDER_WORKBENCH", "Workbench"),
|
|
2088
2088
|
):
|
|
2089
2089
|
existing = _engine_class(identifier)
|
|
2090
|
-
if existing is not None and issubclass(existing,
|
|
2090
|
+
if existing is not None and issubclass(existing, VolterRenderEngine):
|
|
2091
2091
|
made.append(identifier)
|
|
2092
2092
|
continue
|
|
2093
2093
|
if existing is not None:
|
|
@@ -2098,8 +2098,8 @@ def _register_engine():
|
|
|
2098
2098
|
else:
|
|
2099
2099
|
_release_from_owning_addon(existing)
|
|
2100
2100
|
engine = type(
|
|
2101
|
-
"
|
|
2102
|
-
(
|
|
2101
|
+
"Volter" + identifier.title().replace("_", ""),
|
|
2102
|
+
(VolterRenderEngine,),
|
|
2103
2103
|
{"bl_idname": identifier, "bl_label": label, "bl_use_preview": False},
|
|
2104
2104
|
)
|
|
2105
2105
|
try:
|
|
@@ -2115,17 +2115,17 @@ def _register_engine():
|
|
|
2115
2115
|
# in the start reply and refused by name at the render, never a
|
|
2116
2116
|
# warning on every boot.
|
|
2117
2117
|
unavailable.append(identifier)
|
|
2118
|
-
# `
|
|
2118
|
+
# `VOLTER_THREE` is the engine under its own name, registered ONCE: this runs
|
|
2119
2119
|
# again after every file load (the ids have to be retaken), and Blender
|
|
2120
2120
|
# refuses a class it already holds -- which is not a capability anyone
|
|
2121
2121
|
# lost, so it is not a warning.
|
|
2122
|
-
if _engine_class(
|
|
2122
|
+
if _engine_class(VolterRenderEngine.bl_idname) is None:
|
|
2123
2123
|
try:
|
|
2124
|
-
bpy.utils.register_class(
|
|
2124
|
+
bpy.utils.register_class(VolterRenderEngine)
|
|
2125
2125
|
except Exception as error: # noqa: BLE001
|
|
2126
2126
|
warn("could not register the three.js engine: %s" % error)
|
|
2127
|
-
if _engine_class(
|
|
2128
|
-
made.append(
|
|
2127
|
+
if _engine_class(VolterRenderEngine.bl_idname) is not None:
|
|
2128
|
+
made.append(VolterRenderEngine.bl_idname)
|
|
2129
2129
|
return made, unavailable
|
|
2130
2130
|
|
|
2131
2131
|
|
|
@@ -2698,7 +2698,7 @@ def _py_row(target, name, descriptor, owner):
|
|
|
2698
2698
|
from what it answers -- an ID is a POINTER, a sequence of them a
|
|
2699
2699
|
COLLECTION with its count and names, a scalar its scalar. A value no RNA
|
|
2700
2700
|
type covers is not invented into one: the property is dropped, and
|
|
2701
|
-
nothing here is a
|
|
2701
|
+
nothing here is a volter field over Blender's data."""
|
|
2702
2702
|
row = {
|
|
2703
2703
|
"identifier": name,
|
|
2704
2704
|
"name": name,
|
|
@@ -3894,7 +3894,7 @@ def _node_row(node):
|
|
|
3894
3894
|
|
|
3895
3895
|
|
|
3896
3896
|
def _node_tree_of(path, material):
|
|
3897
|
-
"""WHICH TREE. Either an explicit RNA address (so a `
|
|
3897
|
+
"""WHICH TREE. Either an explicit RNA address (so a `volter-model-editor eval` can open a
|
|
3898
3898
|
world's or a group's tree with the engine's own spelling), or a material by
|
|
3899
3899
|
name, or -- given neither -- the active object's active material, which is
|
|
3900
3900
|
what Blender's own Shading header resolves (`space_node.py:89-93`,
|
|
@@ -4152,7 +4152,7 @@ def rna_rig(object_name=None):
|
|
|
4152
4152
|
presented frame carries every object at once, and the presenter has to know
|
|
4153
4153
|
which of its meshes are `THREE.SkinnedMesh`es before it builds them. Asking
|
|
4154
4154
|
per object would be one round trip per mesh; asking by NAME stays available
|
|
4155
|
-
for a `
|
|
4155
|
+
for a `volter-model-editor eval` that wants to read one.
|
|
4156
4156
|
"""
|
|
4157
4157
|
scene_frame = int(bpy.context.scene.frame_current)
|
|
4158
4158
|
if object_name:
|
|
@@ -4796,7 +4796,7 @@ def dispatch(request):
|
|
|
4796
4796
|
if k in request}
|
|
4797
4797
|
if op == "execute":
|
|
4798
4798
|
cause["code"] = str(request.get("code", ""))[:160]
|
|
4799
|
-
_say("@@
|
|
4799
|
+
_say("@@VOLTER-WARN %s removed objects %s (%s); %d remain" % (
|
|
4800
4800
|
op, sorted(removed), json.dumps(cause), len(bpy.data.objects)))
|
|
4801
4801
|
|
|
4802
4802
|
|
|
@@ -4865,14 +4865,14 @@ def _dispatch(request):
|
|
|
4865
4865
|
except Exception as thrown: # noqa: BLE001
|
|
4866
4866
|
# LOUD, because a present that fails leaves the Model document
|
|
4867
4867
|
# showing the state BEFORE this call and nothing else says so.
|
|
4868
|
-
# `@@
|
|
4868
|
+
# `@@VOLTER-PRESENT-FAILED` was not one of the prefixes the engines
|
|
4869
4869
|
# escalate (`blender-emscripten-engine.mts:159`,
|
|
4870
|
-
# `blender-wali-engine.mts:266` raise `@@
|
|
4870
|
+
# `blender-wali-engine.mts:266` raise `@@VOLTER-WARN`/`@@VOLTER-ERROR`
|
|
4871
4871
|
# to the console and nothing else), so it logged at `log` level and
|
|
4872
4872
|
# reached no counter -- measured 2026-09-19 (I4), when a throw in
|
|
4873
4873
|
# the overlay walk froze the viewport through a dozen successful
|
|
4874
|
-
# `blender-execute` calls with `
|
|
4875
|
-
_say("@@
|
|
4874
|
+
# `blender-execute` calls with `volter-model-editor console` silent throughout.
|
|
4875
|
+
_say("@@VOLTER-ERROR the present after this call failed, so the Model document is "
|
|
4876
4876
|
"showing the state before it: " + repr(thrown))
|
|
4877
4877
|
return answer
|
|
4878
4878
|
if op == "scene-info":
|
|
@@ -4915,14 +4915,14 @@ def _dispatch(request):
|
|
|
4915
4915
|
except Exception as thrown: # noqa: BLE001
|
|
4916
4916
|
# LOUD, because a present that fails leaves the Model document
|
|
4917
4917
|
# showing the state BEFORE this call and nothing else says so.
|
|
4918
|
-
# `@@
|
|
4918
|
+
# `@@VOLTER-PRESENT-FAILED` was not one of the prefixes the engines
|
|
4919
4919
|
# escalate (`blender-emscripten-engine.mts:159`,
|
|
4920
|
-
# `blender-wali-engine.mts:266` raise `@@
|
|
4920
|
+
# `blender-wali-engine.mts:266` raise `@@VOLTER-WARN`/`@@VOLTER-ERROR`
|
|
4921
4921
|
# to the console and nothing else), so it logged at `log` level and
|
|
4922
4922
|
# reached no counter -- measured 2026-09-19 (I4), when a throw in
|
|
4923
4923
|
# the overlay walk froze the viewport through a dozen successful
|
|
4924
|
-
# `blender-execute` calls with `
|
|
4925
|
-
_say("@@
|
|
4924
|
+
# `blender-execute` calls with `volter-model-editor console` silent throughout.
|
|
4925
|
+
_say("@@VOLTER-ERROR the present after this call failed, so the Model document is "
|
|
4926
4926
|
"showing the state before it: " + repr(thrown))
|
|
4927
4927
|
return answer
|
|
4928
4928
|
if op == "rna-set":
|
|
@@ -4936,14 +4936,14 @@ def _dispatch(request):
|
|
|
4936
4936
|
except Exception as thrown: # noqa: BLE001
|
|
4937
4937
|
# LOUD, because a present that fails leaves the Model document
|
|
4938
4938
|
# showing the state BEFORE this call and nothing else says so.
|
|
4939
|
-
# `@@
|
|
4939
|
+
# `@@VOLTER-PRESENT-FAILED` was not one of the prefixes the engines
|
|
4940
4940
|
# escalate (`blender-emscripten-engine.mts:159`,
|
|
4941
|
-
# `blender-wali-engine.mts:266` raise `@@
|
|
4941
|
+
# `blender-wali-engine.mts:266` raise `@@VOLTER-WARN`/`@@VOLTER-ERROR`
|
|
4942
4942
|
# to the console and nothing else), so it logged at `log` level and
|
|
4943
4943
|
# reached no counter -- measured 2026-09-19 (I4), when a throw in
|
|
4944
4944
|
# the overlay walk froze the viewport through a dozen successful
|
|
4945
|
-
# `blender-execute` calls with `
|
|
4946
|
-
_say("@@
|
|
4945
|
+
# `blender-execute` calls with `volter-model-editor console` silent throughout.
|
|
4946
|
+
_say("@@VOLTER-ERROR the present after this call failed, so the Model document is "
|
|
4947
4947
|
"showing the state before it: " + repr(thrown))
|
|
4948
4948
|
return answer
|
|
4949
4949
|
if op == "save-document":
|
|
@@ -4971,7 +4971,7 @@ bpy.app.handlers.load_post.append(_load_post)
|
|
|
4971
4971
|
bpy.app.handlers.undo_post.append(_history_post)
|
|
4972
4972
|
bpy.app.handlers.redo_post.append(_history_post)
|
|
4973
4973
|
ENGINES, UNAVAILABLE_ENGINES = _register_engine()
|
|
4974
|
-
_say("@@
|
|
4974
|
+
_say("@@VOLTER-READY " + json.dumps({"blender": bpy.app.version_string, "engines": ENGINES,
|
|
4975
4975
|
"unavailableEngines": UNAVAILABLE_ENGINES}))
|
|
4976
4976
|
|
|
4977
4977
|
# Markers already dispatched, and the answers whose `out/` files this loop
|
|
@@ -5054,7 +5054,7 @@ try:
|
|
|
5054
5054
|
except BaseException as _thrown: # noqa: BLE001
|
|
5055
5055
|
# The one place a channel death can still be named. Without this the
|
|
5056
5056
|
# program ends 0 and the page reports only that Blender is gone.
|
|
5057
|
-
_say("@@
|
|
5057
|
+
_say("@@VOLTER-ERROR the session's channel loop raised %s: %s -- the session is over and "
|
|
5058
5058
|
"every outstanding call will go unanswered. %s"
|
|
5059
5059
|
% (type(_thrown).__name__, _thrown, traceback.format_exc().replace("\n", " | ")))
|
|
5060
5060
|
raise
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ONE PRESENTER, ATTACHED TO ONE BLENDER (WS-AC, cut 3).
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
4
|
+
* `volter_three.py` ships inside our Blender and turns every
|
|
5
5
|
* `bpy.ops.render.render` into an ASK on a directory it owns; this is the
|
|
6
6
|
* other half, for a host that holds that directory as a filesystem in its own
|
|
7
7
|
* realm. It is the whole of what a host must do: hand it the filesystem, the
|
|
@@ -240,7 +240,7 @@ const BONE_SOLID_VERTEX = /* glsl */ `
|
|
|
240
240
|
* ONLY THE `_fragment` HALF: three PREPENDS `colorspace_pars_fragment` to every
|
|
241
241
|
* `ShaderMaterial` already, so including it here declared `LinearTransferOETF`,
|
|
242
242
|
* `sRGBTransferEOTF` and `sRGBTransferOETF` twice and the whole program refused
|
|
243
|
-
* to compile ("function already has a body", measured live in `
|
|
243
|
+
* to compile ("function already has a body", measured live in `volter-model-editor console`).
|
|
244
244
|
*/
|
|
245
245
|
const BONE_SOLID_FRAGMENT = /* glsl */ `
|
|
246
246
|
varying vec3 vColor;
|
|
@@ -127,8 +127,8 @@ export interface ExtrasInput {
|
|
|
127
127
|
* as that draw is done.
|
|
128
128
|
*/
|
|
129
129
|
function standDownInOverridePasses(object: THREE.Object3D): void {
|
|
130
|
-
if (!(object as THREE.Mesh).isMesh || object.userData['
|
|
131
|
-
object.userData['
|
|
130
|
+
if (!(object as THREE.Mesh).isMesh || object.userData['volterOverrideStandDown']) return;
|
|
131
|
+
object.userData['volterOverrideStandDown'] = true;
|
|
132
132
|
const own = object.onBeforeRender.bind(object);
|
|
133
133
|
const ownAfter = object.onAfterRender.bind(object);
|
|
134
134
|
const placed = new THREE.Matrix4();
|
|
@@ -202,7 +202,7 @@ export class ExtrasOverlay {
|
|
|
202
202
|
const previous = this.drawn.get(object.name);
|
|
203
203
|
const stands = input.presented(object.name);
|
|
204
204
|
if (previous?.key === key) {
|
|
205
|
-
for (const part of previous.parts) part.userData['
|
|
205
|
+
for (const part of previous.parts) part.userData['volterPicksAs'] = stands;
|
|
206
206
|
continue;
|
|
207
207
|
}
|
|
208
208
|
if (previous) this.remove(previous.parts);
|
|
@@ -214,7 +214,7 @@ export class ExtrasOverlay {
|
|
|
214
214
|
else if (empty) this.empty(empty, matrix, color, picture);
|
|
215
215
|
const parts = group.children.filter((child) => !before.has(child));
|
|
216
216
|
for (const part of parts) {
|
|
217
|
-
part.userData['
|
|
217
|
+
part.userData['volterPicksAs'] = stands;
|
|
218
218
|
part.traverse(standDownInOverridePasses);
|
|
219
219
|
}
|
|
220
220
|
this.drawn.set(object.name, { key, parts });
|
|
@@ -230,7 +230,7 @@ export class ExtrasOverlay {
|
|
|
230
230
|
for (const part of parts) {
|
|
231
231
|
part.removeFromParent();
|
|
232
232
|
(part as THREE.Mesh).geometry?.dispose();
|
|
233
|
-
if (part.userData['
|
|
233
|
+
if (part.userData['volterOwnMaterial']) ((part as THREE.Mesh).material as THREE.Material).dispose();
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -603,13 +603,13 @@ export class ExtrasOverlay {
|
|
|
603
603
|
)
|
|
604
604
|
.replace('#include <alphatest_fragment>', blends ? '#include <alphatest_fragment>' : '#include <alphatest_fragment>\ndiffuseColor.a = 1.0;');
|
|
605
605
|
};
|
|
606
|
-
material.customProgramCacheKey = () => `
|
|
606
|
+
material.customProgramCacheKey = () => `volter-blender-image:${settings.premultiplied}:${blends}`;
|
|
607
607
|
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
|
|
608
608
|
quad.matrixAutoUpdate = false;
|
|
609
609
|
quad.matrix.copy(pose);
|
|
610
610
|
quad.renderOrder = settings.depth === 'FRONT' ? 1000 : settings.depth === 'BACK' ? -1000 : 0;
|
|
611
611
|
// Its material is its own (the picture and its opacity), freed with the quad.
|
|
612
|
-
quad.userData['
|
|
612
|
+
quad.userData['volterOwnMaterial'] = true;
|
|
613
613
|
hideUnless(quad, true);
|
|
614
614
|
this.empties.add(quad);
|
|
615
615
|
}
|
|
@@ -264,7 +264,7 @@ export const SOLID_LIGHTS: readonly {
|
|
|
264
264
|
* and AgX within 3, so the curve is NOT what row 1's 13-vs-30 spread was made
|
|
265
265
|
* of and the stage keeps its own operator).
|
|
266
266
|
*
|
|
267
|
-
* THE ROUNDS, on a `model-editor create` scaffold driven through `
|
|
267
|
+
* THE ROUNDS, on a `model-editor create` scaffold driven through `volter-model-editor
|
|
268
268
|
* screenshot editor`, 30x30 means (std 0 — every patch inside one flat face)
|
|
269
269
|
* of the factory cube's three visible faces against Blender 5.2's own frame,
|
|
270
270
|
* (141,143,145) top / (129,131,131) left / (111,112,113) front:
|
|
@@ -634,7 +634,7 @@ export class BlenderRuntimeView {
|
|
|
634
634
|
* visibility belongs to the viewport's Helpers menu rather than to this
|
|
635
635
|
* presenter, and the door for that is the host's own — a document hands
|
|
636
636
|
* each group to its stage's `setHelper(kind, object)`
|
|
637
|
-
* (`@
|
|
637
|
+
* (`@volter/editor-sdk/host`, `EditorHostStage`), which marks it
|
|
638
638
|
* `editorHelper`, keeps it out of the hierarchy and the raycast, and turns
|
|
639
639
|
* it on and off with the kind's checkbox. `blender-runtime.document.tsx`
|
|
640
640
|
* is where they are handed over.
|
|
@@ -912,7 +912,7 @@ export class BlenderRuntimeView {
|
|
|
912
912
|
/**
|
|
913
913
|
* BLENDER'S MODE, PUBLISHED ON THE DOCUMENT'S OWN CONTEXT.
|
|
914
914
|
*
|
|
915
|
-
* `
|
|
915
|
+
* `volter.stage.mode` (U6's context key, `editor-host-door.ts`'s `stage()`) had
|
|
916
916
|
* no reporter at all: Edit Mesh went with the mesh kit and the Model document
|
|
917
917
|
* had no mode strip, so every `when` clause that reads it was answering
|
|
918
918
|
* `null`. The engine HAS a mode — `bpy.context.mode`, and `Object.mode` on the
|
|
@@ -1005,9 +1005,9 @@ export class BlenderRuntimeView {
|
|
|
1005
1005
|
this.weightRoot.name = 'BlenderWeights';
|
|
1006
1006
|
this.cursorRoot.name = 'Blender3DCursor';
|
|
1007
1007
|
this.cameraExtrasRoot.name = 'BlenderCameraExtras';
|
|
1008
|
-
// The extras are picked as the objects they draw (`userData.
|
|
1008
|
+
// The extras are picked as the objects they draw (`userData.volterPicksAs` on each part).
|
|
1009
1009
|
for (const root of [this.cameraExtrasRoot, this.lightExtrasRoot, this.emptyExtrasRoot])
|
|
1010
|
-
root.userData['
|
|
1010
|
+
root.userData['volterPickable'] = true;
|
|
1011
1011
|
this.lightExtrasRoot.name = 'BlenderLightExtras';
|
|
1012
1012
|
this.emptyExtrasRoot.name = 'BlenderEmptyExtras';
|
|
1013
1013
|
}
|
|
@@ -1768,8 +1768,8 @@ export class BlenderRuntimeView {
|
|
|
1768
1768
|
object.name = obj.name;
|
|
1769
1769
|
// A camera, a light and an empty are drawn by the overlay's extras, selection colour and
|
|
1770
1770
|
// all (`blender-runtime-extras.ts`); the editor's own marks for them stand down.
|
|
1771
|
-
if (obj.type === 'CAMERA' || obj.type === 'LIGHT' || obj.type === 'EMPTY') object.userData['
|
|
1772
|
-
else delete object.userData['
|
|
1771
|
+
if (obj.type === 'CAMERA' || obj.type === 'LIGHT' || obj.type === 'EMPTY') object.userData['volterOwnOverlay'] = true;
|
|
1772
|
+
else delete object.userData['volterOwnOverlay'];
|
|
1773
1773
|
object.matrixAutoUpdate = false;
|
|
1774
1774
|
object.matrix.set(...(obj.matrix.flat() as Parameters<THREE.Matrix4['set']>));
|
|
1775
1775
|
if (obj.parent) {
|
|
@@ -1804,7 +1804,7 @@ export class BlenderRuntimeView {
|
|
|
1804
1804
|
if (light.parent !== object) object.add(light);
|
|
1805
1805
|
// Blender's overlay draws this light (`blender-runtime-extras.ts`); the editor's own
|
|
1806
1806
|
// light helper stands down for it.
|
|
1807
|
-
light.userData['
|
|
1807
|
+
light.userData['volterOwnOverlay'] = true;
|
|
1808
1808
|
aimLight(light, object);
|
|
1809
1809
|
this.lights.set(obj.light, light);
|
|
1810
1810
|
}
|
|
@@ -48,7 +48,7 @@ vec4 wbWrapped(vec4 NL, vec4 w) {
|
|
|
48
48
|
return clamp((NL + w) / (w1 * w1), 0.0, 1.0);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
vec3
|
|
51
|
+
vec3 volterWorkbenchLighting(vec3 baseColor, vec3 N, vec3 I) {
|
|
52
52
|
vec3 diffuseColor = mix(baseColor, vec3(0.0), wbMetallic);
|
|
53
53
|
vec3 specularColor = mix(vec3(0.05), baseColor, wbMetallic);
|
|
54
54
|
vec3 R = -reflect(I, N);
|
|
@@ -94,7 +94,7 @@ export function workbenchMaterial(display: ViewportDisplay, side: THREE.Side): T
|
|
|
94
94
|
const [r, g, b] = display.color;
|
|
95
95
|
const material = new THREE.MeshLambertMaterial({ side });
|
|
96
96
|
material.color.setRGB(r, g, b, THREE.LinearSRGBColorSpace);
|
|
97
|
-
material.name = '
|
|
97
|
+
material.name = 'volter:blender-solid';
|
|
98
98
|
material.onBeforeCompile = (shader) => {
|
|
99
99
|
shader.uniforms['wbDir'] = { value: LIGHT_DIRECTIONS };
|
|
100
100
|
shader.uniforms['wbDiffuse'] = { value: LIGHT_DIFFUSE };
|
|
@@ -107,9 +107,9 @@ export function workbenchMaterial(display: ViewportDisplay, side: THREE.Side): T
|
|
|
107
107
|
.replace(
|
|
108
108
|
'#include <opaque_fragment>',
|
|
109
109
|
// The view direction toward the eye, as Blender's `I`: constant in an orthographic view.
|
|
110
|
-
'outgoingLight =
|
|
110
|
+
'outgoingLight = volterWorkbenchLighting(diffuseColor.rgb, normal, isOrthographic ? vec3(0.0, 0.0, 1.0) : normalize(vViewPosition));\n#include <opaque_fragment>',
|
|
111
111
|
);
|
|
112
112
|
};
|
|
113
|
-
material.customProgramCacheKey = () => '
|
|
113
|
+
material.customProgramCacheKey = () => 'volter-blender-workbench';
|
|
114
114
|
return material;
|
|
115
115
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and the modules beside it) plus one act: point a camera at the translated
|
|
6
6
|
* scene, render into a scene-linear target, read the pixels, run Blender's
|
|
7
7
|
* display transform, answer. The editor performs that act with its own stage
|
|
8
|
-
* and renderer (`packages/blender/host/blender-runtime-host.ts`), which is why
|
|
8
|
+
* and renderer (`packages/editor-blender/host/blender-runtime-host.ts`), which is why
|
|
9
9
|
* the editor never imports THIS file. A host that has no renderer — a bare
|
|
10
10
|
* page with `@volter/browser-wali`, an Emscripten page, the substrate
|
|
11
11
|
* workbench — attaches one of these to the Blender it started, and the guest's
|
package/browser/three/release.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* to the Blender it started:
|
|
10
10
|
*
|
|
11
11
|
* const { createPresenter, attachPresenter } = await import(url);
|
|
12
|
-
* const detach = attachPresenter(filesystem, "/tmp/
|
|
12
|
+
* const detach = attachPresenter(filesystem, "/tmp/volter-presenter", createPresenter());
|
|
13
13
|
*
|
|
14
14
|
* NO HOST BUNDLES THIS. The module is GPL because it is part of the Blender
|
|
15
15
|
* release; a host loads it the way it loads the wasm -- by URL, under a pinned
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
THE RENDER OVERRIDE IS NOT THE EDITOR'S. `session.py` is the editor tab's
|
|
4
4
|
long-lived session and carries this same override for the session it owns;
|
|
5
5
|
this file is the other delivery of it -- a Blender STARTUP module that ships in
|
|
6
|
-
the pack's resource tree (`scripts/startup/
|
|
6
|
+
the pack's resource tree (`scripts/startup/volter_three.py`), so a bare
|
|
7
7
|
`blender -b ... -a` on ANY surface renders with three.js too. Blender imports
|
|
8
8
|
every module in `scripts/startup` and calls its `register()`.
|
|
9
9
|
|
|
@@ -56,14 +56,14 @@ import bpy
|
|
|
56
56
|
# `{offset, length, dtype, count, stride}` in a small JSON frame.
|
|
57
57
|
import _blender_web
|
|
58
58
|
|
|
59
|
-
ROOT = os.environ.get("
|
|
59
|
+
ROOT = os.environ.get("VOLTER_PRESENTER_ROOT", "/tmp/volter-presenter")
|
|
60
60
|
ASK = os.path.join(ROOT, "ask")
|
|
61
61
|
REPLY = os.path.join(ROOT, "reply")
|
|
62
62
|
# WHERE THE EXPORT DOOR LEAVES THE ARENA. The host cannot reach this program's
|
|
63
63
|
# linear memory, so the arena crosses as a file -- the door writes it when
|
|
64
64
|
# `buffer_path` is set, and the host reads it beside the ask.
|
|
65
65
|
EXPORT_BUFFER_PATH = os.environ.get(
|
|
66
|
-
"
|
|
66
|
+
"VOLTER_EXPORT_BUFFER_PATH", os.path.join(ROOT, "arena.bin")
|
|
67
67
|
)
|
|
68
68
|
# THE FRAME'S IDENTITY IS A PAIR, `(session, revision)`, and the presenter
|
|
69
69
|
# refuses a revision that went backwards under a name it already holds. One
|
|
@@ -78,7 +78,7 @@ EXPORT_BUFFER_PATH = os.environ.get(
|
|
|
78
78
|
# 2026-09-20 in the substrate tab; the first run on a freshly loaded page
|
|
79
79
|
# always worked, which is what hid it). The start time is what makes the name
|
|
80
80
|
# this process's own.
|
|
81
|
-
SESSION = "
|
|
81
|
+
SESSION = "volter-three:%d-%d" % (os.getpid(), time.time_ns())
|
|
82
82
|
HELLO_TIMEOUT_SECONDS = 2.0
|
|
83
83
|
POLL_SECONDS = 0.002
|
|
84
84
|
|
|
@@ -115,14 +115,14 @@ def warn(what):
|
|
|
115
115
|
return
|
|
116
116
|
_WARNED.add(what)
|
|
117
117
|
_WARNINGS.append(what)
|
|
118
|
-
_say("@@
|
|
118
|
+
_say("@@VOLTER-WARN " + what)
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
def _drop(path):
|
|
122
122
|
try:
|
|
123
123
|
os.unlink(path)
|
|
124
124
|
except OSError as error:
|
|
125
|
-
_say("@@
|
|
125
|
+
_say("@@VOLTER-WARN volter_three could not remove its own %s: %r" % (path, error))
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def _prepare_directories():
|
|
@@ -130,7 +130,7 @@ def _prepare_directories():
|
|
|
130
130
|
try:
|
|
131
131
|
os.makedirs(directory, exist_ok=True)
|
|
132
132
|
except OSError as error:
|
|
133
|
-
_say("@@
|
|
133
|
+
_say("@@VOLTER-ERROR volter_three cannot create %s: %r" % (directory, error))
|
|
134
134
|
try:
|
|
135
135
|
os.chmod(directory, 0o777)
|
|
136
136
|
except OSError:
|
|
@@ -371,7 +371,7 @@ def _assert_photographed_from(reported, position, target, up):
|
|
|
371
371
|
)
|
|
372
372
|
|
|
373
373
|
|
|
374
|
-
class
|
|
374
|
+
class VolterRenderEngine(bpy.types.RenderEngine):
|
|
375
375
|
"""The scene's renderer, so `write_still`, `save_render` and Render Result
|
|
376
376
|
behave as Blender's own.
|
|
377
377
|
|
|
@@ -381,7 +381,7 @@ class VgaiRenderEngine(bpy.types.RenderEngine):
|
|
|
381
381
|
starts a path trace nobody asked for.
|
|
382
382
|
"""
|
|
383
383
|
|
|
384
|
-
bl_idname = "
|
|
384
|
+
bl_idname = "VOLTER_THREE"
|
|
385
385
|
bl_label = "three.js"
|
|
386
386
|
bl_use_preview = False
|
|
387
387
|
|
|
@@ -471,7 +471,7 @@ def _register_engine():
|
|
|
471
471
|
("BLENDER_WORKBENCH", "Workbench"),
|
|
472
472
|
):
|
|
473
473
|
existing = _engine_class(identifier)
|
|
474
|
-
if existing is not None and issubclass(existing,
|
|
474
|
+
if existing is not None and issubclass(existing, VolterRenderEngine):
|
|
475
475
|
made.append(identifier)
|
|
476
476
|
continue
|
|
477
477
|
if existing is not None:
|
|
@@ -482,8 +482,8 @@ def _register_engine():
|
|
|
482
482
|
else:
|
|
483
483
|
_release_from_owning_addon(existing)
|
|
484
484
|
engine = type(
|
|
485
|
-
"
|
|
486
|
-
(
|
|
485
|
+
"Volter" + identifier.title().replace("_", ""),
|
|
486
|
+
(VolterRenderEngine,),
|
|
487
487
|
{"bl_idname": identifier, "bl_label": label, "bl_use_preview": False},
|
|
488
488
|
)
|
|
489
489
|
try:
|
|
@@ -496,17 +496,17 @@ def _register_engine():
|
|
|
496
496
|
# keeps `BLENDER_EEVEE` and `BLENDER_WORKBENCH`: a fact about the
|
|
497
497
|
# build, not a warning on every boot.
|
|
498
498
|
unavailable.append(identifier)
|
|
499
|
-
# `
|
|
499
|
+
# `VOLTER_THREE` is the engine under its own name, registered ONCE: this runs
|
|
500
500
|
# again after every file load (the ids have to be retaken), and Blender
|
|
501
501
|
# refuses a class it already holds -- which is not a capability anyone
|
|
502
502
|
# lost, so it is not a warning.
|
|
503
|
-
if _engine_class(
|
|
503
|
+
if _engine_class(VolterRenderEngine.bl_idname) is None:
|
|
504
504
|
try:
|
|
505
|
-
bpy.utils.register_class(
|
|
505
|
+
bpy.utils.register_class(VolterRenderEngine)
|
|
506
506
|
except Exception as error: # noqa: BLE001
|
|
507
507
|
warn("could not register the three.js engine: %s" % error)
|
|
508
|
-
if _engine_class(
|
|
509
|
-
made.append(
|
|
508
|
+
if _engine_class(VolterRenderEngine.bl_idname) is not None:
|
|
509
|
+
made.append(VolterRenderEngine.bl_idname)
|
|
510
510
|
return made, unavailable
|
|
511
511
|
|
|
512
512
|
|
|
@@ -518,7 +518,7 @@ def _adopt():
|
|
|
518
518
|
_register_engine()
|
|
519
519
|
for scene in bpy.data.scenes:
|
|
520
520
|
try:
|
|
521
|
-
scene.render.engine = "
|
|
521
|
+
scene.render.engine = "VOLTER_THREE"
|
|
522
522
|
except (TypeError, AttributeError) as error:
|
|
523
523
|
warn("scene %s kept its own render engine: %s" % (scene.name, error))
|
|
524
524
|
|
|
@@ -536,7 +536,7 @@ def register():
|
|
|
536
536
|
_prepare_directories()
|
|
537
537
|
if ask({"op": "hello"}, timeout=HELLO_TIMEOUT_SECONDS) is None:
|
|
538
538
|
_say(
|
|
539
|
-
"
|
|
539
|
+
"volter: rendering with Cycles — no presenter answered at %s within 2 s"
|
|
540
540
|
% ROOT
|
|
541
541
|
)
|
|
542
542
|
return
|
package/browser/worker.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* picks up BETWEEN calls, never mid-call), and carries the bytes to the
|
|
28
28
|
* project through `/__editor/blender-document` BEFORE acknowledging the edit.
|
|
29
29
|
*
|
|
30
|
-
* WHY THE CARRY IS NOT THE MIRROR'S JOB (`
|
|
30
|
+
* WHY THE CARRY IS NOT THE MIRROR'S JOB (`volter blender-mcp`, class Mirror):
|
|
31
31
|
* the Mirror is pull-based and runs only after an `execute_blender_code`, so
|
|
32
32
|
* a document saved after the LAST call of a modeling session
|
|
33
33
|
* would never leave the worker — which is exactly the state this closes
|
|
@@ -172,7 +172,7 @@ async function saveDocument(): Promise<void> {
|
|
|
172
172
|
throw new Error(`The Blender document ${relative} was not written to the project: ${describeThrown(error)}`);
|
|
173
173
|
}
|
|
174
174
|
setDocumentDirty(false);
|
|
175
|
-
log('log', `@@
|
|
175
|
+
log('log', `@@VOLTER-DOCUMENT ${JSON.stringify({ path: relative, bytes: bytes.length })}`);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
async function startBlender(project: string, document?: string): Promise<unknown> {
|
|
@@ -286,7 +286,7 @@ async function start(project: string, document?: string): Promise<unknown> {
|
|
|
286
286
|
throw new Error(
|
|
287
287
|
'Headless Blender is not served by this editor, so there is no modeling engine: ' +
|
|
288
288
|
`${served.missing.join('; ')}. The engine is Blender compiled to WebAssembly ` +
|
|
289
|
-
'(packages/blender-engine/wasm, or the directory
|
|
289
|
+
'(packages/blender-engine/wasm, or the directory VOLTER_BLENDER_WASM_DIR names); nothing stands in for it.',
|
|
290
290
|
);
|
|
291
291
|
projectRoot = project;
|
|
292
292
|
return startBlender(project, document);
|