@flighthq/scene-document 0.5.1-next.903.f434bc2 → 0.5.1-next.905.5fbf787

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 CHANGED
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/scene-document`. The
13
13
  This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
14
14
 
15
15
  - [Flight project](https://github.com/flighthq/flight)
16
- - [Source for @flighthq/scene-document@0.5.1-next.903.f434bc2](https://github.com/flighthq/flight/tree/f434bc233778d75aeea61995df3a8cfaec459a85/packages/scene-document)
17
- - [License](https://github.com/flighthq/flight/blob/f434bc233778d75aeea61995df3a8cfaec459a85/LICENSE.md)
16
+ - [Source for @flighthq/scene-document@0.5.1-next.905.5fbf787](https://github.com/flighthq/flight/tree/5fbf7874064c384307542d68b4dcb6a895ff0dcf/packages/scene-document)
17
+ - [License](https://github.com/flighthq/flight/blob/5fbf7874064c384307542d68b4dcb6a895ff0dcf/LICENSE.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/scene-document",
3
- "version": "0.5.1-next.903.f434bc2",
3
+ "version": "0.5.1-next.905.5fbf787",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,15 +38,15 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/camera": "0.5.1-next.903.f434bc2",
42
- "@flighthq/entity": "0.5.1-next.903.f434bc2",
43
- "@flighthq/geometry": "0.5.1-next.903.f434bc2",
44
- "@flighthq/lighting": "0.5.1-next.903.f434bc2",
45
- "@flighthq/node": "0.5.1-next.903.f434bc2",
46
- "@flighthq/registry": "0.5.1-next.903.f434bc2",
47
- "@flighthq/scene2d": "0.5.1-next.903.f434bc2",
48
- "@flighthq/scene3d": "0.5.1-next.903.f434bc2",
49
- "@flighthq/types": "0.5.1-next.903.f434bc2"
41
+ "@flighthq/camera": "0.5.1-next.905.5fbf787",
42
+ "@flighthq/entity": "0.5.1-next.905.5fbf787",
43
+ "@flighthq/geometry": "0.5.1-next.905.5fbf787",
44
+ "@flighthq/lighting": "0.5.1-next.905.5fbf787",
45
+ "@flighthq/node": "0.5.1-next.905.5fbf787",
46
+ "@flighthq/registry": "0.5.1-next.905.5fbf787",
47
+ "@flighthq/scene2d": "0.5.1-next.905.5fbf787",
48
+ "@flighthq/scene3d": "0.5.1-next.905.5fbf787",
49
+ "@flighthq/types": "0.5.1-next.905.5fbf787"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@flighthq/camera": "*",
@@ -3,7 +3,12 @@ import { createAmbientLight } from '@flighthq/lighting/contract';
3
3
  import type { FlightDocument } from '@flighthq/types/contract';
4
4
  import { FlightDocumentRefusalReason } from '@flighthq/types/contract';
5
5
 
6
- import { explainFlightDocumentText, formatFlightDocumentText, parseFlightDocumentText } from './flightDocumentText';
6
+ import {
7
+ explainFlightDocumentText,
8
+ formatFlightDocumentText,
9
+ initializeFlightDocumentRefusalExplanation,
10
+ parseFlightDocumentText,
11
+ } from './flightDocumentText';
7
12
  import { parseSceneDocumentYamlSubset } from './sceneDocumentYamlSubset';
8
13
 
9
14
  describe('explainFlightDocumentText', () => {
@@ -265,6 +270,11 @@ describe('formatFlightDocumentText', () => {
265
270
  });
266
271
  });
267
272
 
273
+ describe('initializeFlightDocumentRefusalExplanation', () => {
274
+ it('is the construction initializer of createFlightDocumentRefusalExplanation', () => {
275
+ expect(typeof initializeFlightDocumentRefusalExplanation).toBe('function');
276
+ });
277
+ });
268
278
  describe('parseFlightDocumentText', () => {
269
279
  it('parses mixed dimensions over one shared resource table', () => {
270
280
  const document = parseFlightDocumentText(
@@ -18,6 +18,7 @@ import {
18
18
  checkUnregisteredNodeKindsFromRaw,
19
19
  createDocumentRefusal,
20
20
  createSceneRefusal,
21
+ initializeDocumentRefusal,
21
22
  } from './sceneDocumentRefusal';
22
23
 
23
24
  describe('checkFlightDocumentFields', () => {
@@ -225,3 +226,8 @@ function createTestSchemas(registeredKind: string): FlightDocumentSchemaRegistry
225
226
  shapeCommandSchemas: createKeyedTable('flight-document.shape-command', 'none'),
226
227
  };
227
228
  }
229
+ describe('initializeDocumentRefusal', () => {
230
+ it('is the construction initializer of createDocumentRefusal', () => {
231
+ expect(typeof initializeDocumentRefusal).toBe('function');
232
+ });
233
+ });
@@ -31,6 +31,8 @@ import {
31
31
  createFlightDocumentScene2DMaterializationFromText,
32
32
  explainFlightDocumentRefusal,
33
33
  explainFlightDocumentRefusalFromText,
34
+ initializeFlightDocumentFromScene2D,
35
+ initializeFlightDocumentScene2DMaterialization,
34
36
  } from './sceneDocumentScene2DMaterialization';
35
37
 
36
38
  describe('createFlightDocumentFromScene2D', () => {
@@ -574,6 +576,109 @@ describe('formatFlightDocumentText', () => {
574
576
  });
575
577
  });
576
578
 
579
+ describe('initializeFlightDocumentFromScene2D', () => {
580
+ it('is the construction initializer of createFlightDocumentFromScene2D', () => {
581
+ expect(typeof initializeFlightDocumentFromScene2D).toBe('function');
582
+ });
583
+ });
584
+
585
+ describe('initializeFlightDocumentScene2DMaterialization', () => {
586
+ it('is the construction initializer of createFlightDocumentScene2DMaterialization', () => {
587
+ expect(typeof initializeFlightDocumentScene2DMaterialization).toBe('function');
588
+ });
589
+ });
590
+
591
+ function createTestDocument(
592
+ scene: FlightDocumentScene,
593
+ resources: FlightDocumentResourceDescriptor[] = [],
594
+ ): FlightDocument {
595
+ return { defaultScene: 0, resources, scenes: [scene], version: 1 };
596
+ }
597
+
598
+ function singleTargetLayout(target: string) {
599
+ return {
600
+ targets: [target],
601
+ tree: { nodes: [{ containerStyle: null, itemStyle: null, kind: 'acme.Leaf', parentIndex: -1 }] },
602
+ };
603
+ }
604
+
605
+ function createTestSchemas(): FlightDocumentSchemaRegistry {
606
+ const displayObjectSchema: FlightDocumentNodeSchema = {
607
+ createNode: (fields: Readonly<FlightDocumentFields>, _resources: FlightDocumentResourceLookup) => {
608
+ const obj = createDisplayObject();
609
+ applyTransformFields(obj, fields);
610
+ return obj;
611
+ },
612
+ fields: TRANSFORM_FIELD_SCHEMAS,
613
+ kind: DisplayObjectKind,
614
+ writeNodeFields: (out: FlightDocumentFields, source: Readonly<NodeAny>) => {
615
+ writeTransformFields(out, source as Node2D);
616
+ return true;
617
+ },
618
+ };
619
+
620
+ const spriteSchema: FlightDocumentNodeSchema = {
621
+ createNode: (fields: Readonly<FlightDocumentFields>, _resources: FlightDocumentResourceLookup) => {
622
+ const sprite = createSprite();
623
+ applyTransformFields(sprite, fields);
624
+ return sprite;
625
+ },
626
+ fields: TRANSFORM_FIELD_SCHEMAS,
627
+ kind: SpriteKind,
628
+ writeNodeFields: (out: FlightDocumentFields, source: Readonly<NodeAny>) => {
629
+ writeTransformFields(out, source as Node2D);
630
+ return true;
631
+ },
632
+ };
633
+
634
+ let nodeSchemas = createKeyedTable<FlightDocumentNodeSchema>('flight-document.node', 'none');
635
+ nodeSchemas = withRegistryTableEntry(nodeSchemas, DisplayObjectKind, displayObjectSchema);
636
+ nodeSchemas = withRegistryTableEntry(nodeSchemas, SpriteKind, spriteSchema);
637
+
638
+ return {
639
+ interactiveStateExtensionSchemas: createKeyedTable('flight-document.interactive-state-extension', 'none'),
640
+ interactiveStateTransitionSchemas: createKeyedTable('flight-document.interactive-state-transition', 'none'),
641
+ nodeSchemas,
642
+ resourceSchemas: createKeyedTable('flight-document.resource', 'none'),
643
+ shapeCommandSchemas: createKeyedTable('flight-document.shape-command', 'none'),
644
+ };
645
+ }
646
+
647
+ function applyTransformFields(target: Node2D, fields: Readonly<FlightDocumentFields>): void {
648
+ if (typeof fields['name'] === 'string') target.name = fields['name'];
649
+ if (typeof fields['x'] === 'number') target.x = fields['x'];
650
+ if (typeof fields['y'] === 'number') target.y = fields['y'];
651
+ if (typeof fields['scaleX'] === 'number') target.scaleX = fields['scaleX'];
652
+ if (typeof fields['scaleY'] === 'number') target.scaleY = fields['scaleY'];
653
+ if (typeof fields['rotation'] === 'number') target.rotation = fields['rotation'];
654
+ if (typeof fields['alpha'] === 'number') target.alpha = fields['alpha'];
655
+ }
656
+
657
+ function writeTransformFields(out: FlightDocumentFields, source: Readonly<Node2D>): void {
658
+ if (source.name !== null) out['name'] = source.name;
659
+ if (source.x !== 0) out['x'] = source.x;
660
+ if (source.y !== 0) out['y'] = source.y;
661
+ if (source.scaleX !== 1) out['scaleX'] = source.scaleX;
662
+ if (source.scaleY !== 1) out['scaleY'] = source.scaleY;
663
+ if (source.rotation !== 0) out['rotation'] = source.rotation;
664
+ if (source.alpha !== 1) out['alpha'] = source.alpha;
665
+ }
666
+
667
+ const TRANSFORM_FIELD_SCHEMAS = [
668
+ {
669
+ defaultValue: undefined,
670
+ name: 'name',
671
+ required: false,
672
+ validate: (v: unknown) => typeof v === 'string' || v === null,
673
+ },
674
+ { defaultValue: 0, name: 'x', required: false, validate: (v: unknown) => typeof v === 'number' },
675
+ { defaultValue: 0, name: 'y', required: false, validate: (v: unknown) => typeof v === 'number' },
676
+ { defaultValue: 1, name: 'scaleX', required: false, validate: (v: unknown) => typeof v === 'number' },
677
+ { defaultValue: 1, name: 'scaleY', required: false, validate: (v: unknown) => typeof v === 'number' },
678
+ { defaultValue: 0, name: 'rotation', required: false, validate: (v: unknown) => typeof v === 'number' },
679
+ { defaultValue: 1, name: 'alpha', required: false, validate: (v: unknown) => typeof v === 'number' },
680
+ ] as const;
681
+
577
682
  describe('model-to-text explain parity', () => {
578
683
  it.each<{
579
684
  document: Readonly<FlightDocument>;
@@ -623,6 +728,22 @@ describe('model-to-text explain parity', () => {
623
728
  });
624
729
  });
625
730
 
731
+ function rootDocument2D(kind: string, fields: Record<string, unknown>): FlightDocument {
732
+ return {
733
+ defaultScene: 0,
734
+ resources: [],
735
+ scenes: [
736
+ {
737
+ backgroundColor: null,
738
+ kind: 'Scene2D',
739
+ layouts: [],
740
+ scene: { children: [], fields, kind },
741
+ tokens: [],
742
+ },
743
+ ],
744
+ version: 1,
745
+ } as unknown as FlightDocument;
746
+ }
626
747
  describe('Scene2D interactive-state materialization', () => {
627
748
  it('returns inert bindings for the exact root and nested live nodes', () => {
628
749
  const document = rootDocument2D(DisplayObjectKind, { alpha: 0.5 });
@@ -714,97 +835,6 @@ describe('Scene2D interactive-state materialization', () => {
714
835
  });
715
836
  });
716
837
 
717
- function createTestDocument(
718
- scene: FlightDocumentScene,
719
- resources: FlightDocumentResourceDescriptor[] = [],
720
- ): FlightDocument {
721
- return { defaultScene: 0, resources, scenes: [scene], version: 1 };
722
- }
723
-
724
- function singleTargetLayout(target: string) {
725
- return {
726
- targets: [target],
727
- tree: { nodes: [{ containerStyle: null, itemStyle: null, kind: 'acme.Leaf', parentIndex: -1 }] },
728
- };
729
- }
730
-
731
- function createTestSchemas(): FlightDocumentSchemaRegistry {
732
- const displayObjectSchema: FlightDocumentNodeSchema = {
733
- createNode: (fields: Readonly<FlightDocumentFields>, _resources: FlightDocumentResourceLookup) => {
734
- const obj = createDisplayObject();
735
- applyTransformFields(obj, fields);
736
- return obj;
737
- },
738
- fields: TRANSFORM_FIELD_SCHEMAS,
739
- kind: DisplayObjectKind,
740
- writeNodeFields: (out: FlightDocumentFields, source: Readonly<NodeAny>) => {
741
- writeTransformFields(out, source as Node2D);
742
- return true;
743
- },
744
- };
745
-
746
- const spriteSchema: FlightDocumentNodeSchema = {
747
- createNode: (fields: Readonly<FlightDocumentFields>, _resources: FlightDocumentResourceLookup) => {
748
- const sprite = createSprite();
749
- applyTransformFields(sprite, fields);
750
- return sprite;
751
- },
752
- fields: TRANSFORM_FIELD_SCHEMAS,
753
- kind: SpriteKind,
754
- writeNodeFields: (out: FlightDocumentFields, source: Readonly<NodeAny>) => {
755
- writeTransformFields(out, source as Node2D);
756
- return true;
757
- },
758
- };
759
-
760
- let nodeSchemas = createKeyedTable<FlightDocumentNodeSchema>('flight-document.node', 'none');
761
- nodeSchemas = withRegistryTableEntry(nodeSchemas, DisplayObjectKind, displayObjectSchema);
762
- nodeSchemas = withRegistryTableEntry(nodeSchemas, SpriteKind, spriteSchema);
763
-
764
- return {
765
- interactiveStateExtensionSchemas: createKeyedTable('flight-document.interactive-state-extension', 'none'),
766
- interactiveStateTransitionSchemas: createKeyedTable('flight-document.interactive-state-transition', 'none'),
767
- nodeSchemas,
768
- resourceSchemas: createKeyedTable('flight-document.resource', 'none'),
769
- shapeCommandSchemas: createKeyedTable('flight-document.shape-command', 'none'),
770
- };
771
- }
772
-
773
- function applyTransformFields(target: Node2D, fields: Readonly<FlightDocumentFields>): void {
774
- if (typeof fields['name'] === 'string') target.name = fields['name'];
775
- if (typeof fields['x'] === 'number') target.x = fields['x'];
776
- if (typeof fields['y'] === 'number') target.y = fields['y'];
777
- if (typeof fields['scaleX'] === 'number') target.scaleX = fields['scaleX'];
778
- if (typeof fields['scaleY'] === 'number') target.scaleY = fields['scaleY'];
779
- if (typeof fields['rotation'] === 'number') target.rotation = fields['rotation'];
780
- if (typeof fields['alpha'] === 'number') target.alpha = fields['alpha'];
781
- }
782
-
783
- function writeTransformFields(out: FlightDocumentFields, source: Readonly<Node2D>): void {
784
- if (source.name !== null) out['name'] = source.name;
785
- if (source.x !== 0) out['x'] = source.x;
786
- if (source.y !== 0) out['y'] = source.y;
787
- if (source.scaleX !== 1) out['scaleX'] = source.scaleX;
788
- if (source.scaleY !== 1) out['scaleY'] = source.scaleY;
789
- if (source.rotation !== 0) out['rotation'] = source.rotation;
790
- if (source.alpha !== 1) out['alpha'] = source.alpha;
791
- }
792
-
793
- const TRANSFORM_FIELD_SCHEMAS = [
794
- {
795
- defaultValue: undefined,
796
- name: 'name',
797
- required: false,
798
- validate: (v: unknown) => typeof v === 'string' || v === null,
799
- },
800
- { defaultValue: 0, name: 'x', required: false, validate: (v: unknown) => typeof v === 'number' },
801
- { defaultValue: 0, name: 'y', required: false, validate: (v: unknown) => typeof v === 'number' },
802
- { defaultValue: 1, name: 'scaleX', required: false, validate: (v: unknown) => typeof v === 'number' },
803
- { defaultValue: 1, name: 'scaleY', required: false, validate: (v: unknown) => typeof v === 'number' },
804
- { defaultValue: 0, name: 'rotation', required: false, validate: (v: unknown) => typeof v === 'number' },
805
- { defaultValue: 1, name: 'alpha', required: false, validate: (v: unknown) => typeof v === 'number' },
806
- ] as const;
807
-
808
838
  // ★ ROOT-NODE FIDELITY. The writer captures the root's kind and fields (`writeNode(source.root, …)`), but
809
839
  // the reader built a fresh container with `createScene2D()` and materialized only `scene.children` — so
810
840
  // everything authored ON the root was silently dropped. Write preserved what read discarded, which makes
@@ -879,20 +909,3 @@ describe('Scene2D root fidelity', () => {
879
909
  expect(resourceKeys).toEqual([]);
880
910
  });
881
911
  });
882
-
883
- function rootDocument2D(kind: string, fields: Record<string, unknown>): FlightDocument {
884
- return {
885
- defaultScene: 0,
886
- resources: [],
887
- scenes: [
888
- {
889
- backgroundColor: null,
890
- kind: 'Scene2D',
891
- layouts: [],
892
- scene: { children: [], fields, kind },
893
- tokens: [],
894
- },
895
- ],
896
- version: 1,
897
- } as unknown as FlightDocument;
898
- }
@@ -43,6 +43,8 @@ import {
43
43
  createFlightDocumentScene3DMaterializationFromText,
44
44
  explainFlightDocumentScene3DRefusal,
45
45
  explainFlightDocumentScene3DRefusalFromText,
46
+ initializeFlightDocumentFromScene3D,
47
+ initializeFlightDocumentScene3DMaterialization,
46
48
  } from './sceneDocumentScene3DMaterialization';
47
49
 
48
50
  describe('createFlightDocumentFromScene3D', () => {
@@ -548,6 +550,18 @@ describe('explainFlightDocumentScene3DRefusalFromText', () => {
548
550
  });
549
551
  });
550
552
 
553
+ describe('initializeFlightDocumentFromScene3D', () => {
554
+ it('is the construction initializer of createFlightDocumentFromScene3D', () => {
555
+ expect(typeof initializeFlightDocumentFromScene3D).toBe('function');
556
+ });
557
+ });
558
+
559
+ describe('initializeFlightDocumentScene3DMaterialization', () => {
560
+ it('is the construction initializer of createFlightDocumentScene3DMaterialization', () => {
561
+ expect(typeof initializeFlightDocumentScene3DMaterialization).toBe('function');
562
+ });
563
+ });
564
+
551
565
  describe('model-to-text explain parity', () => {
552
566
  // Keep the table annotated so every fixture is checked against the container schema rather than inferred
553
567
  // only from its own literal.
@@ -763,68 +777,6 @@ describe('Scene3D layout materialization', () => {
763
777
  });
764
778
  });
765
779
 
766
- // ★ ROOT-NODE FIDELITY, 3D half. Same defect as 2D: the reader built a fresh container and materialized
767
- // only the authored root's children, so the root's own kind and fields were dropped while the writer
768
- // captured them.
769
- describe('Scene3D root fidelity', () => {
770
- it('preserves the authored root kind', () => {
771
- const schemas = createTestSchemas();
772
- const document = rootDocument3D(Node3DKind, {}, schemas);
773
- const materialization = createFlightDocumentScene3DMaterialization(document, schemas);
774
- expect(materialization).not.toBeNull();
775
- expect(materialization!.scene.root.kind).toBe(Node3DKind);
776
- });
777
-
778
- it('preserves fields authored on the root', () => {
779
- const schemas = createTestSchemas();
780
- const document = rootDocument3D(Node3DKind, { name: 'authored-root' }, schemas);
781
- const materialization = createFlightDocumentScene3DMaterialization(document, schemas);
782
- expect(materialization).not.toBeNull();
783
- expect(materialization!.scene.root.name).toBe('authored-root');
784
- });
785
-
786
- it('refuses a registered root whose kind belongs to the other dimension', () => {
787
- const schemas = createTestSchemas();
788
- schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, DisplayObjectKind, {
789
- createNode: () => createDisplayObject() as unknown as NodeAny,
790
- fields: [],
791
- kind: DisplayObjectKind,
792
- writeNodeFields: () => true,
793
- });
794
- const document = rootDocument3D(DisplayObjectKind, {}, schemas);
795
- expect(createFlightDocumentScene3DMaterialization(document, schemas)).toBeNull();
796
- expect(explainFlightDocumentScene3DRefusal(document, schemas)).toMatchObject({
797
- reason: FlightDocumentRefusalReason.RootKindMismatch,
798
- });
799
- });
800
-
801
- it('passes a genuinely empty resource map to a root-dimension probe', () => {
802
- const schemas = createTestSchemas();
803
- let resourceKeys: string[] | null = null;
804
- schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, DisplayObjectKind, {
805
- createNode: (_fields, resources) => {
806
- resourceKeys = Object.keys(resources);
807
- return createDisplayObject() as unknown as NodeAny;
808
- },
809
- fields: [],
810
- kind: DisplayObjectKind,
811
- writeNodeFields: () => true,
812
- });
813
-
814
- explainFlightDocumentScene3DRefusal(rootDocument3D(DisplayObjectKind, {}, schemas), schemas);
815
-
816
- expect(resourceKeys).toEqual([]);
817
- });
818
- });
819
-
820
- describe('Scene3DDocumentCamera', () => {
821
- it('has no direction field', () => {
822
- expectTypeOf<keyof Scene3DDocumentCamera>().toEqualTypeOf<
823
- 'far' | 'name' | 'near' | 'node' | 'projection' | 'transform'
824
- >();
825
- });
826
- });
827
-
828
780
  function createTestDocument(
829
781
  scene: FlightDocumentScene,
830
782
  resources: FlightDocumentResourceDescriptor[] = [],
@@ -876,9 +828,57 @@ function createTestSchemas(): FlightDocumentSchemaRegistry {
876
828
  };
877
829
  }
878
830
 
879
- describe('Scene3DDocumentLight', () => {
880
- it('has no direction field', () => {
881
- expectTypeOf<keyof Scene3DDocumentLight>().toEqualTypeOf<'descriptor' | 'name' | 'node' | 'transform'>();
831
+ // ★ ROOT-NODE FIDELITY, 3D half. Same defect as 2D: the reader built a fresh container and materialized
832
+ // only the authored root's children, so the root's own kind and fields were dropped while the writer
833
+ // captured them.
834
+ describe('Scene3D root fidelity', () => {
835
+ it('preserves the authored root kind', () => {
836
+ const schemas = createTestSchemas();
837
+ const document = rootDocument3D(Node3DKind, {}, schemas);
838
+ const materialization = createFlightDocumentScene3DMaterialization(document, schemas);
839
+ expect(materialization).not.toBeNull();
840
+ expect(materialization!.scene.root.kind).toBe(Node3DKind);
841
+ });
842
+
843
+ it('preserves fields authored on the root', () => {
844
+ const schemas = createTestSchemas();
845
+ const document = rootDocument3D(Node3DKind, { name: 'authored-root' }, schemas);
846
+ const materialization = createFlightDocumentScene3DMaterialization(document, schemas);
847
+ expect(materialization).not.toBeNull();
848
+ expect(materialization!.scene.root.name).toBe('authored-root');
849
+ });
850
+
851
+ it('refuses a registered root whose kind belongs to the other dimension', () => {
852
+ const schemas = createTestSchemas();
853
+ schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, DisplayObjectKind, {
854
+ createNode: () => createDisplayObject() as unknown as NodeAny,
855
+ fields: [],
856
+ kind: DisplayObjectKind,
857
+ writeNodeFields: () => true,
858
+ });
859
+ const document = rootDocument3D(DisplayObjectKind, {}, schemas);
860
+ expect(createFlightDocumentScene3DMaterialization(document, schemas)).toBeNull();
861
+ expect(explainFlightDocumentScene3DRefusal(document, schemas)).toMatchObject({
862
+ reason: FlightDocumentRefusalReason.RootKindMismatch,
863
+ });
864
+ });
865
+
866
+ it('passes a genuinely empty resource map to a root-dimension probe', () => {
867
+ const schemas = createTestSchemas();
868
+ let resourceKeys: string[] | null = null;
869
+ schemas.nodeSchemas = withRegistryTableEntry(schemas.nodeSchemas, DisplayObjectKind, {
870
+ createNode: (_fields, resources) => {
871
+ resourceKeys = Object.keys(resources);
872
+ return createDisplayObject() as unknown as NodeAny;
873
+ },
874
+ fields: [],
875
+ kind: DisplayObjectKind,
876
+ writeNodeFields: () => true,
877
+ });
878
+
879
+ explainFlightDocumentScene3DRefusal(rootDocument3D(DisplayObjectKind, {}, schemas), schemas);
880
+
881
+ expect(resourceKeys).toEqual([]);
882
882
  });
883
883
  });
884
884
 
@@ -933,3 +933,16 @@ function getScene3DLights(source: Readonly<Scene3DLightsLike>): Readonly<Light>[
933
933
  }
934
934
  return lights;
935
935
  }
936
+ describe('Scene3DDocumentCamera', () => {
937
+ it('has no direction field', () => {
938
+ expectTypeOf<keyof Scene3DDocumentCamera>().toEqualTypeOf<
939
+ 'far' | 'name' | 'near' | 'node' | 'projection' | 'transform'
940
+ >();
941
+ });
942
+ });
943
+
944
+ describe('Scene3DDocumentLight', () => {
945
+ it('has no direction field', () => {
946
+ expectTypeOf<keyof Scene3DDocumentLight>().toEqualTypeOf<'descriptor' | 'name' | 'node' | 'transform'>();
947
+ });
948
+ });