@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) => setDraft(event.target.value)}
373
- onBlur={() => {
374
- const next = Number(draft);
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') (event.target as HTMLInputElement).blur();
380
- if (event.key === 'Escape') setDraft(null);
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 streamedView: RuntimeView | null = null;
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) { streamedView?.stageFrame(part); streamedView = null; return; }
892
+ if (part.abort) { discardStreamedFrame(); return; }
888
893
  const conflict = modelDocumentConflict();
889
894
  if (conflict) throw new Error(conflict);
890
- if (boundModel === null) return;
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
- streamedView = view;
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.116",
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.114",
74
- "@volter/editor-threejs": "0.5.177",
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.177",
79
- "@volter/editor-project": "0.5.177",
80
- "@volter/editor-sdk": "0.5.177",
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",