@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
@@ -0,0 +1,294 @@
1
+ let projectSeq = 0;
2
+ let activeFakeStudio;
3
+ jest.doMock('@dreamerbird/core', () => ({
4
+ ...jest.requireActual('@dreamerbird/core'),
5
+ getStudioSync: () => activeFakeStudio,
6
+ }));
7
+ jest.doMock('@dreamerbird/studio/getStudio', () => ({
8
+ __esModule: true,
9
+ default: () => activeFakeStudio,
10
+ }));
11
+ const cleanups = [];
12
+ afterEach(() => {
13
+ while (cleanups.length)
14
+ cleanups.pop()();
15
+ });
16
+ const ctx = () => ({
17
+ requestId: 'test',
18
+ signal: new AbortController().signal,
19
+ });
20
+ function setup(options = {}) {
21
+ const { getPointerParts } = require('@dreamerbird/dataverse');
22
+ const core = require('@dreamerbird/core');
23
+ const { globals } = require('@dreamerbird/core/globals');
24
+ const three = require('three');
25
+ const { AgentCommandRegistry } = require('../registry');
26
+ const { createAgentRuntime } = require('../runtime');
27
+ const objectRuntime = require('../runtime/objectRuntime');
28
+ const { registerMaterialCommands } = require('./materials');
29
+ const { registerKeyframeCommands } = require('./keyframes');
30
+ const { registerObjectCommands } = require('./objects');
31
+ const { PLACEMENTS_REGISTRY_KEY } = require('../../PlacementManager');
32
+ const { collectMaterialSlots, createMaterialOverridesConfig } = require('../../materials/modelMaterials');
33
+ const instance = new three.Group();
34
+ const mesh = new three.Mesh(new three.BoxGeometry(), new three.MeshStandardMaterial({ roughness: 0.8 }));
35
+ mesh.name = 'Body';
36
+ instance.add(mesh);
37
+ if (options.secondSlot) {
38
+ const sibling = new three.Mesh(new three.BoxGeometry(), new three.MeshStandardMaterial({ roughness: 0.7 }));
39
+ sibling.name = 'Trim';
40
+ instance.add(sibling);
41
+ }
42
+ const slots = collectMaterialSlots(instance);
43
+ const slot = slots[0];
44
+ const slotKey = slot.slotKey;
45
+ const calls = [];
46
+ const sequenceEditor = {
47
+ setPrimitivePropAsSequenced: jest.fn((args) => {
48
+ calls.push({ op: 'sequenceProp', args });
49
+ }),
50
+ setPrimitivePropAsStatic: jest.fn(),
51
+ transformKeyframes: jest.fn(),
52
+ };
53
+ const fakeStudio = {
54
+ transaction: jest.fn((fn, undoable) => fn({
55
+ set: (pointer, value) => {
56
+ const { path } = getPointerParts(pointer);
57
+ calls.push({ op: 'set', path: path.join('.'), value });
58
+ },
59
+ unset: (pointer) => {
60
+ const { path } = getPointerParts(pointer);
61
+ calls.push({ op: 'unset', path: path.join('.') });
62
+ },
63
+ stateEditors: {
64
+ coreByProject: {
65
+ historic: { sheetsById: { sequence: sequenceEditor } },
66
+ },
67
+ },
68
+ })),
69
+ };
70
+ activeFakeStudio = fakeStudio;
71
+ cleanups.push(() => {
72
+ activeFakeStudio = undefined;
73
+ });
74
+ const trackPath = JSON.stringify(['materialOverrides', slotKey, 'roughness']);
75
+ const state = {
76
+ definitionVersion: globals.currentProjectStateDefinitionVersion,
77
+ sheetsById: {
78
+ Scene: {
79
+ staticOverrides: { byObject: {} },
80
+ sequence: {
81
+ type: 'PositionalSequence',
82
+ length: 10,
83
+ subUnitsPerUnit: 30,
84
+ tracksByObject: options.roughnessTrack
85
+ ? {
86
+ Foo: {
87
+ trackIdByPropPath: { [trackPath]: 'roughness-track' },
88
+ trackData: {
89
+ 'roughness-track': {
90
+ type: 'BasicKeyframedTrack',
91
+ keyframes: {
92
+ byId: {
93
+ kf1: {
94
+ id: 'kf1',
95
+ position: 0,
96
+ connectedRight: true,
97
+ handles: [0.5, 0.5, 0.5, 0.5],
98
+ value: 0.25,
99
+ },
100
+ },
101
+ allIds: { kf1: true },
102
+ },
103
+ },
104
+ },
105
+ },
106
+ }
107
+ : {},
108
+ },
109
+ },
110
+ },
111
+ };
112
+ const project = core.getProject(`agent-materials-${++projectSeq}`, {
113
+ state: state,
114
+ });
115
+ const sheet = project.sheet('Scene');
116
+ sheet.sequence.position = 0;
117
+ const runtime = createAgentRuntime({
118
+ project,
119
+ sheet,
120
+ library: {},
121
+ getStudio: () => fakeStudio,
122
+ subscribeHistoric: () => () => { },
123
+ });
124
+ const registry = new AgentCommandRegistry();
125
+ registerMaterialCommands(registry, runtime);
126
+ const placement = {
127
+ entry: { key: 'Foo', position: { x: 0, y: 0, z: 0 } },
128
+ instance,
129
+ };
130
+ cleanups.push(runtime.bindHandlers({
131
+ getModelPlacement: ({ key }) => (key === 'Foo' ? placement : undefined),
132
+ }));
133
+ function mountFoo() {
134
+ const t = core.types;
135
+ const registryObj = sheet.object(PLACEMENTS_REGISTRY_KEY, { list: t.string('[]') }, { hideFromOutline: true });
136
+ registryObj.initialValue = {
137
+ list: JSON.stringify([placement.entry]),
138
+ };
139
+ const obj = sheet.object('Foo', {
140
+ materialOverrides: createMaterialOverridesConfig(slots),
141
+ });
142
+ objectRuntime.registerSceneSheetObject('model:Foo', obj);
143
+ cleanups.push(() => objectRuntime.unregisterSceneSheetObject('model:Foo', obj));
144
+ return obj;
145
+ }
146
+ return {
147
+ core,
148
+ registry,
149
+ runtime,
150
+ fakeStudio,
151
+ calls,
152
+ mountFoo,
153
+ slot,
154
+ slots,
155
+ instance,
156
+ registerKeyframeCommands: () => registerKeyframeCommands(registry, runtime),
157
+ registerObjectCommands: () => registerObjectCommands(registry, runtime),
158
+ };
159
+ }
160
+ test('materials.list returns evaluated values for the mounted model', async () => {
161
+ const { registry, mountFoo, slot } = setup({ roughnessTrack: true });
162
+ mountFoo();
163
+ const result = (await registry.execute('materials.list', { objectId: 'model:Foo' }, ctx()));
164
+ expect(result).toHaveLength(1);
165
+ expect(result[0]).toMatchObject({
166
+ slotId: slot.slotId,
167
+ slotKey: slot.slotKey,
168
+ meshName: 'Body',
169
+ values: { roughness: 0.25 },
170
+ });
171
+ });
172
+ test('materials.update writes one full compound patch in one undoable transaction', async () => {
173
+ const { registry, fakeStudio, calls, mountFoo, slot } = setup();
174
+ mountFoo();
175
+ const result = await registry.execute('materials.update', {
176
+ objectId: 'model:Foo',
177
+ slotId: slot.slotId,
178
+ values: { roughness: 0.4, color: '#112233' },
179
+ }, ctx());
180
+ expect(result).toEqual({ revision: 1 });
181
+ expect(fakeStudio.transaction).toHaveBeenCalledTimes(1);
182
+ expect(calls).toHaveLength(1);
183
+ expect(calls[0]).toMatchObject({ op: 'set', path: 'materialOverrides' });
184
+ const patch = calls[0].value;
185
+ expect(patch[slot.slotKey]).toMatchObject({
186
+ roughness: 0.4,
187
+ color: '#112233',
188
+ metalness: 0,
189
+ opacity: 1,
190
+ mapBlobId: '',
191
+ });
192
+ });
193
+ test('materials.update validates ranges, colors, textures, slots and empty patches before writing', async () => {
194
+ const { registry, fakeStudio, mountFoo, slot } = setup();
195
+ mountFoo();
196
+ const badValues = [
197
+ {},
198
+ { roughness: -0.1 },
199
+ { metalness: 1.1 },
200
+ { opacity: Number.NaN },
201
+ { color: 'red' },
202
+ { emissive: '#fff' },
203
+ { mapBlobId: 4 },
204
+ { unknown: true },
205
+ ];
206
+ for (const values of badValues) {
207
+ await expect(registry.execute('materials.update', { objectId: 'model:Foo', slotId: slot.slotId, values }, ctx())).rejects.toMatchObject({ code: 'INVALID_ARGUMENT' });
208
+ }
209
+ await expect(registry.execute('materials.update', { objectId: 'model:Foo', slotId: 'missing', values: { roughness: 0.2 } }, ctx())).rejects.toMatchObject({ code: 'INVALID_ARGUMENT' });
210
+ expect(fakeStudio.transaction).not.toHaveBeenCalled();
211
+ });
212
+ test('materials.update honors expectedRevision and rejects sequenced properties', async () => {
213
+ const sequenced = setup({ roughnessTrack: true });
214
+ sequenced.mountFoo();
215
+ await expect(sequenced.registry.execute('materials.update', {
216
+ objectId: 'model:Foo',
217
+ slotId: sequenced.slot.slotId,
218
+ values: { roughness: 0.6 },
219
+ }, ctx())).rejects.toMatchObject({ code: 'INVALID_STATE' });
220
+ expect(sequenced.fakeStudio.transaction).not.toHaveBeenCalled();
221
+ const conflict = setup();
222
+ conflict.mountFoo();
223
+ await expect(conflict.registry.execute('materials.update', {
224
+ objectId: 'model:Foo',
225
+ slotId: conflict.slot.slotId,
226
+ values: { roughness: 0.6 },
227
+ expectedRevision: 99,
228
+ }, ctx())).rejects.toMatchObject({ code: 'REVISION_CONFLICT' });
229
+ expect(conflict.fakeStudio.transaction).not.toHaveBeenCalled();
230
+ });
231
+ test('materials.update guards only changed sequenced leaves and preserves siblings in one transaction', async () => {
232
+ const { registry, fakeStudio, calls, mountFoo, slot, slots } = setup({
233
+ roughnessTrack: true,
234
+ secondSlot: true,
235
+ });
236
+ mountFoo();
237
+ const sibling = slots[1];
238
+ for (const [slotId, values] of [
239
+ [slot.slotId, { color: '#112233' }],
240
+ [slot.slotId, { metalness: 0.6 }],
241
+ [slot.slotId, { mapBlobId: 'texture-new' }],
242
+ [sibling.slotId, { roughness: 0.45 }],
243
+ ]) {
244
+ await expect(registry.execute('materials.update', { objectId: 'model:Foo', slotId, values }, ctx())).resolves.toEqual({ revision: expect.any(Number) });
245
+ }
246
+ expect(fakeStudio.transaction).toHaveBeenCalledTimes(4);
247
+ const patches = calls
248
+ .filter((call) => call.op === 'set')
249
+ .map((call) => call.value);
250
+ expect(patches).toHaveLength(4);
251
+ expect(patches[0][slot.slotKey]).toMatchObject({
252
+ roughness: 0.25,
253
+ color: '#112233',
254
+ });
255
+ expect(patches[1][slot.slotKey]).toMatchObject({
256
+ roughness: 0.25,
257
+ metalness: 0.6,
258
+ });
259
+ expect(patches[2][slot.slotKey]).toMatchObject({
260
+ roughness: 0.25,
261
+ mapBlobId: 'texture-new',
262
+ });
263
+ expect(patches[3]).toMatchObject({
264
+ [slot.slotKey]: { roughness: 0.25 },
265
+ [sibling.slotKey]: { roughness: 0.45 },
266
+ });
267
+ });
268
+ test('roughness accepts keyframes at the materialOverrides compound leaf', async () => {
269
+ const { registry, registerKeyframeCommands, calls, mountFoo, slot } = setup();
270
+ mountFoo();
271
+ registerKeyframeCommands();
272
+ await registry.execute('keyframes.set', {
273
+ objectId: 'model:Foo',
274
+ propPath: ['materialOverrides', slot.slotKey, 'roughness'],
275
+ at: 0,
276
+ value: 0.35,
277
+ }, ctx());
278
+ expect(calls).toContainEqual({
279
+ op: 'set',
280
+ path: `materialOverrides.${slot.slotKey}.roughness`,
281
+ value: 0.35,
282
+ });
283
+ });
284
+ test('objects.update cannot write materialOverrides directly', async () => {
285
+ const { registry, registerObjectCommands, fakeStudio, mountFoo, slot } = setup();
286
+ mountFoo();
287
+ registerObjectCommands();
288
+ await expect(registry.execute('objects.update', {
289
+ objectId: 'model:Foo',
290
+ props: { materialOverrides: { [slot.slotKey]: slot.values } },
291
+ }, ctx())).rejects.toMatchObject({ code: 'INVALID_ARGUMENT' });
292
+ expect(fakeStudio.transaction).not.toHaveBeenCalled();
293
+ });
294
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=modelRecovery.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelRecovery.test.d.ts","sourceRoot":"","sources":["../../../src/agent/commands/modelRecovery.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,122 @@
1
+ let activeFakeStudio;
2
+ jest.doMock('@dreamerbird/studio/getStudio', () => ({
3
+ __esModule: true,
4
+ default: () => activeFakeStudio,
5
+ }));
6
+ const ctx = () => ({
7
+ requestId: 'test',
8
+ signal: new AbortController().signal,
9
+ });
10
+ test('retry and replacement remount an unmounted model in one undoable transaction', async () => {
11
+ const core = require('@dreamerbird/core');
12
+ const { globals } = require('@dreamerbird/core/globals');
13
+ const { AgentCommandRegistry } = require('../registry');
14
+ const { createAgentRuntime } = require('../runtime');
15
+ const { registerModelCommands } = require('./models');
16
+ const { PLACEMENTS_REGISTRY_KEY } = require('../../PlacementManager');
17
+ const project = core.getProject('agent-model-recovery', {
18
+ state: {
19
+ definitionVersion: globals.currentProjectStateDefinitionVersion,
20
+ sheetsById: {
21
+ Scene: {
22
+ staticOverrides: { byObject: {} },
23
+ sequence: {
24
+ type: 'PositionalSequence',
25
+ length: 10,
26
+ subUnitsPerUnit: 30,
27
+ tracksByObject: {},
28
+ },
29
+ },
30
+ },
31
+ },
32
+ });
33
+ const sheet = project.sheet('Scene');
34
+ const registryObject = sheet.object(PLACEMENTS_REGISTRY_KEY, { list: core.types.string('[]') }, { hideFromOutline: true });
35
+ registryObject.initialValue = {
36
+ list: JSON.stringify([
37
+ {
38
+ key: 'Broken',
39
+ libraryEntryId: 'old-model',
40
+ position: { x: 1, y: 2, z: 3 },
41
+ },
42
+ ]),
43
+ };
44
+ const calls = [];
45
+ let notifyHistoric = () => { };
46
+ const fakeStudio = {
47
+ transaction: jest.fn((fn) => {
48
+ fn({
49
+ set: (pointer, value) => {
50
+ const { getPointerParts } = require('@dreamerbird/dataverse');
51
+ calls.push({
52
+ path: getPointerParts(pointer).path.join('.'),
53
+ value,
54
+ });
55
+ },
56
+ });
57
+ queueMicrotask(() => notifyHistoric());
58
+ }),
59
+ };
60
+ activeFakeStudio = fakeStudio;
61
+ const replacement = {
62
+ id: 'new-model',
63
+ kind: 'model',
64
+ name: 'Replacement',
65
+ rig: 'static',
66
+ blobId: 'new-blob',
67
+ contentHash: 'new-hash',
68
+ boneCount: 0,
69
+ vertexCount: 8,
70
+ createdAt: 2,
71
+ };
72
+ const runtime = createAgentRuntime({
73
+ project,
74
+ sheet,
75
+ library: {
76
+ getEntry: async (id) => id === replacement.id ? replacement : undefined,
77
+ },
78
+ getStudio: () => fakeStudio,
79
+ subscribeHistoric: (listener) => {
80
+ notifyHistoric = listener;
81
+ return () => { };
82
+ },
83
+ });
84
+ const registry = new AgentCommandRegistry();
85
+ registerModelCommands(registry, runtime);
86
+ try {
87
+ await expect(registry.execute('models.retry', { objectId: 'model:Broken' }, ctx())).resolves.toEqual({ revision: expect.any(Number) });
88
+ await expect(registry.execute('models.replace', {
89
+ objectId: 'model:Broken',
90
+ assetId: replacement.id,
91
+ }, ctx())).resolves.toEqual({ revision: expect.any(Number) });
92
+ expect(fakeStudio.transaction).toHaveBeenCalledTimes(2);
93
+ const retryEntry = JSON.parse(String(calls[0].value))[0];
94
+ expect(retryEntry).toMatchObject({
95
+ key: 'Broken',
96
+ libraryEntryId: 'old-model',
97
+ loadAttempt: 1,
98
+ position: { x: 1, y: 2, z: 3 },
99
+ });
100
+ const replacementEntry = JSON.parse(String(calls[1].value))[0];
101
+ expect(replacementEntry).toMatchObject({
102
+ key: 'Broken',
103
+ libraryEntryId: replacement.id,
104
+ loadAttempt: 1,
105
+ position: { x: 1, y: 2, z: 3 },
106
+ });
107
+ expect(replacementEntry.assetId).toBeUndefined();
108
+ }
109
+ finally {
110
+ activeFakeStudio = undefined;
111
+ runtime.dispose();
112
+ }
113
+ });
114
+ test('failed model placeholder remains an identifiable scene object', () => {
115
+ const { Group, Mesh } = require('three');
116
+ const { createFailedModelPlaceholder } = require('../../PlacementManager');
117
+ const placeholder = createFailedModelPlaceholder('Broken');
118
+ expect(placeholder).toBeInstanceOf(Group);
119
+ expect(placeholder.name).toBe('Failed/Broken');
120
+ expect(placeholder.children.some((child) => child instanceof Mesh)).toBe(true);
121
+ });
122
+ export {};
@@ -29,6 +29,19 @@ export type ModelsApi = {
29
29
  objectId: SceneObjectId;
30
30
  revision: number;
31
31
  }>;
32
+ retry(args: {
33
+ objectId: string;
34
+ expectedRevision?: number;
35
+ }): Promise<{
36
+ revision: number;
37
+ }>;
38
+ replace(args: {
39
+ objectId: string;
40
+ assetId: string;
41
+ expectedRevision?: number;
42
+ }): Promise<{
43
+ revision: number;
44
+ }>;
32
45
  };
33
46
  export declare function registerModelCommands(registry: AgentCommandRegistry, runtime: AgentRuntime): void;
34
47
  //# sourceMappingURL=models.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../src/agent/commands/models.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,aAAa,CAAA;AACrD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAA;AAG5C,OAAO,KAAK,EAAa,aAAa,EAAE,IAAI,EAAC,MAAM,UAAU,CAAA;AAO7D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAA;AAExD,OAAO,QAAQ,YAAY,CAAC;IAC1B,UAAU,yBAAyB;QACjC;;;;;WAKG;QACH,WAAW,CAAC,IAAI,EAAE;YAAC,KAAK,EAAE,UAAU,CAAC;YAAC,QAAQ,EAAE,IAAI,CAAA;SAAC,GAAG;YACtD,GAAG,EAAE,MAAM,CAAA;YACX,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;SACzB,CAAA;KACF;CACF;AAED,8BAA8B;AAC9B,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,CAAC,IAAI,EAAE;QACX,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,IAAI,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,GAAG,OAAO,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC,CAAA;CACzD,CAAA;AAwDD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,oBAAoB,EAC9B,OAAO,EAAE,YAAY,GACpB,IAAI,CAiGN"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../src/agent/commands/models.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,aAAa,CAAA;AACrD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAA;AAI5C,OAAO,KAAK,EAAa,aAAa,EAAE,IAAI,EAAC,MAAM,UAAU,CAAA;AAO7D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAA;AAExD,OAAO,QAAQ,YAAY,CAAC;IAC1B,UAAU,yBAAyB;QACjC;;;;;WAKG;QACH,WAAW,CAAC,IAAI,EAAE;YAAC,KAAK,EAAE,UAAU,CAAC;YAAC,QAAQ,EAAE,IAAI,CAAA;SAAC,GAAG;YACtD,GAAG,EAAE,MAAM,CAAA;YACX,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;SACzB,CAAA;KACF;CACF;AAED,8BAA8B;AAC9B,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,CAAC,IAAI,EAAE;QACX,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,IAAI,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,GAAG,OAAO,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC,CAAA;IACxD,KAAK,CAAC,IAAI,EAAE;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,GAAG,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC,CAAA;IAC/B,OAAO,CAAC,IAAI,EAAE;QACZ,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,EAAE,MAAM,CAAA;QACf,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,GAAG,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC,CAAA;CAChC,CAAA;AAwDD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,oBAAoB,EAC9B,OAAO,EAAE,YAAY,GACpB,IAAI,CA2MN"}
@@ -14,8 +14,9 @@
14
14
  * 对历史只呈现一步(undoable:single-step)。
15
15
  */
16
16
  import { AgentApiError } from '../errors';
17
- import { formatSceneObjectId } from '../objectIds';
17
+ import { formatSceneObjectId, parseSceneObjectId } from '../objectIds';
18
18
  import { getSceneObjectSummary } from '../objectResolver';
19
+ import { retryPlacement } from '../../PlacementManager';
19
20
  import { requireFiniteNumber, requireObject, requireString, requireVec3, } from '../validation';
20
21
  const vec3Schema = {
21
22
  type: 'object',
@@ -134,4 +135,100 @@ export function registerModelCommands(registry, runtime) {
134
135
  return { objectId, revision: await runtime.nextRevision() };
135
136
  },
136
137
  });
138
+ const recoveryInput = {
139
+ type: 'object',
140
+ properties: {
141
+ objectId: { type: 'string' },
142
+ expectedRevision: { type: 'number' },
143
+ },
144
+ required: ['objectId'],
145
+ additionalProperties: false,
146
+ };
147
+ const revisionOutput = {
148
+ type: 'object',
149
+ properties: { revision: { type: 'number' } },
150
+ required: ['revision'],
151
+ };
152
+ const validateRecovery = (value) => {
153
+ const args = requireObject(value);
154
+ return {
155
+ objectId: requireString(args, 'objectId'),
156
+ expectedRevision: optionalRecoveryRevision(args.expectedRevision),
157
+ };
158
+ };
159
+ const recover = (objectId, expectedRevision, replacementEntryId) => {
160
+ getSceneObjectSummary(runtime.sheet, objectId);
161
+ const { key } = parseSceneObjectId(objectId);
162
+ if (expectedRevision !== undefined)
163
+ runtime.assertRevision(expectedRevision);
164
+ if (!retryPlacement(runtime.sheet, key, replacementEntryId)) {
165
+ throw new AgentApiError('SCENE_UNAVAILABLE', 'Studio 不可用,无法重试模型加载');
166
+ }
167
+ };
168
+ registry.register({
169
+ name: 'models.retry',
170
+ kind: 'mutation',
171
+ stateDomain: 'project',
172
+ undoable: 'single-step',
173
+ completion: 'committed',
174
+ inputSchema: recoveryInput,
175
+ outputSchema: revisionOutput,
176
+ errorCodes: [
177
+ 'API_DISPOSED',
178
+ 'INVALID_ARGUMENT',
179
+ 'OBJECT_NOT_FOUND',
180
+ 'REVISION_CONFLICT',
181
+ 'SCENE_UNAVAILABLE',
182
+ ],
183
+ validate: validateRecovery,
184
+ execute: async ({ objectId, expectedRevision }) => {
185
+ runtime.assertActive();
186
+ recover(objectId, expectedRevision);
187
+ return { revision: await runtime.nextRevision() };
188
+ },
189
+ });
190
+ registry.register({
191
+ name: 'models.replace',
192
+ kind: 'mutation',
193
+ stateDomain: 'project',
194
+ undoable: 'single-step',
195
+ completion: 'committed',
196
+ inputSchema: {
197
+ ...recoveryInput,
198
+ properties: { ...recoveryInput.properties, assetId: { type: 'string' } },
199
+ required: ['objectId', 'assetId'],
200
+ },
201
+ outputSchema: revisionOutput,
202
+ errorCodes: [
203
+ 'API_DISPOSED',
204
+ 'INVALID_ARGUMENT',
205
+ 'ASSET_NOT_FOUND',
206
+ 'OBJECT_NOT_FOUND',
207
+ 'REVISION_CONFLICT',
208
+ 'SCENE_UNAVAILABLE',
209
+ ],
210
+ validate: (value) => ({
211
+ ...validateRecovery(value),
212
+ assetId: requireString(requireObject(value), 'assetId'),
213
+ }),
214
+ execute: async ({ objectId, assetId, expectedRevision }) => {
215
+ runtime.assertActive();
216
+ const entry = await runtime.library.getEntry(assetId);
217
+ if (!entry) {
218
+ throw new AgentApiError('ASSET_NOT_FOUND', `资产不存在: ${assetId}`, {
219
+ details: { assetId },
220
+ });
221
+ }
222
+ if (entry.kind !== 'model') {
223
+ throw new AgentApiError('INVALID_ARGUMENT', `资产 ${assetId} 不是模型`);
224
+ }
225
+ recover(objectId, expectedRevision, assetId);
226
+ return { revision: await runtime.nextRevision() };
227
+ },
228
+ });
229
+ }
230
+ function optionalRecoveryRevision(value) {
231
+ return value === undefined
232
+ ? undefined
233
+ : requireFiniteNumber(value, 'expectedRevision');
137
234
  }
@@ -0,0 +1,27 @@
1
+ import type { AgentCommandRegistry } from '../registry';
2
+ import type { AgentRuntime } from '../runtime';
3
+ import type { SceneObjectId, Vec3 } from '../types';
4
+ import type { PrimitiveKind } from '../../primitives';
5
+ declare module '../runtime' {
6
+ interface AgentSceneRuntimeHandlers {
7
+ createPrimitive(args: {
8
+ kind: PrimitiveKind;
9
+ position: Vec3;
10
+ }): {
11
+ key: string;
12
+ persisted: Promise<void>;
13
+ };
14
+ }
15
+ }
16
+ export type PrimitivesApi = {
17
+ create(args: {
18
+ kind: PrimitiveKind;
19
+ position: Vec3;
20
+ expectedRevision?: number;
21
+ }): Promise<{
22
+ objectId: SceneObjectId;
23
+ revision: number;
24
+ }>;
25
+ };
26
+ export declare function registerPrimitiveCommands(registry: AgentCommandRegistry, runtime: AgentRuntime): void;
27
+ //# sourceMappingURL=primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../../src/agent/commands/primitives.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,aAAa,CAAA;AACrD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAA;AAG5C,OAAO,KAAK,EAAa,aAAa,EAAE,IAAI,EAAC,MAAM,UAAU,CAAA;AAQ7D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,kBAAkB,CAAA;AAEnD,OAAO,QAAQ,YAAY,CAAC;IAC1B,UAAU,yBAAyB;QACjC,eAAe,CAAC,IAAI,EAAE;YAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,EAAE,IAAI,CAAA;SAAC,GAAG;YAC5D,GAAG,EAAE,MAAM,CAAA;YACX,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;SACzB,CAAA;KACF;CACF;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,IAAI,EAAE;QACX,IAAI,EAAE,aAAa,CAAA;QACnB,QAAQ,EAAE,IAAI,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,GAAG,OAAO,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC,CAAA;CACzD,CAAA;AAgED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,oBAAoB,EAC9B,OAAO,EAAE,YAAY,GACpB,IAAI,CA0EN"}