@volter/editor-blender 0.1.116 → 0.1.118
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.
|
@@ -53,7 +53,7 @@ import type {
|
|
|
53
53
|
BlenderRnaRow,
|
|
54
54
|
BlenderRnaView,
|
|
55
55
|
} from '@volter/blender-engine/browser/rna';
|
|
56
|
-
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
56
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
57
57
|
import { ColorPicker, hexToRgb, parseAlpha, toHex } from '@volter/editor-sdk/widgets';
|
|
58
58
|
import {
|
|
59
59
|
type BlenderSubject,
|
|
@@ -353,6 +353,17 @@ function Numeric({
|
|
|
353
353
|
readonly onWrite: (next: number, index?: number) => void;
|
|
354
354
|
}) {
|
|
355
355
|
const [draft, setDraft] = useState<string | null>(null);
|
|
356
|
+
const pendingDraft = useRef<string | null>(null);
|
|
357
|
+
const commit = () => {
|
|
358
|
+
const typed = pendingDraft.current;
|
|
359
|
+
// Enter can also cause blur. Consume the edit before writing, once.
|
|
360
|
+
pendingDraft.current = null;
|
|
361
|
+
setDraft(null);
|
|
362
|
+
// Empty or incomplete number inputs have no numeric value to commit.
|
|
363
|
+
if (typed === null || typed.trim() === '') return;
|
|
364
|
+
const next = Number(typed);
|
|
365
|
+
if (Number.isFinite(next) && next !== value) onWrite(next, index);
|
|
366
|
+
};
|
|
356
367
|
const decimals = row.type === 'FLOAT' ? (row.precision ?? 3) : 0;
|
|
357
368
|
const shown = draft ?? (row.type === 'FLOAT' ? value.toFixed(decimals) : String(value));
|
|
358
369
|
if (row.readonly) return <StaticValue>{shown}</StaticValue>;
|
|
@@ -369,15 +380,20 @@ function Numeric({
|
|
|
369
380
|
{...(row.softMin === undefined ? {} : { min: row.softMin })}
|
|
370
381
|
{...(row.softMax === undefined ? {} : { max: row.softMax })}
|
|
371
382
|
{...(row.step === undefined ? {} : { step: row.type === 'FLOAT' ? row.step / 100 : 1 })}
|
|
372
|
-
onChange={(event) =>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
setDraft(null);
|
|
376
|
-
if (draft !== null && Number.isFinite(next) && next !== value) onWrite(next, index);
|
|
383
|
+
onChange={(event) => {
|
|
384
|
+
pendingDraft.current = event.target.value;
|
|
385
|
+
setDraft(event.target.value);
|
|
377
386
|
}}
|
|
387
|
+
onBlur={commit}
|
|
378
388
|
onKeyDown={(event) => {
|
|
379
|
-
if (event.key === 'Enter')
|
|
380
|
-
|
|
389
|
+
if (event.key === 'Enter') {
|
|
390
|
+
commit();
|
|
391
|
+
event.currentTarget.blur();
|
|
392
|
+
}
|
|
393
|
+
if (event.key === 'Escape') {
|
|
394
|
+
pendingDraft.current = null;
|
|
395
|
+
setDraft(null);
|
|
396
|
+
}
|
|
381
397
|
}}
|
|
382
398
|
style={fieldStyle}
|
|
383
399
|
/>
|
|
@@ -853,7 +853,12 @@ export function blenderRuntime(): BlenderRuntime {
|
|
|
853
853
|
const lifetime = new AbortController();
|
|
854
854
|
captureLifetime = lifetime;
|
|
855
855
|
let photographing = false;
|
|
856
|
-
let
|
|
856
|
+
let streamedFrame: { view: RuntimeView; session: string; revision: number } | null = null;
|
|
857
|
+
const discardStreamedFrame = () => {
|
|
858
|
+
const staged = streamedFrame;
|
|
859
|
+
streamedFrame = null;
|
|
860
|
+
if (staged) staged.view.stageFrame({ session: staged.session, revision: staged.revision, abort: true });
|
|
861
|
+
};
|
|
857
862
|
runtime = new BlenderRuntime({
|
|
858
863
|
work: beginBlenderWork,
|
|
859
864
|
history: (entries) => {
|
|
@@ -884,16 +889,21 @@ export function blenderRuntime(): BlenderRuntime {
|
|
|
884
889
|
}
|
|
885
890
|
},
|
|
886
891
|
stage: async part => {
|
|
887
|
-
if (part.abort) {
|
|
892
|
+
if (part.abort) { discardStreamedFrame(); return; }
|
|
888
893
|
const conflict = modelDocumentConflict();
|
|
889
894
|
if (conflict) throw new Error(conflict);
|
|
890
|
-
if (boundModel === null)
|
|
895
|
+
if (boundModel === null) {
|
|
896
|
+
discardStreamedFrame();
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
891
899
|
const documentId = presentationDocumentId();
|
|
892
900
|
const view = await runtimeView();
|
|
893
901
|
lifetime.signal.throwIfAborted();
|
|
894
902
|
if (presentationDocumentId() !== documentId) throw new Error('Blender document changed during frame transfer');
|
|
895
|
-
|
|
903
|
+
if (streamedFrame && streamedFrame.view !== view)
|
|
904
|
+
throw new Error('Blender presenter changed during frame transfer');
|
|
896
905
|
view.stageFrame(part);
|
|
906
|
+
streamedFrame = { view, session: part.session, revision: part.revision };
|
|
897
907
|
},
|
|
898
908
|
present: async (frame, description, capture) => {
|
|
899
909
|
const conflict = modelDocumentConflict();
|
|
@@ -903,6 +913,9 @@ export function blenderRuntime(): BlenderRuntime {
|
|
|
903
913
|
// on close). The Model document presents the session's current state when it binds. Only a
|
|
904
914
|
// photograph needs a view, so only a capture is refused.
|
|
905
915
|
if (boundModel === null) {
|
|
916
|
+
// A headless present accepts no staged geometry. Abort its owned
|
|
917
|
+
// transfer before the retained Model view can be opened again.
|
|
918
|
+
discardStreamedFrame();
|
|
906
919
|
if (capture?.render) {
|
|
907
920
|
throw new Error('Rendering a Blender frame needs the Model document open; nothing is presenting.');
|
|
908
921
|
}
|
|
@@ -915,6 +928,8 @@ export function blenderRuntime(): BlenderRuntime {
|
|
|
915
928
|
lifetime.signal.throwIfAborted();
|
|
916
929
|
if (presentationDocumentId() !== documentId)
|
|
917
930
|
throw new Error('Blender document changed before its frame could be presented');
|
|
931
|
+
if (streamedFrame && streamedFrame.view !== view)
|
|
932
|
+
throw new Error('Blender presenter changed before its frame could be presented');
|
|
918
933
|
// WHAT THE PRESENTER HELD BEFORE THIS FRAME, carried back to the session
|
|
919
934
|
// beside whatever this present produced. The session only has a RECORD of
|
|
920
935
|
// what it sent; this view is the authority on what it actually holds, and
|
|
@@ -927,6 +942,7 @@ export function blenderRuntime(): BlenderRuntime {
|
|
|
927
942
|
try { return view.applyFrame(frame) as { held?: unknown } | null | undefined; }
|
|
928
943
|
finally { endApply(); }
|
|
929
944
|
})();
|
|
945
|
+
streamedFrame = null;
|
|
930
946
|
const reports = typeof applied === 'object' && applied !== null && 'held' in applied;
|
|
931
947
|
const held = reports ? (applied.held as { session: string; revision: number } | null) : null;
|
|
932
948
|
const answer = (capture: unknown): PresentAnswer => ({
|
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.118",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
73
|
-
"@volter/blender-engine": "0.1.
|
|
74
|
-
"@volter/editor-threejs": "0.5.
|
|
73
|
+
"@volter/blender-engine": "0.1.116",
|
|
74
|
+
"@volter/editor-threejs": "0.5.179",
|
|
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.179",
|
|
79
|
+
"@volter/editor-project": "0.5.179",
|
|
80
|
+
"@volter/editor-sdk": "0.5.179",
|
|
81
81
|
"react": "^19.0.0",
|
|
82
82
|
"react-dom": "^19.0.0",
|
|
83
83
|
"three": "^0.180.0",
|