@flighthq/scene3d-resources 0.3.0-next.906.07cea63 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/anisotropyPbrScene3DMaterialTextures.d.ts +3 -0
  2. package/dist/anisotropyPbrScene3DMaterialTextures.d.ts.map +1 -0
  3. package/dist/anisotropyPbrScene3DMaterialTextures.js +10 -0
  4. package/dist/anisotropyPbrScene3DMaterialTextures.js.map +1 -0
  5. package/dist/awd2Load.d.ts.map +1 -1
  6. package/dist/awd2Load.js +3 -2
  7. package/dist/awd2Load.js.map +1 -1
  8. package/dist/clearcoatPbrScene3DMaterialTextures.d.ts +3 -0
  9. package/dist/clearcoatPbrScene3DMaterialTextures.d.ts.map +1 -0
  10. package/dist/clearcoatPbrScene3DMaterialTextures.js +14 -0
  11. package/dist/clearcoatPbrScene3DMaterialTextures.js.map +1 -0
  12. package/dist/contract.d.ts +10 -1
  13. package/dist/contract.d.ts.map +1 -1
  14. package/dist/contract.js +10 -1
  15. package/dist/contract.js.map +1 -1
  16. package/dist/enableScene3DResourceFailureGuards.d.ts.map +1 -1
  17. package/dist/enableScene3DResourceFailureGuards.js +2 -2
  18. package/dist/enableScene3DResourceFailureGuards.js.map +1 -1
  19. package/dist/explainScene3DResourceCoverage.d.ts +4 -0
  20. package/dist/explainScene3DResourceCoverage.d.ts.map +1 -0
  21. package/dist/explainScene3DResourceCoverage.js +42 -0
  22. package/dist/explainScene3DResourceCoverage.js.map +1 -0
  23. package/dist/getScene3DResourceTextures.d.ts +3 -2
  24. package/dist/getScene3DResourceTextures.d.ts.map +1 -1
  25. package/dist/getScene3DResourceTextures.js +23 -27
  26. package/dist/getScene3DResourceTextures.js.map +1 -1
  27. package/dist/imageResourceFetch.d.ts +2 -2
  28. package/dist/imageResourceFetch.d.ts.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/iridescencePbrScene3DMaterialTextures.d.ts +3 -0
  34. package/dist/iridescencePbrScene3DMaterialTextures.d.ts.map +1 -0
  35. package/dist/iridescencePbrScene3DMaterialTextures.js +12 -0
  36. package/dist/iridescencePbrScene3DMaterialTextures.js.map +1 -0
  37. package/dist/loadScene3DResources.d.ts +4 -0
  38. package/dist/loadScene3DResources.d.ts.map +1 -0
  39. package/dist/{resolveScene3DResourcesAndWait.js → loadScene3DResources.js} +11 -21
  40. package/dist/loadScene3DResources.js.map +1 -0
  41. package/dist/resolveScene3DResources.d.ts +4 -3
  42. package/dist/resolveScene3DResources.d.ts.map +1 -1
  43. package/dist/resolveScene3DResources.js +49 -26
  44. package/dist/resolveScene3DResources.js.map +1 -1
  45. package/dist/revealScene3DResourcesOnResolve.d.ts +2 -2
  46. package/dist/revealScene3DResourcesOnResolve.d.ts.map +1 -1
  47. package/dist/revealScene3DResourcesOnResolve.js +9 -6
  48. package/dist/revealScene3DResourcesOnResolve.js.map +1 -1
  49. package/dist/sceneMaterialTextureRegistry.d.ts +6 -2
  50. package/dist/sceneMaterialTextureRegistry.d.ts.map +1 -1
  51. package/dist/sceneMaterialTextureRegistry.js +31 -8
  52. package/dist/sceneMaterialTextureRegistry.js.map +1 -1
  53. package/dist/sceneResourceRecovery.d.ts +2 -4
  54. package/dist/sceneResourceRecovery.d.ts.map +1 -1
  55. package/dist/sceneResourceRecovery.js +9 -28
  56. package/dist/sceneResourceRecovery.js.map +1 -1
  57. package/dist/sceneResourceResolver.d.ts.map +1 -1
  58. package/dist/sceneResourceResolver.js +6 -2
  59. package/dist/sceneResourceResolver.js.map +1 -1
  60. package/dist/shadedScene3DMaterialTextures.d.ts +3 -0
  61. package/dist/shadedScene3DMaterialTextures.d.ts.map +1 -0
  62. package/dist/shadedScene3DMaterialTextures.js +30 -0
  63. package/dist/shadedScene3DMaterialTextures.js.map +1 -0
  64. package/dist/sheenPbrScene3DMaterialTextures.d.ts +3 -0
  65. package/dist/sheenPbrScene3DMaterialTextures.d.ts.map +1 -0
  66. package/dist/sheenPbrScene3DMaterialTextures.js +12 -0
  67. package/dist/sheenPbrScene3DMaterialTextures.js.map +1 -0
  68. package/dist/specularPbrScene3DMaterialTextures.d.ts +3 -0
  69. package/dist/specularPbrScene3DMaterialTextures.d.ts.map +1 -0
  70. package/dist/specularPbrScene3DMaterialTextures.js +12 -0
  71. package/dist/specularPbrScene3DMaterialTextures.js.map +1 -0
  72. package/dist/transmissionVolumePbrScene3DMaterialTextures.d.ts +3 -0
  73. package/dist/transmissionVolumePbrScene3DMaterialTextures.d.ts.map +1 -0
  74. package/dist/transmissionVolumePbrScene3DMaterialTextures.js +12 -0
  75. package/dist/transmissionVolumePbrScene3DMaterialTextures.js.map +1 -0
  76. package/dist/wrappedDiffusePbrScene3DMaterialTextures.d.ts +3 -0
  77. package/dist/wrappedDiffusePbrScene3DMaterialTextures.d.ts.map +1 -0
  78. package/dist/wrappedDiffusePbrScene3DMaterialTextures.js +12 -0
  79. package/dist/wrappedDiffusePbrScene3DMaterialTextures.js.map +1 -0
  80. package/package.json +16 -13
  81. package/src/anisotropyPbrScene3DMaterialTextures.test.ts +26 -0
  82. package/src/awd2Load.test.ts +2 -2
  83. package/src/clearcoatPbrScene3DMaterialTextures.test.ts +36 -0
  84. package/src/enableScene3DResourceFailureGuards.test.ts +9 -8
  85. package/src/explainScene3DResourceCoverage.test.ts +115 -0
  86. package/src/getScene3DResourceTextures.test.ts +65 -38
  87. package/src/gltfLoad.test.ts +2 -2
  88. package/src/imageResourceFetch.test.ts +3 -3
  89. package/src/iridescencePbrScene3DMaterialTextures.test.ts +34 -0
  90. package/src/{resolveScene3DResourcesAndWait.test.ts → loadScene3DResources.test.ts} +40 -18
  91. package/src/md2Load.test.ts +2 -2
  92. package/src/md5Load.test.ts +2 -2
  93. package/src/objLoad.test.ts +2 -2
  94. package/src/resolveScene3DResources.test.ts +136 -53
  95. package/src/revealScene3DResourcesOnResolve.test.ts +39 -21
  96. package/src/sceneMaterialTextureRegistry.test.ts +118 -26
  97. package/src/sceneResourceRecovery.test.ts +13 -47
  98. package/src/sceneResourceResolver.test.ts +14 -11
  99. package/src/sceneResourceResolverTreeShaking.test.ts +1 -1
  100. package/src/shadedScene3DMaterialTextures.test.ts +52 -0
  101. package/src/sheenPbrScene3DMaterialTextures.test.ts +29 -0
  102. package/src/specularPbrScene3DMaterialTextures.test.ts +29 -0
  103. package/src/threeDsLoad.test.ts +2 -2
  104. package/src/transmissionVolumePbrScene3DMaterialTextures.test.ts +34 -0
  105. package/src/wrappedDiffusePbrScene3DMaterialTextures.test.ts +34 -0
  106. package/dist/resolveScene3DResourcesAndWait.d.ts +0 -4
  107. package/dist/resolveScene3DResourcesAndWait.d.ts.map +0 -1
  108. package/dist/resolveScene3DResourcesAndWait.js.map +0 -1
@@ -3,18 +3,20 @@ import { createBoxMeshGeometry } from '@flighthq/mesh/contract';
3
3
  import { addNodeChild } from '@flighthq/node/contract';
4
4
  import { createMesh, createScene3D } from '@flighthq/scene3d/contract';
5
5
  import { connectSignal, createSignal } from '@flighthq/signals/contract';
6
- import { createTexture } from '@flighthq/texture/contract';
7
- import type { ImageResource, ImageResourceReference, Scene3DResourceLoadProgress } from '@flighthq/types/contract';
6
+ import { createTexture, getTextureSource } from '@flighthq/texture/contract';
7
+ import type { Image, ImageResourceReference, Scene3DResourceLoadProgress, Texture } from '@flighthq/types/contract';
8
8
  import { ResourceResolutionState, ImageResourceReferenceKind } from '@flighthq/types/contract';
9
9
  import { describe, expect, it, vi } from 'vitest';
10
10
 
11
- import { loadScene3DResources, waitForScene3DResourceResolver } from './resolveScene3DResourcesAndWait';
11
+ import { loadScene3DResources, waitForScene3DResourceResolver } from './loadScene3DResources';
12
12
  import { createBuiltInScene3DResourceResolver, disposeScene3DResourceResolver } from './sceneResourceResolver';
13
13
 
14
- const fakeImage = { height: 1, width: 1 } as unknown as ImageResource;
14
+ const fakeImage = { height: 1, width: 1 } as unknown as Image;
15
+ const testResources: ImageResourceReference[] = [];
16
+ let sceneResources: ImageResourceReference[] = [];
15
17
 
16
18
  function externalRef(): ImageResourceReference {
17
- return {
19
+ const ref: ImageResourceReference = {
18
20
  basePath: null,
19
21
  failure: null,
20
22
  kind: ImageResourceReferenceKind.External,
@@ -22,6 +24,21 @@ function externalRef(): ImageResourceReference {
22
24
  state: ResourceResolutionState.Unresolved,
23
25
  uri: 'leaf.png',
24
26
  };
27
+ testResources.push(ref);
28
+ return ref;
29
+ }
30
+
31
+ // Hands the scene only the references its own test created, then drains the accumulator. Copying the
32
+ // whole accumulator used to give every scene the previous tests' references as well — harmless only
33
+ // while discovery narrowed by mesh attachment and quietly dropped whatever no mesh referenced.
34
+ function configureResources(scene: ReturnType<typeof createScene3D>): void {
35
+ sceneResources = testResources.slice();
36
+ testResources.length = 0;
37
+ scene.resources = sceneResources;
38
+ }
39
+
40
+ function resourceOf(texture: Texture): ImageResourceReference | undefined {
41
+ return sceneResources.find((ref) => ref.textures?.includes(texture) === true);
25
42
  }
26
43
 
27
44
  describe('loadScene3DResources', () => {
@@ -30,22 +47,24 @@ describe('loadScene3DResources', () => {
30
47
  const a = createTexture({ resource: externalRef() });
31
48
  const b = createTexture({ resource: externalRef() });
32
49
  const scene = createScene3D();
50
+ configureResources(scene);
33
51
  addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: a })]));
34
52
  addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: b })]));
35
53
  const resolver = createBuiltInScene3DResourceResolver({ fetch });
36
54
 
37
- await loadScene3DResources(scene.root, resolver);
55
+ await loadScene3DResources(scene, resolver);
38
56
 
39
- expect(a.storage.image).toBe(fakeImage);
40
- expect(b.storage.image).toBe(fakeImage);
41
- expect(a.resource?.state).toBe(ResourceResolutionState.Resolved);
57
+ expect(getTextureSource(a)).toBe(fakeImage);
58
+ expect(getTextureSource(b)).toBe(fakeImage);
59
+ expect(resourceOf(a)?.state).toBe(ResourceResolutionState.Resolved);
42
60
  disposeScene3DResourceResolver(resolver);
43
61
  });
44
62
 
45
63
  it('resolves immediately when there is nothing pending', async () => {
46
64
  const scene = createScene3D();
65
+ configureResources(scene);
47
66
  const resolver = createBuiltInScene3DResourceResolver({ fetch: async () => fakeImage });
48
- await expect(loadScene3DResources(scene.root, resolver)).resolves.toBeUndefined();
67
+ await expect(loadScene3DResources(scene, resolver)).resolves.toBeUndefined();
49
68
  disposeScene3DResourceResolver(resolver);
50
69
  });
51
70
 
@@ -53,6 +72,7 @@ describe('loadScene3DResources', () => {
53
72
  const a = createTexture({ resource: externalRef() });
54
73
  const b = createTexture({ resource: externalRef() });
55
74
  const scene = createScene3D();
75
+ configureResources(scene);
56
76
  addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: a })]));
57
77
  addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: b })]));
58
78
  const resolver = createBuiltInScene3DResourceResolver({ fetch: async () => fakeImage });
@@ -60,7 +80,7 @@ describe('loadScene3DResources', () => {
60
80
  const progress = createSignal<(event: Readonly<Scene3DResourceLoadProgress>) => void>();
61
81
  connectSignal(progress, (event) => events.push({ ...event }));
62
82
 
63
- await loadScene3DResources(scene.root, resolver, { progress });
83
+ await loadScene3DResources(scene, resolver, { progress });
64
84
 
65
85
  expect(events[0]).toEqual({ loaded: 0, total: 2 });
66
86
  expect(events.at(-1)).toEqual({ loaded: 2, total: 2 });
@@ -70,16 +90,17 @@ describe('loadScene3DResources', () => {
70
90
  it('counts failed references as terminal without hiding their failure state', async () => {
71
91
  const texture = createTexture({ resource: externalRef() });
72
92
  const scene = createScene3D();
93
+ configureResources(scene);
73
94
  addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: texture })]));
74
95
  const resolver = createBuiltInScene3DResourceResolver({ fetch: async () => null });
75
96
  const events: Scene3DResourceLoadProgress[] = [];
76
97
  const progress = createSignal<(event: Readonly<Scene3DResourceLoadProgress>) => void>();
77
98
  connectSignal(progress, (event) => events.push({ ...event }));
78
99
 
79
- await loadScene3DResources(scene.root, resolver, { progress });
100
+ await loadScene3DResources(scene, resolver, { progress });
80
101
 
81
- expect(texture.storage.image).toBeNull();
82
- expect(texture.resource?.state).toBe(ResourceResolutionState.Failed);
102
+ expect(getTextureSource(texture)).toBeNull();
103
+ expect(resourceOf(texture)?.state).toBe(ResourceResolutionState.Failed);
83
104
  expect(events).toEqual([
84
105
  { loaded: 0, total: 1 },
85
106
  { loaded: 1, total: 1 },
@@ -91,6 +112,7 @@ describe('loadScene3DResources', () => {
91
112
  const selected = createTexture({ resource: externalRef() });
92
113
  const deferred = createTexture({ resource: externalRef() });
93
114
  const scene = createScene3D();
115
+ configureResources(scene);
94
116
  addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: selected })]));
95
117
  addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: deferred })]));
96
118
  const fetch = vi.fn(async () => fakeImage);
@@ -99,11 +121,11 @@ describe('loadScene3DResources', () => {
99
121
  const progress = createSignal<(event: Readonly<Scene3DResourceLoadProgress>) => void>();
100
122
  connectSignal(progress, (event) => events.push({ ...event }));
101
123
 
102
- await loadScene3DResources(scene.root, resolver, { progress, select: (texture) => texture === selected });
124
+ await loadScene3DResources(scene, resolver, { progress, select: (texture) => texture === selected });
103
125
 
104
- expect(selected.storage.image).toBe(fakeImage);
105
- expect(deferred.storage.image).toBeNull();
106
- expect(deferred.resource?.state).toBe(ResourceResolutionState.Unresolved);
126
+ expect(getTextureSource(selected)).toBe(fakeImage);
127
+ expect(getTextureSource(deferred)).toBeNull();
128
+ expect(resourceOf(deferred)?.state).toBe(ResourceResolutionState.Unresolved);
107
129
  expect(fetch).toHaveBeenCalledOnce();
108
130
  expect(events.at(-1)).toEqual({ loaded: 1, total: 1 });
109
131
  disposeScene3DResourceResolver(resolver);
@@ -39,8 +39,8 @@ beforeAll(async () => {
39
39
  });
40
40
 
41
41
  afterAll(() => {
42
- vi.doUnmock('@flighthq/net');
43
- vi.doUnmock('@flighthq/scene3d-formats');
42
+ vi.doUnmock('@flighthq/net/contract');
43
+ vi.doUnmock('@flighthq/scene3d-formats/contract');
44
44
  vi.resetModules();
45
45
  });
46
46
 
@@ -39,8 +39,8 @@ beforeAll(async () => {
39
39
  });
40
40
 
41
41
  afterAll(() => {
42
- vi.doUnmock('@flighthq/net');
43
- vi.doUnmock('@flighthq/scene3d-formats');
42
+ vi.doUnmock('@flighthq/net/contract');
43
+ vi.doUnmock('@flighthq/scene3d-formats/contract');
44
44
  vi.resetModules();
45
45
  });
46
46
 
@@ -39,8 +39,8 @@ beforeAll(async () => {
39
39
  });
40
40
 
41
41
  afterAll(() => {
42
- vi.doUnmock('@flighthq/net');
43
- vi.doUnmock('@flighthq/scene3d-formats');
42
+ vi.doUnmock('@flighthq/net/contract');
43
+ vi.doUnmock('@flighthq/scene3d-formats/contract');
44
44
  vi.resetModules();
45
45
  });
46
46
 
@@ -4,22 +4,23 @@ import { createBoxMeshGeometry } from '@flighthq/mesh/contract';
4
4
  import { addNodeChild } from '@flighthq/node/contract';
5
5
  import { createMesh, createScene3D } from '@flighthq/scene3d/contract';
6
6
  import { connectSignal } from '@flighthq/signals/contract';
7
- import { createTexture } from '@flighthq/texture/contract';
8
- import type { ImageResource, ImageResourceReference, Scene3DResourceResolver, Texture } from '@flighthq/types/contract';
7
+ import { createTexture, getTextureSource } from '@flighthq/texture/contract';
8
+ import type { Image, ImageResourceReference, Scene3DResourceResolver, Texture } from '@flighthq/types/contract';
9
9
  import {
10
10
  ImageResourceFailureKind,
11
+ ImageTextureSourceKind,
11
12
  ResourceResolutionState,
12
13
  ImageResourceReferenceKind,
13
14
  } from '@flighthq/types/contract';
14
15
  import type { Mock } from 'vitest';
15
16
  import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
16
17
 
18
+ import type * as LoadScene3DResourcesModule from './loadScene3DResources';
17
19
  import type * as ResolveScene3DResourcesModule from './resolveScene3DResources';
18
- import type * as ResolveScene3DResourcesAndWaitModule from './resolveScene3DResourcesAndWait';
19
20
  import type * as Scene3DResourceResolverModule from './sceneResourceResolver';
20
21
  import type * as Scene3DResourceSignalsModule from './sceneResourceSignals';
21
22
 
22
- const fakeImage = { height: 2, width: 2 } as unknown as ImageResource;
23
+ const fakeImage = { height: 2, kind: ImageTextureSourceKind, width: 2 } as unknown as Image;
23
24
  type LoadImageResourceFromBytes = typeof ImageModule.loadImageResourceFromBytes;
24
25
 
25
26
  let createBuiltInScene3DResourceResolver: typeof Scene3DResourceResolverModule.createBuiltInScene3DResourceResolver;
@@ -28,20 +29,25 @@ let enableScene3DResourceSignals: typeof Scene3DResourceSignalsModule.enableScen
28
29
  let loadFromBytes: Mock<LoadImageResourceFromBytes>;
29
30
  let resolveOneScene3DResourceTexture: typeof ResolveScene3DResourcesModule.resolveOneScene3DResourceTexture;
30
31
  let resolveScene3DResources: typeof ResolveScene3DResourcesModule.resolveScene3DResources;
31
- let waitForScene3DResourceResolver: typeof ResolveScene3DResourcesAndWaitModule.waitForScene3DResourceResolver;
32
+ let updateScene3DResourceStreaming: typeof ResolveScene3DResourcesModule.updateScene3DResourceStreaming;
33
+ let waitForScene3DResourceResolver: typeof LoadScene3DResourcesModule.waitForScene3DResourceResolver;
34
+ const testResources: ImageResourceReference[] = [];
35
+ let sceneResources: ImageResourceReference[] = [];
32
36
 
33
37
  function embeddedRef(mimeType: string | null = 'image/png'): ImageResourceReference {
34
- return {
38
+ const ref: ImageResourceReference = {
35
39
  bytes: new Uint8Array([9, 9]),
36
40
  failure: null,
37
41
  kind: ImageResourceReferenceKind.Embedded,
38
42
  mimeType,
39
43
  state: ResourceResolutionState.Unresolved,
40
44
  };
45
+ testResources.push(ref);
46
+ return ref;
41
47
  }
42
48
 
43
49
  function externalRef(uri = 'leaf.png'): ImageResourceReference {
44
- return {
50
+ const ref: ImageResourceReference = {
45
51
  basePath: null,
46
52
  failure: null,
47
53
  kind: ImageResourceReferenceKind.External,
@@ -49,6 +55,12 @@ function externalRef(uri = 'leaf.png'): ImageResourceReference {
49
55
  state: ResourceResolutionState.Unresolved,
50
56
  uri,
51
57
  };
58
+ testResources.push(ref);
59
+ return ref;
60
+ }
61
+
62
+ function resourceOf(texture: Texture): ImageResourceReference | undefined {
63
+ return sceneResources.find((ref) => ref.textures?.includes(texture) === true);
52
64
  }
53
65
 
54
66
  function pendingTexture(): Texture {
@@ -57,6 +69,11 @@ function pendingTexture(): Texture {
57
69
 
58
70
  function meshScene3D(...textures: Texture[]) {
59
71
  const scene = createScene3D();
72
+ // Only this test's references. Copying the whole accumulator used to hand each scene the previous
73
+ // tests' references too, which discovery quietly dropped while it narrowed by mesh attachment.
74
+ sceneResources = testResources.slice();
75
+ testResources.length = 0;
76
+ scene.resources = sceneResources;
60
77
  for (const texture of textures) {
61
78
  addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: texture })]));
62
79
  }
@@ -70,18 +87,22 @@ async function settle(resolver: Scene3DResourceResolver): Promise<void> {
70
87
  beforeAll(async () => {
71
88
  vi.resetModules();
72
89
  loadFromBytes = vi.fn<LoadImageResourceFromBytes>();
73
- vi.doMock('@flighthq/image/contract', () => ({
90
+ // Override only the two loaders; the rest of the module stays real, so growing @flighthq/image cannot
91
+ // silently blank out a code path under test.
92
+ vi.doMock('@flighthq/image/contract', async (importOriginal) => ({
93
+ ...(await importOriginal<typeof ImageModule>()),
74
94
  loadImageResourceFromBytes: loadFromBytes,
75
95
  loadImageResourceFromUrl: vi.fn(),
76
96
  }));
77
- ({ resolveOneScene3DResourceTexture, resolveScene3DResources } = await import('./resolveScene3DResources'));
78
- ({ waitForScene3DResourceResolver } = await import('./resolveScene3DResourcesAndWait'));
97
+ ({ resolveOneScene3DResourceTexture, resolveScene3DResources, updateScene3DResourceStreaming } =
98
+ await import('./resolveScene3DResources'));
99
+ ({ waitForScene3DResourceResolver } = await import('./loadScene3DResources'));
79
100
  ({ createBuiltInScene3DResourceResolver, disposeScene3DResourceResolver } = await import('./sceneResourceResolver'));
80
101
  ({ enableScene3DResourceSignals } = await import('./sceneResourceSignals'));
81
102
  });
82
103
 
83
104
  afterAll(() => {
84
- vi.doUnmock('@flighthq/image');
105
+ vi.doUnmock('@flighthq/image/contract');
85
106
  vi.resetModules();
86
107
  });
87
108
 
@@ -122,6 +143,68 @@ describe('resolveOneScene3DResourceTexture', () => {
122
143
  });
123
144
 
124
145
  describe('resolveScene3DResources', () => {
146
+ it('does not cancel acquisition owned by a streaming update', async () => {
147
+ let loadSignal: AbortSignal | undefined;
148
+ loadFromBytes.mockImplementation(
149
+ (_bytes, _mime, signal) =>
150
+ new Promise<Image>((_resolve, reject) => {
151
+ loadSignal = signal;
152
+ signal?.addEventListener('abort', () => reject(signal.reason));
153
+ }),
154
+ );
155
+ const ref = embeddedRef();
156
+ const scene = meshScene3D(createTexture({ resource: ref }));
157
+ const resolver = createBuiltInScene3DResourceResolver();
158
+ updateScene3DResourceStreaming(scene, resolver);
159
+ await vi.waitFor(() => expect(loadSignal).toBeDefined());
160
+
161
+ const resources = resolveScene3DResources(scene, resolver, { select: () => false });
162
+
163
+ expect(resources.resolved).toEqual([]);
164
+ expect(resources.unresolved).toEqual([]);
165
+ expect(loadSignal?.aborted).toBe(false);
166
+ expect(ref.state).toBe(ResourceResolutionState.Loading);
167
+ disposeScene3DResourceResolver(resolver);
168
+ });
169
+
170
+ it('returns the selected unresolved working set without starting acquisition', () => {
171
+ loadFromBytes.mockResolvedValue(fakeImage);
172
+ const ref = embeddedRef();
173
+ const a = createTexture({ resource: ref });
174
+ const b = createTexture({ resource: ref });
175
+ const scene = meshScene3D(a, b);
176
+ const resolver = createBuiltInScene3DResourceResolver();
177
+
178
+ const resources = resolveScene3DResources(scene, resolver);
179
+
180
+ expect(resources.scene).toBe(scene);
181
+ expect(resources.resolved).toEqual([]);
182
+ expect(resources.unresolved).toEqual([{ ref, textures: [a, b] }]);
183
+ expect(ref.state).toBe(ResourceResolutionState.Unresolved);
184
+ expect(loadFromBytes).not.toHaveBeenCalled();
185
+ disposeScene3DResourceResolver(resolver);
186
+ });
187
+
188
+ it('binds ready content across subscribers and returns its resolved group', () => {
189
+ const ref = embeddedRef();
190
+ const a = createTexture({ resource: ref });
191
+ const b = createTexture({ resource: ref });
192
+ if (a.dimension !== '2d') throw new Error('test texture must be 2d');
193
+ a.source = fakeImage;
194
+ const scene = meshScene3D(a, b);
195
+ const resolver = createBuiltInScene3DResourceResolver();
196
+
197
+ const resources = resolveScene3DResources(scene, resolver);
198
+
199
+ expect(resources.unresolved).toEqual([]);
200
+ expect(resources.resolved).toEqual([{ image: fakeImage, ref, textures: [a, b] }]);
201
+ expect(getTextureSource(b)).toBe(fakeImage);
202
+ expect(ref.state).toBe(ResourceResolutionState.Resolved);
203
+ disposeScene3DResourceResolver(resolver);
204
+ });
205
+ });
206
+
207
+ describe('updateScene3DResourceStreaming', () => {
125
208
  it('resolves every pending texture, binding the image and advancing state', async () => {
126
209
  loadFromBytes.mockResolvedValue(fakeImage);
127
210
  const a = pendingTexture();
@@ -129,14 +212,14 @@ describe('resolveScene3DResources', () => {
129
212
  const scene = meshScene3D(a, b);
130
213
  const resolver = createBuiltInScene3DResourceResolver();
131
214
 
132
- resolveScene3DResources(scene.root, resolver);
215
+ updateScene3DResourceStreaming(scene, resolver);
133
216
  await settle(resolver);
134
217
 
135
- expect(a.storage.image).toBe(fakeImage);
136
- expect(b.storage.image).toBe(fakeImage);
137
- expect(a.resource?.state).toBe(ResourceResolutionState.Resolved);
138
- expect(a.resource?.failure).toBeNull();
139
- expect(b.resource?.state).toBe(ResourceResolutionState.Resolved);
218
+ expect(getTextureSource(a)).toBe(fakeImage);
219
+ expect(getTextureSource(b)).toBe(fakeImage);
220
+ expect(resourceOf(a)?.state).toBe(ResourceResolutionState.Resolved);
221
+ expect(resourceOf(a)?.failure).toBeNull();
222
+ expect(resourceOf(b)?.state).toBe(ResourceResolutionState.Resolved);
140
223
  disposeScene3DResourceResolver(resolver);
141
224
  });
142
225
 
@@ -148,12 +231,12 @@ describe('resolveScene3DResources', () => {
148
231
  const scene = meshScene3D(a, b);
149
232
  const resolver = createBuiltInScene3DResourceResolver();
150
233
 
151
- resolveScene3DResources(scene.root, resolver);
234
+ updateScene3DResourceStreaming(scene, resolver);
152
235
  await settle(resolver);
153
236
 
154
237
  expect(loadFromBytes).toHaveBeenCalledTimes(1);
155
- expect(a.storage.image).toBe(fakeImage);
156
- expect(b.storage.image).toBe(fakeImage);
238
+ expect(getTextureSource(a)).toBe(fakeImage);
239
+ expect(getTextureSource(b)).toBe(fakeImage);
157
240
  expect(ref.state).toBe(ResourceResolutionState.Resolved);
158
241
  disposeScene3DResourceResolver(resolver);
159
242
  });
@@ -162,7 +245,7 @@ describe('resolveScene3DResources', () => {
162
245
  let loadSignal: AbortSignal | undefined;
163
246
  loadFromBytes.mockImplementation(
164
247
  (_bytes, _mime, signal) =>
165
- new Promise<ImageResource>((_resolve, reject) => {
248
+ new Promise<Image>((_resolve, reject) => {
166
249
  loadSignal = signal;
167
250
  signal?.addEventListener('abort', () => reject(signal.reason));
168
251
  }),
@@ -173,13 +256,13 @@ describe('resolveScene3DResources', () => {
173
256
  const scene = meshScene3D(a, b);
174
257
  const resolver = createBuiltInScene3DResourceResolver();
175
258
 
176
- resolveScene3DResources(scene.root, resolver);
259
+ updateScene3DResourceStreaming(scene, resolver);
177
260
  await vi.waitFor(() => expect(loadSignal).toBeDefined());
178
- resolveScene3DResources(scene.root, resolver, { select: (texture) => texture === b });
261
+ updateScene3DResourceStreaming(scene, resolver, { select: (texture) => texture === b });
179
262
  expect(loadSignal?.aborted).toBe(false);
180
263
  expect(ref.state).toBe(ResourceResolutionState.Loading);
181
264
 
182
- resolveScene3DResources(scene.root, resolver, { select: () => false });
265
+ updateScene3DResourceStreaming(scene, resolver, { select: () => false });
183
266
  expect(loadSignal?.aborted).toBe(true);
184
267
  expect(ref.state).toBe(ResourceResolutionState.Unresolved);
185
268
  disposeScene3DResourceResolver(resolver);
@@ -193,13 +276,13 @@ describe('resolveScene3DResources', () => {
193
276
  const scene = meshScene3D(a, b);
194
277
  const resolver = createBuiltInScene3DResourceResolver();
195
278
 
196
- resolveScene3DResources(scene.root, resolver, { select: (texture) => texture === a });
279
+ updateScene3DResourceStreaming(scene, resolver, { select: (texture) => texture === a });
197
280
  await settle(resolver);
198
- expect(a.storage.image).toBe(fakeImage);
199
- expect(b.storage.image).toBeNull();
281
+ expect(getTextureSource(a)).toBe(fakeImage);
282
+ expect(getTextureSource(b)).toBeNull();
200
283
 
201
- resolveScene3DResources(scene.root, resolver, { select: (texture) => texture === b });
202
- expect(b.storage.image).toBe(fakeImage);
284
+ updateScene3DResourceStreaming(scene, resolver, { select: (texture) => texture === b });
285
+ expect(getTextureSource(b)).toBe(fakeImage);
203
286
  expect(loadFromBytes).toHaveBeenCalledTimes(1);
204
287
  disposeScene3DResourceResolver(resolver);
205
288
  });
@@ -211,12 +294,12 @@ describe('resolveScene3DResources', () => {
211
294
  const scene = meshScene3D(wanted, skipped);
212
295
  const resolver = createBuiltInScene3DResourceResolver();
213
296
 
214
- resolveScene3DResources(scene.root, resolver, { select: (texture) => texture === wanted });
297
+ updateScene3DResourceStreaming(scene, resolver, { select: (texture) => texture === wanted });
215
298
  await settle(resolver);
216
299
 
217
- expect(wanted.resource?.state).toBe(ResourceResolutionState.Resolved);
218
- expect(skipped.resource?.state).toBe(ResourceResolutionState.Unresolved);
219
- expect(skipped.storage.image).toBeNull();
300
+ expect(resourceOf(wanted)?.state).toBe(ResourceResolutionState.Resolved);
301
+ expect(resourceOf(skipped)?.state).toBe(ResourceResolutionState.Unresolved);
302
+ expect(getTextureSource(skipped)).toBeNull();
220
303
  disposeScene3DResourceResolver(resolver);
221
304
  });
222
305
 
@@ -225,12 +308,12 @@ describe('resolveScene3DResources', () => {
225
308
  const scene = meshScene3D(texture);
226
309
  const resolver = createBuiltInScene3DResourceResolver({ fetch: async () => null });
227
310
 
228
- resolveScene3DResources(scene.root, resolver);
311
+ updateScene3DResourceStreaming(scene, resolver);
229
312
  await settle(resolver);
230
313
 
231
- expect(texture.storage.image).toBeNull();
232
- expect(texture.resource?.state).toBe(ResourceResolutionState.Failed);
233
- expect(texture.resource?.failure).toEqual({
314
+ expect(getTextureSource(texture)).toBeNull();
315
+ expect(resourceOf(texture)?.state).toBe(ResourceResolutionState.Failed);
316
+ expect(resourceOf(texture)?.failure).toEqual({
234
317
  kind: ImageResourceFailureKind.Unavailable,
235
318
  message: 'Image resource resolution returned no image',
236
319
  name: null,
@@ -244,11 +327,11 @@ describe('resolveScene3DResources', () => {
244
327
  const scene = meshScene3D(texture);
245
328
  const resolver = createBuiltInScene3DResourceResolver();
246
329
 
247
- resolveScene3DResources(scene.root, resolver);
330
+ updateScene3DResourceStreaming(scene, resolver);
248
331
  await settle(resolver);
249
332
 
250
- expect(texture.resource?.state).toBe(ResourceResolutionState.Failed);
251
- expect(texture.resource?.failure).toEqual({
333
+ expect(resourceOf(texture)?.state).toBe(ResourceResolutionState.Failed);
334
+ expect(resourceOf(texture)?.failure).toEqual({
252
335
  kind: ImageResourceFailureKind.Error,
253
336
  message: 'bad image',
254
337
  name: 'Error',
@@ -261,7 +344,7 @@ describe('resolveScene3DResources', () => {
261
344
  // A load that hangs until its signal aborts, so we can drop it mid-flight.
262
345
  loadFromBytes.mockImplementation(
263
346
  (_bytes, _mime, signal) =>
264
- new Promise<ImageResource>((_resolve, reject) => {
347
+ new Promise<Image>((_resolve, reject) => {
265
348
  if (signal !== undefined) loadSignals.push(signal);
266
349
  signal?.addEventListener('abort', () => reject(signal.reason));
267
350
  }),
@@ -270,33 +353,33 @@ describe('resolveScene3DResources', () => {
270
353
  const scene = meshScene3D(texture);
271
354
  const resolver = createBuiltInScene3DResourceResolver();
272
355
 
273
- resolveScene3DResources(scene.root, resolver, { select: () => true });
274
- expect(texture.resource?.state).toBe(ResourceResolutionState.Loading);
356
+ updateScene3DResourceStreaming(scene, resolver, { select: () => true });
357
+ expect(resourceOf(texture)?.state).toBe(ResourceResolutionState.Loading);
275
358
  await vi.waitFor(() => expect(loadSignals).toHaveLength(1));
276
359
 
277
360
  // Drop it: not in the working set this pass → abort + revert.
278
- resolveScene3DResources(scene.root, resolver, { select: () => false });
279
- expect(texture.resource?.state).toBe(ResourceResolutionState.Unresolved);
280
- expect(texture.storage.image).toBeNull();
361
+ updateScene3DResourceStreaming(scene, resolver, { select: () => false });
362
+ expect(resourceOf(texture)?.state).toBe(ResourceResolutionState.Unresolved);
363
+ expect(getTextureSource(texture)).toBeNull();
281
364
  expect(loadSignals[0].aborted).toBe(true);
282
365
 
283
366
  // Re-entry re-requests from scratch.
284
- resolveScene3DResources(scene.root, resolver, { select: () => true });
285
- expect(texture.resource?.state).toBe(ResourceResolutionState.Loading);
367
+ updateScene3DResourceStreaming(scene, resolver, { select: () => true });
368
+ expect(resourceOf(texture)?.state).toBe(ResourceResolutionState.Loading);
286
369
  await vi.waitFor(() => expect(loadSignals).toHaveLength(2));
287
370
 
288
371
  disposeScene3DResourceResolver(resolver);
289
372
  });
290
373
 
291
374
  it('does not re-request a texture already in flight', async () => {
292
- loadFromBytes.mockImplementation(() => new Promise<ImageResource>(() => {}));
375
+ loadFromBytes.mockImplementation(() => new Promise<Image>(() => {}));
293
376
  const texture = pendingTexture();
294
377
  const scene = meshScene3D(texture);
295
378
  const resolver = createBuiltInScene3DResourceResolver();
296
379
 
297
- resolveScene3DResources(scene.root, resolver);
380
+ updateScene3DResourceStreaming(scene, resolver);
298
381
  await vi.waitFor(() => expect(loadFromBytes).toHaveBeenCalledTimes(1));
299
- resolveScene3DResources(scene.root, resolver);
382
+ updateScene3DResourceStreaming(scene, resolver);
300
383
  expect(loadFromBytes).toHaveBeenCalledTimes(1);
301
384
 
302
385
  disposeScene3DResourceResolver(resolver);
@@ -313,7 +396,7 @@ describe('resolveScene3DResources', () => {
313
396
  connectSignal(signals.onResourceResolved, (event) => resolved.push(event.texture));
314
397
  connectSignal(signals.onResourceFailed, (event) => failed.push(event.texture));
315
398
 
316
- resolveScene3DResources(scene.root, resolver);
399
+ updateScene3DResourceStreaming(scene, resolver);
317
400
  await settle(resolver);
318
401
 
319
402
  expect(resolved).toEqual([texture]);
@@ -330,7 +413,7 @@ describe('resolveScene3DResources', () => {
330
413
  const failed: Texture[] = [];
331
414
  connectSignal(signals.onResourceFailed, (event) => failed.push(event.texture));
332
415
 
333
- resolveScene3DResources(scene.root, resolver);
416
+ updateScene3DResourceStreaming(scene, resolver);
334
417
  await settle(resolver);
335
418
 
336
419
  expect(failed).toEqual([texture]);