@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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createKeyedTable, withRegistryTableEntry } from '@flighthq/registry/contract';
|
|
2
|
+
import type {
|
|
3
|
+
FlightDocumentFields,
|
|
4
|
+
FlightDocumentNode,
|
|
5
|
+
FlightDocumentNodeSchema,
|
|
6
|
+
FlightDocumentResourceLookup,
|
|
7
|
+
FlightDocumentSchemaRegistry,
|
|
8
|
+
NodeAny,
|
|
9
|
+
} from '@flighthq/types/contract';
|
|
10
|
+
import { FlightDocumentRefusalReason } from '@flighthq/types/contract';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
checkUnregisteredNodeKinds,
|
|
15
|
+
checkUnregisteredNodeKindsFromRaw,
|
|
16
|
+
createDocumentRefusal,
|
|
17
|
+
createSceneRefusal,
|
|
18
|
+
} from './sceneDocumentRefusal';
|
|
19
|
+
|
|
20
|
+
describe('checkUnregisteredNodeKinds', () => {
|
|
21
|
+
it('returns null when all kinds are registered', () => {
|
|
22
|
+
const schemas = createTestSchemas('TestKind');
|
|
23
|
+
const node: FlightDocumentNode = { children: [], fields: {}, kind: 'TestKind' };
|
|
24
|
+
expect(checkUnregisteredNodeKinds(node, schemas, 0, 'scene')).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns refusal naming the unregistered kind', () => {
|
|
28
|
+
const schemas = createTestSchemas('TestKind');
|
|
29
|
+
const node: FlightDocumentNode = { children: [], fields: {}, kind: 'acme.Missing' };
|
|
30
|
+
const refusal = checkUnregisteredNodeKinds(node, schemas, 0, 'scene');
|
|
31
|
+
expect(refusal).not.toBeNull();
|
|
32
|
+
expect(refusal!.reason).toBe(FlightDocumentRefusalReason.NodeKindUnregistered);
|
|
33
|
+
expect(refusal!.kind).toBe('acme.Missing');
|
|
34
|
+
expect(refusal!.path).toBe('scenes[0].scene');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('walks children and qualifies nested path', () => {
|
|
38
|
+
const schemas = createTestSchemas('TestKind');
|
|
39
|
+
const node: FlightDocumentNode = {
|
|
40
|
+
children: [
|
|
41
|
+
{
|
|
42
|
+
children: [{ children: [], fields: {}, kind: 'acme.Deep' }],
|
|
43
|
+
fields: {},
|
|
44
|
+
kind: 'TestKind',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
fields: {},
|
|
48
|
+
kind: 'TestKind',
|
|
49
|
+
};
|
|
50
|
+
const refusal = checkUnregisteredNodeKinds(node, schemas, 0, 'scene');
|
|
51
|
+
expect(refusal).not.toBeNull();
|
|
52
|
+
expect(refusal!.path).toBe('scenes[0].scene.children[0].children[0]');
|
|
53
|
+
expect(refusal!.kind).toBe('acme.Deep');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('checkUnregisteredNodeKindsFromRaw', () => {
|
|
58
|
+
it('returns null for non-object value', () => {
|
|
59
|
+
const schemas = createTestSchemas('TestKind');
|
|
60
|
+
expect(checkUnregisteredNodeKindsFromRaw(null, schemas, 0, 'scene')).toBeNull();
|
|
61
|
+
expect(checkUnregisteredNodeKindsFromRaw(42, schemas, 0, 'scene')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns refusal for unregistered kind in raw object', () => {
|
|
65
|
+
const schemas = createTestSchemas('TestKind');
|
|
66
|
+
const raw = { children: [], kind: 'acme.Unknown' };
|
|
67
|
+
const refusal = checkUnregisteredNodeKindsFromRaw(raw, schemas, 0, 'scene');
|
|
68
|
+
expect(refusal).not.toBeNull();
|
|
69
|
+
expect(refusal!.reason).toBe(FlightDocumentRefusalReason.NodeKindUnregistered);
|
|
70
|
+
expect(refusal!.kind).toBe('acme.Unknown');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('walks raw children recursively', () => {
|
|
74
|
+
const schemas = createTestSchemas('TestKind');
|
|
75
|
+
const raw = {
|
|
76
|
+
children: [{ children: [], kind: 'acme.Nested' }],
|
|
77
|
+
kind: 'TestKind',
|
|
78
|
+
};
|
|
79
|
+
const refusal = checkUnregisteredNodeKindsFromRaw(raw, schemas, 0, 'scene');
|
|
80
|
+
expect(refusal).not.toBeNull();
|
|
81
|
+
expect(refusal!.path).toBe('scenes[0].scene.children[0]');
|
|
82
|
+
expect(refusal!.kind).toBe('acme.Nested');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('createDocumentRefusal', () => {
|
|
87
|
+
it('produces a refusal with the given path', () => {
|
|
88
|
+
const refusal = createDocumentRefusal(FlightDocumentRefusalReason.VersionUnsupported, 'version');
|
|
89
|
+
expect(refusal.reason).toBe(FlightDocumentRefusalReason.VersionUnsupported);
|
|
90
|
+
expect(refusal.path).toBe('version');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('nulls all optional fields', () => {
|
|
94
|
+
const refusal = createDocumentRefusal(FlightDocumentRefusalReason.StructureInvalid, '');
|
|
95
|
+
expect(refusal.actual).toBeNull();
|
|
96
|
+
expect(refusal.column).toBeNull();
|
|
97
|
+
expect(refusal.kind).toBeNull();
|
|
98
|
+
expect(refusal.limit).toBeNull();
|
|
99
|
+
expect(refusal.line).toBeNull();
|
|
100
|
+
expect(refusal.offset).toBeNull();
|
|
101
|
+
expect(refusal.resourceKey).toBeNull();
|
|
102
|
+
expect(refusal.version).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('createSceneRefusal', () => {
|
|
107
|
+
it('qualifies path with scene index', () => {
|
|
108
|
+
const refusal = createSceneRefusal(FlightDocumentRefusalReason.DuplicateAmbientLight, 0, 'lights');
|
|
109
|
+
expect(refusal.path).toBe('scenes[0].lights');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('qualifies with non-zero scene index', () => {
|
|
113
|
+
const refusal = createSceneRefusal(FlightDocumentRefusalReason.DuplicateDirectionalLight, 2, 'lights');
|
|
114
|
+
expect(refusal.path).toBe('scenes[2].lights');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('omits trailing dot for empty inner path', () => {
|
|
118
|
+
const refusal = createSceneRefusal(FlightDocumentRefusalReason.StructureInvalid, 1, '');
|
|
119
|
+
expect(refusal.path).toBe('scenes[1]');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('carries the reason through', () => {
|
|
123
|
+
const refusal = createSceneRefusal(FlightDocumentRefusalReason.StructureInvalid, 0, 'kind');
|
|
124
|
+
expect(refusal.reason).toBe(FlightDocumentRefusalReason.StructureInvalid);
|
|
125
|
+
expect(refusal.path).toBe('scenes[0].kind');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
function createTestSchemas(registeredKind: string): FlightDocumentSchemaRegistry {
|
|
130
|
+
const schema: FlightDocumentNodeSchema = {
|
|
131
|
+
createNode: (_fields: Readonly<FlightDocumentFields>, _resources: FlightDocumentResourceLookup) => null,
|
|
132
|
+
fields: [],
|
|
133
|
+
kind: registeredKind,
|
|
134
|
+
writeNodeFields: (_out: FlightDocumentFields, _source: Readonly<NodeAny>) => true,
|
|
135
|
+
};
|
|
136
|
+
let nodeSchemas = createKeyedTable<FlightDocumentNodeSchema>('flight-document.node', 'none');
|
|
137
|
+
nodeSchemas = withRegistryTableEntry(nodeSchemas, registeredKind, schema);
|
|
138
|
+
return {
|
|
139
|
+
nodeSchemas,
|
|
140
|
+
resourceSchemas: createKeyedTable('flight-document.resource', 'none'),
|
|
141
|
+
shapeCommandSchemas: createKeyedTable('flight-document.shape-command', 'none'),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { addNodeChild, getNodeChildren } from '@flighthq/node/contract';
|
|
2
2
|
import { createKeyedTable, withRegistryTableEntry } from '@flighthq/registry/contract';
|
|
3
3
|
import { createDisplayObject, createScene2D, createSprite } from '@flighthq/scene2d/contract';
|
|
4
|
-
import {
|
|
4
|
+
import { createNode3D } from '@flighthq/scene3d/contract';
|
|
5
|
+
import { DisplayObjectKind, FlightDocumentRefusalReason, Node3DKind, SpriteKind } from '@flighthq/types/contract';
|
|
5
6
|
import type {
|
|
6
7
|
FlightDocument,
|
|
7
8
|
FlightDocumentFields,
|
|
8
9
|
FlightDocumentNodeSchema,
|
|
10
|
+
FlightDocumentResourceDescriptor,
|
|
9
11
|
FlightDocumentResourceLookup,
|
|
10
|
-
|
|
12
|
+
FlightDocumentScene,
|
|
11
13
|
FlightDocumentScene2DMaterialization,
|
|
12
14
|
FlightDocumentSchemaRegistry,
|
|
13
15
|
Node2D,
|
|
@@ -15,13 +17,13 @@ import type {
|
|
|
15
17
|
} from '@flighthq/types/contract';
|
|
16
18
|
import { describe, expect, it } from 'vitest';
|
|
17
19
|
|
|
20
|
+
import { formatFlightDocumentText } from './flightDocumentText';
|
|
18
21
|
import {
|
|
19
22
|
createFlightDocumentFromScene2D,
|
|
20
23
|
createFlightDocumentScene2DMaterialization,
|
|
21
24
|
createFlightDocumentScene2DMaterializationFromText,
|
|
22
25
|
explainFlightDocumentRefusal,
|
|
23
26
|
explainFlightDocumentRefusalFromText,
|
|
24
|
-
serializeFlightDocument,
|
|
25
27
|
} from './sceneDocumentScene2DMaterialization';
|
|
26
28
|
|
|
27
29
|
describe('createFlightDocumentFromScene2D', () => {
|
|
@@ -29,7 +31,6 @@ describe('createFlightDocumentFromScene2D', () => {
|
|
|
29
31
|
const scene = createScene2D();
|
|
30
32
|
const document = createFlightDocumentFromScene2D(scene, createTestSchemas());
|
|
31
33
|
expect(document.kind).toBe('Scene2D');
|
|
32
|
-
expect(document.version).toBe(1);
|
|
33
34
|
expect(document.scene.children).toHaveLength(0);
|
|
34
35
|
});
|
|
35
36
|
|
|
@@ -81,13 +82,11 @@ describe('createFlightDocumentFromScene2D', () => {
|
|
|
81
82
|
|
|
82
83
|
describe('createFlightDocumentScene2DMaterialization', () => {
|
|
83
84
|
it('materializes an empty Scene2D from a minimal model', () => {
|
|
84
|
-
const document
|
|
85
|
+
const document = createTestDocument({
|
|
85
86
|
backgroundColor: null,
|
|
86
87
|
kind: 'Scene2D',
|
|
87
|
-
resources: [],
|
|
88
88
|
scene: { children: [], fields: {}, kind: DisplayObjectKind },
|
|
89
|
-
|
|
90
|
-
};
|
|
89
|
+
});
|
|
91
90
|
const result = createFlightDocumentScene2DMaterialization(document, createTestSchemas());
|
|
92
91
|
expect(result).not.toBeNull();
|
|
93
92
|
const materialization = result as FlightDocumentScene2DMaterialization;
|
|
@@ -96,10 +95,9 @@ describe('createFlightDocumentScene2DMaterialization', () => {
|
|
|
96
95
|
});
|
|
97
96
|
|
|
98
97
|
it('materializes a tree with nested children', () => {
|
|
99
|
-
const document
|
|
98
|
+
const document = createTestDocument({
|
|
100
99
|
backgroundColor: null,
|
|
101
100
|
kind: 'Scene2D',
|
|
102
|
-
resources: [],
|
|
103
101
|
scene: {
|
|
104
102
|
children: [
|
|
105
103
|
{
|
|
@@ -111,8 +109,7 @@ describe('createFlightDocumentScene2DMaterialization', () => {
|
|
|
111
109
|
fields: {},
|
|
112
110
|
kind: DisplayObjectKind,
|
|
113
111
|
},
|
|
114
|
-
|
|
115
|
-
};
|
|
112
|
+
});
|
|
116
113
|
const schemas = createTestSchemas();
|
|
117
114
|
const result = createFlightDocumentScene2DMaterialization(document, schemas);
|
|
118
115
|
expect(result).not.toBeNull();
|
|
@@ -126,26 +123,25 @@ describe('createFlightDocumentScene2DMaterialization', () => {
|
|
|
126
123
|
});
|
|
127
124
|
|
|
128
125
|
it('returns null for a Scene3D document', () => {
|
|
129
|
-
const document = {
|
|
126
|
+
const document = createTestDocument({
|
|
130
127
|
cameras: [],
|
|
131
128
|
kind: 'Scene3D',
|
|
132
129
|
lights: [],
|
|
133
|
-
resources: [],
|
|
134
130
|
scene: { children: [], fields: {}, kind: 'Node3D' },
|
|
135
|
-
|
|
136
|
-
} as FlightDocument;
|
|
131
|
+
});
|
|
137
132
|
const result = createFlightDocumentScene2DMaterialization(document, createTestSchemas());
|
|
138
133
|
expect(result).toBeNull();
|
|
139
134
|
});
|
|
140
135
|
|
|
141
136
|
it('returns null for an unsupported version', () => {
|
|
142
137
|
const document = {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
...createTestDocument({
|
|
139
|
+
backgroundColor: null,
|
|
140
|
+
kind: 'Scene2D',
|
|
141
|
+
scene: { children: [], fields: {}, kind: DisplayObjectKind },
|
|
142
|
+
}),
|
|
147
143
|
version: 99,
|
|
148
|
-
} as unknown as
|
|
144
|
+
} as unknown as FlightDocument;
|
|
149
145
|
const result = createFlightDocumentScene2DMaterialization(document, createTestSchemas());
|
|
150
146
|
expect(result).toBeNull();
|
|
151
147
|
});
|
|
@@ -155,12 +151,14 @@ describe('createFlightDocumentScene2DMaterializationFromText', () => {
|
|
|
155
151
|
it('materializes a Scene2D from YAML text', () => {
|
|
156
152
|
const yaml = [
|
|
157
153
|
'flight: 1',
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
' kind:
|
|
161
|
-
'
|
|
162
|
-
'
|
|
163
|
-
'
|
|
154
|
+
'defaultScene: 0',
|
|
155
|
+
'scenes:',
|
|
156
|
+
' - kind: Scene2D',
|
|
157
|
+
' scene:',
|
|
158
|
+
' kind: DisplayObject',
|
|
159
|
+
' children:',
|
|
160
|
+
' - kind: Sprite',
|
|
161
|
+
' name: bg',
|
|
164
162
|
].join('\n');
|
|
165
163
|
const schemas = createTestSchemas();
|
|
166
164
|
const result = createFlightDocumentScene2DMaterializationFromText(yaml, schemas);
|
|
@@ -170,7 +168,14 @@ describe('createFlightDocumentScene2DMaterializationFromText', () => {
|
|
|
170
168
|
});
|
|
171
169
|
|
|
172
170
|
it('returns null for a Scene3D YAML document', () => {
|
|
173
|
-
const yaml = [
|
|
171
|
+
const yaml = [
|
|
172
|
+
'flight: 1',
|
|
173
|
+
'defaultScene: 0',
|
|
174
|
+
'scenes:',
|
|
175
|
+
' - kind: Scene3D',
|
|
176
|
+
' scene:',
|
|
177
|
+
' kind: Node3D',
|
|
178
|
+
].join('\n');
|
|
174
179
|
const result = createFlightDocumentScene2DMaterializationFromText(yaml, createTestSchemas());
|
|
175
180
|
expect(result).toBeNull();
|
|
176
181
|
});
|
|
@@ -184,43 +189,40 @@ describe('createFlightDocumentScene2DMaterializationFromText', () => {
|
|
|
184
189
|
|
|
185
190
|
describe('explainFlightDocumentRefusal', () => {
|
|
186
191
|
it('explains a dimension mismatch', () => {
|
|
187
|
-
const document = {
|
|
192
|
+
const document = createTestDocument({
|
|
188
193
|
cameras: [],
|
|
189
194
|
kind: 'Scene3D',
|
|
190
195
|
lights: [],
|
|
191
|
-
resources: [],
|
|
192
196
|
scene: { children: [], fields: {}, kind: 'Node3D' },
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const explanation = explainFlightDocumentRefusal(document, 'Scene2D');
|
|
197
|
+
});
|
|
198
|
+
const explanation = explainFlightDocumentRefusal(document, 'Scene2D', createTestSchemas());
|
|
196
199
|
expect(explanation).not.toBeNull();
|
|
197
200
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.StructureInvalid);
|
|
198
|
-
expect(explanation!.path).toBe('kind');
|
|
201
|
+
expect(explanation!.path).toBe('scenes[0].kind');
|
|
199
202
|
});
|
|
200
203
|
|
|
201
204
|
it('explains an unsupported version', () => {
|
|
202
205
|
const document = {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
...createTestDocument({
|
|
207
|
+
backgroundColor: null,
|
|
208
|
+
kind: 'Scene2D',
|
|
209
|
+
scene: { children: [], fields: {}, kind: DisplayObjectKind },
|
|
210
|
+
}),
|
|
207
211
|
version: 99,
|
|
208
|
-
} as unknown as
|
|
209
|
-
const explanation = explainFlightDocumentRefusal(document, 'Scene2D');
|
|
212
|
+
} as unknown as FlightDocument;
|
|
213
|
+
const explanation = explainFlightDocumentRefusal(document, 'Scene2D', createTestSchemas());
|
|
210
214
|
expect(explanation).not.toBeNull();
|
|
211
215
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.VersionUnsupported);
|
|
212
216
|
expect(explanation!.version).toBe(99);
|
|
213
217
|
});
|
|
214
218
|
|
|
215
219
|
it('returns null when the document is valid', () => {
|
|
216
|
-
const document
|
|
220
|
+
const document = createTestDocument({
|
|
217
221
|
backgroundColor: null,
|
|
218
222
|
kind: 'Scene2D',
|
|
219
|
-
resources: [],
|
|
220
223
|
scene: { children: [], fields: {}, kind: DisplayObjectKind },
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const explanation = explainFlightDocumentRefusal(document, 'Scene2D');
|
|
224
|
+
});
|
|
225
|
+
const explanation = explainFlightDocumentRefusal(document, 'Scene2D', createTestSchemas());
|
|
224
226
|
expect(explanation).toBeNull();
|
|
225
227
|
});
|
|
226
228
|
});
|
|
@@ -228,7 +230,7 @@ describe('explainFlightDocumentRefusal', () => {
|
|
|
228
230
|
describe('explainFlightDocumentRefusalFromText', () => {
|
|
229
231
|
it('explains an anchor refusal with parser position', () => {
|
|
230
232
|
const yaml = 'flight: 1\nkind: Scene2D\nanchor: &ref value\n';
|
|
231
|
-
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D');
|
|
233
|
+
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D', createTestSchemas());
|
|
232
234
|
expect(explanation).not.toBeNull();
|
|
233
235
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.AnchorUnsupported);
|
|
234
236
|
expect(explanation!.line).toBeGreaterThan(0);
|
|
@@ -238,21 +240,21 @@ describe('explainFlightDocumentRefusalFromText', () => {
|
|
|
238
240
|
|
|
239
241
|
it('explains an alias refusal', () => {
|
|
240
242
|
const yaml = 'flight: 1\nkind: Scene2D\nref: *alias\n';
|
|
241
|
-
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D');
|
|
243
|
+
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D', createTestSchemas());
|
|
242
244
|
expect(explanation).not.toBeNull();
|
|
243
245
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.AliasUnsupported);
|
|
244
246
|
});
|
|
245
247
|
|
|
246
248
|
it('explains a tag refusal', () => {
|
|
247
249
|
const yaml = 'flight: 1\nkind: Scene2D\ntyped: !custom value\n';
|
|
248
|
-
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D');
|
|
250
|
+
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D', createTestSchemas());
|
|
249
251
|
expect(explanation).not.toBeNull();
|
|
250
252
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.TagUnsupported);
|
|
251
253
|
});
|
|
252
254
|
|
|
253
255
|
it('explains a document separator refusal', () => {
|
|
254
256
|
const yaml = 'flight: 1\n---\nkind: Scene2D\n';
|
|
255
|
-
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D');
|
|
257
|
+
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D', createTestSchemas());
|
|
256
258
|
expect(explanation).not.toBeNull();
|
|
257
259
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.DocumentSeparatorUnsupported);
|
|
258
260
|
});
|
|
@@ -260,7 +262,7 @@ describe('explainFlightDocumentRefusalFromText', () => {
|
|
|
260
262
|
it('explains a key limit refusal with limit and actual', () => {
|
|
261
263
|
const longKey = 'k'.repeat(300);
|
|
262
264
|
const yaml = 'flight: 1\nkind: Scene2D\n' + longKey + ': value\n';
|
|
263
|
-
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D');
|
|
265
|
+
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D', createTestSchemas());
|
|
264
266
|
expect(explanation).not.toBeNull();
|
|
265
267
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.KeyCodeUnitsLimitExceeded);
|
|
266
268
|
expect(explanation!.limit).toBe(256);
|
|
@@ -271,14 +273,14 @@ describe('explainFlightDocumentRefusalFromText', () => {
|
|
|
271
273
|
});
|
|
272
274
|
|
|
273
275
|
it('returns null for valid Scene2D text', () => {
|
|
274
|
-
const yaml = 'flight: 1\
|
|
275
|
-
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D');
|
|
276
|
+
const yaml = 'flight: 1\ndefaultScene: 0\nscenes:\n - kind: Scene2D\n scene:\n kind: DisplayObject\n';
|
|
277
|
+
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D', createTestSchemas());
|
|
276
278
|
expect(explanation).toBeNull();
|
|
277
279
|
});
|
|
278
280
|
|
|
279
281
|
it('explains an unsupported version in valid YAML', () => {
|
|
280
|
-
const yaml = 'flight: 99\
|
|
281
|
-
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D');
|
|
282
|
+
const yaml = 'flight: 99\ndefaultScene: 0\nscenes:\n - kind: Scene2D\n scene:\n kind: DisplayObject\n';
|
|
283
|
+
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D', createTestSchemas());
|
|
282
284
|
expect(explanation).not.toBeNull();
|
|
283
285
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.VersionUnsupported);
|
|
284
286
|
expect(explanation!.path).toBe('version');
|
|
@@ -286,20 +288,20 @@ describe('explainFlightDocumentRefusalFromText', () => {
|
|
|
286
288
|
});
|
|
287
289
|
|
|
288
290
|
it('explains a dimension mismatch in valid YAML', () => {
|
|
289
|
-
const yaml = 'flight: 1\
|
|
290
|
-
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D');
|
|
291
|
+
const yaml = 'flight: 1\ndefaultScene: 0\nscenes:\n - kind: Scene3D\n scene:\n kind: Node3D\n';
|
|
292
|
+
const explanation = explainFlightDocumentRefusalFromText(yaml, 'Scene2D', createTestSchemas());
|
|
291
293
|
expect(explanation).not.toBeNull();
|
|
292
294
|
expect(explanation!.reason).toBe(FlightDocumentRefusalReason.StructureInvalid);
|
|
293
|
-
expect(explanation!.path).toBe('kind');
|
|
295
|
+
expect(explanation!.path).toBe('scenes[0].kind');
|
|
294
296
|
});
|
|
295
297
|
});
|
|
296
298
|
|
|
297
|
-
describe('
|
|
299
|
+
describe('formatFlightDocumentText', () => {
|
|
298
300
|
it('round-trips a minimal Scene2D through model and text', () => {
|
|
299
301
|
const scene = createScene2D();
|
|
300
302
|
const schemas = createTestSchemas();
|
|
301
|
-
const document = createFlightDocumentFromScene2D(scene, schemas);
|
|
302
|
-
const text =
|
|
303
|
+
const document = createTestDocument(createFlightDocumentFromScene2D(scene, schemas));
|
|
304
|
+
const text = formatFlightDocumentText(document);
|
|
303
305
|
expect(typeof text).toBe('string');
|
|
304
306
|
expect(text).toContain('flight: 1');
|
|
305
307
|
expect(text).toContain('kind: Scene2D');
|
|
@@ -312,13 +314,65 @@ describe('serializeFlightDocument', () => {
|
|
|
312
314
|
sprite.y = 100;
|
|
313
315
|
addNodeChild(scene.root, sprite);
|
|
314
316
|
const schemas = createTestSchemas();
|
|
315
|
-
const document = createFlightDocumentFromScene2D(scene, schemas);
|
|
316
|
-
const text =
|
|
317
|
+
const document = createTestDocument(createFlightDocumentFromScene2D(scene, schemas));
|
|
318
|
+
const text = formatFlightDocumentText(document);
|
|
317
319
|
const reparsed = createFlightDocumentScene2DMaterializationFromText(text, schemas);
|
|
318
320
|
expect(reparsed).not.toBeNull();
|
|
319
321
|
});
|
|
320
322
|
});
|
|
321
323
|
|
|
324
|
+
describe('model-to-text explain parity', () => {
|
|
325
|
+
it.each<{
|
|
326
|
+
document: Readonly<FlightDocument>;
|
|
327
|
+
expectedPath: string;
|
|
328
|
+
expectedReason: FlightDocumentRefusalReason;
|
|
329
|
+
label: string;
|
|
330
|
+
}>([
|
|
331
|
+
{
|
|
332
|
+
label: 'wrong dimension (scene 0)',
|
|
333
|
+
document: createTestDocument({
|
|
334
|
+
cameras: [],
|
|
335
|
+
kind: 'Scene3D',
|
|
336
|
+
lights: [],
|
|
337
|
+
scene: { children: [], fields: {}, kind: 'Node3D' },
|
|
338
|
+
}),
|
|
339
|
+
expectedReason: FlightDocumentRefusalReason.StructureInvalid,
|
|
340
|
+
expectedPath: 'scenes[0].kind',
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
label: 'unregistered node kind (scene 0)',
|
|
344
|
+
document: createTestDocument({
|
|
345
|
+
backgroundColor: null,
|
|
346
|
+
kind: 'Scene2D',
|
|
347
|
+
scene: {
|
|
348
|
+
children: [{ children: [], fields: {}, kind: 'acme.Unknown' }],
|
|
349
|
+
fields: {},
|
|
350
|
+
kind: DisplayObjectKind,
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
expectedReason: FlightDocumentRefusalReason.NodeKindUnregistered,
|
|
354
|
+
expectedPath: 'scenes[0].scene.children[0]',
|
|
355
|
+
},
|
|
356
|
+
])('model and text explain agree on $label', ({ document, expectedPath, expectedReason }) => {
|
|
357
|
+
const modelResult = explainFlightDocumentRefusal(document, 'Scene2D', createTestSchemas());
|
|
358
|
+
const text = formatFlightDocumentText(document);
|
|
359
|
+
const textResult = explainFlightDocumentRefusalFromText(text, 'Scene2D', createTestSchemas());
|
|
360
|
+
expect(modelResult).not.toBeNull();
|
|
361
|
+
expect(textResult).not.toBeNull();
|
|
362
|
+
expect(textResult!.reason).toBe(modelResult!.reason);
|
|
363
|
+
expect(textResult!.path).toBe(modelResult!.path);
|
|
364
|
+
expect(modelResult!.reason).toBe(expectedReason);
|
|
365
|
+
expect(modelResult!.path).toBe(expectedPath);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
function createTestDocument(
|
|
370
|
+
scene: FlightDocumentScene,
|
|
371
|
+
resources: FlightDocumentResourceDescriptor[] = [],
|
|
372
|
+
): FlightDocument {
|
|
373
|
+
return { defaultScene: 0, resources, scenes: [scene], version: 1 };
|
|
374
|
+
}
|
|
375
|
+
|
|
322
376
|
function createTestSchemas(): FlightDocumentSchemaRegistry {
|
|
323
377
|
const displayObjectSchema: FlightDocumentNodeSchema = {
|
|
324
378
|
createNode: (fields: Readonly<FlightDocumentFields>, _resources: FlightDocumentResourceLookup) => {
|
|
@@ -393,3 +447,75 @@ const TRANSFORM_FIELD_SCHEMAS = [
|
|
|
393
447
|
{ defaultValue: 0, name: 'rotation', required: false, validate: (v: unknown) => typeof v === 'number' },
|
|
394
448
|
{ defaultValue: 1, name: 'alpha', required: false, validate: (v: unknown) => typeof v === 'number' },
|
|
395
449
|
] as const;
|
|
450
|
+
|
|
451
|
+
// ★ ROOT-NODE FIDELITY. The writer captures the root's kind and fields (`writeNode(source.root, …)`), but
|
|
452
|
+
// the reader built a fresh container with `createScene2D()` and materialized only `scene.children` — so
|
|
453
|
+
// everything authored ON the root was silently dropped. Write preserved what read discarded, which makes
|
|
454
|
+
// the round trip lossy in a way no per-child test could see.
|
|
455
|
+
describe('Scene2D root fidelity', () => {
|
|
456
|
+
it('preserves the authored root kind', () => {
|
|
457
|
+
const document = rootDocument2D(SpriteKind, {});
|
|
458
|
+
const materialization = createFlightDocumentScene2DMaterialization(document, createTestSchemas());
|
|
459
|
+
expect(materialization).not.toBeNull();
|
|
460
|
+
expect(materialization!.scene.root.kind).toBe(SpriteKind);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('preserves fields authored on the root', () => {
|
|
464
|
+
const document = rootDocument2D(DisplayObjectKind, { x: 100, y: 200 });
|
|
465
|
+
const materialization = createFlightDocumentScene2DMaterialization(document, createTestSchemas());
|
|
466
|
+
expect(materialization).not.toBeNull();
|
|
467
|
+
const root = materialization!.scene.root as unknown as { x: number; y: number };
|
|
468
|
+
expect(root.x).toBe(100);
|
|
469
|
+
expect(root.y).toBe(200);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('round-trips a root with authored fields without losing them', () => {
|
|
473
|
+
const schemas = createTestSchemas();
|
|
474
|
+
const scene = createScene2D();
|
|
475
|
+
(scene.root as unknown as { x: number; y: number }).x = 7;
|
|
476
|
+
(scene.root as unknown as { x: number; y: number }).y = 9;
|
|
477
|
+
|
|
478
|
+
const document = createFlightDocumentFromScene2D(scene, schemas);
|
|
479
|
+
const wrapped = { defaultScene: 0, resources: [], scenes: [document], version: 1 } as unknown as FlightDocument;
|
|
480
|
+
const back = createFlightDocumentScene2DMaterialization(wrapped, schemas);
|
|
481
|
+
|
|
482
|
+
expect(back).not.toBeNull();
|
|
483
|
+
const root = back!.scene.root as unknown as { x: number; y: number };
|
|
484
|
+
expect(root.x).toBe(7);
|
|
485
|
+
expect(root.y).toBe(9);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ★ A REGISTERED kind of the WRONG DIMENSION is the interesting refusal: it is not an unregistered kind,
|
|
489
|
+
// so the unregistered-kind check passes it through, and it then builds a Node3D as a 2D scene root.
|
|
490
|
+
it('refuses a registered root whose kind belongs to the other dimension', () => {
|
|
491
|
+
// The kind must be REGISTERED for this to be the case under test — an unregistered kind is caught one
|
|
492
|
+
// check earlier, and that is a different refusal with a different remedy.
|
|
493
|
+
const schemas = createTestSchemas();
|
|
494
|
+
schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, Node3DKind, {
|
|
495
|
+
createNode: () => createNode3D() as unknown as NodeAny,
|
|
496
|
+
fields: [],
|
|
497
|
+
kind: Node3DKind,
|
|
498
|
+
writeNodeFields: () => true,
|
|
499
|
+
});
|
|
500
|
+
const document = rootDocument2D(Node3DKind, {});
|
|
501
|
+
expect(createFlightDocumentScene2DMaterialization(document, schemas)).toBeNull();
|
|
502
|
+
expect(explainFlightDocumentRefusal(document, 'Scene2D', schemas)).toMatchObject({
|
|
503
|
+
reason: FlightDocumentRefusalReason.RootKindMismatch,
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
function rootDocument2D(kind: string, fields: Record<string, unknown>): FlightDocument {
|
|
509
|
+
return {
|
|
510
|
+
defaultScene: 0,
|
|
511
|
+
resources: [],
|
|
512
|
+
scenes: [
|
|
513
|
+
{
|
|
514
|
+
backgroundColor: null,
|
|
515
|
+
kind: 'Scene2D',
|
|
516
|
+
scene: { children: [], fields, kind },
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
version: 1,
|
|
520
|
+
} as unknown as FlightDocument;
|
|
521
|
+
}
|