@dreamerbird/camera-studio 0.1.0 → 0.3.0

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 (139) hide show
  1. package/LICENSE +866 -0
  2. package/assets/models/Y Bot.glb +0 -0
  3. package/dist/CameraScene.d.ts +1 -1
  4. package/dist/CameraScene.d.ts.map +1 -1
  5. package/dist/CameraScene.js +100 -9
  6. package/dist/PlacementManager.d.ts +15 -1
  7. package/dist/PlacementManager.d.ts.map +1 -1
  8. package/dist/PlacementManager.js +151 -15
  9. package/dist/agent/api.d.ts +5 -0
  10. package/dist/agent/api.d.ts.map +1 -1
  11. package/dist/agent/api.js +25 -0
  12. package/dist/agent/api.test.js +75 -1
  13. package/dist/agent/commands/assets.d.ts +5 -0
  14. package/dist/agent/commands/assets.d.ts.map +1 -1
  15. package/dist/agent/commands/assets.js +93 -21
  16. package/dist/agent/commands/assets.test.js +53 -0
  17. package/dist/agent/commands/materials.d.ts +18 -0
  18. package/dist/agent/commands/materials.d.ts.map +1 -0
  19. package/dist/agent/commands/materials.js +192 -0
  20. package/dist/agent/commands/materials.test.d.ts +2 -0
  21. package/dist/agent/commands/materials.test.d.ts.map +1 -0
  22. package/dist/agent/commands/materials.test.js +294 -0
  23. package/dist/agent/commands/modelRecovery.test.d.ts +2 -0
  24. package/dist/agent/commands/modelRecovery.test.d.ts.map +1 -0
  25. package/dist/agent/commands/modelRecovery.test.js +122 -0
  26. package/dist/agent/commands/models.d.ts +13 -0
  27. package/dist/agent/commands/models.d.ts.map +1 -1
  28. package/dist/agent/commands/models.js +98 -1
  29. package/dist/agent/commands/primitives.d.ts +27 -0
  30. package/dist/agent/commands/primitives.d.ts.map +1 -0
  31. package/dist/agent/commands/primitives.js +117 -0
  32. package/dist/agent/commands/primitives.test.d.ts +2 -0
  33. package/dist/agent/commands/primitives.test.d.ts.map +1 -0
  34. package/dist/agent/commands/primitives.test.js +135 -0
  35. package/dist/agent/expectedCommands.d.ts +1 -1
  36. package/dist/agent/expectedCommands.d.ts.map +1 -1
  37. package/dist/agent/expectedCommands.js +6 -0
  38. package/dist/agent/propAccess.d.ts +2 -0
  39. package/dist/agent/propAccess.d.ts.map +1 -1
  40. package/dist/agent/propAccess.js +21 -0
  41. package/dist/agent/registerCommands.d.ts.map +1 -1
  42. package/dist/agent/registerCommands.js +5 -1
  43. package/dist/agent/runtime/objectRuntime.d.ts +2 -0
  44. package/dist/agent/runtime/objectRuntime.d.ts.map +1 -1
  45. package/dist/agent/runtime/objectRuntime.js +15 -1
  46. package/dist/agent/runtime.d.ts +1 -0
  47. package/dist/agent/runtime.d.ts.map +1 -1
  48. package/dist/agent/runtime.js +2 -0
  49. package/dist/assetLibrary/dispose.d.ts +4 -2
  50. package/dist/assetLibrary/dispose.d.ts.map +1 -1
  51. package/dist/assetLibrary/dispose.js +7 -5
  52. package/dist/assetLibrary/exportGLB.d.ts +25 -0
  53. package/dist/assetLibrary/exportGLB.d.ts.map +1 -1
  54. package/dist/assetLibrary/exportGLB.js +43 -1
  55. package/dist/assetLibrary/exportGLB.test.js +56 -2
  56. package/dist/assetLibrary/gltfLoader.d.ts +2 -1
  57. package/dist/assetLibrary/gltfLoader.d.ts.map +1 -1
  58. package/dist/assetLibrary/gltfLoader.js +2 -2
  59. package/dist/assetLibrary/gltfResourceSet.d.ts +6 -0
  60. package/dist/assetLibrary/gltfResourceSet.d.ts.map +1 -0
  61. package/dist/assetLibrary/gltfResourceSet.js +27 -0
  62. package/dist/assetLibrary/gltfResourceSet.test.d.ts +2 -0
  63. package/dist/assetLibrary/gltfResourceSet.test.d.ts.map +1 -0
  64. package/dist/assetLibrary/gltfResourceSet.test.js +40 -0
  65. package/dist/assetLibrary/importModelFile.d.ts +5 -0
  66. package/dist/assetLibrary/importModelFile.d.ts.map +1 -1
  67. package/dist/assetLibrary/importModelFile.js +3 -16
  68. package/dist/assetLibrary/importModelFiles.d.ts +15 -0
  69. package/dist/assetLibrary/importModelFiles.d.ts.map +1 -0
  70. package/dist/assetLibrary/importModelFiles.js +58 -0
  71. package/dist/assetLibrary/importModelFiles.test.d.ts +2 -0
  72. package/dist/assetLibrary/importModelFiles.test.d.ts.map +1 -0
  73. package/dist/assetLibrary/importModelFiles.test.js +217 -0
  74. package/dist/assetLibrary/importPipeline.d.ts +15 -1
  75. package/dist/assetLibrary/importPipeline.d.ts.map +1 -1
  76. package/dist/assetLibrary/importPipeline.js +74 -38
  77. package/dist/assetLibrary/importPipeline.test.js +25 -0
  78. package/dist/assetLibrary/library.d.ts +1 -0
  79. package/dist/assetLibrary/library.d.ts.map +1 -1
  80. package/dist/assetLibrary/library.js +22 -1
  81. package/dist/assetLibrary/library.test.js +76 -0
  82. package/dist/assetLibrary/seedAssetsManifest.js +1 -1
  83. package/dist/bootstrap.d.ts +2 -1
  84. package/dist/bootstrap.d.ts.map +1 -1
  85. package/dist/bootstrap.js +69 -7
  86. package/dist/bootstrap.test.js +111 -0
  87. package/dist/config.d.ts +10 -1
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/firstScreenGate.d.ts +28 -0
  90. package/dist/firstScreenGate.d.ts.map +1 -0
  91. package/dist/firstScreenGate.js +67 -0
  92. package/dist/firstScreenGate.test.d.ts +2 -0
  93. package/dist/firstScreenGate.test.d.ts.map +1 -0
  94. package/dist/firstScreenGate.test.js +83 -0
  95. package/dist/hostFlows.d.ts +1 -0
  96. package/dist/hostFlows.d.ts.map +1 -1
  97. package/dist/hostFlows.js +9 -0
  98. package/dist/index.d.ts +10 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +6 -1
  101. package/dist/materials/modelMaterials.d.ts +19 -0
  102. package/dist/materials/modelMaterials.d.ts.map +1 -0
  103. package/dist/materials/modelMaterials.js +253 -0
  104. package/dist/materials/modelMaterials.test.d.ts +2 -0
  105. package/dist/materials/modelMaterials.test.d.ts.map +1 -0
  106. package/dist/materials/modelMaterials.test.js +188 -0
  107. package/dist/materials/types.d.ts +20 -0
  108. package/dist/materials/types.d.ts.map +1 -0
  109. package/dist/materials/types.js +1 -0
  110. package/dist/materials/useModelMaterials.d.ts +10 -0
  111. package/dist/materials/useModelMaterials.d.ts.map +1 -0
  112. package/dist/materials/useModelMaterials.js +26 -0
  113. package/dist/materials/useModelMaterials.test.d.ts +2 -0
  114. package/dist/materials/useModelMaterials.test.d.ts.map +1 -0
  115. package/dist/materials/useModelMaterials.test.js +75 -0
  116. package/dist/poses.d.ts +38 -0
  117. package/dist/poses.d.ts.map +1 -0
  118. package/dist/poses.js +41 -0
  119. package/dist/poses.test.d.ts +2 -0
  120. package/dist/poses.test.d.ts.map +1 -0
  121. package/dist/poses.test.js +38 -0
  122. package/dist/primitives.d.ts +5 -0
  123. package/dist/primitives.d.ts.map +1 -0
  124. package/dist/primitives.js +15 -0
  125. package/dist/primitives.test.d.ts +2 -0
  126. package/dist/primitives.test.d.ts.map +1 -0
  127. package/dist/primitives.test.js +27 -0
  128. package/dist/reset.realStudio.test.d.ts +2 -0
  129. package/dist/reset.realStudio.test.d.ts.map +1 -0
  130. package/dist/reset.realStudio.test.js +60 -0
  131. package/dist/studioRoot.d.ts +4 -0
  132. package/dist/studioRoot.d.ts.map +1 -0
  133. package/dist/studioRoot.js +36 -0
  134. package/dist/studioRoot.test.d.ts +2 -0
  135. package/dist/studioRoot.test.d.ts.map +1 -0
  136. package/dist/studioRoot.test.js +62 -0
  137. package/dist/useCameraStudio.d.ts.map +1 -1
  138. package/dist/useCameraStudio.js +3 -2
  139. package/package.json +18 -10
@@ -1,17 +1,28 @@
1
1
  import { translate } from '@dreamerbird/studio/i18n';
2
2
  import { classifyModel, validatePoseJson } from './classify';
3
3
  import { createGLTFLoader } from './gltfLoader';
4
+ import { createResourceUrlModifier } from './gltfResourceSet';
4
5
  import { exportAnimationGLB, exportModelGLB } from './exportGLB';
5
6
  import { hashBytes } from './hash';
6
7
  import { collectBoneNames, meshStats, rigIdOf } from './rig';
7
- export const realImportDeps = () => ({
8
- load: defaultLoad,
8
+ export const realImportDeps = (files) => ({
9
+ load: (file) => defaultLoad(file, files),
9
10
  exportModel: exportModelGLB,
10
11
  exportAnimation: exportAnimationGLB,
11
12
  });
12
- async function defaultLoad(file) {
13
+ async function defaultLoad(file, resourceFiles) {
13
14
  const { FBXLoader } = await import('three-stdlib');
14
15
  const { LoadingManager } = await import('three');
16
+ if (resourceFiles && /\.gltf$/i.test(file.name)) {
17
+ const resources = createResourceUrlModifier(resourceFiles);
18
+ try {
19
+ const gltf = await createGLTFLoader(resources.manager).parseAsync(await file.text(), '');
20
+ return { root: gltf.scene, animations: gltf.animations };
21
+ }
22
+ finally {
23
+ resources.dispose();
24
+ }
25
+ }
15
26
  const url = URL.createObjectURL(file);
16
27
  try {
17
28
  if (/\.glb$|\.gltf$/i.test(file.name)) {
@@ -81,6 +92,7 @@ export async function planImport(files, library, deps, tr = translate) {
81
92
  items.push({
82
93
  type: 'skip',
83
94
  name: file.name,
95
+ sourceFile: file.name,
84
96
  reason: tr('pg.import.reason.badPoseJson'),
85
97
  });
86
98
  }
@@ -88,6 +100,7 @@ export async function planImport(files, library, deps, tr = translate) {
88
100
  items.push({
89
101
  type: 'pose',
90
102
  name: baseName(file.name),
103
+ sourceFile: file.name,
91
104
  rig: parsed.rig,
92
105
  bones: parsed.bones,
93
106
  });
@@ -102,6 +115,7 @@ export async function planImport(files, library, deps, tr = translate) {
102
115
  items.push({
103
116
  type: 'skip',
104
117
  name: file.name,
118
+ sourceFile: file.name,
105
119
  reason: tr('pg.import.reason.parseFailed'),
106
120
  });
107
121
  continue;
@@ -111,6 +125,7 @@ export async function planImport(files, library, deps, tr = translate) {
111
125
  items.push({
112
126
  type: 'skip',
113
127
  name: file.name,
128
+ sourceFile: file.name,
114
129
  reason: tr('pg.import.reason.noContent'),
115
130
  });
116
131
  continue;
@@ -128,6 +143,7 @@ export async function planImport(files, library, deps, tr = translate) {
128
143
  items.push({
129
144
  type: 'skip',
130
145
  name: baseName(file.name),
146
+ sourceFile: file.name,
131
147
  reason: tr('pg.import.reason.modelExportFailed'),
132
148
  });
133
149
  }
@@ -140,6 +156,7 @@ export async function planImport(files, library, deps, tr = translate) {
140
156
  const index = items.push({
141
157
  type: 'model',
142
158
  name: baseName(file.name),
159
+ sourceFile: file.name,
143
160
  rig,
144
161
  contentHash,
145
162
  boneCount,
@@ -163,7 +180,9 @@ export async function planImport(files, library, deps, tr = translate) {
163
180
  items.push({
164
181
  type: 'skip',
165
182
  name: baseName(file.name),
183
+ sourceFile: file.name,
166
184
  reason: tr('pg.import.reason.modelExists', { name: dup.name }),
185
+ duplicate: true,
167
186
  });
168
187
  }
169
188
  // dup 命中:模型跳过,下面的动作照常提取
@@ -180,6 +199,7 @@ export async function planImport(files, library, deps, tr = translate) {
180
199
  items.push({
181
200
  type: 'skip',
182
201
  name: `${baseName(file.name)}/${clip.name}`,
202
+ sourceFile: file.name,
183
203
  reason: tr('pg.import.reason.animationExportFailed'),
184
204
  });
185
205
  continue;
@@ -189,13 +209,16 @@ export async function planImport(files, library, deps, tr = translate) {
189
209
  items.push({
190
210
  type: 'skip',
191
211
  name: `${baseName(file.name)}/${clip.name}`,
212
+ sourceFile: file.name,
192
213
  reason: tr('pg.import.reason.animationExists'),
214
+ duplicate: true,
193
215
  });
194
216
  continue;
195
217
  }
196
218
  items.push({
197
219
  type: 'animation',
198
220
  name: clip.name || baseName(file.name),
221
+ sourceFile: file.name,
199
222
  rig,
200
223
  contentHash,
201
224
  duration: clip.duration,
@@ -205,6 +228,14 @@ export async function planImport(files, library, deps, tr = translate) {
205
228
  }
206
229
  return { items, similar, summary: summarize(items, tr) };
207
230
  }
231
+ export class ImportItemWriteError extends Error {
232
+ constructor(item, cause) {
233
+ super(`导入项 ${item.name} 写入失败: ${cause instanceof Error ? cause.message : String(cause)}`);
234
+ this.item = item;
235
+ this.cause = cause;
236
+ this.name = 'ImportItemWriteError';
237
+ }
238
+ }
208
239
  /** 落库:choices 与 plan.similar 一一对应,'reuse' 丢弃对应 model 项 */
209
240
  export async function executeImport(plan, choices, library) {
210
241
  const drop = new Set(plan.similar.filter((_, i) => choices[i] === 'reuse').map((s) => s.index));
@@ -212,42 +243,47 @@ export async function executeImport(plan, choices, library) {
212
243
  const item = plan.items[i];
213
244
  if (item.type === 'skip')
214
245
  continue;
215
- if (item.type === 'model') {
216
- if (drop.has(i))
217
- continue;
218
- await library.putBlobWithEntry(new Blob([item.glb]), (blobId) => ({
219
- id: newId(),
220
- kind: 'model',
221
- name: item.name,
222
- rig: item.rig,
223
- blobId,
224
- contentHash: item.contentHash,
225
- boneCount: item.boneCount,
226
- vertexCount: item.vertexCount,
227
- createdAt: Date.now(),
228
- }));
229
- }
230
- else if (item.type === 'animation') {
231
- await library.putBlobWithEntry(new Blob([item.glb]), (blobId) => ({
232
- id: newId(),
233
- kind: 'animation',
234
- name: item.name,
235
- rig: item.rig,
236
- blobId,
237
- contentHash: item.contentHash,
238
- duration: item.duration,
239
- createdAt: Date.now(),
240
- }));
246
+ try {
247
+ if (item.type === 'model') {
248
+ if (drop.has(i))
249
+ continue;
250
+ await library.putBlobWithEntry(new Blob([item.glb]), (blobId) => ({
251
+ id: newId(),
252
+ kind: 'model',
253
+ name: item.name,
254
+ rig: item.rig,
255
+ blobId,
256
+ contentHash: item.contentHash,
257
+ boneCount: item.boneCount,
258
+ vertexCount: item.vertexCount,
259
+ createdAt: Date.now(),
260
+ }));
261
+ }
262
+ else if (item.type === 'animation') {
263
+ await library.putBlobWithEntry(new Blob([item.glb]), (blobId) => ({
264
+ id: newId(),
265
+ kind: 'animation',
266
+ name: item.name,
267
+ rig: item.rig,
268
+ blobId,
269
+ contentHash: item.contentHash,
270
+ duration: item.duration,
271
+ createdAt: Date.now(),
272
+ }));
273
+ }
274
+ else {
275
+ await library.putEntry({
276
+ id: newId(),
277
+ kind: 'pose',
278
+ name: item.name,
279
+ rig: item.rig,
280
+ bones: item.bones,
281
+ createdAt: Date.now(),
282
+ });
283
+ }
241
284
  }
242
- else {
243
- await library.putEntry({
244
- id: newId(),
245
- kind: 'pose',
246
- name: item.name,
247
- rig: item.rig,
248
- bones: item.bones,
249
- createdAt: Date.now(),
250
- });
285
+ catch (error) {
286
+ throw new ImportItemWriteError(item, error);
251
287
  }
252
288
  }
253
289
  }
@@ -185,4 +185,29 @@ describe('executeImport', () => {
185
185
  await executeImport(plan, ['keep'], lib);
186
186
  expect((await backend.listEntries('model')).length).toBe(1);
187
187
  });
188
+ it('第二个模型写库失败时保留实际失败项与原始错误', async () => {
189
+ const backend = memoryBackend();
190
+ let writes = 0;
191
+ backend.putBlob = async () => {
192
+ writes++;
193
+ if (writes === 2)
194
+ throw new Error('disk full');
195
+ return `blob-${writes}`;
196
+ };
197
+ const lib = new Library({ sources: [backend] });
198
+ const deps = depsFor({
199
+ 'a.fbx': { root: skinnedModel('Hips'), animations: [] },
200
+ 'b.fbx': { root: skinnedModel('Spine'), animations: [] },
201
+ });
202
+ deps.exportModel = async (root) => {
203
+ const names = [];
204
+ root.traverse((object) => names.push(object.name));
205
+ return new TextEncoder().encode(names.join('|')).buffer;
206
+ };
207
+ const plan = await planImportZh([file('a.fbx'), file('b.fbx')], backend, deps);
208
+ await expect(executeImport(plan, [], lib)).rejects.toMatchObject({
209
+ item: { name: 'b', sourceFile: 'b.fbx' },
210
+ cause: new Error('disk full'),
211
+ });
212
+ });
188
213
  });
@@ -84,6 +84,7 @@ export declare class Library {
84
84
  listBlobIds(): Promise<string[]>;
85
85
  /** 所有条目引用的 blob id 集合(模型 blobId + thumbnailId) */
86
86
  private referencedBlobIds;
87
+ private referencedBlobIdsForRollback;
87
88
  /** 删条目并清理它的 blob;blob 被其他条目共享时保留 */
88
89
  deleteEntryDeep(id: string): Promise<void>;
89
90
  /** 清理无条目引用的孤儿 blob,返回删除数量 */
@@ -1 +1 @@
1
- {"version":3,"file":"library.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/library.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"library.d.ts","sourceRoot":"","sources":["../../src/assetLibrary/library.ts"],"names":[],"mappings":"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"}
@@ -238,7 +238,16 @@ export class Library {
238
238
  await this.enqueue(async () => {
239
239
  const writable = this.requireWritable();
240
240
  const blobId = await writable.putBlob(data);
241
- await writable.putEntry(makeEntry(blobId));
241
+ try {
242
+ await writable.putEntry(makeEntry(blobId));
243
+ }
244
+ catch (error) {
245
+ const referenced = await this.referencedBlobIdsForRollback().catch(() => undefined);
246
+ if (referenced && !referenced.has(blobId)) {
247
+ await writable.deleteBlob(blobId).catch(() => { });
248
+ }
249
+ throw error;
250
+ }
242
251
  this.emit();
243
252
  });
244
253
  }
@@ -265,6 +274,18 @@ export class Library {
265
274
  }
266
275
  return ids;
267
276
  }
277
+ async referencedBlobIdsForRollback() {
278
+ const catalogs = await Promise.all(this.sources.map((source) => source.listEntries()));
279
+ const ids = new Set();
280
+ for (const entry of catalogs.flat()) {
281
+ if ('blobId' in entry)
282
+ ids.add(entry.blobId);
283
+ if ('thumbnailId' in entry && entry.thumbnailId) {
284
+ ids.add(entry.thumbnailId);
285
+ }
286
+ }
287
+ return ids;
288
+ }
268
289
  /** 删条目并清理它的 blob;blob 被其他条目共享时保留 */
269
290
  async deleteEntryDeep(id) {
270
291
  await this.enqueue(async () => {
@@ -400,6 +400,82 @@ describe('骨架族别名并集读(终审 M-4)', () => {
400
400
  });
401
401
  });
402
402
  describe('blob 生命周期', () => {
403
+ test('putBlobWithEntry rolls back the upload when entry creation fails', async () => {
404
+ const backend = memoryBackend();
405
+ const failure = new Error('entry write failed');
406
+ backend.putEntry = async () => {
407
+ throw failure;
408
+ };
409
+ backend.deleteBlob = jest.fn(async () => { });
410
+ const lib = new Library({ sources: [backend] });
411
+ await expect(lib.putBlobWithEntry(new Blob(['model']), model)).rejects.toBe(failure);
412
+ expect(backend.deleteBlob).toHaveBeenCalledWith('blob-x');
413
+ });
414
+ test('putBlobWithEntry preserves a blob referenced by an existing entry after a later failure', async () => {
415
+ const backend = new IndexedDBBackend();
416
+ const lib = new Library({ sources: [backend] });
417
+ const bytes = new Blob(['shared-model-bytes']);
418
+ let sharedBlobId = '';
419
+ await lib.putBlobWithEntry(bytes, (blobId) => {
420
+ sharedBlobId = blobId;
421
+ return { ...model('shared-original'), blobId, contentHash: blobId };
422
+ });
423
+ const failure = new Error('second entry write failed');
424
+ backend.putEntry = async () => {
425
+ throw failure;
426
+ };
427
+ await expect(lib.putBlobWithEntry(bytes, (blobId) => ({
428
+ ...model('shared-second'),
429
+ blobId,
430
+ contentHash: blobId,
431
+ }))).rejects.toBe(failure);
432
+ expect(await lib.listBlobIds()).toContain(sharedBlobId);
433
+ expect(await lib.getEntry('shared-original')).toMatchObject({
434
+ blobId: sharedBlobId,
435
+ });
436
+ });
437
+ test('putBlobWithEntry preserves shared bytes and the original error when any source cannot be enumerated', async () => {
438
+ const backend = new IndexedDBBackend();
439
+ const sourceFailure = new Error('catalog source unavailable');
440
+ let sourceEntries = [];
441
+ let sourceUnavailable = false;
442
+ const unavailableSource = {
443
+ id: 'unavailable-source',
444
+ listEntries: async () => {
445
+ if (sourceUnavailable)
446
+ throw sourceFailure;
447
+ return sourceEntries;
448
+ },
449
+ getEntry: async (id) => sourceEntries.find((entry) => entry.id === id),
450
+ getBlobUrl: async () => {
451
+ throw sourceFailure;
452
+ },
453
+ };
454
+ const lib = new Library({ sources: [backend, unavailableSource] });
455
+ const bytes = new Blob(['shared-partial-catalog-bytes']);
456
+ let sharedBlobId = '';
457
+ await lib.putBlobWithEntry(bytes, (blobId) => {
458
+ sharedBlobId = blobId;
459
+ return { ...model('shared-partial-original'), blobId, contentHash: blobId };
460
+ });
461
+ sourceEntries = [(await backend.getEntry('shared-partial-original'))];
462
+ await backend.deleteEntry('shared-partial-original');
463
+ sourceUnavailable = true;
464
+ const failure = new Error('partial catalog entry write failed');
465
+ backend.putEntry = async () => {
466
+ throw failure;
467
+ };
468
+ await expect(lib.putBlobWithEntry(bytes, (blobId) => ({
469
+ ...model('shared-partial-second'),
470
+ blobId,
471
+ contentHash: blobId,
472
+ }))).rejects.toBe(failure);
473
+ await expect(backend.getBlobUrl(sharedBlobId)).resolves.toBe('blob:mock-url');
474
+ expect(await unavailableSource.getEntry('shared-partial-original')).toMatchObject({
475
+ blobId: sharedBlobId,
476
+ });
477
+ await backend.deleteBlob(sharedBlobId);
478
+ });
403
479
  test('deleteEntryDeep: 缩略图被两个模型共享时,删一个不删 blob', async () => {
404
480
  const lib = new Library({ sources: [new IndexedDBBackend()] });
405
481
  const thumbId = await lib.putBlob(new Blob(['thumb']));
@@ -3,7 +3,7 @@ export const seedAssetsManifest = {
3
3
  {
4
4
  key: 'y-bot',
5
5
  file: 'Y Bot.glb',
6
- contentHash: '95cbcdd2192dbe68',
6
+ contentHash: 'fc55d96ed9b27445',
7
7
  name: 'Y Bot',
8
8
  rig: 'rig-16a71e249f45355b',
9
9
  boneCount: 117,
@@ -7,7 +7,7 @@ export interface BootCtx {
7
7
  library: Library;
8
8
  unregisterEditors: () => void;
9
9
  /** 节流通道手动 flush:dispose 时调用,尾部变更不丢 */
10
- flushState: () => void;
10
+ flushState: () => Promise<void>;
11
11
  /**
12
12
  * 状态通道入口:生产由 project 历史指针订阅喂入,测试可直接调。
13
13
  * 尾部节流(缺省 1000ms)后转给 config.state.onStateChange。
@@ -34,4 +34,5 @@ export declare function isBootedWithDifferentConfig(config: CameraStudioConfig):
34
34
  export declare function getBootConfig(): CameraStudioConfig | undefined;
35
35
  export declare function initCameraStudio(config: CameraStudioConfig): Promise<BootCtx>;
36
36
  export declare function disposeCameraStudio(ctx?: BootCtx): Promise<void>;
37
+ export declare function resetCameraStudio(): Promise<void>;
37
38
  //# sourceMappingURL=bootstrap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAuBA,OAAO,qBAAqB,CAAA;AAO5B,OAAO,KAAK,EAAC,gBAAgB,EAAE,kBAAkB,EAAC,MAAM,UAAU,CAAA;AAKlE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,wBAAwB,CAAA;AAmBnD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,aAAa,CAAA;AAOhD,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,iBAAiB,EAAE,MAAM,IAAI,CAAA;IAC7B,sCAAsC;IACtC,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB;;;OAGG;IACH,iBAAiB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7C,UAAU,EAAE,gBAAgB,CAAA;IAC5B,uDAAuD;IACvD,GAAG,EAAE,eAAe,CAAA;CACrB;AAYD;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD;AACD,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD;AAqBD,+DAA+D;AAC/D,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,GACzB,gBAAgB,CAMlB;AA0CD;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAIT;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,IAAI,kBAAkB,GAAG,SAAS,CAE9D;AA6QD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CA4B7E;AAED,wBAAsB,mBAAmB,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAMtE"}
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAwBA,OAAO,qBAAqB,CAAA;AAS5B,OAAO,KAAK,EAAC,gBAAgB,EAAE,kBAAkB,EAAC,MAAM,UAAU,CAAA;AAKlE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,wBAAwB,CAAA;AAqBnD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,aAAa,CAAA;AAOhD,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,iBAAiB,EAAE,MAAM,IAAI,CAAA;IAC7B,sCAAsC;IACtC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B;;;OAGG;IACH,iBAAiB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7C,UAAU,EAAE,gBAAgB,CAAA;IAC5B,uDAAuD;IACvD,GAAG,EAAE,eAAe,CAAA;CACrB;AAaD;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD;AACD,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD;AAuBD,+DAA+D;AAC/D,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,GACzB,gBAAgB,CASlB;AA0CD;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAIT;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,IAAI,kBAAkB,GAAG,SAAS,CAE9D;AAoRD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CA6B7E;AAED,wBAAsB,mBAAmB,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAMtE;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4CjD"}
package/dist/bootstrap.js CHANGED
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * 生命周期分层(R14):
7
7
  * - boot 级(一次性,initPromise 保留):Library 单例、IDB 连接、词典、
8
- * theatre.init、project 解析、节流机械。重挂幂等复用,dispose 不拆。
8
+ * theatre.init、project 解析、节流机械。重挂幂等复用,dispose 不拆;
9
+ * 显式场景切换可在全部消费者卸载后经 resetCameraStudio 清空重 boot。
9
10
  * - 会话级(每次挂载 acquire,dispose 拆解):3 个自定义属性编辑器注册、
10
11
  * state 通道订阅、studio.ui 显隐。picker 注册表与宿主句柄由各自模块
11
12
  * 自管(CameraScene 组件挂载时接线),这里不碰。
@@ -18,6 +19,8 @@ import theatre, { getProject, getStudio, getStudioSync, onChange, } from '@dream
18
19
  import '@dreamerbird/studio';
19
20
  import { initState } from '@dreamerbird/core/initState';
20
21
  import { privateAPI } from '@dreamerbird/core/privateAPIs';
22
+ import projectsSingleton from '@dreamerbird/core/projects/projectsSingleton';
23
+ import getStudioInternal from '@dreamerbird/studio/getStudio';
21
24
  import { registerCustomPropEditor, unregisterCustomPropEditor, } from '@dreamerbird/studio/propEditors/customPropEditors';
22
25
  import { cameraErrorFromLibrary, cameraErrorFromUnknown } from './errors';
23
26
  import { registerCameraMessages } from './i18n';
@@ -28,9 +31,11 @@ import { StaticCatalogSource } from './assetLibrary/staticCatalogSource';
28
31
  import { isWritableSource } from './assetLibrary/source';
29
32
  import { cleanupLegacySeeds } from './assetLibrary/legacySeedCleanup';
30
33
  import { setUiConfig } from './uiConfig';
34
+ import { mountStudioRootWhenReady } from './studioRoot';
31
35
  import PosePropEditor from './poseMode/PosePropEditor';
32
36
  import { GesturePropEditorLeft, GesturePropEditorRight, } from './gesturePropsEditor';
33
37
  import { createAgentRuntime } from './agent/runtime';
38
+ import { clearSceneSheetObjects } from './agent/runtime/objectRuntime';
34
39
  import { activateCameraStudioApi, disposeCameraStudioApi, getActiveAgentRegistry, getCameraStudioApi, } from './agent/api';
35
40
  import { createAgentBridgeClient } from './agent/bridgeClient';
36
41
  /** 内置 IDB 缺省库名(与现状一字不差,硬约束:老用户浏览器里的库零迁移) */
@@ -40,6 +45,7 @@ let initPromise;
40
45
  let bootConfig;
41
46
  /** 漂移告警只发一次(同页多次重挂不同 config 不刷屏) */
42
47
  let warnedConfigDrift = false;
48
+ let resetPromise;
43
49
  /** 会话拆解函数;undefined = 当前无活跃会话(acquire 幂等守卫) */
44
50
  let sessionTeardown;
45
51
  /** 消费者计数由 hook 管(acquire/release 成对);dispose 据此做末位守卫 */
@@ -65,7 +71,12 @@ function resolveProject(config) {
65
71
  }
66
72
  const name = config.project ?? 'Space';
67
73
  const initialState = config.state?.initialState;
68
- return getProject(name, initialState === undefined ? {} : { state: initialState });
74
+ return getProject(name, {
75
+ ...(initialState === undefined ? {} : { state: initialState }),
76
+ ...(config.ui?.projectName === undefined
77
+ ? {}
78
+ : { name: config.ui.projectName }),
79
+ });
69
80
  }
70
81
  /** sceneProps 同步解析(hook 首帧就要返回;getProject 渲染期调用安全,App 既有先例) */
71
82
  export function resolveSceneProps(config) {
@@ -73,6 +84,9 @@ export function resolveSceneProps(config) {
73
84
  project: resolveProject(config),
74
85
  onFlows: config.flows?.onFlows,
75
86
  onError: config.onError,
87
+ ...(config.scene?.includeDemoModel === undefined
88
+ ? {}
89
+ : { includeDemoModel: config.scene.includeDemoModel }),
76
90
  };
77
91
  }
78
92
  /** 参与漂移比对的顶层配置键(flows 回调引用每次渲染都新,不比) */
@@ -185,6 +199,13 @@ async function boot(config) {
185
199
  if (config.ui?.hideLeftStrip) {
186
200
  withStudioUi((ui) => ui.setLeftStripHidden(true));
187
201
  }
202
+ if (config.ui?.panels) {
203
+ const panels = config.ui.panels;
204
+ withStudioUi((ui) => ui.setPanelVisibility(panels));
205
+ }
206
+ withStudioUi(() => {
207
+ void mountStudioRootWhenReady(config.ui?.portalContainer);
208
+ });
188
209
  // 1. 数据源:可写源(若有) + 内置目录,聚合进 Library 单例
189
210
  const { sources, idbName } = resolveSources(config);
190
211
  const catalog = new StaticCatalogSource({
@@ -238,7 +259,7 @@ async function boot(config) {
238
259
  let pendingContent;
239
260
  let hasPending = false;
240
261
  let timer;
241
- const flushState = () => {
262
+ const flushState = async () => {
242
263
  if (timer !== undefined) {
243
264
  clearTimeout(timer);
244
265
  timer = undefined;
@@ -248,7 +269,7 @@ async function boot(config) {
248
269
  hasPending = false;
249
270
  const content = pendingContent;
250
271
  pendingContent = undefined;
251
- config.state?.onStateChange?.(content);
272
+ await config.state?.onStateChange?.(content);
252
273
  };
253
274
  const notifyStateChange = (content) => {
254
275
  if (!config.state?.onStateChange)
@@ -277,7 +298,7 @@ async function boot(config) {
277
298
  }
278
299
  /** studio.ui 显隐操作:studio 未挂载(宿主以 studio:false init 等)时静默降级 */
279
300
  function withStudioUi(fn) {
280
- Promise.resolve()
301
+ return Promise.resolve()
281
302
  .then(() => getStudio())
282
303
  .then(async (studio) => {
283
304
  // 必须等 studio 初始化收尾(持久化/同步状态恢复落库)再读写显隐:
@@ -352,11 +373,13 @@ function acquireSession(ctx) {
352
373
  ctx.unregisterEditors();
353
374
  unsubState?.();
354
375
  // 先断订阅再 flush:尾部变更带给宿主,不丢
355
- ctx.flushState();
356
- withStudioUi((ui) => ui.hide());
376
+ await ctx.flushState();
377
+ await withStudioUi((ui) => ui.hide());
357
378
  };
358
379
  }
359
380
  export function initCameraStudio(config) {
381
+ if (resetPromise)
382
+ return resetPromise.then(() => initCameraStudio(config));
360
383
  if (!initPromise) {
361
384
  bootConfig = config;
362
385
  const p = boot(config);
@@ -392,3 +415,42 @@ export async function disposeCameraStudio(ctx) {
392
415
  return;
393
416
  await sessionTeardown?.();
394
417
  }
418
+ export function resetCameraStudio() {
419
+ if (resetPromise)
420
+ return resetPromise;
421
+ const previousInit = initPromise;
422
+ resetPromise = (async () => {
423
+ if (activeConsumers > 0) {
424
+ throw new Error('resetCameraStudio requires all camera-studio consumers to be unmounted');
425
+ }
426
+ const previousCtx = previousInit
427
+ ? await previousInit.catch(() => undefined)
428
+ : undefined;
429
+ if (activeConsumers > 0) {
430
+ throw new Error('resetCameraStudio requires all camera-studio consumers to be unmounted');
431
+ }
432
+ await sessionTeardown?.();
433
+ clearSceneSheetObjects();
434
+ if (previousCtx) {
435
+ const projectId = previousCtx.sceneProps.project.address.projectId;
436
+ projectsSingleton.remove(projectId);
437
+ const studio = getStudioInternal();
438
+ if (studio) {
439
+ studio.transaction(({ stateEditors }) => {
440
+ stateEditors.deleteProject({ projectId });
441
+ }, false);
442
+ studio.ephemeralAtom.reduceByPointer((p) => p.coreByProject, (projects) => {
443
+ const next = { ...projects };
444
+ delete next[projectId];
445
+ return next;
446
+ });
447
+ }
448
+ }
449
+ initPromise = undefined;
450
+ bootConfig = undefined;
451
+ warnedConfigDrift = false;
452
+ })().finally(() => {
453
+ resetPromise = undefined;
454
+ });
455
+ return resetPromise;
456
+ }