@volter/editor-blender 0.1.3 → 0.1.5
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.
|
@@ -166,6 +166,10 @@ let frameSubscription: (() => void) | null = null;
|
|
|
166
166
|
const inFlight = new Set<string>();
|
|
167
167
|
/** The subject the last context was read for, so a re-render does not refetch. */
|
|
168
168
|
let readFor: string | null = null;
|
|
169
|
+
/** Reads belong to a model revision and selected subject, not merely a path.
|
|
170
|
+
* A late answer for a deleted duplicate must not overwrite the new selection. */
|
|
171
|
+
let readGeneration = 0;
|
|
172
|
+
let contextRequest = 0;
|
|
169
173
|
|
|
170
174
|
function publish(next: Partial<BlenderPropertiesState>, tabsMayHaveChanged = false): void {
|
|
171
175
|
state = { ...state, ...next };
|
|
@@ -204,8 +208,10 @@ export function blenderPropertiesState(): BlenderPropertiesState {
|
|
|
204
208
|
* values it had. */
|
|
205
209
|
function onEngineMoved(): void {
|
|
206
210
|
const subject = readFor;
|
|
211
|
+
readGeneration += 1;
|
|
212
|
+
inFlight.clear();
|
|
207
213
|
readFor = null;
|
|
208
|
-
publish({ views: new Map() });
|
|
214
|
+
publish({ context: null, views: new Map(), error: null, loading: subject !== null });
|
|
209
215
|
if (subject !== null) scheduleContextRead(subject);
|
|
210
216
|
}
|
|
211
217
|
|
|
@@ -264,11 +270,17 @@ function watchFrames(): void {
|
|
|
264
270
|
async function readContext(key: string): Promise<void> {
|
|
265
271
|
const collection = key.startsWith('collection:') ? key.slice('collection:'.length) : null;
|
|
266
272
|
const object = collection === null && key !== '' ? key : null;
|
|
273
|
+
if (readFor !== key) {
|
|
274
|
+
readGeneration += 1;
|
|
275
|
+
inFlight.clear();
|
|
276
|
+
}
|
|
277
|
+
const generation = readGeneration;
|
|
278
|
+
const request = ++contextRequest;
|
|
267
279
|
readFor = key;
|
|
268
|
-
publish({ loading: true, object, collection });
|
|
280
|
+
publish({ loading: true, object, collection, context: null, error: null, views: new Map() });
|
|
269
281
|
try {
|
|
270
282
|
const context = await blenderRnaContext(object ?? undefined, collection ?? undefined);
|
|
271
|
-
if (
|
|
283
|
+
if (generation !== readGeneration || request !== contextRequest) return;
|
|
272
284
|
// ALWAYS a tab-rail notification, and that is a correction rather than a
|
|
273
285
|
// convenience: the gate a tab matches on is BOTH the subject this context
|
|
274
286
|
// was read for and the tab list, so comparing only the list missed the
|
|
@@ -279,7 +291,7 @@ async function readContext(key: string): Promise<void> {
|
|
|
279
291
|
// disappeared on the first selection and only came back on a re-select.
|
|
280
292
|
publish({ context, loading: false, error: null }, true);
|
|
281
293
|
} catch (error) {
|
|
282
|
-
if (
|
|
294
|
+
if (generation !== readGeneration || request !== contextRequest) return;
|
|
283
295
|
publish({ context: null, loading: false, error: describe(error) }, true);
|
|
284
296
|
}
|
|
285
297
|
}
|
|
@@ -305,8 +317,10 @@ export function blenderRnaViewFor(path: string): BlenderRnaView | undefined {
|
|
|
305
317
|
if (held !== undefined) return held;
|
|
306
318
|
if (inFlight.has(path) || !blenderSessionStarted()) return undefined;
|
|
307
319
|
inFlight.add(path);
|
|
320
|
+
const generation = readGeneration;
|
|
308
321
|
void blenderRna(path)
|
|
309
322
|
.then((view) => {
|
|
323
|
+
if (generation !== readGeneration) return;
|
|
310
324
|
inFlight.delete(path);
|
|
311
325
|
if (view === null) return;
|
|
312
326
|
const views = new Map(state.views);
|
|
@@ -314,6 +328,7 @@ export function blenderRnaViewFor(path: string): BlenderRnaView | undefined {
|
|
|
314
328
|
publish({ views, error: null });
|
|
315
329
|
})
|
|
316
330
|
.catch((error: unknown) => {
|
|
331
|
+
if (generation !== readGeneration) return;
|
|
317
332
|
inFlight.delete(path);
|
|
318
333
|
publish({ error: describe(error) });
|
|
319
334
|
});
|
|
@@ -1050,6 +1050,7 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1050
1050
|
};
|
|
1051
1051
|
try {
|
|
1052
1052
|
const project = cmd.type === 'blender-start' ? string(cmd, 'project') : null;
|
|
1053
|
+
let requestedDocument = typeof cmd['document'] === 'string' ? cmd['document'] : undefined;
|
|
1053
1054
|
if (cmd.type === 'blender-stop' || (cmd.type === 'blender-start' && cmd['fresh'] === true)) {
|
|
1054
1055
|
// Do not invalidate history or discard the worker if persistence fails.
|
|
1055
1056
|
await runtime?.stop();
|
|
@@ -1064,13 +1065,30 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1064
1065
|
// build's answer that nothing here opens that address; the Model
|
|
1065
1066
|
// document is this package's own contribution, so it is a build without
|
|
1066
1067
|
// `@volter/editor-blender` rather than a broken call.
|
|
1067
|
-
|
|
1068
|
+
// During startup the Model may not have mounted/bound yet. Resolve the
|
|
1069
|
+
// real declared table, including custom entry ids. A generic fallback
|
|
1070
|
+
// exists only in projects that have no file-backed Model of their own.
|
|
1071
|
+
const table = await host.project.documentTable();
|
|
1072
|
+
const models = table.entries.filter(entry => entry.kind === 'model');
|
|
1073
|
+
const entry = requestedDocument !== undefined
|
|
1074
|
+
? models.find(candidate => candidate.source?.path === requestedDocument)
|
|
1075
|
+
: models.find(candidate => candidate.id === (boundModel?.entryId ?? table.default))
|
|
1076
|
+
?? (models.length === 1 ? models[0] : undefined);
|
|
1077
|
+
if (!entry) return {
|
|
1078
|
+
ok: false,
|
|
1079
|
+
error: requestedDocument !== undefined
|
|
1080
|
+
? `No declared Model document names ${requestedDocument}.`
|
|
1081
|
+
: `Choose a Model document before starting Blender. Available: ${models.map(model => model.id).join(', ') || '(none)'}.`,
|
|
1082
|
+
};
|
|
1083
|
+
const entryId = entry.id;
|
|
1084
|
+
requestedDocument = entry.source?.path;
|
|
1085
|
+
const opened = await host.workspace.open({ kind: 'document', id: entryId });
|
|
1068
1086
|
if (!opened)
|
|
1069
1087
|
return {
|
|
1070
1088
|
ok: false,
|
|
1071
1089
|
error:
|
|
1072
|
-
|
|
1073
|
-
|
|
1090
|
+
`blender-start could not open the Blender Model document ${entryId}: nothing in this editor ` +
|
|
1091
|
+
'opens that document (the Model document ships with @volter/editor-blender).',
|
|
1074
1092
|
};
|
|
1075
1093
|
}
|
|
1076
1094
|
const session = blenderRuntime();
|
|
@@ -1081,8 +1099,8 @@ export async function handleBlenderCommand(cmd: {
|
|
|
1081
1099
|
// `blender-start` that named none is asking for the session this tab
|
|
1082
1100
|
// is showing, not for a second file beside it.
|
|
1083
1101
|
const document =
|
|
1084
|
-
|
|
1085
|
-
?
|
|
1102
|
+
requestedDocument !== undefined
|
|
1103
|
+
? requestedDocument
|
|
1086
1104
|
: (boundModel?.blend ?? DEFAULT_BLENDER_DOCUMENT);
|
|
1087
1105
|
return { ok: true, data: { ...(await session.start(project!, document)) } };
|
|
1088
1106
|
}
|
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.5",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
]
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@volter/blender-engine": "0.1.
|
|
65
|
-
"@volter/editor-threejs": "0.5.
|
|
64
|
+
"@volter/blender-engine": "0.1.5",
|
|
65
|
+
"@volter/editor-threejs": "0.5.62",
|
|
66
66
|
"zod": "^4.3.6"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|