@volter/editor-blender 0.1.126 → 0.1.128
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.
|
@@ -328,7 +328,7 @@ export default function BlenderModelDocument(props: ToolContributionProps) {
|
|
|
328
328
|
const requestOpen = () => {
|
|
329
329
|
if (cancelled || queued) return;
|
|
330
330
|
queued = true;
|
|
331
|
-
|
|
331
|
+
setTimeout(() => { queued = false; void open(); }, 0);
|
|
332
332
|
};
|
|
333
333
|
const unsubscribe = documents.subscribe(requestOpen);
|
|
334
334
|
requestOpen();
|
|
@@ -432,6 +432,11 @@ function BlenderModelViewport(props: ToolContributionProps) {
|
|
|
432
432
|
() => false,
|
|
433
433
|
);
|
|
434
434
|
const [played, setPlayed] = useState<ReturnType<AreaView['detach']> | null>(null);
|
|
435
|
+
const playAspect = useSyncExternalStore(
|
|
436
|
+
subscribeDocumentPlayExtensions,
|
|
437
|
+
() => (documentId ? documentPlayExtension('model')?.aspectRatio?.(documentId) ?? null : null),
|
|
438
|
+
() => null,
|
|
439
|
+
);
|
|
435
440
|
const [playedReady, setPlayedReady] = useState(false);
|
|
436
441
|
useEffect(() => {
|
|
437
442
|
if (!playing || !documentId) return;
|
|
@@ -482,8 +487,14 @@ function BlenderModelViewport(props: ToolContributionProps) {
|
|
|
482
487
|
<BlenderViewportArea {...props} view={view} main />
|
|
483
488
|
</div>
|
|
484
489
|
{game && (
|
|
485
|
-
<div key="play" style={{ position: 'absolute', inset: 12 }} data-testid="blender-play-area">
|
|
490
|
+
<div key="play" style={{ position: 'absolute', inset: 12, containerType: 'size' }} data-testid="blender-play-area">
|
|
491
|
+
<div data-volter-play-frame style={playAspect === null ? { position: 'absolute', inset: 0 } : {
|
|
492
|
+
position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)',
|
|
493
|
+
width: `min(100cqw, calc(100cqh * ${playAspect}))`,
|
|
494
|
+
height: `min(100cqh, calc(100cqw / ${playAspect}))`,
|
|
495
|
+
}}>
|
|
486
496
|
<BlenderViewportArea {...props} documentId={`${documentId}#play`} view={game.view} main={false} playing onPlayReady={() => setPlayedReady(true)} onPlayReturn={() => setPlayedReady(false)} />
|
|
497
|
+
</div>
|
|
487
498
|
</div>
|
|
488
499
|
)}
|
|
489
500
|
{second && (
|
|
@@ -694,7 +705,7 @@ function BlenderViewportArea({
|
|
|
694
705
|
preparing = true;
|
|
695
706
|
orbit = stage.rig().orbit;
|
|
696
707
|
orbit.enabled = false;
|
|
697
|
-
void view.prepareRendered(stage.rig().drawCamera()).then(() => {
|
|
708
|
+
void view.prepareRendered(stage.rig().drawCamera(), 'render').then(() => {
|
|
698
709
|
if (stopped) return;
|
|
699
710
|
stopScript = documentPlayExtension('model')?.run({
|
|
700
711
|
documentId: modelId,
|
|
@@ -864,7 +875,7 @@ function BlenderViewportArea({
|
|
|
864
875
|
* BLENDER'S RENDERED SHADING IS THE SCENE'S OWN LIGHT. When this stage's view lights by the
|
|
865
876
|
* `scene` (the Rendered shading cell, or the Lighting row's Scene), the presenter holds the viewport
|
|
866
877
|
* in the lighting its render photographs with (`BlenderRuntimeView.holdRendered`): the
|
|
867
|
-
* scene's lights, its World
|
|
878
|
+
* scene's lights, its World and shadows, through the camera the stage draws
|
|
868
879
|
* with, re-applied from the stage's frame loop when that camera or the World changes. Any
|
|
869
880
|
* other source returns it to modeling. The stage itself draws a `scene` source unlit
|
|
870
881
|
* (`standard-viewport-dressing.ts`), because Blender's lights live in the presenter.
|
|
@@ -889,7 +900,9 @@ function BlenderViewportArea({
|
|
|
889
900
|
const preview = drawMode === 'preview';
|
|
890
901
|
const sceneLighting = preview ? { world: lighting.source === 'scene', lights: lighting.preview.sceneLights } : undefined;
|
|
891
902
|
const needsScene = lighting.source === 'scene' || (preview && lighting.preview.sceneLights);
|
|
892
|
-
|
|
903
|
+
// Shading changes lighting, not which objects an editing viewport shows.
|
|
904
|
+
// Only the detached game uses render visibility, as a clean capture does.
|
|
905
|
+
view.holdRendered(needsScene && stage ? drawCamera : null, sceneLighting, playing ? 'render' : 'viewport');
|
|
893
906
|
// BLENDER'S SOLID IS BLENDER'S OWN FUNCTION: while the stage lights by Blender's studio, the
|
|
894
907
|
// presenter draws every surface by it (`blender-workbench-material.ts`).
|
|
895
908
|
view.setWorkbench(lighting.source === 'studio' && lighting.studioPreset === DOCUMENT_STUDIO_PRESET.id);
|
|
@@ -904,7 +917,7 @@ function BlenderViewportArea({
|
|
|
904
917
|
view.holdRendered(null);
|
|
905
918
|
view.setWorkbench(false);
|
|
906
919
|
};
|
|
907
|
-
}, [documentId, view]);
|
|
920
|
+
}, [documentId, view, playing]);
|
|
908
921
|
/**
|
|
909
922
|
* SHIFT+RIGHT-CLICK PLACES THE 3D CURSOR, Blender's own chord for
|
|
910
923
|
* `view3d.cursor3d` (`blender_default.py`, `params.cursor_set_event`). The
|
|
@@ -976,6 +989,7 @@ function BlenderViewportArea({
|
|
|
976
989
|
>
|
|
977
990
|
<Surface
|
|
978
991
|
active={active ?? true}
|
|
992
|
+
fillContainer={playing}
|
|
979
993
|
documentId={documentId}
|
|
980
994
|
sourcePath={blend ?? 'blender:runtime'}
|
|
981
995
|
displayName={document?.label ?? 'Model'}
|
package/dist-node/serving.mjs
CHANGED
|
@@ -560,8 +560,12 @@ function blenderRoutesPlugin(services) {
|
|
|
560
560
|
}
|
|
561
561
|
res.setHeader("content-type", "application/octet-stream");
|
|
562
562
|
res.setHeader("content-length", String(size));
|
|
563
|
+
if (size === 0) {
|
|
564
|
+
res.end();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
563
567
|
await new Promise((resolve) => {
|
|
564
|
-
const stream = createReadStream2(absolute);
|
|
568
|
+
const stream = createReadStream2(absolute, { start: 0, end: size - 1 });
|
|
565
569
|
stream.once("error", () => {
|
|
566
570
|
res.destroy();
|
|
567
571
|
resolve();
|
|
@@ -901,16 +901,26 @@ async function runtimeView(): Promise<RuntimeView> {
|
|
|
901
901
|
// command (3 s of boot, then the full wait).
|
|
902
902
|
const deadline = Date.now() + 15_000;
|
|
903
903
|
let id = presentationDocumentId();
|
|
904
|
+
let publication = 'no context';
|
|
904
905
|
for (;;) {
|
|
905
906
|
id = presentationDocumentId();
|
|
906
907
|
const published = await documents.waitForContext(id, Math.max(0, Math.min(250, deadline - Date.now())));
|
|
907
908
|
if (isRuntimeView(published)) return published;
|
|
909
|
+
if (published !== undefined) {
|
|
910
|
+
const required = ['applyFrame', 'stageFrame', 'snapshot', 'captureSnapshot', 'recordPresentation', 'recordPhotograph'];
|
|
911
|
+
const handle = published as Record<string, unknown> | null;
|
|
912
|
+
publication = `${typeof published} context missing ${required.filter(method => typeof handle?.[method] !== 'function').join(', ')}`;
|
|
913
|
+
// An incompatible published context resolves waitForContext immediately.
|
|
914
|
+
// Yield so a pending document commit can replace it; a microtask loop
|
|
915
|
+
// otherwise prevents that publication for the whole timeout window.
|
|
916
|
+
await new Promise<void>(resolve => setTimeout(resolve, Math.max(0, Math.min(25, deadline - Date.now()))));
|
|
917
|
+
}
|
|
908
918
|
if (Date.now() >= deadline) break;
|
|
909
919
|
}
|
|
910
920
|
throw new Error(
|
|
911
921
|
`The Blender Model document is not open, or the open one is not a Model this engine can present ` +
|
|
912
922
|
`to (it must answer stageFrame, applyFrame, captureSnapshot, recordPresentation and recordPhotograph): nothing ` +
|
|
913
|
-
`published a presentable view as ${id} within 15 s (bound model: ${boundModel?.documentId ?? 'none'}). ` +
|
|
923
|
+
`published a presentable view as ${id} within 15 s (bound model: ${boundModel?.documentId ?? 'none'}; last publication: ${publication}). ` +
|
|
914
924
|
'Open the Model document first (`volter blender-mcp` opens it before its first call).',
|
|
915
925
|
);
|
|
916
926
|
}
|
package/node/blender-mcp.ts
CHANGED
|
@@ -304,7 +304,8 @@ export async function serveBlenderMcp(
|
|
|
304
304
|
// it goes rendered one, wrote it into the project and ran the OS `open` on it: Preview started
|
|
305
305
|
// outside the editor. The editor already shows a picture as one of its documents.
|
|
306
306
|
const photographs = `A photograph is shown to the person in the editor, never in another app: get_viewport_screenshot returns one, and an image file written inside the project opens as an editor document with \`npx ${command} eval 'editor.openAsset("<project-relative path>")'\`. Do not run the OS \`open\` on it.`;
|
|
307
|
-
const
|
|
307
|
+
const filesystem = `Project files are mounted in Blender at the project's actual absolute path: ${project}. Use that root for image loads, saves and exports; /project is not an alias.`;
|
|
308
|
+
const instructions = `${runtimeIdentity}\n${filesystem}\n${photographs}`;
|
|
308
309
|
const tools = (blenderTools as ToolShape[]).map((tool) =>
|
|
309
310
|
tool.name === 'execute_blender_code'
|
|
310
311
|
? { ...tool, description: `${instructions}\n${tool.description}` }
|
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.128",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
73
|
-
"@volter/blender-engine": "0.1.
|
|
74
|
-
"@volter/editor-threejs": "0.5.
|
|
73
|
+
"@volter/blender-engine": "0.1.126",
|
|
74
|
+
"@volter/editor-threejs": "0.5.189",
|
|
75
75
|
"zod": "^4.3.6"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@volter/editor-live": "0.5.
|
|
79
|
-
"@volter/editor-project": "0.5.
|
|
80
|
-
"@volter/editor-sdk": "0.5.
|
|
78
|
+
"@volter/editor-live": "0.5.189",
|
|
79
|
+
"@volter/editor-project": "0.5.189",
|
|
80
|
+
"@volter/editor-sdk": "0.5.189",
|
|
81
81
|
"react": "^19.0.0",
|
|
82
82
|
"react-dom": "^19.0.0",
|
|
83
83
|
"three": "^0.180.0",
|
|
@@ -333,8 +333,10 @@ export function blenderRoutesPlugin(services: ProjectServingServices): Plugin {
|
|
|
333
333
|
}
|
|
334
334
|
res.setHeader('content-type', 'application/octet-stream');
|
|
335
335
|
res.setHeader('content-length', String(size));
|
|
336
|
+
if (size === 0) { res.end(); return; }
|
|
336
337
|
await new Promise<void>((resolve) => {
|
|
337
|
-
|
|
338
|
+
// Bound an append-only file to the exact response length advertised above.
|
|
339
|
+
const stream = createReadStream(absolute, { start: 0, end: size - 1 });
|
|
338
340
|
stream.once('error', () => {
|
|
339
341
|
res.destroy();
|
|
340
342
|
resolve();
|