@flighthq/scene-document 0.4.1-next.37.ff47e13 → 0.4.1-next.515.b0834fe
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 +145 -291
- 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 +10 -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 +485 -159
- package/src/sceneDocumentYamlSubset.test.ts +8 -0
|
@@ -1,29 +1,40 @@
|
|
|
1
|
-
import { createTransform3D } from '@flighthq/geometry/contract';
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { createTransform3D, createVector3, setQuaternionFromAxisAngle } from '@flighthq/geometry/contract';
|
|
2
|
+
import {
|
|
3
|
+
createAmbientLight,
|
|
4
|
+
createDirectionalLight,
|
|
5
|
+
createHemisphereLight,
|
|
6
|
+
createPointLight,
|
|
7
|
+
createSpotLight,
|
|
8
|
+
} from '@flighthq/lighting/contract';
|
|
9
|
+
import { getNodeChildren, invalidateNodeLocalTransform } from '@flighthq/node/contract';
|
|
4
10
|
import { createKeyedTable, withRegistryTableEntry } from '@flighthq/registry/contract';
|
|
11
|
+
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
5
12
|
import { createNode3D, createScene3D } from '@flighthq/scene3d/contract';
|
|
6
13
|
import type {
|
|
7
14
|
FlightDocument,
|
|
8
15
|
FlightDocumentFields,
|
|
9
16
|
FlightDocumentNodeSchema,
|
|
17
|
+
FlightDocumentResourceDescriptor,
|
|
10
18
|
FlightDocumentResourceLookup,
|
|
11
|
-
|
|
19
|
+
FlightDocumentScene,
|
|
12
20
|
FlightDocumentScene3DMaterialization,
|
|
13
21
|
FlightDocumentSchemaRegistry,
|
|
22
|
+
Light,
|
|
14
23
|
NodeAny,
|
|
24
|
+
Scene3DLightsLike,
|
|
15
25
|
Scene3DDocumentCamera,
|
|
16
26
|
Scene3DDocumentLight,
|
|
17
27
|
} from '@flighthq/types/contract';
|
|
18
28
|
import {
|
|
19
29
|
AmbientLightKind,
|
|
20
30
|
DirectionalLightKind,
|
|
31
|
+
DisplayObjectKind,
|
|
21
32
|
FlightDocumentRefusalReason,
|
|
22
33
|
Node3DKind,
|
|
23
34
|
} from '@flighthq/types/contract';
|
|
24
35
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
25
36
|
|
|
26
|
-
import {
|
|
37
|
+
import { formatFlightDocumentText } from './flightDocumentText';
|
|
27
38
|
import {
|
|
28
39
|
createFlightDocumentFromScene3D,
|
|
29
40
|
createFlightDocumentScene3DMaterialization,
|
|
@@ -37,7 +48,6 @@ describe('createFlightDocumentFromScene3D', () => {
|
|
|
37
48
|
const scene = createScene3D();
|
|
38
49
|
const document = createFlightDocumentFromScene3D(scene, [], [], createTestSchemas());
|
|
39
50
|
expect(document.kind).toBe('Scene3D');
|
|
40
|
-
expect(document.version).toBe(1);
|
|
41
51
|
expect(document.scene.children).toHaveLength(0);
|
|
42
52
|
expect(document.cameras).toHaveLength(0);
|
|
43
53
|
expect(document.lights).toHaveLength(0);
|
|
@@ -71,14 +81,12 @@ describe('createFlightDocumentFromScene3D', () => {
|
|
|
71
81
|
|
|
72
82
|
describe('createFlightDocumentScene3DMaterialization', () => {
|
|
73
83
|
it('materializes an empty Scene3D from a minimal model', () => {
|
|
74
|
-
const document
|
|
84
|
+
const document = createTestDocument({
|
|
75
85
|
cameras: [],
|
|
76
86
|
kind: 'Scene3D',
|
|
77
87
|
lights: [],
|
|
78
|
-
resources: [],
|
|
79
88
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
80
|
-
|
|
81
|
-
};
|
|
89
|
+
});
|
|
82
90
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
83
91
|
expect(result).not.toBeNull();
|
|
84
92
|
const materialization = result as FlightDocumentScene3DMaterialization;
|
|
@@ -88,7 +96,7 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
88
96
|
});
|
|
89
97
|
|
|
90
98
|
it('materializes cameras from document camera descriptors', () => {
|
|
91
|
-
const document
|
|
99
|
+
const document = createTestDocument({
|
|
92
100
|
cameras: [
|
|
93
101
|
{
|
|
94
102
|
far: 500,
|
|
@@ -99,10 +107,8 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
99
107
|
],
|
|
100
108
|
kind: 'Scene3D',
|
|
101
109
|
lights: [],
|
|
102
|
-
resources: [],
|
|
103
110
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
104
|
-
|
|
105
|
-
};
|
|
111
|
+
});
|
|
106
112
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
107
113
|
expect(result).not.toBeNull();
|
|
108
114
|
const materialization = result as FlightDocumentScene3DMaterialization;
|
|
@@ -112,19 +118,133 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
112
118
|
expect(materialization.cameras[0].projection.kind).toBe('perspective');
|
|
113
119
|
});
|
|
114
120
|
|
|
121
|
+
it('applies authored camera placement to the runtime view matrix', () => {
|
|
122
|
+
const transform = createTransform3D();
|
|
123
|
+
transform.position.x = 7;
|
|
124
|
+
const document = createTestDocument({
|
|
125
|
+
cameras: [
|
|
126
|
+
{
|
|
127
|
+
far: 500,
|
|
128
|
+
near: 0.5,
|
|
129
|
+
projection: { aspect: 1.77, fovY: 1.047, kind: 'perspective' },
|
|
130
|
+
transform,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
kind: 'Scene3D',
|
|
134
|
+
lights: [],
|
|
135
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
139
|
+
|
|
140
|
+
expect(result?.cameras[0].view.m[12]).toBe(-7);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('applies a bound depth-first node placement to the runtime camera', () => {
|
|
144
|
+
const transform = createTransform3D();
|
|
145
|
+
transform.position.x = 7;
|
|
146
|
+
const document = createTestDocument({
|
|
147
|
+
cameras: [
|
|
148
|
+
{
|
|
149
|
+
far: 500,
|
|
150
|
+
near: 0.5,
|
|
151
|
+
node: 2,
|
|
152
|
+
projection: { aspect: 1.77, fovY: 1.047, kind: 'perspective' },
|
|
153
|
+
transform,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
kind: 'Scene3D',
|
|
157
|
+
lights: [],
|
|
158
|
+
scene: {
|
|
159
|
+
children: [
|
|
160
|
+
{
|
|
161
|
+
children: [{ children: [], fields: { positionX: 3 }, kind: Node3DKind }],
|
|
162
|
+
fields: { positionX: 2 },
|
|
163
|
+
kind: Node3DKind,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
fields: {},
|
|
167
|
+
kind: Node3DKind,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
172
|
+
|
|
173
|
+
expect(result?.cameras[0].view.m[12]).toBe(-5);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('preserves the source-derived representable light population and authored placement', () => {
|
|
177
|
+
const transform = createTransform3D();
|
|
178
|
+
transform.position.x = 10;
|
|
179
|
+
transform.position.y = 20;
|
|
180
|
+
transform.position.z = 30;
|
|
181
|
+
transform.scale.x = 2;
|
|
182
|
+
transform.scale.y = 3;
|
|
183
|
+
transform.scale.z = 4;
|
|
184
|
+
setQuaternionFromAxisAngle(transform.rotation, createVector3(0, 1, 0), Math.PI / 2);
|
|
185
|
+
const authoredLights: Required<Scene3DLightsLike> = {
|
|
186
|
+
ambient: createAmbientLight(),
|
|
187
|
+
directional: createDirectionalLight({ direction: { x: 0, y: 0, z: -1 } }),
|
|
188
|
+
hemisphere: [createHemisphereLight()],
|
|
189
|
+
point: [createPointLight({ position: { x: 1, y: 0, z: 0 } })],
|
|
190
|
+
spot: [createSpotLight({ direction: { x: 0, y: 0, z: -1 }, position: { x: 1, y: 0, z: 0 } })],
|
|
191
|
+
};
|
|
192
|
+
const document = createTestDocument({
|
|
193
|
+
cameras: [],
|
|
194
|
+
kind: 'Scene3D',
|
|
195
|
+
lights: getDocumentLights(authoredLights, transform),
|
|
196
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
200
|
+
|
|
201
|
+
expect(getScene3DLightKinds(result?.lights ?? null).sort()).toEqual(getScene3DLightKinds(authoredLights).sort());
|
|
202
|
+
expect(result?.lights.directional?.direction.x).toBeCloseTo(-1);
|
|
203
|
+
expect(result?.lights.directional?.direction.y).toBeCloseTo(0);
|
|
204
|
+
expect(result?.lights.directional?.direction.z).toBeCloseTo(0);
|
|
205
|
+
expect(result?.lights.point?.[0].position).toMatchObject({ x: 10, y: 20, z: 28 });
|
|
206
|
+
expect(result?.lights.spot?.[0].position).toMatchObject({ x: 10, y: 20, z: 28 });
|
|
207
|
+
expect(result?.lights.spot?.[0].direction.x).toBeCloseTo(-1);
|
|
208
|
+
expect(result?.lights.spot?.[0].direction.y).toBeCloseTo(0);
|
|
209
|
+
expect(result?.lights.spot?.[0].direction.z).toBeCloseTo(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('applies a bound depth-first node placement to a punctual light', () => {
|
|
213
|
+
const transform = createTransform3D();
|
|
214
|
+
transform.position.x = 7;
|
|
215
|
+
const document = createTestDocument({
|
|
216
|
+
cameras: [],
|
|
217
|
+
kind: 'Scene3D',
|
|
218
|
+
lights: [{ descriptor: createPointLight(), node: 2, transform }],
|
|
219
|
+
scene: {
|
|
220
|
+
children: [
|
|
221
|
+
{
|
|
222
|
+
children: [{ children: [], fields: { positionX: 3 }, kind: Node3DKind }],
|
|
223
|
+
fields: { positionX: 2 },
|
|
224
|
+
kind: Node3DKind,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
fields: {},
|
|
228
|
+
kind: Node3DKind,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
233
|
+
|
|
234
|
+
expect(result?.lights.point?.[0].position).toMatchObject({ x: 5, y: 0, z: 0 });
|
|
235
|
+
});
|
|
236
|
+
|
|
115
237
|
it('materializes a tree with children', () => {
|
|
116
|
-
const document
|
|
238
|
+
const document = createTestDocument({
|
|
117
239
|
cameras: [],
|
|
118
240
|
kind: 'Scene3D',
|
|
119
241
|
lights: [],
|
|
120
|
-
resources: [],
|
|
121
242
|
scene: {
|
|
122
243
|
children: [{ children: [], fields: {}, kind: Node3DKind }],
|
|
123
244
|
fields: {},
|
|
124
245
|
kind: Node3DKind,
|
|
125
246
|
},
|
|
126
|
-
|
|
127
|
-
};
|
|
247
|
+
});
|
|
128
248
|
const schemas = createTestSchemas();
|
|
129
249
|
const result = createFlightDocumentScene3DMaterialization(document, schemas);
|
|
130
250
|
expect(result).not.toBeNull();
|
|
@@ -135,58 +255,53 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
135
255
|
});
|
|
136
256
|
|
|
137
257
|
it('returns null for a Scene2D document', () => {
|
|
138
|
-
const document = {
|
|
258
|
+
const document = createTestDocument({
|
|
139
259
|
backgroundColor: null,
|
|
140
260
|
kind: 'Scene2D',
|
|
141
|
-
resources: [],
|
|
142
261
|
scene: { children: [], fields: {}, kind: 'DisplayObject' },
|
|
143
|
-
|
|
144
|
-
} as FlightDocument;
|
|
262
|
+
});
|
|
145
263
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
146
264
|
expect(result).toBeNull();
|
|
147
265
|
});
|
|
148
266
|
|
|
149
267
|
it('returns null for an unsupported version', () => {
|
|
150
268
|
const document = {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
269
|
+
...createTestDocument({
|
|
270
|
+
cameras: [],
|
|
271
|
+
kind: 'Scene3D',
|
|
272
|
+
lights: [],
|
|
273
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
274
|
+
}),
|
|
156
275
|
version: 99,
|
|
157
|
-
} as unknown as
|
|
276
|
+
} as unknown as FlightDocument;
|
|
158
277
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
159
278
|
expect(result).toBeNull();
|
|
160
279
|
});
|
|
161
280
|
|
|
162
281
|
it('returns null for duplicate ambient lights', () => {
|
|
163
|
-
const document
|
|
282
|
+
const document = createTestDocument({
|
|
164
283
|
cameras: [],
|
|
165
284
|
kind: 'Scene3D',
|
|
166
285
|
lights: [
|
|
167
286
|
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
168
287
|
{ descriptor: createAmbientLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
169
288
|
],
|
|
170
|
-
resources: [],
|
|
171
289
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
172
|
-
|
|
173
|
-
};
|
|
290
|
+
});
|
|
174
291
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
175
292
|
expect(result).toBeNull();
|
|
176
293
|
});
|
|
177
294
|
|
|
178
295
|
it('returns null for duplicate directional lights', () => {
|
|
179
|
-
const document
|
|
296
|
+
const document = createTestDocument({
|
|
180
297
|
cameras: [],
|
|
181
298
|
kind: 'Scene3D',
|
|
182
299
|
lights: [
|
|
183
300
|
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
184
301
|
{ descriptor: createDirectionalLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
185
302
|
],
|
|
186
|
-
resources: [],
|
|
187
303
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
188
|
-
|
|
189
|
-
};
|
|
304
|
+
});
|
|
190
305
|
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
191
306
|
expect(result).toBeNull();
|
|
192
307
|
});
|
|
@@ -194,7 +309,14 @@ describe('createFlightDocumentScene3DMaterialization', () => {
|
|
|
194
309
|
|
|
195
310
|
describe('createFlightDocumentScene3DMaterializationFromText', () => {
|
|
196
311
|
it('materializes a Scene3D from YAML text', () => {
|
|
197
|
-
const yaml = [
|
|
312
|
+
const yaml = [
|
|
313
|
+
'flight: 1',
|
|
314
|
+
'defaultScene: 0',
|
|
315
|
+
'scenes:',
|
|
316
|
+
' - kind: Scene3D',
|
|
317
|
+
' scene:',
|
|
318
|
+
' kind: Node3D',
|
|
319
|
+
].join('\n');
|
|
198
320
|
const schemas = createTestSchemas();
|
|
199
321
|
const result = createFlightDocumentScene3DMaterializationFromText(yaml, schemas);
|
|
200
322
|
expect(result).not.toBeNull();
|
|
@@ -203,87 +325,43 @@ describe('createFlightDocumentScene3DMaterializationFromText', () => {
|
|
|
203
325
|
});
|
|
204
326
|
|
|
205
327
|
it('returns null for a Scene2D YAML document', () => {
|
|
206
|
-
const yaml = [
|
|
328
|
+
const yaml = [
|
|
329
|
+
'flight: 1',
|
|
330
|
+
'defaultScene: 0',
|
|
331
|
+
'scenes:',
|
|
332
|
+
' - kind: Scene2D',
|
|
333
|
+
' scene:',
|
|
334
|
+
' kind: DisplayObject',
|
|
335
|
+
].join('\n');
|
|
207
336
|
const result = createFlightDocumentScene3DMaterializationFromText(yaml, createTestSchemas());
|
|
208
337
|
expect(result).toBeNull();
|
|
209
338
|
});
|
|
210
339
|
});
|
|
211
340
|
|
|
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
341
|
describe('explainFlightDocumentScene3DRefusal', () => {
|
|
263
342
|
it('explains a dimension mismatch', () => {
|
|
264
|
-
const document = {
|
|
343
|
+
const document = createTestDocument({
|
|
265
344
|
backgroundColor: null,
|
|
266
345
|
kind: 'Scene2D',
|
|
267
|
-
resources: [],
|
|
268
346
|
scene: { children: [], fields: {}, kind: 'DisplayObject' },
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
347
|
+
});
|
|
348
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
272
349
|
expect(explanation).not.toBeNull();
|
|
273
350
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.StructureInvalid);
|
|
274
|
-
expect(explanation!.path).toBe('kind');
|
|
351
|
+
expect(explanation!.path).toBe('scenes[0].kind');
|
|
275
352
|
});
|
|
276
353
|
|
|
277
354
|
it('explains an unsupported version', () => {
|
|
278
355
|
const document = {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
356
|
+
...createTestDocument({
|
|
357
|
+
cameras: [],
|
|
358
|
+
kind: 'Scene3D',
|
|
359
|
+
lights: [],
|
|
360
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
361
|
+
}),
|
|
284
362
|
version: 99,
|
|
285
|
-
} as unknown as
|
|
286
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
363
|
+
} as unknown as FlightDocument;
|
|
364
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
287
365
|
expect(explanation).not.toBeNull();
|
|
288
366
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.VersionUnsupported);
|
|
289
367
|
expect(explanation!.path).toBe('version');
|
|
@@ -291,51 +369,45 @@ describe('explainFlightDocumentScene3DRefusal', () => {
|
|
|
291
369
|
});
|
|
292
370
|
|
|
293
371
|
it('explains a duplicate ambient light', () => {
|
|
294
|
-
const document
|
|
372
|
+
const document = createTestDocument({
|
|
295
373
|
cameras: [],
|
|
296
374
|
kind: 'Scene3D',
|
|
297
375
|
lights: [
|
|
298
376
|
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
299
377
|
{ descriptor: createAmbientLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
300
378
|
],
|
|
301
|
-
resources: [],
|
|
302
379
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
380
|
+
});
|
|
381
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
306
382
|
expect(explanation).not.toBeNull();
|
|
307
383
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DuplicateAmbientLight);
|
|
308
|
-
expect(explanation!.path).toBe('lights');
|
|
384
|
+
expect(explanation!.path).toBe('scenes[0].lights');
|
|
309
385
|
});
|
|
310
386
|
|
|
311
387
|
it('explains a duplicate directional light', () => {
|
|
312
|
-
const document
|
|
388
|
+
const document = createTestDocument({
|
|
313
389
|
cameras: [],
|
|
314
390
|
kind: 'Scene3D',
|
|
315
391
|
lights: [
|
|
316
392
|
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
317
393
|
{ descriptor: createDirectionalLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
318
394
|
],
|
|
319
|
-
resources: [],
|
|
320
395
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
396
|
+
});
|
|
397
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
324
398
|
expect(explanation).not.toBeNull();
|
|
325
399
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DuplicateDirectionalLight);
|
|
326
|
-
expect(explanation!.path).toBe('lights');
|
|
400
|
+
expect(explanation!.path).toBe('scenes[0].lights');
|
|
327
401
|
});
|
|
328
402
|
|
|
329
403
|
it('returns null when the document is valid', () => {
|
|
330
|
-
const document
|
|
404
|
+
const document = createTestDocument({
|
|
331
405
|
cameras: [],
|
|
332
406
|
kind: 'Scene3D',
|
|
333
407
|
lights: [],
|
|
334
|
-
resources: [],
|
|
335
408
|
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const explanation = explainFlightDocumentScene3DRefusal(document);
|
|
409
|
+
});
|
|
410
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
339
411
|
expect(explanation).toBeNull();
|
|
340
412
|
});
|
|
341
413
|
});
|
|
@@ -343,15 +415,15 @@ describe('explainFlightDocumentScene3DRefusal', () => {
|
|
|
343
415
|
describe('explainFlightDocumentScene3DRefusalFromText', () => {
|
|
344
416
|
it('explains an anchor refusal with parser position', () => {
|
|
345
417
|
const yaml = 'flight: 1\nkind: Scene3D\nanchor: &ref value\n';
|
|
346
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
418
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
347
419
|
expect(explanation).not.toBeNull();
|
|
348
420
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.AnchorUnsupported);
|
|
349
421
|
expect(explanation!.line).toBeGreaterThan(0);
|
|
350
422
|
});
|
|
351
423
|
|
|
352
424
|
it('explains a version mismatch in valid YAML', () => {
|
|
353
|
-
const yaml = 'flight: 99\
|
|
354
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
425
|
+
const yaml = 'flight: 99\ndefaultScene: 0\nscenes:\n - kind: Scene3D\n scene:\n kind: Node3D\n';
|
|
426
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
355
427
|
expect(explanation).not.toBeNull();
|
|
356
428
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.VersionUnsupported);
|
|
357
429
|
expect(explanation!.path).toBe('version');
|
|
@@ -359,54 +431,251 @@ describe('explainFlightDocumentScene3DRefusalFromText', () => {
|
|
|
359
431
|
});
|
|
360
432
|
|
|
361
433
|
it('explains a dimension mismatch in valid YAML', () => {
|
|
362
|
-
const yaml = 'flight: 1\
|
|
363
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
434
|
+
const yaml = 'flight: 1\ndefaultScene: 0\nscenes:\n - kind: Scene2D\n scene:\n kind: DisplayObject\n';
|
|
435
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
364
436
|
expect(explanation).not.toBeNull();
|
|
365
437
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.StructureInvalid);
|
|
366
|
-
expect(explanation!.path).toBe('kind');
|
|
438
|
+
expect(explanation!.path).toBe('scenes[0].kind');
|
|
367
439
|
});
|
|
368
440
|
|
|
369
441
|
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);
|
|
442
|
+
const yaml = formatFlightDocumentText(
|
|
443
|
+
createTestDocument({
|
|
444
|
+
cameras: [],
|
|
445
|
+
kind: 'Scene3D',
|
|
446
|
+
lights: [
|
|
447
|
+
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
448
|
+
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
449
|
+
],
|
|
450
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
382
454
|
expect(explanation).not.toBeNull();
|
|
383
455
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DuplicateAmbientLight);
|
|
384
|
-
expect(explanation!.path).toBe('lights');
|
|
456
|
+
expect(explanation!.path).toBe('scenes[0].lights');
|
|
385
457
|
});
|
|
386
458
|
|
|
387
459
|
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);
|
|
460
|
+
const yaml = formatFlightDocumentText(
|
|
461
|
+
createTestDocument({
|
|
462
|
+
cameras: [],
|
|
463
|
+
kind: 'Scene3D',
|
|
464
|
+
lights: [
|
|
465
|
+
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
466
|
+
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
467
|
+
],
|
|
468
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
400
472
|
expect(explanation).not.toBeNull();
|
|
401
473
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DuplicateDirectionalLight);
|
|
402
|
-
expect(explanation!.path).toBe('lights');
|
|
474
|
+
expect(explanation!.path).toBe('scenes[0].lights');
|
|
403
475
|
});
|
|
404
476
|
|
|
405
477
|
it('returns null for valid Scene3D text', () => {
|
|
406
|
-
const yaml = 'flight: 1\
|
|
407
|
-
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml);
|
|
478
|
+
const yaml = 'flight: 1\ndefaultScene: 0\nscenes:\n - kind: Scene3D\n scene:\n kind: Node3D\n';
|
|
479
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
480
|
+
expect(explanation).toBeNull();
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('model-to-text explain parity', () => {
|
|
485
|
+
// Keep the table annotated so every fixture is checked against the container schema rather than inferred
|
|
486
|
+
// only from its own literal.
|
|
487
|
+
it.each<{
|
|
488
|
+
document: Readonly<FlightDocument>;
|
|
489
|
+
expectedPath: string;
|
|
490
|
+
expectedReason: FlightDocumentRefusalReason;
|
|
491
|
+
label: string;
|
|
492
|
+
}>([
|
|
493
|
+
{
|
|
494
|
+
label: 'duplicate ambient (scene 0)',
|
|
495
|
+
document: createTestDocument({
|
|
496
|
+
cameras: [],
|
|
497
|
+
kind: 'Scene3D',
|
|
498
|
+
lights: [
|
|
499
|
+
{ descriptor: createAmbientLight(), transform: createTransform3D() },
|
|
500
|
+
{ descriptor: createAmbientLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
501
|
+
],
|
|
502
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
503
|
+
}),
|
|
504
|
+
expectedReason: FlightDocumentRefusalReason.DuplicateAmbientLight,
|
|
505
|
+
expectedPath: 'scenes[0].lights',
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
label: 'duplicate directional (scene 0)',
|
|
509
|
+
document: createTestDocument({
|
|
510
|
+
cameras: [],
|
|
511
|
+
kind: 'Scene3D',
|
|
512
|
+
lights: [
|
|
513
|
+
{ descriptor: createDirectionalLight(), transform: createTransform3D() },
|
|
514
|
+
{ descriptor: createDirectionalLight({ color: 0xccccccff, intensity: 0.5 }), transform: createTransform3D() },
|
|
515
|
+
],
|
|
516
|
+
scene: { children: [], fields: {}, kind: Node3DKind },
|
|
517
|
+
}),
|
|
518
|
+
expectedReason: FlightDocumentRefusalReason.DuplicateDirectionalLight,
|
|
519
|
+
expectedPath: 'scenes[0].lights',
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
label: 'wrong dimension (scene 0)',
|
|
523
|
+
document: createTestDocument({
|
|
524
|
+
backgroundColor: null,
|
|
525
|
+
kind: 'Scene2D',
|
|
526
|
+
scene: { children: [], fields: {}, kind: 'DisplayObject' },
|
|
527
|
+
}),
|
|
528
|
+
expectedReason: FlightDocumentRefusalReason.StructureInvalid,
|
|
529
|
+
expectedPath: 'scenes[0].kind',
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
label: 'unregistered node kind (scene 0)',
|
|
533
|
+
document: createTestDocument({
|
|
534
|
+
cameras: [],
|
|
535
|
+
kind: 'Scene3D',
|
|
536
|
+
lights: [],
|
|
537
|
+
scene: { children: [{ children: [], fields: {}, kind: 'acme.Unknown' }], fields: {}, kind: Node3DKind },
|
|
538
|
+
}),
|
|
539
|
+
expectedReason: FlightDocumentRefusalReason.NodeKindUnregistered,
|
|
540
|
+
expectedPath: 'scenes[0].scene.children[0]',
|
|
541
|
+
},
|
|
542
|
+
])('model and text explain agree on $label', ({ document, expectedPath, expectedReason }) => {
|
|
543
|
+
const modelResult = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
544
|
+
const text = formatFlightDocumentText(document);
|
|
545
|
+
const textResult = explainFlightDocumentScene3DRefusalFromText(text, createTestSchemas());
|
|
546
|
+
expect(modelResult).not.toBeNull();
|
|
547
|
+
expect(textResult).not.toBeNull();
|
|
548
|
+
expect(textResult!.reason).toBe(modelResult!.reason);
|
|
549
|
+
expect(textResult!.path).toBe(modelResult!.path);
|
|
550
|
+
expect(modelResult!.reason).toBe(expectedReason);
|
|
551
|
+
expect(modelResult!.path).toBe(expectedPath);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe('NodeKindUnregistered', () => {
|
|
556
|
+
it('materializer returns null for nested unregistered kind two levels deep', () => {
|
|
557
|
+
const document = createTestDocument({
|
|
558
|
+
cameras: [],
|
|
559
|
+
kind: 'Scene3D',
|
|
560
|
+
lights: [],
|
|
561
|
+
scene: {
|
|
562
|
+
children: [
|
|
563
|
+
{
|
|
564
|
+
children: [{ children: [], fields: {}, kind: 'acme.Unknown' }],
|
|
565
|
+
fields: {},
|
|
566
|
+
kind: Node3DKind,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
fields: {},
|
|
570
|
+
kind: Node3DKind,
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
574
|
+
expect(result).toBeNull();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('explain names the unregistered kind with qualified scene path', () => {
|
|
578
|
+
const document = createTestDocument({
|
|
579
|
+
cameras: [],
|
|
580
|
+
kind: 'Scene3D',
|
|
581
|
+
lights: [],
|
|
582
|
+
scene: {
|
|
583
|
+
children: [
|
|
584
|
+
{
|
|
585
|
+
children: [{ children: [], fields: {}, kind: 'acme.Unknown' }],
|
|
586
|
+
fields: {},
|
|
587
|
+
kind: Node3DKind,
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
fields: {},
|
|
591
|
+
kind: Node3DKind,
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
595
|
+
expect(explanation).not.toBeNull();
|
|
596
|
+
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.NodeKindUnregistered);
|
|
597
|
+
expect(explanation!.kind).toBe('acme.Unknown');
|
|
598
|
+
expect(explanation!.path).toBe('scenes[0].scene.children[0].children[0]');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('registration mutation control: same tree passes when kind is registered', () => {
|
|
602
|
+
const document = createTestDocument({
|
|
603
|
+
cameras: [],
|
|
604
|
+
kind: 'Scene3D',
|
|
605
|
+
lights: [],
|
|
606
|
+
scene: {
|
|
607
|
+
children: [
|
|
608
|
+
{
|
|
609
|
+
children: [{ children: [], fields: {}, kind: Node3DKind }],
|
|
610
|
+
fields: {},
|
|
611
|
+
kind: Node3DKind,
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
fields: {},
|
|
615
|
+
kind: Node3DKind,
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
const result = createFlightDocumentScene3DMaterialization(document, createTestSchemas());
|
|
619
|
+
expect(result).not.toBeNull();
|
|
620
|
+
const explanation = explainFlightDocumentScene3DRefusal(document, createTestSchemas());
|
|
408
621
|
expect(explanation).toBeNull();
|
|
409
622
|
});
|
|
623
|
+
|
|
624
|
+
it('text explain reports unregistered kind from YAML', () => {
|
|
625
|
+
const yaml = formatFlightDocumentText(
|
|
626
|
+
createTestDocument({
|
|
627
|
+
cameras: [],
|
|
628
|
+
kind: 'Scene3D',
|
|
629
|
+
lights: [],
|
|
630
|
+
scene: {
|
|
631
|
+
children: [{ children: [], fields: {}, kind: 'acme.Unknown' }],
|
|
632
|
+
fields: {},
|
|
633
|
+
kind: Node3DKind,
|
|
634
|
+
},
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
const explanation = explainFlightDocumentScene3DRefusalFromText(yaml, createTestSchemas());
|
|
638
|
+
expect(explanation).not.toBeNull();
|
|
639
|
+
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.NodeKindUnregistered);
|
|
640
|
+
expect(explanation!.kind).toBe('acme.Unknown');
|
|
641
|
+
expect(explanation!.path).toBe('scenes[0].scene.children[0]');
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// ★ ROOT-NODE FIDELITY, 3D half. Same defect as 2D: the reader built a fresh container and materialized
|
|
646
|
+
// only the authored root's children, so the root's own kind and fields were dropped while the writer
|
|
647
|
+
// captured them.
|
|
648
|
+
describe('Scene3D root fidelity', () => {
|
|
649
|
+
it('preserves the authored root kind', () => {
|
|
650
|
+
const schemas = createTestSchemas();
|
|
651
|
+
const document = rootDocument3D(Node3DKind, {}, schemas);
|
|
652
|
+
const materialization = createFlightDocumentScene3DMaterialization(document, schemas);
|
|
653
|
+
expect(materialization).not.toBeNull();
|
|
654
|
+
expect(materialization!.scene.root.kind).toBe(Node3DKind);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('preserves fields authored on the root', () => {
|
|
658
|
+
const schemas = createTestSchemas();
|
|
659
|
+
const document = rootDocument3D(Node3DKind, { name: 'authored-root' }, schemas);
|
|
660
|
+
const materialization = createFlightDocumentScene3DMaterialization(document, schemas);
|
|
661
|
+
expect(materialization).not.toBeNull();
|
|
662
|
+
expect(materialization!.scene.root.name).toBe('authored-root');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('refuses a registered root whose kind belongs to the other dimension', () => {
|
|
666
|
+
const schemas = createTestSchemas();
|
|
667
|
+
schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, DisplayObjectKind, {
|
|
668
|
+
createNode: () => createDisplayObject() as unknown as NodeAny,
|
|
669
|
+
fields: [],
|
|
670
|
+
kind: DisplayObjectKind,
|
|
671
|
+
writeNodeFields: () => true,
|
|
672
|
+
});
|
|
673
|
+
const document = rootDocument3D(DisplayObjectKind, {}, schemas);
|
|
674
|
+
expect(createFlightDocumentScene3DMaterialization(document, schemas)).toBeNull();
|
|
675
|
+
expect(explainFlightDocumentScene3DRefusal(document, schemas)).toMatchObject({
|
|
676
|
+
reason: FlightDocumentRefusalReason.RootKindMismatch,
|
|
677
|
+
});
|
|
678
|
+
});
|
|
410
679
|
});
|
|
411
680
|
|
|
412
681
|
describe('Scene3DDocumentCamera', () => {
|
|
@@ -417,16 +686,23 @@ describe('Scene3DDocumentCamera', () => {
|
|
|
417
686
|
});
|
|
418
687
|
});
|
|
419
688
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
689
|
+
function createTestDocument(
|
|
690
|
+
scene: FlightDocumentScene,
|
|
691
|
+
resources: FlightDocumentResourceDescriptor[] = [],
|
|
692
|
+
): FlightDocument {
|
|
693
|
+
return { defaultScene: 0, resources, scenes: [scene], version: 1 };
|
|
694
|
+
}
|
|
425
695
|
|
|
426
696
|
function createTestSchemas(): FlightDocumentSchemaRegistry {
|
|
427
697
|
const node3DSchema: FlightDocumentNodeSchema = {
|
|
428
|
-
createNode: (
|
|
429
|
-
|
|
698
|
+
createNode: (fields: Readonly<FlightDocumentFields>, _resources: FlightDocumentResourceLookup) => {
|
|
699
|
+
const node = createNode3D();
|
|
700
|
+
const positionX = fields['positionX'];
|
|
701
|
+
if (typeof positionX === 'number') {
|
|
702
|
+
node.position.x = positionX;
|
|
703
|
+
invalidateNodeLocalTransform(node);
|
|
704
|
+
}
|
|
705
|
+
return node;
|
|
430
706
|
},
|
|
431
707
|
fields: [],
|
|
432
708
|
kind: Node3DKind,
|
|
@@ -444,3 +720,53 @@ function createTestSchemas(): FlightDocumentSchemaRegistry {
|
|
|
444
720
|
shapeCommandSchemas: createKeyedTable('flight-document.shape-command', 'none'),
|
|
445
721
|
};
|
|
446
722
|
}
|
|
723
|
+
|
|
724
|
+
describe('Scene3DDocumentLight', () => {
|
|
725
|
+
it('has no direction field', () => {
|
|
726
|
+
expectTypeOf<keyof Scene3DDocumentLight>().toEqualTypeOf<'descriptor' | 'name' | 'node' | 'transform'>();
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
function rootDocument3D(
|
|
731
|
+
kind: string,
|
|
732
|
+
fields: Record<string, unknown>,
|
|
733
|
+
schemas: FlightDocumentSchemaRegistry,
|
|
734
|
+
): FlightDocument {
|
|
735
|
+
schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, Node3DKind, {
|
|
736
|
+
createNode: (nodeFields) => {
|
|
737
|
+
const node = createNode3D();
|
|
738
|
+
if (typeof nodeFields['name'] === 'string') node.name = nodeFields['name'];
|
|
739
|
+
return node as unknown as NodeAny;
|
|
740
|
+
},
|
|
741
|
+
fields: [],
|
|
742
|
+
kind: Node3DKind,
|
|
743
|
+
writeNodeFields: () => true,
|
|
744
|
+
});
|
|
745
|
+
return {
|
|
746
|
+
defaultScene: 0,
|
|
747
|
+
resources: [],
|
|
748
|
+
scenes: [{ cameras: [], kind: 'Scene3D', lights: [], scene: { children: [], fields, kind } }],
|
|
749
|
+
version: 1,
|
|
750
|
+
} as unknown as FlightDocument;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function getDocumentLights(
|
|
754
|
+
source: Readonly<Required<Scene3DLightsLike>>,
|
|
755
|
+
transform: Readonly<Scene3DDocumentLight['transform']>,
|
|
756
|
+
): Scene3DDocumentLight[] {
|
|
757
|
+
return getScene3DLights(source).map((descriptor) => ({ descriptor, transform }));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function getScene3DLightKinds(source: Readonly<Scene3DLightsLike> | null): string[] {
|
|
761
|
+
return source === null ? [] : getScene3DLights(source).map((light) => light.kind);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function getScene3DLights(source: Readonly<Scene3DLightsLike>): Readonly<Light>[] {
|
|
765
|
+
const lights: Readonly<Light>[] = [];
|
|
766
|
+
for (const value of Object.values(source)) {
|
|
767
|
+
if (value === null) continue;
|
|
768
|
+
if ('kind' in value) lights.push(value);
|
|
769
|
+
else lights.push(...value);
|
|
770
|
+
}
|
|
771
|
+
return lights;
|
|
772
|
+
}
|