@skenora/sdk 0.1.2

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.
Files changed (58) hide show
  1. package/README.md +167 -0
  2. package/dist/editor.d.ts +14 -0
  3. package/dist/editor.d.ts.map +1 -0
  4. package/dist/editor.js +14 -0
  5. package/dist/editor.js.map +1 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lightbox.d.ts +3 -0
  11. package/dist/lightbox.d.ts.map +1 -0
  12. package/dist/lightbox.js +2 -0
  13. package/dist/lightbox.js.map +1 -0
  14. package/dist/playback-config.d.ts +27 -0
  15. package/dist/playback-config.d.ts.map +1 -0
  16. package/dist/playback-config.js +87 -0
  17. package/dist/playback-config.js.map +1 -0
  18. package/dist/renderer-diagnostics.d.ts +4 -0
  19. package/dist/renderer-diagnostics.d.ts.map +1 -0
  20. package/dist/renderer-diagnostics.js +85 -0
  21. package/dist/renderer-diagnostics.js.map +1 -0
  22. package/dist/renderer.d.ts +10 -0
  23. package/dist/renderer.d.ts.map +1 -0
  24. package/dist/renderer.js +8 -0
  25. package/dist/renderer.js.map +1 -0
  26. package/dist/scene-config.d.ts +58 -0
  27. package/dist/scene-config.d.ts.map +1 -0
  28. package/dist/scene-config.js +223 -0
  29. package/dist/scene-config.js.map +1 -0
  30. package/dist/scene-plan/apply-patch.d.ts +5 -0
  31. package/dist/scene-plan/apply-patch.d.ts.map +1 -0
  32. package/dist/scene-plan/apply-patch.js +174 -0
  33. package/dist/scene-plan/apply-patch.js.map +1 -0
  34. package/dist/scene-plan/executor.d.ts +26 -0
  35. package/dist/scene-plan/executor.d.ts.map +1 -0
  36. package/dist/scene-plan/executor.js +928 -0
  37. package/dist/scene-plan/executor.js.map +1 -0
  38. package/dist/scene-plan/gateway.d.ts +15 -0
  39. package/dist/scene-plan/gateway.d.ts.map +1 -0
  40. package/dist/scene-plan/gateway.js +104 -0
  41. package/dist/scene-plan/gateway.js.map +1 -0
  42. package/dist/scene-plan/index.d.ts +4 -0
  43. package/dist/scene-plan/index.d.ts.map +1 -0
  44. package/dist/scene-plan/index.js +4 -0
  45. package/dist/scene-plan/index.js.map +1 -0
  46. package/dist/scene-plan/types.d.ts +181 -0
  47. package/dist/scene-plan/types.d.ts.map +1 -0
  48. package/dist/scene-plan/types.js +2 -0
  49. package/dist/scene-plan/types.js.map +1 -0
  50. package/dist/skenora-renderer.d.ts +117 -0
  51. package/dist/skenora-renderer.d.ts.map +1 -0
  52. package/dist/skenora-renderer.js +464 -0
  53. package/dist/skenora-renderer.js.map +1 -0
  54. package/dist/skenora-scene.d.ts +263 -0
  55. package/dist/skenora-scene.d.ts.map +1 -0
  56. package/dist/skenora-scene.js +832 -0
  57. package/dist/skenora-scene.js.map +1 -0
  58. package/package.json +55 -0
@@ -0,0 +1,832 @@
1
+ import { assertSceneDocument, createEmptySceneDocument, normalizeSceneDocument, } from "@skenora/contracts";
2
+ import { addEntityCommand, createProceduralEntityCommand, createPrimitiveCommand, importModelCommand, removeAssetCommand, removeEntityCommand, removeLightCommand, reparentEntityCommand, replaceDocumentCommand, setEntityEnabledCommand, setEntityTransformCommand, setEntityVisibleCommand, setEnvironmentCommand, setFogCommand, setGroundCommand, setPostProcessCommand, setPropertyCommand, setWeatherCommand, setViewportCameraCommand, upsertAssetCommand, upsertLightCommand, upsertMaterialCommand, } from "@skenora/editor";
3
+ import { EditorSession } from "@skenora/editor";
4
+ import { createCausationMetadata, EventHub, } from "@skenora/events";
5
+ import { assetReferenceFromDescriptor, supportsResourceDiscovery, } from "@skenora/resources";
6
+ import { createWorkspaceResourceContext, } from "@skenora/resources/workspace";
7
+ import { EditorFlowBridge, EditorRuntimeBridge, } from "@skenora/babylon-editor";
8
+ import { RUNTIME_CAMERA_CAPABILITY, RuntimeSession, } from "@skenora/runtime/projection";
9
+ import { DEFAULT_SCENE_FILE, MemoryWorkspaceSource, WorkspaceSession, } from "@skenora/workspace";
10
+ import { applySceneConfig, exportSceneJsonBundle, getSceneConfig, parseSceneConfig, parseSceneJson, serializeSceneConfig, serializeSceneJson, } from "./scene-config.js";
11
+ import { getSkenoraPlaybackConfig, normalizeSkenoraPlaybackConfig, SKENORA_PLAYBACK_EXTENSION_ID, } from "./playback-config.js";
12
+ import { ScenePlanExecutor } from "./scene-plan/executor.js";
13
+ import { ScenePlanGateway } from "./scene-plan/gateway.js";
14
+ /**
15
+ * High-level, instance-scoped scene SDK.
16
+ *
17
+ * Persistent writes are translated to EditorCommand objects. Runtime state is
18
+ * always a disposable projection of the current SceneDocument snapshot.
19
+ */
20
+ export class SkenoraScene {
21
+ events;
22
+ #options;
23
+ #ownsEvents;
24
+ #workspace = null;
25
+ #resources = null;
26
+ #resourceContext = null;
27
+ #editor = null;
28
+ #runtime = null;
29
+ #bridge = null;
30
+ #scenePlanExecutor = null;
31
+ #scenePlan = null;
32
+ #flow = null;
33
+ #lifetime = null;
34
+ #initializing = null;
35
+ #modelFailures = [];
36
+ #previewing = false;
37
+ #status = "idle";
38
+ #error = null;
39
+ constructor(options) {
40
+ this.#options = options;
41
+ this.events = options.events ?? new EventHub();
42
+ this.#ownsEvents = options.events === undefined;
43
+ }
44
+ static async create(options) {
45
+ const scene = new SkenoraScene(options);
46
+ try {
47
+ await scene.init();
48
+ return scene;
49
+ }
50
+ catch (error) {
51
+ scene.dispose();
52
+ throw error;
53
+ }
54
+ }
55
+ get status() {
56
+ return this.#status;
57
+ }
58
+ get error() {
59
+ return this.#error;
60
+ }
61
+ get ready() {
62
+ return this.#status === "ready";
63
+ }
64
+ get modelFailures() {
65
+ return Object.freeze(this.#modelFailures.map((item) => ({ ...item })));
66
+ }
67
+ get workspace() {
68
+ this.#assertReady();
69
+ return this.#workspace;
70
+ }
71
+ get editor() {
72
+ this.#assertReady();
73
+ return this.#editor;
74
+ }
75
+ get resources() {
76
+ this.#assertReady();
77
+ return this.#resources;
78
+ }
79
+ get runtime() {
80
+ this.#assertReady();
81
+ return this.#runtime;
82
+ }
83
+ getBackendSnapshot() {
84
+ return this.runtime.getBackendSnapshot();
85
+ }
86
+ getGpuProgramCacheSnapshot() {
87
+ return this.runtime.getGpuProgramCacheSnapshot();
88
+ }
89
+ probe() {
90
+ return this.runtime.probe();
91
+ }
92
+ get flow() {
93
+ this.#assertReady();
94
+ return this.#flow;
95
+ }
96
+ /** Declarative SceneBlueprint/ScenePatch gateway for AI and host callers. */
97
+ get scenePlan() {
98
+ this.#assertReady();
99
+ return this.#scenePlan;
100
+ }
101
+ get selectionController() {
102
+ this.#assertReady();
103
+ return this.#bridge.selection;
104
+ }
105
+ get document() {
106
+ return this.editor.snapshot;
107
+ }
108
+ get revision() {
109
+ return this.editor.revision;
110
+ }
111
+ get dirty() {
112
+ return this.editor.dirty;
113
+ }
114
+ /** Whether manually requested scene behavior preview is active. */
115
+ get previewing() {
116
+ return this.#previewing;
117
+ }
118
+ get selection() {
119
+ return this.editor.selection;
120
+ }
121
+ get canUndo() {
122
+ return this.editor.canUndo;
123
+ }
124
+ get canRedo() {
125
+ return this.editor.canRedo;
126
+ }
127
+ async init() {
128
+ if (this.#status === "ready")
129
+ return this;
130
+ if (this.#status === "disposed") {
131
+ throw new Error("SkenoraScene has been disposed");
132
+ }
133
+ if (this.#initializing)
134
+ return (await this.#initializing);
135
+ const initializing = this.#initialize();
136
+ this.#initializing = initializing;
137
+ try {
138
+ return (await initializing);
139
+ }
140
+ finally {
141
+ if (this.#initializing === initializing)
142
+ this.#initializing = null;
143
+ }
144
+ }
145
+ on(type, handler, signal) {
146
+ return this.events.subscribe(type, handler, signal ? { signal } : {});
147
+ }
148
+ getConfig(section) {
149
+ return section
150
+ ? getSceneConfig(this.document, section)
151
+ : structuredClone(this.document);
152
+ }
153
+ getJson(sectionOrOptions, options = {}) {
154
+ return typeof sectionOrOptions === "string"
155
+ ? serializeSceneConfig(this.document, sectionOrOptions, options)
156
+ : serializeSceneJson(this.document, sectionOrOptions ?? {});
157
+ }
158
+ exportJson(options = {}) {
159
+ return options.mode === "split"
160
+ ? exportSceneJsonBundle(this.document, options)
161
+ : serializeSceneJson(this.document, options);
162
+ }
163
+ async loadConfig(document) {
164
+ const owned = normalizeSceneDocument(structuredClone(document));
165
+ assertSceneDocument(owned);
166
+ return this.#execute(replaceDocumentCommand(owned, "Load scene config"));
167
+ }
168
+ async loadJson(input) {
169
+ return this.loadConfig(parseSceneJson(input));
170
+ }
171
+ async applyConfig(section, config) {
172
+ const next = applySceneConfig(this.document, section, config);
173
+ return this.#execute(replaceDocumentCommand(next, `Apply ${section} config`));
174
+ }
175
+ async applyJson(section, json) {
176
+ return this.applyConfig(section, parseSceneConfig(json, section));
177
+ }
178
+ async setEnvironment(update) {
179
+ const current = this.document.environment;
180
+ return this.#execute(setEnvironmentCommand({
181
+ ...current,
182
+ ...update,
183
+ background: {
184
+ ...current.background,
185
+ ...update.background,
186
+ },
187
+ }));
188
+ }
189
+ async setScene(update) {
190
+ const commands = [];
191
+ if (update.id !== undefined) {
192
+ commands.push(setPropertyCommand(["id"], update.id, "Set scene id"));
193
+ }
194
+ if (update.name !== undefined) {
195
+ commands.push(setPropertyCommand(["name"], update.name, "Set scene name"));
196
+ }
197
+ return this.#executeMany("Update scene", commands);
198
+ }
199
+ async setSky(update) {
200
+ const current = this.document.environment;
201
+ const background = {
202
+ ...current.background,
203
+ ...(update.mode === undefined ? {} : { mode: update.mode }),
204
+ ...(update.color === undefined ? {} : { color: update.color }),
205
+ ...(update.blur === undefined ? {} : { blur: update.blur }),
206
+ };
207
+ if (update.backgroundAssetId === null)
208
+ delete background.assetId;
209
+ else if (update.backgroundAssetId !== undefined) {
210
+ background.assetId = update.backgroundAssetId;
211
+ }
212
+ if (update.skybox === null)
213
+ delete background.skybox;
214
+ else if (update.skybox !== undefined) {
215
+ background.skybox = structuredClone(update.skybox);
216
+ }
217
+ const next = {
218
+ ...current,
219
+ background,
220
+ ...(update.intensity === undefined
221
+ ? {}
222
+ : { intensity: update.intensity }),
223
+ ...(update.rotationY === undefined
224
+ ? {}
225
+ : { rotationY: update.rotationY }),
226
+ };
227
+ if (update.environmentAssetId === null)
228
+ delete next.assetId;
229
+ else if (update.environmentAssetId !== undefined) {
230
+ next.assetId = update.environmentAssetId;
231
+ next.enabled = true;
232
+ }
233
+ return this.#execute(setEnvironmentCommand(next));
234
+ }
235
+ async setViewportCamera(update) {
236
+ const current = this.document.camera;
237
+ return this.#execute(setViewportCameraCommand({
238
+ ...current,
239
+ ...update,
240
+ position: { ...current.position, ...update.position },
241
+ target: { ...current.target, ...update.target },
242
+ }));
243
+ }
244
+ async setGround(update) {
245
+ return this.#execute(setGroundCommand({ ...this.document.ground, ...update }));
246
+ }
247
+ async setFog(update) {
248
+ const current = this.document.fog;
249
+ return this.#execute(setFogCommand({
250
+ ...current,
251
+ ...update,
252
+ color: { ...current.color, ...update.color },
253
+ }));
254
+ }
255
+ async setWeather(update) {
256
+ const current = this.document.weather;
257
+ return this.#execute(setWeatherCommand({
258
+ ...current,
259
+ ...update,
260
+ wind: { ...current.wind, ...update.wind },
261
+ }));
262
+ }
263
+ async setPostProcess(update) {
264
+ const current = this.document.postProcess;
265
+ return this.#execute(setPostProcessCommand({
266
+ ...current,
267
+ ...update,
268
+ colorCurves: {
269
+ ...current.colorCurves,
270
+ ...update.colorCurves,
271
+ },
272
+ ssao: { ...current.ssao, ...update.ssao },
273
+ }));
274
+ }
275
+ async setLights(lights) {
276
+ return this.#execute(setPropertyCommand(["lights"], [...lights], "Set scene lights"));
277
+ }
278
+ async upsertLight(light) {
279
+ return this.#execute(upsertLightCommand(light));
280
+ }
281
+ async removeLight(lightId) {
282
+ return this.#execute(removeLightCommand(lightId));
283
+ }
284
+ async upsertAsset(asset) {
285
+ return this.#execute(upsertAssetCommand(asset));
286
+ }
287
+ async searchResources(query = {}) {
288
+ this.#assertReady();
289
+ if (!supportsResourceDiscovery(this.resources)) {
290
+ throw new Error("Configured resource resolver does not support search");
291
+ }
292
+ return this.resources.search(query);
293
+ }
294
+ async describeResource(locator) {
295
+ this.#assertReady();
296
+ if (!supportsResourceDiscovery(this.resources)) {
297
+ throw new Error("Configured resource resolver does not support describe");
298
+ }
299
+ return this.resources.describe(locator);
300
+ }
301
+ async attachResource(descriptor, assetId) {
302
+ const asset = assetReferenceFromDescriptor(descriptor, assetId);
303
+ await this.upsertAsset(asset);
304
+ return this.document.assets[assetId];
305
+ }
306
+ async preflightResources(assetIds = Object.keys(this.document.assets)) {
307
+ this.#assertReady();
308
+ if (!supportsResourceDiscovery(this.resources)) {
309
+ throw new Error("Configured resource resolver does not support preflight");
310
+ }
311
+ const assets = assetIds.map((assetId) => {
312
+ const asset = this.document.assets[assetId];
313
+ if (!asset)
314
+ throw new Error(`Asset not found: ${assetId}`);
315
+ return asset;
316
+ });
317
+ return this.resources.preflight(assets);
318
+ }
319
+ async removeAsset(assetId) {
320
+ return this.#execute(removeAssetCommand(assetId));
321
+ }
322
+ async upsertMaterial(material) {
323
+ return this.#execute(upsertMaterialCommand(material));
324
+ }
325
+ async addModel(input) {
326
+ this.#assertReady();
327
+ const asset = typeof input.asset === "string"
328
+ ? this.document.assets[input.asset]
329
+ : input.asset;
330
+ if (!asset)
331
+ throw new Error(`Model asset not found: ${input.asset}`);
332
+ const commands = [
333
+ importModelCommand(asset, input.id, input.name ?? asset.label ?? "Model"),
334
+ ];
335
+ if (input.parentId !== undefined) {
336
+ commands.push(reparentEntityCommand(input.id, input.parentId));
337
+ }
338
+ if (input.transform !== undefined) {
339
+ commands.push(setEntityTransformCommand(input.id, input.transform));
340
+ }
341
+ if (input.alignment !== undefined) {
342
+ commands.push(setPropertyCommand(["entities", input.id, "properties"], { alignment: structuredClone(input.alignment) }, "Align model"));
343
+ }
344
+ if (input.enabled !== undefined) {
345
+ commands.push(setEntityEnabledCommand(input.id, input.enabled));
346
+ }
347
+ if (input.visible !== undefined) {
348
+ commands.push(setEntityVisibleCommand(input.id, input.visible));
349
+ }
350
+ await this.#executeMany("Add model", commands);
351
+ return this.#requireEntity(input.id);
352
+ }
353
+ async importModel(input) {
354
+ this.#assertReady();
355
+ const primary = {
356
+ file: input.file,
357
+ ...(input.relativePath === undefined
358
+ ? {}
359
+ : { relativePath: input.relativePath }),
360
+ };
361
+ const imported = await this.workspace.importModelBundle(primary, input.dependencies ?? [], {
362
+ ...(input.targetDirectory === undefined
363
+ ? {}
364
+ : { targetDirectory: input.targetDirectory }),
365
+ ...(input.conflict === undefined ? {} : { conflict: input.conflict }),
366
+ });
367
+ const baseId = normalizeId(input.file.name.replace(/\.(?:glb|gltf)$/iu, ""));
368
+ const assetId = input.assetId ?? availableId(this.document.assets, baseId || "model");
369
+ const entityId = input.entityId ?? availableId(this.document.entities, assetId);
370
+ const asset = {
371
+ id: assetId,
372
+ uri: imported.primaryPath,
373
+ source: { type: "workspace", path: imported.primaryPath },
374
+ kind: "model",
375
+ label: input.name ?? input.file.name,
376
+ ...(input.file.type ? { mediaType: input.file.type } : {}),
377
+ };
378
+ const entity = await this.addModel({
379
+ id: entityId,
380
+ asset,
381
+ name: input.name ?? input.file.name,
382
+ ...(input.transform === undefined ? {} : { transform: input.transform }),
383
+ ...(input.alignment === undefined ? {} : { alignment: input.alignment }),
384
+ });
385
+ return { ...imported, asset, entity };
386
+ }
387
+ async updateEntity(entityId, update) {
388
+ this.#requireEntity(entityId);
389
+ const commands = [];
390
+ if (update.name !== undefined) {
391
+ commands.push(setPropertyCommand(["entities", entityId, "name"], update.name, "Rename entity"));
392
+ }
393
+ if (update.parentId !== undefined) {
394
+ commands.push(reparentEntityCommand(entityId, update.parentId));
395
+ }
396
+ if (update.transform !== undefined) {
397
+ commands.push(setEntityTransformCommand(entityId, update.transform));
398
+ }
399
+ if (update.alignment !== undefined) {
400
+ const entity = this.#requireModel(entityId);
401
+ commands.push(setPropertyCommand(["entities", entityId, "properties"], {
402
+ ...entity.properties,
403
+ alignment: structuredClone(update.alignment),
404
+ }, "Align model"));
405
+ }
406
+ if (update.enabled !== undefined) {
407
+ commands.push(setEntityEnabledCommand(entityId, update.enabled));
408
+ }
409
+ if (update.visible !== undefined) {
410
+ commands.push(setEntityVisibleCommand(entityId, update.visible));
411
+ }
412
+ await this.#executeMany("Update entity", commands);
413
+ return this.#requireEntity(entityId);
414
+ }
415
+ async setModelTransform(entityId, transform) {
416
+ this.#requireModel(entityId);
417
+ return this.#execute(setEntityTransformCommand(entityId, transform));
418
+ }
419
+ async setModelAlignment(entityId, alignment) {
420
+ const entity = this.#requireModel(entityId);
421
+ return this.#execute(setPropertyCommand(["entities", entityId, "properties"], {
422
+ ...entity.properties,
423
+ alignment: structuredClone(alignment),
424
+ }, "Align model"));
425
+ }
426
+ async setModelVisible(entityId, visible) {
427
+ this.#requireModel(entityId);
428
+ return this.#execute(setEntityVisibleCommand(entityId, visible));
429
+ }
430
+ async setModelMaterial(entityId, material) {
431
+ this.#requireModel(entityId);
432
+ return this.#execute(upsertMaterialCommand({
433
+ ...material,
434
+ targetEntityId: entityId,
435
+ }));
436
+ }
437
+ async removeModel(entityId, options = {}) {
438
+ const entity = this.#requireModel(entityId);
439
+ const commands = [removeEntityCommand(entityId)];
440
+ if (options.removeAsset && entity.assetId) {
441
+ commands.push(removeAssetCommand(entity.assetId));
442
+ }
443
+ return this.#executeMany("Remove model", commands);
444
+ }
445
+ async addPrimitive(options) {
446
+ await this.#execute(createPrimitiveCommand(options));
447
+ return this.#requireEntity(options.id);
448
+ }
449
+ async addProcedural(options) {
450
+ await this.#execute(createProceduralEntityCommand(options));
451
+ return this.#requireEntity(options.id);
452
+ }
453
+ async addEntity(entity) {
454
+ await this.#execute(addEntityCommand(entity));
455
+ return this.#requireEntity(entity.id);
456
+ }
457
+ async removeEntity(entityId) {
458
+ return this.#execute(removeEntityCommand(entityId));
459
+ }
460
+ getEntity(entityId) {
461
+ return this.document.entities[entityId] ?? null;
462
+ }
463
+ getEntities() {
464
+ return Object.freeze(Object.values(this.document.entities));
465
+ }
466
+ select(entityIds) {
467
+ this.editor.select(entityIds);
468
+ }
469
+ async execute(command) {
470
+ return this.#execute(command);
471
+ }
472
+ async executeMany(label, commands) {
473
+ return this.#executeMany(label, commands);
474
+ }
475
+ async undo() {
476
+ this.#assertReady();
477
+ const change = this.editor.undo();
478
+ await this.#bridge.flush();
479
+ return change;
480
+ }
481
+ async redo() {
482
+ this.#assertReady();
483
+ const change = this.editor.redo();
484
+ await this.#bridge.flush();
485
+ return change;
486
+ }
487
+ async save(path, options = {}) {
488
+ this.#assertReady();
489
+ await this.workspace.saveDocument(structuredClone(this.document), path, options);
490
+ this.editor.markSaved();
491
+ }
492
+ async checkpoint(sourcePath = null) {
493
+ this.#assertReady();
494
+ return this.workspace.checkpointDocument(structuredClone(this.document), sourcePath);
495
+ }
496
+ async listCheckpoints(documentId = this.document.id) {
497
+ this.#assertReady();
498
+ return this.workspace.listCheckpoints(documentId);
499
+ }
500
+ async restoreCheckpoint(checkpointPath, documentPath = this.workspace.documentPath ?? DEFAULT_SCENE_FILE) {
501
+ this.#assertReady();
502
+ await this.workspace.restoreCheckpoint(checkpointPath, documentPath);
503
+ const restored = await this.workspace.loadDocument(documentPath);
504
+ await this.loadConfig(restored);
505
+ this.editor.markSaved();
506
+ }
507
+ frame(entityIds) {
508
+ this.runtime.capabilities
509
+ .require(RUNTIME_CAMERA_CAPABILITY)
510
+ .frame(entityIds);
511
+ }
512
+ showGrid(visible) {
513
+ return this.runtime.showGrid(visible);
514
+ }
515
+ getPlaybackConfig() {
516
+ return getSkenoraPlaybackConfig(this.document);
517
+ }
518
+ async setPlaybackConfig(config) {
519
+ const normalized = normalizeSkenoraPlaybackConfig(config);
520
+ return this.#execute(setPropertyCommand(["extensions", SKENORA_PLAYBACK_EXTENSION_ID], normalized, "Set renderer playback"));
521
+ }
522
+ async startFlows(payload) {
523
+ this.#assertReady();
524
+ if (!this.#flow)
525
+ throw new Error("Flow support is disabled");
526
+ await this.#flow.controller.start(payload);
527
+ this.events.publish("flow.changed", { running: this.#flow.controller.running }, { source: "sdk" });
528
+ }
529
+ stopFlows() {
530
+ this.#assertReady();
531
+ if (!this.#flow)
532
+ return 0;
533
+ const cancelled = this.#flow.controller.stop();
534
+ this.events.publish("flow.changed", { running: false }, { source: "sdk" });
535
+ return cancelled;
536
+ }
537
+ /**
538
+ * Manually starts document behaviors and Flow while keeping authoring APIs
539
+ * available. Editor initialization itself never starts preview.
540
+ */
541
+ async startPreview(payload) {
542
+ this.#assertReady();
543
+ if (this.#previewing)
544
+ return;
545
+ this.runtime.startDocumentBehaviors();
546
+ this.#previewing = true;
547
+ this.events.publish("preview.changed", { running: true }, { source: "sdk" });
548
+ try {
549
+ if (this.#flow) {
550
+ const start = this.#flow.controller.start(payload);
551
+ this.events.publish("flow.changed", { running: this.#flow.controller.running }, { source: "sdk" });
552
+ await start;
553
+ }
554
+ }
555
+ catch (error) {
556
+ this.#flow?.controller.stop();
557
+ this.runtime.stopDocumentBehaviors();
558
+ this.#previewing = false;
559
+ this.events.publish("flow.changed", { running: false }, { source: "sdk" });
560
+ this.events.publish("preview.changed", { running: false }, { source: "sdk" });
561
+ throw error;
562
+ }
563
+ }
564
+ /** Stops manual preview without unloading the editable scene. */
565
+ stopPreview() {
566
+ this.#assertReady();
567
+ const cancelled = this.#flow?.controller.stop() ?? 0;
568
+ this.runtime.stopDocumentBehaviors();
569
+ const changed = this.#previewing;
570
+ this.#previewing = false;
571
+ if (this.#flow) {
572
+ this.events.publish("flow.changed", { running: false }, { source: "sdk" });
573
+ }
574
+ if (changed) {
575
+ this.events.publish("preview.changed", { running: false }, { source: "sdk" });
576
+ }
577
+ return cancelled;
578
+ }
579
+ dispose() {
580
+ if (this.#status === "disposed")
581
+ return;
582
+ this.#lifetime?.abort();
583
+ this.#previewing = false;
584
+ this.#flow?.dispose();
585
+ this.#scenePlanExecutor?.dispose();
586
+ this.#bridge?.dispose();
587
+ this.#runtime?.dispose();
588
+ this.#editor?.dispose();
589
+ this.#resourceContext?.ownedResources?.dispose();
590
+ this.#workspace?.dispose();
591
+ this.#flow = null;
592
+ this.#scenePlan = null;
593
+ this.#scenePlanExecutor = null;
594
+ this.#bridge = null;
595
+ this.#runtime = null;
596
+ this.#editor = null;
597
+ this.#resources = null;
598
+ this.#resourceContext = null;
599
+ this.#workspace = null;
600
+ this.#lifetime = null;
601
+ this.#setStatus("disposed");
602
+ if (this.#ownsEvents)
603
+ this.events.dispose();
604
+ }
605
+ async #initialize() {
606
+ this.#setStatus("initializing");
607
+ const source = this.#options.source ?? new MemoryWorkspaceSource();
608
+ const workspace = new WorkspaceSession(source);
609
+ const resourceContext = createWorkspaceResourceContext(source, {
610
+ ...(this.#options.resources
611
+ ? { resources: this.#options.resources }
612
+ : {}),
613
+ ...(this.#options.resourceProviders
614
+ ? { resourceProviders: this.#options.resourceProviders }
615
+ : {}),
616
+ });
617
+ const resources = resourceContext.resources;
618
+ let editor = null;
619
+ let runtime = null;
620
+ let bridge = null;
621
+ let scenePlanExecutor = null;
622
+ let scenePlan = null;
623
+ let flow = null;
624
+ const lifetime = new AbortController();
625
+ this.#lifetime = lifetime;
626
+ try {
627
+ const document = await this.#resolveInitialDocument(workspace);
628
+ throwIfInitializationCancelled(lifetime.signal);
629
+ editor = new EditorSession(document, this.#options.editor);
630
+ runtime = new RuntimeSession(this.#options.canvas, {
631
+ adaptToDeviceRatio: true,
632
+ ...this.#options.runtime,
633
+ executionMode: "authoring",
634
+ });
635
+ bridge = new EditorRuntimeBridge(editor, runtime, resources, {
636
+ modelLoadPolicy: "best-effort",
637
+ ...this.#options.bridge,
638
+ });
639
+ const capabilityAvailability = runtime.getCapabilityAvailability();
640
+ scenePlanExecutor = new ScenePlanExecutor({
641
+ editor,
642
+ bridge,
643
+ resources,
644
+ capabilities: capabilityAvailability.map(({ id, version }) => ({
645
+ id,
646
+ version,
647
+ })),
648
+ getCapabilityAvailability: () => runtime.getCapabilityAvailability(),
649
+ signal: lifetime.signal,
650
+ });
651
+ scenePlan = new ScenePlanGateway(scenePlanExecutor);
652
+ flow =
653
+ this.#options.flow === false
654
+ ? null
655
+ : new EditorFlowBridge(editor, runtime, {
656
+ ...this.#options.flow,
657
+ startOnFlowChange: false,
658
+ });
659
+ this.#workspace = workspace;
660
+ this.#resources = resources;
661
+ this.#resourceContext = resourceContext;
662
+ this.#editor = editor;
663
+ this.#runtime = runtime;
664
+ this.#bridge = bridge;
665
+ this.#scenePlanExecutor = scenePlanExecutor;
666
+ this.#scenePlan = scenePlan;
667
+ this.#flow = flow;
668
+ this.#bindEvents(lifetime.signal);
669
+ await bridge.initialize();
670
+ throwIfInitializationCancelled(lifetime.signal);
671
+ this.#setStatus("ready");
672
+ return this;
673
+ }
674
+ catch (error) {
675
+ const disposed = this.#status === "disposed";
676
+ lifetime.abort();
677
+ flow?.dispose();
678
+ scenePlanExecutor?.dispose();
679
+ bridge?.dispose();
680
+ runtime?.dispose();
681
+ editor?.dispose();
682
+ resourceContext.ownedResources?.dispose();
683
+ workspace.dispose();
684
+ this.#flow = null;
685
+ this.#scenePlan = null;
686
+ this.#scenePlanExecutor = null;
687
+ this.#bridge = null;
688
+ this.#runtime = null;
689
+ this.#editor = null;
690
+ this.#resources = null;
691
+ this.#resourceContext = null;
692
+ this.#workspace = null;
693
+ this.#lifetime = null;
694
+ if (!disposed) {
695
+ this.#setStatus("error", error);
696
+ this.#publishError(error, "initialize");
697
+ }
698
+ throw error;
699
+ }
700
+ }
701
+ async #resolveInitialDocument(workspace) {
702
+ if (this.#options.document) {
703
+ const document = normalizeSceneDocument(structuredClone(this.#options.document));
704
+ assertSceneDocument(document);
705
+ return document;
706
+ }
707
+ await workspace.scan();
708
+ if (this.#options.documentPath) {
709
+ return workspace.loadDocument(this.#options.documentPath);
710
+ }
711
+ const documents = await workspace.discoverSceneDocuments();
712
+ if (documents.length > 0)
713
+ return workspace.loadDocument();
714
+ return createEmptySceneDocument(this.#options.id ?? "scene", this.#options.name ?? "Untitled scene");
715
+ }
716
+ #bindEvents(signal) {
717
+ const editor = this.#editor;
718
+ const runtime = this.#runtime;
719
+ editor.subscribe("document.changed", (change, event) => this.events.publish("document.changed", { change, document: editor.snapshot }, createCausationMetadata(event, "sdk")), signal);
720
+ editor.subscribe("selection.changed", ({ entityIds }, event) => this.events.publish("selection.changed", { entityIds }, createCausationMetadata(event, "sdk")), signal);
721
+ editor.subscribe("dirty.changed", ({ dirty }, event) => this.events.publish("dirty.changed", { dirty }, createCausationMetadata(event, "sdk")), signal);
722
+ runtime.events.subscribe("runtime.loading", ({ loading }, event) => {
723
+ if (loading)
724
+ this.#modelFailures.length = 0;
725
+ this.events.publish("runtime.loading", { loading }, createCausationMetadata(event, "sdk"));
726
+ }, { signal });
727
+ runtime.events.subscribe("runtime.document.applied", () => {
728
+ if (!this.#previewing)
729
+ return;
730
+ try {
731
+ runtime.startDocumentBehaviors();
732
+ }
733
+ catch (error) {
734
+ this.#publishError(error, "restartPreviewBehaviors");
735
+ }
736
+ }, { signal });
737
+ runtime.events.subscribe("runtime.loading.progress", (progress, event) => this.events.publish("model.loading.progress", progress, createCausationMetadata(event, "sdk")), { signal });
738
+ runtime.events.subscribe("runtime.model.loaded", ({ entityId }, event) => this.events.publish("model.loaded", { entityId }, createCausationMetadata(event, "sdk")), { signal });
739
+ runtime.events.subscribe("runtime.model.failed", ({ entityId, error }, event) => {
740
+ this.#modelFailures.push({ entityId, error });
741
+ this.events.publish("model.failed", { entityId, error }, createCausationMetadata(event, "sdk"));
742
+ }, { signal });
743
+ runtime.events.subscribe("runtime.error", ({ error, operation }, event) => this.#publishError(error, operation, createCausationMetadata(event, "sdk")), { signal });
744
+ }
745
+ async #execute(command) {
746
+ this.#assertReady();
747
+ try {
748
+ const change = this.editor.execute(command);
749
+ await this.#bridge.flush();
750
+ return change;
751
+ }
752
+ catch (error) {
753
+ this.#publishError(error, command.id);
754
+ throw error;
755
+ }
756
+ }
757
+ async #executeMany(label, commands) {
758
+ this.#assertReady();
759
+ if (commands.length === 0)
760
+ return null;
761
+ try {
762
+ const change = this.editor.transaction(label, commands);
763
+ await this.#bridge.flush();
764
+ return change;
765
+ }
766
+ catch (error) {
767
+ this.#publishError(error, label);
768
+ throw error;
769
+ }
770
+ }
771
+ #requireEntity(entityId) {
772
+ const entity = this.document.entities[entityId];
773
+ if (!entity)
774
+ throw new Error(`Entity not found: ${entityId}`);
775
+ return entity;
776
+ }
777
+ #requireModel(entityId) {
778
+ const entity = this.#requireEntity(entityId);
779
+ if (entity.type !== "model") {
780
+ throw new Error(`Model entity not found: ${entityId}`);
781
+ }
782
+ return entity;
783
+ }
784
+ #setStatus(status, error = null) {
785
+ this.#status = status;
786
+ this.#error = error;
787
+ this.events.publish("status.changed", { status, error }, { source: "sdk" });
788
+ }
789
+ #publishError(error, operation, metadata = { source: "sdk" }) {
790
+ this.events.publish("error", { error, operation }, metadata);
791
+ }
792
+ #assertReady() {
793
+ if (this.#status !== "ready") {
794
+ throw new Error(`SkenoraScene is not ready: ${this.#status}`);
795
+ }
796
+ }
797
+ }
798
+ function normalizeId(value) {
799
+ return value
800
+ .trim()
801
+ .toLowerCase()
802
+ .replace(/[^a-z0-9_-]+/gu, "-")
803
+ .replace(/^-+|-+$/gu, "");
804
+ }
805
+ function availableId(record, requested) {
806
+ if (!record[requested])
807
+ return requested;
808
+ let suffix = 2;
809
+ while (record[`${requested}-${suffix}`])
810
+ suffix += 1;
811
+ return `${requested}-${suffix}`;
812
+ }
813
+ function throwIfInitializationCancelled(signal) {
814
+ if (!signal.aborted)
815
+ return;
816
+ throw new DOMException("SkenoraScene initialization was cancelled", "AbortError");
817
+ }
818
+ /** Preferred name for the authoring-only high-level facade. */
819
+ export class SkenoraEditor extends SkenoraScene {
820
+ static async create(options) {
821
+ const editor = new SkenoraEditor(options);
822
+ try {
823
+ await editor.init();
824
+ return editor;
825
+ }
826
+ catch (error) {
827
+ editor.dispose();
828
+ throw error;
829
+ }
830
+ }
831
+ }
832
+ //# sourceMappingURL=skenora-scene.js.map