@volter/editor-core 0.5.57 → 0.5.58
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.
- package/BUNDLED_NOTICES +3 -3
- package/dist-server/packaged.mjs +3 -1
- package/package.json +4 -4
- package/server/blender-wasm-artifact.ts +3 -1
- package/src/action-registry.ts +2 -2
- package/src/authoring/consumer-actions.ts +11 -0
- package/src/command-listener.ts +5 -0
- package/src/editor-host-door.ts +8 -0
- package/src/editor-hotkeys.ts +16 -1
- package/src/frame/bridge.tsx +8 -2
- package/src/history/history-delegate.ts +28 -4
- package/src/history/history-service.ts +24 -8
package/BUNDLED_NOTICES
CHANGED
|
@@ -31,7 +31,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
31
31
|
SOFTWARE.
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
@volter/editor-project@0.5.
|
|
34
|
+
@volter/editor-project@0.5.58
|
|
35
35
|
Declared license: Apache-2.0
|
|
36
36
|
|
|
37
37
|
--- packages/editor-project/LICENSE ---
|
|
@@ -240,7 +240,7 @@ Declared license: Apache-2.0
|
|
|
240
240
|
limitations under the License.
|
|
241
241
|
|
|
242
242
|
|
|
243
|
-
@volter/editor-sdk@0.5.
|
|
243
|
+
@volter/editor-sdk@0.5.58
|
|
244
244
|
Declared license: Apache-2.0
|
|
245
245
|
|
|
246
246
|
--- packages/editor-sdk/LICENSE ---
|
|
@@ -461,7 +461,7 @@ The project output-root policy is Apache-2.0, transferred from the original
|
|
|
461
461
|
SDK; its exact source is recorded in provenance/editor-server.json.
|
|
462
462
|
|
|
463
463
|
|
|
464
|
-
@volter/editor-threejs@0.5.
|
|
464
|
+
@volter/editor-threejs@0.5.58
|
|
465
465
|
Declared license: AGPL-3.0-only AND Apache-2.0
|
|
466
466
|
|
|
467
467
|
--- packages/editor-threejs/LICENSE ---
|
package/dist-server/packaged.mjs
CHANGED
|
@@ -80769,7 +80769,9 @@ import { brotliDecompressSync } from "node:zlib";
|
|
|
80769
80769
|
var BLENDER_WASM_FILES = [
|
|
80770
80770
|
"blender_browser.js",
|
|
80771
80771
|
"blender_browser.wasm",
|
|
80772
|
-
"blender_browser.data"
|
|
80772
|
+
"blender_browser.data",
|
|
80773
|
+
"essentials.json",
|
|
80774
|
+
"essentials.bin"
|
|
80773
80775
|
];
|
|
80774
80776
|
var BLENDER_WALI_ARTIFACT = "blender.wasm";
|
|
80775
80777
|
var BLENDER_WALI_RUNTIME = "runtime";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/editor-core",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.58",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -82,9 +82,9 @@
|
|
|
82
82
|
"three.quarks": "0.16.0",
|
|
83
83
|
"typescript": "5.9.3",
|
|
84
84
|
"zod": "4.3.6",
|
|
85
|
-
"@volter/editor-project": "0.5.
|
|
86
|
-
"@volter/editor-sdk": "0.5.
|
|
87
|
-
"@volter/editor-threejs": "0.5.
|
|
85
|
+
"@volter/editor-project": "0.5.58",
|
|
86
|
+
"@volter/editor-sdk": "0.5.58",
|
|
87
|
+
"@volter/editor-threejs": "0.5.58",
|
|
88
88
|
"vite": "^6.4.3",
|
|
89
89
|
"express": "^5.2.1",
|
|
90
90
|
"chokidar": "^5.0.0",
|
|
@@ -61,11 +61,13 @@ import { brotliDecompressSync } from 'node:zlib';
|
|
|
61
61
|
|
|
62
62
|
export type BlenderSkew = 'emscripten' | 'wali';
|
|
63
63
|
|
|
64
|
-
/**
|
|
64
|
+
/** Engine artifacts plus the separately packaged Essentials asset payload. */
|
|
65
65
|
export const BLENDER_WASM_FILES = [
|
|
66
66
|
'blender_browser.js',
|
|
67
67
|
'blender_browser.wasm',
|
|
68
68
|
'blender_browser.data',
|
|
69
|
+
'essentials.json',
|
|
70
|
+
'essentials.bin',
|
|
69
71
|
] as const;
|
|
70
72
|
|
|
71
73
|
export type BlenderWasmFile = (typeof BLENDER_WASM_FILES)[number];
|
package/src/action-registry.ts
CHANGED
|
@@ -211,14 +211,14 @@ export function buildStaticActions(
|
|
|
211
211
|
label: 'Duplicate',
|
|
212
212
|
category: 'action',
|
|
213
213
|
shortcut: shortcutFor('edit.duplicate'),
|
|
214
|
-
execute: () =>
|
|
214
|
+
execute: () => duplicateSelection(store),
|
|
215
215
|
},
|
|
216
216
|
{
|
|
217
217
|
id: 'editor.delete',
|
|
218
218
|
label: 'Delete Selected',
|
|
219
219
|
category: 'action',
|
|
220
220
|
shortcut: shortcutFor('edit.delete'),
|
|
221
|
-
execute: () =>
|
|
221
|
+
execute: () => deleteSelection(store),
|
|
222
222
|
},
|
|
223
223
|
{
|
|
224
224
|
id: 'editor.select-all',
|
|
@@ -320,6 +320,17 @@ export function duplicateAuthoringNode(
|
|
|
320
320
|
);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
export function duplicateManyAuthoringNodes(
|
|
324
|
+
adapter: AuthoringAdapter,
|
|
325
|
+
nodeIds: readonly string[],
|
|
326
|
+
): StructuralWriteOutcome {
|
|
327
|
+
const provider = adapter.structure;
|
|
328
|
+
if (!provider?.duplicateMany) return;
|
|
329
|
+
return structureWrite(adapter, 'duplicateMany',
|
|
330
|
+
`the editor duplicated ${nodeIds.length} selected node(s) as one operation`,
|
|
331
|
+
() => provider.duplicateMany?.(nodeIds));
|
|
332
|
+
}
|
|
333
|
+
|
|
323
334
|
export function reparentAuthoringNode(
|
|
324
335
|
adapter: AuthoringAdapter,
|
|
325
336
|
nodeId: string,
|
package/src/command-listener.ts
CHANGED
|
@@ -139,6 +139,7 @@ import {
|
|
|
139
139
|
createAuthoringNode,
|
|
140
140
|
cutAuthoringNodes,
|
|
141
141
|
duplicateAuthoringNode,
|
|
142
|
+
duplicateManyAuthoringNodes,
|
|
142
143
|
groupAuthoringNodes,
|
|
143
144
|
pasteAuthoringNodes,
|
|
144
145
|
removeAuthoringNode,
|
|
@@ -1943,6 +1944,10 @@ export async function handleCommand(
|
|
|
1943
1944
|
return { ok: true, data: { write } };
|
|
1944
1945
|
}
|
|
1945
1946
|
case 'duplicate': {
|
|
1947
|
+
if (ids.length > 1 && structure.duplicateMany) {
|
|
1948
|
+
const write = await duplicateManyAuthoringNodes(adapter, ids);
|
|
1949
|
+
return { ok: true, data: { write } };
|
|
1950
|
+
}
|
|
1946
1951
|
const copy = duplicateAuthoringNode(adapter, needsId());
|
|
1947
1952
|
return { ok: true, data: { id: copy.id, write: await copy.ack } };
|
|
1948
1953
|
}
|
package/src/editor-host-door.ts
CHANGED
|
@@ -38,6 +38,10 @@ import { setFilesProvider } from './files/file-provider';
|
|
|
38
38
|
import { projectFiles } from './files/project-files';
|
|
39
39
|
import {
|
|
40
40
|
onHistoryElement,
|
|
41
|
+
invalidateHistoryResources,
|
|
42
|
+
onHistoryInvalidated,
|
|
43
|
+
notifyHistoryDelegateChanged,
|
|
44
|
+
emitHistoryElement,
|
|
41
45
|
recordedHistoryElements,
|
|
42
46
|
setHistoryDelegate,
|
|
43
47
|
setHistoryDocumentResolver,
|
|
@@ -332,8 +336,12 @@ export function installEditorHostDoor(): void {
|
|
|
332
336
|
// keyed by the document's own file; `history-service.ts` stays the
|
|
333
337
|
// RECORDER and stops deciding which entry is next.
|
|
334
338
|
history: {
|
|
339
|
+
record: emitHistoryElement,
|
|
335
340
|
setDelegate: setHistoryDelegate,
|
|
336
341
|
onElement: onHistoryElement,
|
|
342
|
+
invalidate: invalidateHistoryResources,
|
|
343
|
+
onInvalidated: onHistoryInvalidated,
|
|
344
|
+
changed: notifyHistoryDelegateChanged,
|
|
337
345
|
elements: recordedHistoryElements,
|
|
338
346
|
focusedResource: () => {
|
|
339
347
|
const document = activeWorkspaceDocument();
|
package/src/editor-hotkeys.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
copyAuthoringNodes,
|
|
4
4
|
cutAuthoringNodes,
|
|
5
5
|
duplicateAuthoringNode,
|
|
6
|
+
duplicateManyAuthoringNodes,
|
|
6
7
|
groupAuthoringNodes,
|
|
7
8
|
pasteAuthoringNodes,
|
|
8
9
|
removeAuthoringNode,
|
|
@@ -216,7 +217,21 @@ async function runDuplicateSelection(store: EditorShellStore): Promise<void> {
|
|
|
216
217
|
? (adapter.hierarchy.node(selection[0] as string)?.label ?? 'selection')
|
|
217
218
|
: `${selection.length} objects`;
|
|
218
219
|
showTransientHint(`Duplicating ${label}…`);
|
|
219
|
-
|
|
220
|
+
if (structure.duplicateMany) {
|
|
221
|
+
const ack = await duplicateManyAuthoringNodes(adapter, selection);
|
|
222
|
+
if (ack && !ack.persisted) {
|
|
223
|
+
showTransientHint(ack.destination);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
for (const id of selection) {
|
|
228
|
+
const ack = await duplicateAuthoringNode(adapter, id).ack;
|
|
229
|
+
if (ack && !ack.persisted) {
|
|
230
|
+
showTransientHint(ack.destination);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
220
235
|
showTransientHint(`Duplicated ${label}`);
|
|
221
236
|
}
|
|
222
237
|
|
package/src/frame/bridge.tsx
CHANGED
|
@@ -200,15 +200,19 @@ export interface VgaiKeyboardHandle {
|
|
|
200
200
|
export interface VgaiHistoryHandle {
|
|
201
201
|
elements(): readonly VgaiHistoryElementHandle[];
|
|
202
202
|
onElement(listener: (element: VgaiHistoryElementHandle) => void): () => void;
|
|
203
|
+
onInvalidated(listener: (resources: readonly string[]) => void): () => void;
|
|
204
|
+
changed(): void;
|
|
203
205
|
focusedResource(): string | null;
|
|
204
206
|
focusedDocument(): { id: string; label: string } | null;
|
|
205
207
|
/** Hand the frame's OWN undo to the editor, so its Edit menu, palette and
|
|
206
208
|
* `vgai eval` reach the one stack instead of a cursor nobody drives. */
|
|
207
209
|
setDelegate(delegate: {
|
|
208
|
-
undo(): void
|
|
209
|
-
redo(): void
|
|
210
|
+
undo(): void | boolean | Promise<void | boolean>;
|
|
211
|
+
redo(): void | boolean | Promise<void | boolean>;
|
|
210
212
|
canUndo(): boolean;
|
|
211
213
|
canRedo(): boolean;
|
|
214
|
+
undoLabel?(): string | null;
|
|
215
|
+
redoLabel?(): string | null;
|
|
212
216
|
}): void;
|
|
213
217
|
report(level: 'warn' | 'error', message: string): void;
|
|
214
218
|
}
|
|
@@ -1181,6 +1185,8 @@ export async function mountEditor(next: VscodeParts): Promise<{
|
|
|
1181
1185
|
const history: VgaiHistoryHandle = {
|
|
1182
1186
|
elements: () => editorHost().history.elements(),
|
|
1183
1187
|
onElement: (listener) => editorHost().history.onElement(listener),
|
|
1188
|
+
onInvalidated: (listener) => editorHost().history.onInvalidated(listener),
|
|
1189
|
+
changed: () => editorHost().history.changed(),
|
|
1184
1190
|
focusedResource: () => editorHost().history.focusedResource(),
|
|
1185
1191
|
// The document's TITLE is what a refusal should say ("Nothing to undo in
|
|
1186
1192
|
// MainScene"), and the id is what scopes the stack; both come from the
|
|
@@ -80,10 +80,12 @@ export interface HistoryElement {
|
|
|
80
80
|
* is worse than one that is absent.
|
|
81
81
|
*/
|
|
82
82
|
export interface HistoryDelegate {
|
|
83
|
-
undo(): void
|
|
84
|
-
redo(): void
|
|
83
|
+
undo(): void | boolean | Promise<void | boolean>;
|
|
84
|
+
redo(): void | boolean | Promise<void | boolean>;
|
|
85
85
|
canUndo(): boolean;
|
|
86
86
|
canRedo(): boolean;
|
|
87
|
+
undoLabel?(): string | null;
|
|
88
|
+
redoLabel?(): string | null;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
let delegate: HistoryDelegate | null = null;
|
|
@@ -93,6 +95,7 @@ let delegate: HistoryDelegate | null = null;
|
|
|
93
95
|
let documentResolver: (() => string | null) | null = null;
|
|
94
96
|
const ownerListeners = new Set<() => void>();
|
|
95
97
|
const elementListeners = new Set<(element: HistoryElement) => void>();
|
|
98
|
+
const invalidationListeners = new Set<(resources: readonly string[]) => void>();
|
|
96
99
|
/** Everything recorded while a delegate was installed, so a LATE subscriber
|
|
97
100
|
* (the bridge mounts after the editor's first edits are possible) can catch
|
|
98
101
|
* up rather than start with a stack that is missing its beginning. */
|
|
@@ -131,16 +134,24 @@ export function subscribeHistoryDelegate(listener: () => void): () => void {
|
|
|
131
134
|
return () => ownerListeners.delete(listener);
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
export function notifyHistoryDelegateChanged(): void {
|
|
138
|
+
for (const listener of ownerListeners) listener();
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
/**
|
|
135
142
|
* Offer one committed transaction to the frame. Recorded even before the
|
|
136
143
|
* delegate arrives — that buffer is what lets a frame that mounts after the
|
|
137
144
|
* editor's first edits push what it missed, in order, rather than start with a
|
|
138
145
|
* stack missing its beginning.
|
|
139
146
|
*/
|
|
140
|
-
export function emitHistoryElement(element: Omit<HistoryElement, 'document'>): void {
|
|
141
|
-
const stamped: HistoryElement = {
|
|
147
|
+
export function emitHistoryElement(element: Omit<HistoryElement, 'document'> & { document?: string | null }): void {
|
|
148
|
+
const stamped: HistoryElement = {
|
|
149
|
+
...element,
|
|
150
|
+
document: element.document === undefined ? documentResolver?.() ?? null : element.document,
|
|
151
|
+
};
|
|
142
152
|
recorded.push(stamped);
|
|
143
153
|
for (const listener of elementListeners) listener(stamped);
|
|
154
|
+
for (const listener of ownerListeners) listener();
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
/** Install the reader of "which document is active". Called once by the host
|
|
@@ -161,3 +172,16 @@ export function onHistoryElement(listener: (element: HistoryElement) => void): (
|
|
|
161
172
|
elementListeners.add(listener);
|
|
162
173
|
return () => elementListeners.delete(listener);
|
|
163
174
|
}
|
|
175
|
+
|
|
176
|
+
/** A native document was replaced or its snapshot owner was closed. */
|
|
177
|
+
export function invalidateHistoryResources(resources: readonly string[]): void {
|
|
178
|
+
const expired = new Set(resources);
|
|
179
|
+
recorded = recorded.filter(element => !element.resources.some(path => expired.has(path)));
|
|
180
|
+
for (const listener of invalidationListeners) listener(resources);
|
|
181
|
+
for (const listener of ownerListeners) listener();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function onHistoryInvalidated(listener: (resources: readonly string[]) => void): () => void {
|
|
185
|
+
invalidationListeners.add(listener);
|
|
186
|
+
return () => invalidationListeners.delete(listener);
|
|
187
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { emitHistoryElement, historyDelegate, historyDelegateInstalled } from './history-delegate';
|
|
1
|
+
import { emitHistoryElement, historyDelegate, historyDelegateInstalled, subscribeHistoryDelegate } from './history-delegate';
|
|
2
2
|
import { PersistenceCoordinator } from './persistence-coordinator';
|
|
3
3
|
import { ResourceRegistry } from './resource-registry';
|
|
4
4
|
import { SnapshotStore } from './snapshot-store';
|
|
@@ -236,6 +236,7 @@ export class HistoryService {
|
|
|
236
236
|
private droppedTotal = 0;
|
|
237
237
|
private executionTail: Promise<void> = Promise.resolve();
|
|
238
238
|
private publicSnapshot: HistorySnapshot;
|
|
239
|
+
private readonly unsubscribeOwner: () => void;
|
|
239
240
|
|
|
240
241
|
constructor(options: HistoryServiceOptions = {}) {
|
|
241
242
|
this.registry = options.registry ?? new ResourceRegistry();
|
|
@@ -257,6 +258,7 @@ export class HistoryService {
|
|
|
257
258
|
throw new Error('History limits must be positive.');
|
|
258
259
|
}
|
|
259
260
|
this.publicSnapshot = this.buildSnapshot();
|
|
261
|
+
this.unsubscribeOwner = subscribeHistoryDelegate(() => this.notify());
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
subscribe = (listener: () => void): (() => void) => {
|
|
@@ -265,7 +267,17 @@ export class HistoryService {
|
|
|
265
267
|
return () => this.listeners.delete(listener);
|
|
266
268
|
};
|
|
267
269
|
|
|
268
|
-
getSnapshot = (): HistorySnapshot =>
|
|
270
|
+
getSnapshot = (): HistorySnapshot => {
|
|
271
|
+
// Focus may move between documents without changing any history entry.
|
|
272
|
+
// Keep the external-store snapshot stable unless the native answer changes.
|
|
273
|
+
if (historyDelegateInstalled()) {
|
|
274
|
+
const next = this.buildSnapshot();
|
|
275
|
+
if (next.canUndo !== this.publicSnapshot.canUndo || next.canRedo !== this.publicSnapshot.canRedo ||
|
|
276
|
+
next.undoLabel !== this.publicSnapshot.undoLabel || next.redoLabel !== this.publicSnapshot.redoLabel)
|
|
277
|
+
this.publicSnapshot = next;
|
|
278
|
+
}
|
|
279
|
+
return this.publicSnapshot;
|
|
280
|
+
};
|
|
269
281
|
|
|
270
282
|
subscribeCommits(listener: (transaction: HistoryTransaction) => void): () => void {
|
|
271
283
|
this.assertNotDisposed();
|
|
@@ -447,15 +459,18 @@ export class HistoryService {
|
|
|
447
459
|
* ONE stack instead of walking a cursor nobody is driving. A frame that has
|
|
448
460
|
* not installed its undo yet gets a named refusal rather than silence.
|
|
449
461
|
*/
|
|
450
|
-
private delegateOrRefuse(direction: 'undo' | 'redo'): boolean {
|
|
462
|
+
private async delegateOrRefuse(direction: 'undo' | 'redo'): Promise<boolean> {
|
|
451
463
|
const delegate = historyDelegate();
|
|
452
464
|
if (delegate) {
|
|
453
465
|
// A handled keyboard command is not evidence that history moved. In
|
|
454
466
|
// particular, a native document may have no entry on Code-OSS's stack.
|
|
455
467
|
if (!(direction === 'undo' ? delegate.canUndo() : delegate.canRedo())) return false;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
468
|
+
try {
|
|
469
|
+
const moved = direction === 'undo' ? await delegate.undo() : await delegate.redo();
|
|
470
|
+
return moved !== false;
|
|
471
|
+
} finally {
|
|
472
|
+
this.notify();
|
|
473
|
+
}
|
|
459
474
|
}
|
|
460
475
|
this.lastErrorValue = historyError(
|
|
461
476
|
'busy',
|
|
@@ -634,6 +649,7 @@ export class HistoryService {
|
|
|
634
649
|
dispose(): void {
|
|
635
650
|
if (this.disposeRequested) return;
|
|
636
651
|
this.disposeRequested = true;
|
|
652
|
+
this.unsubscribeOwner();
|
|
637
653
|
this.listeners.clear();
|
|
638
654
|
this.commitListeners.clear();
|
|
639
655
|
this.finishDisposeIfIdle();
|
|
@@ -1162,8 +1178,8 @@ export class HistoryService {
|
|
|
1162
1178
|
canRedo: historyDelegateInstalled()
|
|
1163
1179
|
? (historyDelegate()?.canRedo() ?? false)
|
|
1164
1180
|
: idle && !!redo && redo.status !== 'expired' && !this.blockedValue,
|
|
1165
|
-
undoLabel: undo?.label ?? null,
|
|
1166
|
-
redoLabel: redo?.label ?? null,
|
|
1181
|
+
undoLabel: historyDelegateInstalled() ? historyDelegate()?.undoLabel?.() ?? null : undo?.label ?? null,
|
|
1182
|
+
redoLabel: historyDelegateInstalled() ? historyDelegate()?.redoLabel?.() ?? null : redo?.label ?? null,
|
|
1167
1183
|
uniqueSnapshotBytes: this.snapshots.uniqueByteLength,
|
|
1168
1184
|
lastError: this.lastErrorValue,
|
|
1169
1185
|
limitWarning: this.limitWarningValue,
|