@flighthq/scene-document 0.4.1-edge.37.ff47e13 → 0.4.1-next.109.0f3397e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/contract.d.ts +2 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -1
- package/dist/contract.js.map +1 -1
- package/dist/flightDocumentText.d.ts +5 -0
- package/dist/flightDocumentText.d.ts.map +1 -0
- package/dist/flightDocumentText.js +617 -0
- package/dist/flightDocumentText.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/sceneDocumentMaterializationSelection.d.ts +13 -0
- package/dist/sceneDocumentMaterializationSelection.d.ts.map +1 -0
- package/dist/sceneDocumentMaterializationSelection.js +55 -0
- package/dist/sceneDocumentMaterializationSelection.js.map +1 -0
- package/dist/sceneDocumentRefusal.d.ts +6 -0
- package/dist/sceneDocumentRefusal.d.ts.map +1 -0
- package/dist/sceneDocumentRefusal.js +60 -0
- package/dist/sceneDocumentRefusal.js.map +1 -0
- package/dist/sceneDocumentScene2DMaterialization.d.ts +4 -5
- package/dist/sceneDocumentScene2DMaterialization.d.ts.map +1 -1
- package/dist/sceneDocumentScene2DMaterialization.js +81 -277
- package/dist/sceneDocumentScene2DMaterialization.js.map +1 -1
- package/dist/sceneDocumentScene3DMaterialization.d.ts +4 -4
- package/dist/sceneDocumentScene3DMaterialization.d.ts.map +1 -1
- package/dist/sceneDocumentScene3DMaterialization.js +79 -278
- package/dist/sceneDocumentScene3DMaterialization.js.map +1 -1
- package/dist/sceneDocumentYamlSubset.d.ts +3 -1
- package/dist/sceneDocumentYamlSubset.d.ts.map +1 -1
- package/dist/sceneDocumentYamlSubset.js.map +1 -1
- package/package.json +9 -9
- package/src/flightDocumentText.test.ts +179 -0
- package/src/sceneDocumentMaterializationSelection.test.ts +56 -0
- package/src/sceneDocumentMultiSceneMaterialization.test.ts +209 -0
- package/src/sceneDocumentRefusal.test.ts +143 -0
- package/src/sceneDocumentScene2DMaterialization.test.ts +188 -62
- package/src/sceneDocumentScene3DMaterialization.test.ts +329 -154
- package/src/sceneDocumentYamlSubset.test.ts +8 -0
|
@@ -2,13 +2,15 @@ import { createTransform3D } from '@flighthq/geometry/contract';
|
|
|
2
2
|
import { createAmbientLight, createDirectionalLight } from '@flighthq/lighting/contract';
|
|
3
3
|
import { getNodeChildren } from '@flighthq/node/contract';
|
|
4
4
|
import { createKeyedTable, withRegistryTableEntry } from '@flighthq/registry/contract';
|
|
5
|
+
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
5
6
|
import { createNode3D, createScene3D } from '@flighthq/scene3d/contract';
|
|
6
7
|
import type {
|
|
7
8
|
FlightDocument,
|
|
8
9
|
FlightDocumentFields,
|
|
9
10
|
FlightDocumentNodeSchema,
|
|
11
|
+
FlightDocumentResourceDescriptor,
|
|
10
12
|
FlightDocumentResourceLookup,
|
|
11
|
-
|
|
13
|
+
FlightDocumentScene,
|
|
12
14
|
FlightDocumentScene3DMaterialization,
|
|
13
15
|
FlightDocumentSchemaRegistry,
|
|
14
16
|
NodeAny,
|
|
@@ -18,12 +20,13 @@ import type {
|
|
|
18
20
|
import {
|
|
19
21
|
AmbientLightKind,
|
|
20
22
|
DirectionalLightKind,
|
|
23
|
+
DisplayObjectKind,
|
|
21
24
|
FlightDocumentRefusalReason,
|
|
22
25
|
Node3DKind,
|
|
23
26
|
} from '@flighthq/types/contract';
|
|
24
27
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
25
28
|
|
|
26
|
-
import {
|
|
29
|
+
import { formatFlightDocumentText } from './flightDocumentText';
|
|
27
30
|
import {
|
|
28
31
|
createFlightDocumentFromScene3D,
|
|
29
32
|
createFlightDocumentScene3DMaterialization,
|
|
@@ -37,7 +40,6 @@ describe('createFlightDocumentFromScene3D', () => {
|
|
|
37
40
|
const scene = createScene3D();
|
|
38
41
|
const document = createFlightDocumentFromScene3D(scene, [], [], createTestSchemas());
|
|
39
42
|
expect(document.kind).toBe('Scene3D');
|
|
40
|
-
expect(document.version).toBe(1);
|
|
41
43
|
expect(document.scene.children).toHaveLength(0);
|
|
42
44
|
expect(document.cameras).toHaveLength(0);
|
|
43
45
|
expect(document.lights).toHaveLength(0);
|
|
@@ -71,14 +73,12 @@ describe('createFlightDocumentFromScene3D', () => {
|
|
|
71
73
|
|
|
72
74
|
describe('createFlightDocumentScene3DMaterialization', () => {
|
|
73
75
|
it('materializes an empty Scene3D from a minimal model', () => {
|
|
74
|
-
const document
|
|
76
|
+
const document = createTestDocument({
|
|
75
77
|
cameras: [],
|
|
76
78
|
kind: 'Scene3D',
|
|
77
79
|
lights: [],
|
|
78
|
-
resources: [],
|
|
79
80
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
80
|
-
|
|
81
|
-
};
|
|
81
|
+
});
|
|
82
82
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
83
83
|
expect(result).not.toBeNull();
|
|
84
84
|
const materialization = result as FlightDocumentScene3DMaterialization;
|
|
@@ -88,7 +88,7 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
it('materializes cameras from document camera descriptors', () => {
|
|
91
|
-
const document
|
|
91
|
+
const document = createTestDocument({
|
|
92
92
|
cameras: [
|
|
93
93
|
{
|
|
94
94
|
far: 500,
|
|
@@ -99,10 +99,8 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
99
99
|
],
|
|
100
100
|
kind: 'Scene3D',
|
|
101
101
|
lights: [],
|
|
102
|
-
resources: [],
|
|
103
102
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
104
|
-
|
|
105
|
-
};
|
|
103
|
+
});
|
|
106
104
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
107
105
|
expect(result).not.toBeNull();
|
|
108
106
|
const materialization = result as FlightDocumentScene3DMaterialization;
|
|
@@ -113,18 +111,16 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
113
111
|
});
|
|
114
112
|
|
|
115
113
|
it('materializes a tree with children', () => {
|
|
116
|
-
const document
|
|
114
|
+
const document = createTestDocument({
|
|
117
115
|
cameras: [],
|
|
118
116
|
kind: 'Scene3D',
|
|
119
117
|
lights: [],
|
|
120
|
-
resources: [],
|
|
121
118
|
scene: {
|
|
122
119
|
children: [{ children: [], fields: {}, kind: Node3DKind }],
|
|
123
120
|
fields: {},
|
|
124
121
|
kind: Node3DKind,
|
|
125
122
|
},
|
|
126
|
-
|
|
127
|
-
};
|
|
123
|
+
});
|
|
128
124
|
const schemas = createTestSchemas();
|
|
129
125
|
const result = createFlightDocumentScene3DMaterialization(document, schemas);
|
|
130
126
|
expect(result).not.toBeNull();
|
|
@@ -135,58 +131,53 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
135
131
|
});
|
|
136
132
|
|
|
137
133
|
it('returns null for a Scene2D document', () => {
|
|
138
|
-
const document = {
|
|
134
|
+
const document = createTestDocument({
|
|
139
135
|
backgroundColor: null,
|
|
140
136
|
kind: 'Scene2D',
|
|
141
|
-
resources: [],
|
|
142
137
|
scene: { children: [], fields: {}, kind: 'DisplayObject' },
|
|
143
|
-
|
|
144
|
-
} as FlightDocument;
|
|
138
|
+
});
|
|
145
139
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
146
140
|
expect(result).toBeNull();
|
|
147
141
|
});
|
|
148
142
|
|
|
149
143
|
it('returns null for an unsupported version', () => {
|
|
150
144
|
const document = {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
145
|
+
...createTestDocument({
|
|
146
|
+
cameras: [],
|
|
147
|
+
kind: 'Scene3D',
|
|
148
|
+
lights: [],
|
|
149
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
150
|
+
}),
|
|
156
151
|
version: 99,
|
|
157
|
-
} as unknown as
|
|
152
|
+
} as unknown as FlightDocument;
|
|
158
153
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
159
154
|
expect(result).toBeNull();
|
|
160
155
|
});
|
|
161
156
|
|
|
162
157
|
it('returns null for duplicate ambient lights', () => {
|
|
163
|
-
const document
|
|
158
|
+
const document = createTestDocument({
|
|
164
159
|
cameras: [],
|
|
165
160
|
kind: 'Scene3D',
|
|
166
161
|
lights: [
|
|
167
162
|
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
168
163
|
{ descriptor: createAmbientLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
169
164
|
],
|
|
170
|
-
resources: [],
|
|
171
165
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
172
|
-
|
|
173
|
-
};
|
|
166
|
+
});
|
|
174
167
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
175
168
|
expect(result).toBeNull();
|
|
176
169
|
});
|
|
177
170
|
|
|
178
171
|
it('returns null for duplicate directional lights', () => {
|
|
179
|
-
const document
|
|
172
|
+
const document = createTestDocument({
|
|
180
173
|
cameras: [],
|
|
181
174
|
kind: 'Scene3D',
|
|
182
175
|
lights: [
|
|
183
176
|
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
184
177
|
{ descriptor: createDirectionalLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
185
178
|
],
|
|
186
|
-
resources: [],
|
|
187
179
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
188
|
-
|
|
189
|
-
};
|
|
180
|
+
});
|
|
190
181
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
191
182
|
expect(result).toBeNull();
|
|
192
183
|
});
|
|
@@ -194,7 +185,14 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
194
185
|
|
|
195
186
|
describe('createFlightDocumentScene3DMaterializationFromText', () => {
|
|
196
187
|
it('materializes a Scene3D from YAML text', () => {
|
|
197
|
-
const yaml = [
|
|
188
|
+
const yaml = [
|
|
189
|
+
'flight: 1',
|
|
190
|
+
'defaultScene: 0',
|
|
191
|
+
'scenes:',
|
|
192
|
+
' - kind: Scene3D',
|
|
193
|
+
' scene:',
|
|
194
|
+
' kind: Node3D',
|
|
195
|
+
].join('\n');
|
|
198
196
|
const schemas = createTestSchemas();
|
|
199
197
|
const result = createFlightDocumentScene3DMaterializationFromText(yaml, schemas);
|
|
200
198
|
expect(result).not.toBeNull();
|
|
@@ -203,87 +201,43 @@ describe('createFlightDocumentScene3DMaterializationFromText', () => {
|
|
|
203
201
|
});
|
|
204
202
|
|
|
205
203
|
it('returns null for a Scene2D YAML document', () => {
|
|
206
|
-
const yaml = [
|
|
204
|
+
const yaml = [
|
|
205
|
+
'flight: 1',
|
|
206
|
+
'defaultScene: 0',
|
|
207
|
+
'scenes:',
|
|
208
|
+
' - kind: Scene2D',
|
|
209
|
+
' scene:',
|
|
210
|
+
' kind: DisplayObject',
|
|
211
|
+
].join('\n');
|
|
207
212
|
const result = createFlightDocumentScene3DMaterializationFromText(yaml, createTestSchemas());
|
|
208
213
|
expect(result).toBeNull();
|
|
209
214
|
});
|
|
210
215
|
});
|
|
211
216
|
|
|
212
|
-
describe('duplicate-light explain parity', () => {
|
|
213
|
-
// The table is annotated so each fixture is checked AGAINST THE SCHEMA rather than inferred from its own
|
|
214
|
-
// literal. Without this, `version: 1` widens to `number` and the row stops being a `FlightDocument` — and
|
|
215
|
-
// the two obvious local repairs both make the fixture worse: `1 as const` and a cast silence the widening
|
|
216
|
-
// while also silencing every other way a row could stop matching the document type, which is the only
|
|
217
|
-
// thing this parity test has to hold the fixtures to.
|
|
218
|
-
it.each<{ document: Readonly<FlightDocument>; expectedReason: FlightDocumentRefusalReason; label: string }>([
|
|
219
|
-
{
|
|
220
|
-
label: 'duplicate ambient',
|
|
221
|
-
document: {
|
|
222
|
-
cameras: [],
|
|
223
|
-
kind: 'Scene3D',
|
|
224
|
-
lights: [
|
|
225
|
-
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
226
|
-
{ descriptor: createAmbientLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
227
|
-
],
|
|
228
|
-
resources: [],
|
|
229
|
-
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
230
|
-
version: 1,
|
|
231
|
-
},
|
|
232
|
-
expectedReason: FlightDocumentRefusalReason.DuplicateAmbientLight,
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
label: 'duplicate directional',
|
|
236
|
-
document: {
|
|
237
|
-
cameras: [],
|
|
238
|
-
kind: 'Scene3D',
|
|
239
|
-
lights: [
|
|
240
|
-
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
241
|
-
{ descriptor: createDirectionalLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
242
|
-
],
|
|
243
|
-
resources: [],
|
|
244
|
-
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
245
|
-
version: 1,
|
|
246
|
-
},
|
|
247
|
-
expectedReason: FlightDocumentRefusalReason.DuplicateDirectionalLight,
|
|
248
|
-
},
|
|
249
|
-
])('model and text explain agree on $label', ({ document, expectedReason }) => {
|
|
250
|
-
const modelResult = explainFlightDocumentScene3DRefusal(document);
|
|
251
|
-
const text = serializeFlightDocument(document);
|
|
252
|
-
const textResult = explainFlightDocumentScene3DRefusalFromText(text);
|
|
253
|
-
expect(modelResult).not.toBeNull();
|
|
254
|
-
expect(textResult).not.toBeNull();
|
|
255
|
-
expect(textResult!.reason).toBe(modelResult!.reason);
|
|
256
|
-
expect(textResult!.path).toBe(modelResult!.path);
|
|
257
|
-
expect(modelResult!.reason).toBe(expectedReason);
|
|
258
|
-
expect(modelResult!.path).toBe('lights');
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
217
|
describe('explainFlightDocumentScene3DRefusal', () => {
|
|
263
218
|
it('explains a dimension mismatch', () => {
|
|
264
|
-
const document = {
|
|
219
|
+
const document = createTestDocument({
|
|
265
220
|
backgroundColor: null,
|
|
266
221
|
kind: 'Scene2D',
|
|
267
|
-
resources: [],
|
|
268
222
|
scene: { children: [], fields: {}, kind: 'DisplayObject' },
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
223
|
+
});
|
|
224
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
272
225
|
expect(explanation).not.toBeNull();
|
|
273
226
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.StructureInvalid);
|
|
274
|
-
expect(explanation!.path).toBe('kind');
|
|
227
|
+
expect(explanation!.path).toBe('scenes[0].kind');
|
|
275
228
|
});
|
|
276
229
|
|
|
277
230
|
it('explains an unsupported version', () => {
|
|
278
231
|
const document = {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
232
|
+
...createTestDocument({
|
|
233
|
+
cameras: [],
|
|
234
|
+
kind: 'Scene3D',
|
|
235
|
+
lights: [],
|
|
236
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
237
|
+
}),
|
|
284
238
|
version: 99,
|
|
285
|
-
} as unknown as
|
|
286
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
239
|
+
} as unknown as FlightDocument;
|
|
240
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
287
241
|
expect(explanation).not.toBeNull();
|
|
288
242
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.VersionUnsupported);
|
|
289
243
|
expect(explanation!.path).toBe('version');
|
|
@@ -291,51 +245,45 @@ describe('explainFlightDocumentScene3DRefusal', () => {
|
|
|
291
245
|
});
|
|
292
246
|
|
|
293
247
|
it('explains a duplicate ambient light', () => {
|
|
294
|
-
const document
|
|
248
|
+
const document = createTestDocument({
|
|
295
249
|
cameras: [],
|
|
296
250
|
kind: 'Scene3D',
|
|
297
251
|
lights: [
|
|
298
252
|
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
299
253
|
{ descriptor: createAmbientLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
300
254
|
],
|
|
301
|
-
resources: [],
|
|
302
255
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
256
|
+
});
|
|
257
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
306
258
|
expect(explanation).not.toBeNull();
|
|
307
259
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DuplicateAmbientLight);
|
|
308
|
-
expect(explanation!.path).toBe('lights');
|
|
260
|
+
expect(explanation!.path).toBe('scenes[0].lights');
|
|
309
261
|
});
|
|
310
262
|
|
|
311
263
|
it('explains a duplicate directional light', () => {
|
|
312
|
-
const document
|
|
264
|
+
const document = createTestDocument({
|
|
313
265
|
cameras: [],
|
|
314
266
|
kind: 'Scene3D',
|
|
315
267
|
lights: [
|
|
316
268
|
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
317
269
|
{ descriptor: createDirectionalLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
318
270
|
],
|
|
319
|
-
resources: [],
|
|
320
271
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
272
|
+
});
|
|
273
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
324
274
|
expect(explanation).not.toBeNull();
|
|
325
275
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DuplicateDirectionalLight);
|
|
326
|
-
expect(explanation!.path).toBe('lights');
|
|
276
|
+
expect(explanation!.path).toBe('scenes[0].lights');
|
|
327
277
|
});
|
|
328
278
|
|
|
329
279
|
it('returns null when the document is valid', () => {
|
|
330
|
-
const document
|
|
280
|
+
const document = createTestDocument({
|
|
331
281
|
cameras: [],
|
|
332
282
|
kind: 'Scene3D',
|
|
333
283
|
lights: [],
|
|
334
|
-
resources: [],
|
|
335
284
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
285
|
+
});
|
|
286
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
339
287
|
expect(explanation).toBeNull();
|
|
340
288
|
});
|
|
341
289
|
});
|
|
@@ -343,15 +291,15 @@ describe('explainFlightDocumentScene3DRefusal', () => {
|
|
|
343
291
|
describe('explainFlightDocumentScene3DRefusalFromText', () => {
|
|
344
292
|
it('explains an anchor refusal with parser position', () => {
|
|
345
293
|
const yaml = 'flight: 1\nkind: Scene3D\nanchor: &ref value\n';
|
|
346
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
294
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
347
295
|
expect(explanation).not.toBeNull();
|
|
348
296
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.AnchorUnsupported);
|
|
349
297
|
expect(explanation!.line).toBeGreaterThan(0);
|
|
350
298
|
});
|
|
351
299
|
|
|
352
300
|
it('explains a version mismatch in valid YAML', () => {
|
|
353
|
-
const yaml = 'flight: 99\
|
|
354
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
301
|
+
const yaml = 'flight: 99\ndefaultScene: 0\nscenes:\n - kind: Scene3D\n scene:\n kind: Node3D\n';
|
|
302
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
355
303
|
expect(explanation).not.toBeNull();
|
|
356
304
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.VersionUnsupported);
|
|
357
305
|
expect(explanation!.path).toBe('version');
|
|
@@ -359,54 +307,251 @@ describe('explainFlightDocumentScene3DRefusalFromText', () => {
|
|
|
359
307
|
});
|
|
360
308
|
|
|
361
309
|
it('explains a dimension mismatch in valid YAML', () => {
|
|
362
|
-
const yaml = 'flight: 1\
|
|
363
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
310
|
+
const yaml = 'flight: 1\ndefaultScene: 0\nscenes:\n - kind: Scene2D\n scene:\n kind: DisplayObject\n';
|
|
311
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
364
312
|
expect(explanation).not.toBeNull();
|
|
365
313
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.StructureInvalid);
|
|
366
|
-
expect(explanation!.path).toBe('kind');
|
|
314
|
+
expect(explanation!.path).toBe('scenes[0].kind');
|
|
367
315
|
});
|
|
368
316
|
|
|
369
317
|
it('explains a duplicate ambient light from text', () => {
|
|
370
|
-
const yaml =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
318
|
+
const yaml = formatFlightDocumentText(
|
|
319
|
+
createTestDocument({
|
|
320
|
+
cameras: [],
|
|
321
|
+
kind: 'Scene3D',
|
|
322
|
+
lights: [
|
|
323
|
+
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
324
|
+
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
325
|
+
],
|
|
326
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
382
330
|
expect(explanation).not.toBeNull();
|
|
383
331
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DuplicateAmbientLight);
|
|
384
|
-
expect(explanation!.path).toBe('lights');
|
|
332
|
+
expect(explanation!.path).toBe('scenes[0].lights');
|
|
385
333
|
});
|
|
386
334
|
|
|
387
335
|
it('explains a duplicate directional light from text', () => {
|
|
388
|
-
const yaml =
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
336
|
+
const yaml = formatFlightDocumentText(
|
|
337
|
+
createTestDocument({
|
|
338
|
+
cameras: [],
|
|
339
|
+
kind: 'Scene3D',
|
|
340
|
+
lights: [
|
|
341
|
+
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
342
|
+
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
343
|
+
],
|
|
344
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
400
348
|
expect(explanation).not.toBeNull();
|
|
401
349
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DuplicateDirectionalLight);
|
|
402
|
-
expect(explanation!.path).toBe('lights');
|
|
350
|
+
expect(explanation!.path).toBe('scenes[0].lights');
|
|
403
351
|
});
|
|
404
352
|
|
|
405
353
|
it('returns null for valid Scene3D text', () => {
|
|
406
|
-
const yaml = 'flight: 1\
|
|
407
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
354
|
+
const yaml = 'flight: 1\ndefaultScene: 0\nscenes:\n - kind: Scene3D\n scene:\n kind: Node3D\n';
|
|
355
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
356
|
+
expect(explanation).toBeNull();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('model-to-text explain parity', () => {
|
|
361
|
+
// Keep the table annotated so every fixture is checked against the container schema rather than inferred
|
|
362
|
+
// only from its own literal.
|
|
363
|
+
it.each<{
|
|
364
|
+
document: Readonly<FlightDocument>;
|
|
365
|
+
expectedPath: string;
|
|
366
|
+
expectedReason: FlightDocumentRefusalReason;
|
|
367
|
+
label: string;
|
|
368
|
+
}>([
|
|
369
|
+
{
|
|
370
|
+
label: 'duplicate ambient (scene 0)',
|
|
371
|
+
document: createTestDocument({
|
|
372
|
+
cameras: [],
|
|
373
|
+
kind: 'Scene3D',
|
|
374
|
+
lights: [
|
|
375
|
+
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
376
|
+
{ descriptor: createAmbientLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
377
|
+
],
|
|
378
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
379
|
+
}),
|
|
380
|
+
expectedReason: FlightDocumentRefusalReason.DuplicateAmbientLight,
|
|
381
|
+
expectedPath: 'scenes[0].lights',
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
label: 'duplicate directional (scene 0)',
|
|
385
|
+
document: createTestDocument({
|
|
386
|
+
cameras: [],
|
|
387
|
+
kind: 'Scene3D',
|
|
388
|
+
lights: [
|
|
389
|
+
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
390
|
+
{ descriptor: createDirectionalLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
391
|
+
],
|
|
392
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
393
|
+
}),
|
|
394
|
+
expectedReason: FlightDocumentRefusalReason.DuplicateDirectionalLight,
|
|
395
|
+
expectedPath: 'scenes[0].lights',
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
label: 'wrong dimension (scene 0)',
|
|
399
|
+
document: createTestDocument({
|
|
400
|
+
backgroundColor: null,
|
|
401
|
+
kind: 'Scene2D',
|
|
402
|
+
scene: { children: [], fields: {}, kind: 'DisplayObject' },
|
|
403
|
+
}),
|
|
404
|
+
expectedReason: FlightDocumentRefusalReason.StructureInvalid,
|
|
405
|
+
expectedPath: 'scenes[0].kind',
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
label: 'unregistered node kind (scene 0)',
|
|
409
|
+
document: createTestDocument({
|
|
410
|
+
cameras: [],
|
|
411
|
+
kind: 'Scene3D',
|
|
412
|
+
lights: [],
|
|
413
|
+
scene: { children: [{ children: [], fields: {}, kind: 'acme.Unknown' }], fields: {}, kind: Node3DKind },
|
|
414
|
+
}),
|
|
415
|
+
expectedReason: FlightDocumentRefusalReason.NodeKindUnregistered,
|
|
416
|
+
expectedPath: 'scenes[0].scene.children[0]',
|
|
417
|
+
},
|
|
418
|
+
])('model and text explain agree on $label', ({ document, expectedPath, expectedReason }) => {
|
|
419
|
+
const modelResult = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
420
|
+
const text = formatFlightDocumentText(document);
|
|
421
|
+
const textResult = explainFlightDocumentScene3DRefusalFromText(text, createTestSchemas());
|
|
422
|
+
expect(modelResult).not.toBeNull();
|
|
423
|
+
expect(textResult).not.toBeNull();
|
|
424
|
+
expect(textResult!.reason).toBe(modelResult!.reason);
|
|
425
|
+
expect(textResult!.path).toBe(modelResult!.path);
|
|
426
|
+
expect(modelResult!.reason).toBe(expectedReason);
|
|
427
|
+
expect(modelResult!.path).toBe(expectedPath);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('NodeKindUnregistered', () => {
|
|
432
|
+
it('materializer returns null for nested unregistered kind two levels deep', () => {
|
|
433
|
+
const document = createTestDocument({
|
|
434
|
+
cameras: [],
|
|
435
|
+
kind: 'Scene3D',
|
|
436
|
+
lights: [],
|
|
437
|
+
scene: {
|
|
438
|
+
children: [
|
|
439
|
+
{
|
|
440
|
+
children: [{ children: [], fields: {}, kind: 'acme.Unknown' }],
|
|
441
|
+
fields: {},
|
|
442
|
+
kind: Node3DKind,
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
fields: {},
|
|
446
|
+
kind: Node3DKind,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
450
|
+
expect(result).toBeNull();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('explain names the unregistered kind with qualified scene path', () => {
|
|
454
|
+
const document = createTestDocument({
|
|
455
|
+
cameras: [],
|
|
456
|
+
kind: 'Scene3D',
|
|
457
|
+
lights: [],
|
|
458
|
+
scene: {
|
|
459
|
+
children: [
|
|
460
|
+
{
|
|
461
|
+
children: [{ children: [], fields: {}, kind: 'acme.Unknown' }],
|
|
462
|
+
fields: {},
|
|
463
|
+
kind: Node3DKind,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
fields: {},
|
|
467
|
+
kind: Node3DKind,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
471
|
+
expect(explanation).not.toBeNull();
|
|
472
|
+
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.NodeKindUnregistered);
|
|
473
|
+
expect(explanation!.kind).toBe('acme.Unknown');
|
|
474
|
+
expect(explanation!.path).toBe('scenes[0].scene.children[0].children[0]');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('registration mutation control: same tree passes when kind is registered', () => {
|
|
478
|
+
const document = createTestDocument({
|
|
479
|
+
cameras: [],
|
|
480
|
+
kind: 'Scene3D',
|
|
481
|
+
lights: [],
|
|
482
|
+
scene: {
|
|
483
|
+
children: [
|
|
484
|
+
{
|
|
485
|
+
children: [{ children: [], fields: {}, kind: Node3DKind }],
|
|
486
|
+
fields: {},
|
|
487
|
+
kind: Node3DKind,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
fields: {},
|
|
491
|
+
kind: Node3DKind,
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
495
|
+
expect(result).not.toBeNull();
|
|
496
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
408
497
|
expect(explanation).toBeNull();
|
|
409
498
|
});
|
|
499
|
+
|
|
500
|
+
it('text explain reports unregistered kind from YAML', () => {
|
|
501
|
+
const yaml = formatFlightDocumentText(
|
|
502
|
+
createTestDocument({
|
|
503
|
+
cameras: [],
|
|
504
|
+
kind: 'Scene3D',
|
|
505
|
+
lights: [],
|
|
506
|
+
scene: {
|
|
507
|
+
children: [{ children: [], fields: {}, kind: 'acme.Unknown' }],
|
|
508
|
+
fields: {},
|
|
509
|
+
kind: Node3DKind,
|
|
510
|
+
},
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
513
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
514
|
+
expect(explanation).not.toBeNull();
|
|
515
|
+
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.NodeKindUnregistered);
|
|
516
|
+
expect(explanation!.kind).toBe('acme.Unknown');
|
|
517
|
+
expect(explanation!.path).toBe('scenes[0].scene.children[0]');
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// ★ ROOT-NODE FIDELITY, 3D half. Same defect as 2D: the reader built a fresh container and materialized
|
|
522
|
+
// only the authored root's children, so the root's own kind and fields were dropped while the writer
|
|
523
|
+
// captured them.
|
|
524
|
+
describe('Scene3D root fidelity', () => {
|
|
525
|
+
it('preserves the authored root kind', () => {
|
|
526
|
+
const schemas = createTestSchemas();
|
|
527
|
+
const document = rootDocument3D(Node3DKind, {}, schemas);
|
|
528
|
+
const materialization = createFlightDocumentScene3DMaterialization(document, schemas);
|
|
529
|
+
expect(materialization).not.toBeNull();
|
|
530
|
+
expect(materialization!.scene.root.kind).toBe(Node3DKind);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('preserves fields authored on the root', () => {
|
|
534
|
+
const schemas = createTestSchemas();
|
|
535
|
+
const document = rootDocument3D(Node3DKind, { name: 'authored-root' }, schemas);
|
|
536
|
+
const materialization = createFlightDocumentScene3DMaterialization(document, schemas);
|
|
537
|
+
expect(materialization).not.toBeNull();
|
|
538
|
+
expect(materialization!.scene.root.name).toBe('authored-root');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('refuses a registered root whose kind belongs to the other dimension', () => {
|
|
542
|
+
const schemas = createTestSchemas();
|
|
543
|
+
schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, DisplayObjectKind, {
|
|
544
|
+
createNode: () => createDisplayObject() as unknown as NodeAny,
|
|
545
|
+
fields: [],
|
|
546
|
+
kind: DisplayObjectKind,
|
|
547
|
+
writeNodeFields: () => true,
|
|
548
|
+
});
|
|
549
|
+
const document = rootDocument3D(DisplayObjectKind, {}, schemas);
|
|
550
|
+
expect(createFlightDocumentScene3DMaterialization(document, schemas)).toBeNull();
|
|
551
|
+
expect(explainFlightDocumentScene3DRefusal(document, schemas)).toMatchObject({
|
|
552
|
+
reason: FlightDocumentRefusalReason.RootKindMismatch,
|
|
553
|
+
});
|
|
554
|
+
});
|
|
410
555
|
});
|
|
411
556
|
|
|
412
557
|
describe('Scene3DDocumentCamera', () => {
|
|
@@ -417,11 +562,12 @@ describe('Scene3DDocumentCamera', () => {
|
|
|
417
562
|
});
|
|
418
563
|
});
|
|
419
564
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
565
|
+
function createTestDocument(
|
|
566
|
+
scene: FlightDocumentScene,
|
|
567
|
+
resources: FlightDocumentResourceDescriptor[] = [],
|
|
568
|
+
): FlightDocument {
|
|
569
|
+
return { defaultScene: 0, resources, scenes: [scene], version: 1 };
|
|
570
|
+
}
|
|
425
571
|
|
|
426
572
|
function createTestSchemas(): FlightDocumentSchemaRegistry {
|
|
427
573
|
const node3DSchema: FlightDocumentNodeSchema = {
|
|
@@ -444,3 +590,32 @@ function createTestSchemas(): FlightDocumentSchemaRegistry {
|
|
|
444
590
|
shapeCommandSchemas: createKeyedTable('flight-document.shape-command', 'none'),
|
|
445
591
|
};
|
|
446
592
|
}
|
|
593
|
+
|
|
594
|
+
describe('Scene3DDocumentLight', () => {
|
|
595
|
+
it('has no direction field', () => {
|
|
596
|
+
expectTypeOf<keyof Scene3DDocumentLight>().toEqualTypeOf<'descriptor' | 'name' | 'node' | 'transform'>();
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
function rootDocument3D(
|
|
601
|
+
kind: string,
|
|
602
|
+
fields: Record<string, unknown>,
|
|
603
|
+
schemas: FlightDocumentSchemaRegistry,
|
|
604
|
+
): FlightDocument {
|
|
605
|
+
schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, Node3DKind, {
|
|
606
|
+
createNode: (nodeFields) => {
|
|
607
|
+
const node = createNode3D();
|
|
608
|
+
if (typeof nodeFields['name'] === 'string') node.name = nodeFields['name'];
|
|
609
|
+
return node as unknown as NodeAny;
|
|
610
|
+
},
|
|
611
|
+
fields: [],
|
|
612
|
+
kind: Node3DKind,
|
|
613
|
+
writeNodeFields: () => true,
|
|
614
|
+
});
|
|
615
|
+
return {
|
|
616
|
+
defaultScene: 0,
|
|
617
|
+
resources: [],
|
|
618
|
+
scenes: [{ cameras: [], kind: 'Scene3D', lights: [], scene: { children: [], fields, kind } }],
|
|
619
|
+
version: 1,
|
|
620
|
+
} as unknown as FlightDocument;
|
|
621
|
+
}
|