@volter/editor-blender 0.1.127 → 0.1.129
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.
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type BlenderRuntimeView,
|
|
31
31
|
blenderModelView,
|
|
32
32
|
} from '@volter/blender-engine/browser/three/blender-runtime-view';
|
|
33
|
+
import { ownedSurfaceMaterial } from '@volter/blender-engine/browser/three/blender-physical-material';
|
|
33
34
|
import type { ToolContributionProps, ToolDocumentToolbar } from '@volter/editor-sdk/contributions';
|
|
34
35
|
import { editorHost } from '@volter/editor-sdk/host';
|
|
35
36
|
import {
|
|
@@ -328,7 +329,7 @@ export default function BlenderModelDocument(props: ToolContributionProps) {
|
|
|
328
329
|
const requestOpen = () => {
|
|
329
330
|
if (cancelled || queued) return;
|
|
330
331
|
queued = true;
|
|
331
|
-
|
|
332
|
+
setTimeout(() => { queued = false; void open(); }, 0);
|
|
332
333
|
};
|
|
333
334
|
const unsubscribe = documents.subscribe(requestOpen);
|
|
334
335
|
requestOpen();
|
|
@@ -432,6 +433,11 @@ function BlenderModelViewport(props: ToolContributionProps) {
|
|
|
432
433
|
() => false,
|
|
433
434
|
);
|
|
434
435
|
const [played, setPlayed] = useState<ReturnType<AreaView['detach']> | null>(null);
|
|
436
|
+
const playAspect = useSyncExternalStore(
|
|
437
|
+
subscribeDocumentPlayExtensions,
|
|
438
|
+
() => (documentId ? documentPlayExtension('model')?.aspectRatio?.(documentId) ?? null : null),
|
|
439
|
+
() => null,
|
|
440
|
+
);
|
|
435
441
|
const [playedReady, setPlayedReady] = useState(false);
|
|
436
442
|
useEffect(() => {
|
|
437
443
|
if (!playing || !documentId) return;
|
|
@@ -482,8 +488,14 @@ function BlenderModelViewport(props: ToolContributionProps) {
|
|
|
482
488
|
<BlenderViewportArea {...props} view={view} main />
|
|
483
489
|
</div>
|
|
484
490
|
{game && (
|
|
485
|
-
<div key="play" style={{ position: 'absolute', inset: 12 }} data-testid="blender-play-area">
|
|
491
|
+
<div key="play" style={{ position: 'absolute', inset: 12, containerType: 'size' }} data-testid="blender-play-area">
|
|
492
|
+
<div data-volter-play-frame style={playAspect === null ? { position: 'absolute', inset: 0 } : {
|
|
493
|
+
position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)',
|
|
494
|
+
width: `min(100cqw, calc(100cqh * ${playAspect}))`,
|
|
495
|
+
height: `min(100cqh, calc(100cqw / ${playAspect}))`,
|
|
496
|
+
}}>
|
|
486
497
|
<BlenderViewportArea {...props} documentId={`${documentId}#play`} view={game.view} main={false} playing onPlayReady={() => setPlayedReady(true)} onPlayReturn={() => setPlayedReady(false)} />
|
|
498
|
+
</div>
|
|
487
499
|
</div>
|
|
488
500
|
)}
|
|
489
501
|
{second && (
|
|
@@ -694,7 +706,7 @@ function BlenderViewportArea({
|
|
|
694
706
|
preparing = true;
|
|
695
707
|
orbit = stage.rig().orbit;
|
|
696
708
|
orbit.enabled = false;
|
|
697
|
-
void view.prepareRendered(stage.rig().drawCamera()).then(() => {
|
|
709
|
+
void view.prepareRendered(stage.rig().drawCamera(), 'render').then(() => {
|
|
698
710
|
if (stopped) return;
|
|
699
711
|
stopScript = documentPlayExtension('model')?.run({
|
|
700
712
|
documentId: modelId,
|
|
@@ -706,6 +718,7 @@ function BlenderViewportArea({
|
|
|
706
718
|
camera: () => stage.rig().drawCamera(),
|
|
707
719
|
editingCamera: () => viewportStages().find(one => one.documentId === modelId)?.rig().drawCamera() ?? stage.rig().drawCamera(),
|
|
708
720
|
onFrame: (fn) => stage.onFrame(fn),
|
|
721
|
+
ownMaterial: (material) => ownedSurfaceMaterial(material as THREE.Material),
|
|
709
722
|
report: (title, detail) => {
|
|
710
723
|
editorHost().console.error(`${title}: ${detail}`, 'blender-play');
|
|
711
724
|
notifyRef.current?.({ tone: 'error', title, detail });
|
|
@@ -864,7 +877,7 @@ function BlenderViewportArea({
|
|
|
864
877
|
* BLENDER'S RENDERED SHADING IS THE SCENE'S OWN LIGHT. When this stage's view lights by the
|
|
865
878
|
* `scene` (the Rendered shading cell, or the Lighting row's Scene), the presenter holds the viewport
|
|
866
879
|
* in the lighting its render photographs with (`BlenderRuntimeView.holdRendered`): the
|
|
867
|
-
* scene's lights, its World
|
|
880
|
+
* scene's lights, its World and shadows, through the camera the stage draws
|
|
868
881
|
* with, re-applied from the stage's frame loop when that camera or the World changes. Any
|
|
869
882
|
* other source returns it to modeling. The stage itself draws a `scene` source unlit
|
|
870
883
|
* (`standard-viewport-dressing.ts`), because Blender's lights live in the presenter.
|
|
@@ -889,7 +902,9 @@ function BlenderViewportArea({
|
|
|
889
902
|
const preview = drawMode === 'preview';
|
|
890
903
|
const sceneLighting = preview ? { world: lighting.source === 'scene', lights: lighting.preview.sceneLights } : undefined;
|
|
891
904
|
const needsScene = lighting.source === 'scene' || (preview && lighting.preview.sceneLights);
|
|
892
|
-
|
|
905
|
+
// Shading changes lighting, not which objects an editing viewport shows.
|
|
906
|
+
// Only the detached game uses render visibility, as a clean capture does.
|
|
907
|
+
view.holdRendered(needsScene && stage ? drawCamera : null, sceneLighting, playing ? 'render' : 'viewport');
|
|
893
908
|
// BLENDER'S SOLID IS BLENDER'S OWN FUNCTION: while the stage lights by Blender's studio, the
|
|
894
909
|
// presenter draws every surface by it (`blender-workbench-material.ts`).
|
|
895
910
|
view.setWorkbench(lighting.source === 'studio' && lighting.studioPreset === DOCUMENT_STUDIO_PRESET.id);
|
|
@@ -904,7 +919,7 @@ function BlenderViewportArea({
|
|
|
904
919
|
view.holdRendered(null);
|
|
905
920
|
view.setWorkbench(false);
|
|
906
921
|
};
|
|
907
|
-
}, [documentId, view]);
|
|
922
|
+
}, [documentId, view, playing]);
|
|
908
923
|
/**
|
|
909
924
|
* SHIFT+RIGHT-CLICK PLACES THE 3D CURSOR, Blender's own chord for
|
|
910
925
|
* `view3d.cursor3d` (`blender_default.py`, `params.cursor_set_event`). The
|
|
@@ -976,6 +991,7 @@ function BlenderViewportArea({
|
|
|
976
991
|
>
|
|
977
992
|
<Surface
|
|
978
993
|
active={active ?? true}
|
|
994
|
+
fillContainer={playing}
|
|
979
995
|
documentId={documentId}
|
|
980
996
|
sourcePath={blend ?? 'blender:runtime'}
|
|
981
997
|
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.129",
|
|
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.127",
|
|
74
|
+
"@volter/editor-threejs": "0.5.190",
|
|
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.190",
|
|
79
|
+
"@volter/editor-project": "0.5.190",
|
|
80
|
+
"@volter/editor-sdk": "0.5.190",
|
|
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();
|