@flighthq/scene3d-resources 0.2.1-next.840.857425c

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 (91) hide show
  1. package/dist/awd2Load.d.ts +3 -0
  2. package/dist/awd2Load.d.ts.map +1 -0
  3. package/dist/awd2Load.js +15 -0
  4. package/dist/awd2Load.js.map +1 -0
  5. package/dist/enableScene3DResourceFailureGuards.d.ts +5 -0
  6. package/dist/enableScene3DResourceFailureGuards.d.ts.map +1 -0
  7. package/dist/enableScene3DResourceFailureGuards.js +52 -0
  8. package/dist/enableScene3DResourceFailureGuards.js.map +1 -0
  9. package/dist/getScene3DResourceTextures.d.ts +3 -0
  10. package/dist/getScene3DResourceTextures.d.ts.map +1 -0
  11. package/dist/getScene3DResourceTextures.js +37 -0
  12. package/dist/getScene3DResourceTextures.js.map +1 -0
  13. package/dist/gltfLoad.d.ts +4 -0
  14. package/dist/gltfLoad.d.ts.map +1 -0
  15. package/dist/gltfLoad.js +58 -0
  16. package/dist/gltfLoad.js.map +1 -0
  17. package/dist/imageResourceFetch.d.ts +4 -0
  18. package/dist/imageResourceFetch.d.ts.map +1 -0
  19. package/dist/imageResourceFetch.js +29 -0
  20. package/dist/imageResourceFetch.js.map +1 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +18 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/md2Load.d.ts +3 -0
  26. package/dist/md2Load.d.ts.map +1 -0
  27. package/dist/md2Load.js +15 -0
  28. package/dist/md2Load.js.map +1 -0
  29. package/dist/md5Load.d.ts +3 -0
  30. package/dist/md5Load.d.ts.map +1 -0
  31. package/dist/md5Load.js +15 -0
  32. package/dist/md5Load.js.map +1 -0
  33. package/dist/objLoad.d.ts +3 -0
  34. package/dist/objLoad.d.ts.map +1 -0
  35. package/dist/objLoad.js +16 -0
  36. package/dist/objLoad.js.map +1 -0
  37. package/dist/resolveScene3DResources.d.ts +4 -0
  38. package/dist/resolveScene3DResources.d.ts.map +1 -0
  39. package/dist/resolveScene3DResources.js +175 -0
  40. package/dist/resolveScene3DResources.js.map +1 -0
  41. package/dist/resolveScene3DResourcesAndWait.d.ts +4 -0
  42. package/dist/resolveScene3DResourcesAndWait.d.ts.map +1 -0
  43. package/dist/resolveScene3DResourcesAndWait.js +57 -0
  44. package/dist/resolveScene3DResourcesAndWait.js.map +1 -0
  45. package/dist/revealScene3DResourcesOnResolve.d.ts +3 -0
  46. package/dist/revealScene3DResourcesOnResolve.d.ts.map +1 -0
  47. package/dist/revealScene3DResourcesOnResolve.js +95 -0
  48. package/dist/revealScene3DResourcesOnResolve.js.map +1 -0
  49. package/dist/sceneDocumentSource.d.ts +6 -0
  50. package/dist/sceneDocumentSource.d.ts.map +1 -0
  51. package/dist/sceneDocumentSource.js +55 -0
  52. package/dist/sceneDocumentSource.js.map +1 -0
  53. package/dist/sceneMaterialTextureRegistry.d.ts +6 -0
  54. package/dist/sceneMaterialTextureRegistry.d.ts.map +1 -0
  55. package/dist/sceneMaterialTextureRegistry.js +43 -0
  56. package/dist/sceneMaterialTextureRegistry.js.map +1 -0
  57. package/dist/sceneResourceRecovery.d.ts +5 -0
  58. package/dist/sceneResourceRecovery.d.ts.map +1 -0
  59. package/dist/sceneResourceRecovery.js +43 -0
  60. package/dist/sceneResourceRecovery.js.map +1 -0
  61. package/dist/sceneResourceResolver.d.ts +5 -0
  62. package/dist/sceneResourceResolver.d.ts.map +1 -0
  63. package/dist/sceneResourceResolver.js +41 -0
  64. package/dist/sceneResourceResolver.js.map +1 -0
  65. package/dist/sceneResourceSignals.d.ts +5 -0
  66. package/dist/sceneResourceSignals.d.ts.map +1 -0
  67. package/dist/sceneResourceSignals.js +21 -0
  68. package/dist/sceneResourceSignals.js.map +1 -0
  69. package/dist/threeDsLoad.d.ts +3 -0
  70. package/dist/threeDsLoad.d.ts.map +1 -0
  71. package/dist/threeDsLoad.js +14 -0
  72. package/dist/threeDsLoad.js.map +1 -0
  73. package/package.json +55 -0
  74. package/src/awd2Load.test.ts +63 -0
  75. package/src/enableScene3DResourceFailureGuards.test.ts +116 -0
  76. package/src/getScene3DResourceTextures.test.ts +101 -0
  77. package/src/gltfLoad.test.ts +118 -0
  78. package/src/imageResourceFetch.test.ts +95 -0
  79. package/src/md2Load.test.ts +63 -0
  80. package/src/md5Load.test.ts +63 -0
  81. package/src/objLoad.test.ts +63 -0
  82. package/src/resolveScene3DResources.test.ts +335 -0
  83. package/src/resolveScene3DResourcesAndWait.test.ts +119 -0
  84. package/src/revealScene3DResourcesOnResolve.test.ts +164 -0
  85. package/src/sceneDocumentSource.test.ts +116 -0
  86. package/src/sceneMaterialTextureRegistry.test.ts +95 -0
  87. package/src/sceneResourceRecovery.test.ts +98 -0
  88. package/src/sceneResourceResolver.test.ts +80 -0
  89. package/src/sceneResourceResolverTreeShaking.test.ts +51 -0
  90. package/src/sceneResourceSignals.test.ts +51 -0
  91. package/src/threeDsLoad.test.ts +63 -0
@@ -0,0 +1,63 @@
1
+ import type * as NetModule from '@flighthq/net';
2
+ import type * as Scene3DFormatsModule from '@flighthq/scene3d-formats';
3
+ import type { NetResponse, Scene3DDocument } from '@flighthq/types';
4
+ import type { Mock } from 'vitest';
5
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
6
+
7
+ import type * as LoadAwd2Module from './awd2Load';
8
+
9
+ let loadScene3DDocumentFromAwd2Url: typeof LoadAwd2Module.loadScene3DDocumentFromAwd2Url;
10
+ let parseAwd2: Mock<typeof Scene3DFormatsModule.parseAwd2>;
11
+ let sendNetRequest: Mock<typeof NetModule.sendNetRequest>;
12
+
13
+ function emptyDocument(): Scene3DDocument {
14
+ return {
15
+ animations: [],
16
+ cameras: [],
17
+ lights: [],
18
+ materials: [],
19
+ meshes: [],
20
+ metadata: null,
21
+ nodes: [],
22
+ resources: [],
23
+ scenes: [],
24
+ skins: [],
25
+ };
26
+ }
27
+
28
+ function okResponse(body: ArrayBuffer): NetResponse {
29
+ return { body, headers: {}, ok: true, status: 200, statusText: 'OK', url: 'u' };
30
+ }
31
+
32
+ beforeAll(async () => {
33
+ vi.resetModules();
34
+ parseAwd2 = vi.fn<typeof Scene3DFormatsModule.parseAwd2>();
35
+ sendNetRequest = vi.fn<typeof NetModule.sendNetRequest>();
36
+ vi.doMock('@flighthq/net', () => ({ sendNetRequest }));
37
+ vi.doMock('@flighthq/scene3d-formats', () => ({ parseAwd2 }));
38
+ ({ loadScene3DDocumentFromAwd2Url } = await import('./awd2Load'));
39
+ });
40
+
41
+ afterAll(() => {
42
+ vi.doUnmock('@flighthq/net');
43
+ vi.doUnmock('@flighthq/scene3d-formats');
44
+ vi.resetModules();
45
+ });
46
+
47
+ afterEach(() => {
48
+ parseAwd2.mockReset();
49
+ sendNetRequest.mockReset();
50
+ });
51
+
52
+ describe('loadScene3DDocumentFromAwd2Url', () => {
53
+ it('fetches bytes and returns the parsed CPU document without resolving resources', async () => {
54
+ const document = emptyDocument();
55
+ parseAwd2.mockReturnValue(document);
56
+ sendNetRequest.mockResolvedValue(okResponse(new Uint8Array([5, 6]).buffer));
57
+
58
+ const loaded = await loadScene3DDocumentFromAwd2Url('model.awd');
59
+
60
+ expect(Array.from(parseAwd2.mock.calls[0][0])).toEqual([5, 6]);
61
+ expect(loaded).toBe(document);
62
+ });
63
+ });
@@ -0,0 +1,116 @@
1
+ import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log';
2
+ import { emitSignal } from '@flighthq/signals';
3
+ import { createTexture } from '@flighthq/texture';
4
+ import type { ImageResourceReference } from '@flighthq/types';
5
+ import {
6
+ ImageResourceFailureKind,
7
+ ImageResourceReferenceKind,
8
+ LogLevel,
9
+ ResourceResolutionState,
10
+ } from '@flighthq/types';
11
+ import { afterEach, describe, expect, it } from 'vitest';
12
+
13
+ import {
14
+ areScene3DResourceFailureGuardsEnabled,
15
+ disableScene3DResourceFailureGuards,
16
+ enableScene3DResourceFailureGuards,
17
+ } from './enableScene3DResourceFailureGuards';
18
+ import { createScene3DResourceResolver, disposeScene3DResourceResolver } from './sceneResourceResolver';
19
+ import { enableScene3DResourceSignals } from './sceneResourceSignals';
20
+
21
+ const sinks: ReturnType<typeof createMemoryLogSink>[] = [];
22
+
23
+ afterEach(() => {
24
+ for (const sink of sinks) removeLogSink(sink.sink);
25
+ sinks.length = 0;
26
+ });
27
+
28
+ function failedRef(): ImageResourceReference {
29
+ return {
30
+ basePath: null,
31
+ failure: { kind: ImageResourceFailureKind.Error, message: 'bad image', name: 'CodecError' },
32
+ kind: ImageResourceReferenceKind.External,
33
+ mimeType: null,
34
+ state: ResourceResolutionState.Failed,
35
+ uri: 'bad.png',
36
+ };
37
+ }
38
+
39
+ describe('areScene3DResourceFailureGuardsEnabled', () => {
40
+ it('tracks resolver-scoped enable and disable state', () => {
41
+ const resolver = createScene3DResourceResolver();
42
+ expect(areScene3DResourceFailureGuardsEnabled(resolver)).toBe(false);
43
+ enableScene3DResourceFailureGuards(resolver);
44
+ expect(areScene3DResourceFailureGuardsEnabled(resolver)).toBe(true);
45
+ disableScene3DResourceFailureGuards(resolver);
46
+ expect(areScene3DResourceFailureGuardsEnabled(resolver)).toBe(false);
47
+ disposeScene3DResourceResolver(resolver);
48
+ });
49
+ });
50
+
51
+ describe('disableScene3DResourceFailureGuards', () => {
52
+ it('is harmless when the guard is absent', () => {
53
+ const resolver = createScene3DResourceResolver();
54
+ expect(() => disableScene3DResourceFailureGuards(resolver)).not.toThrow();
55
+ disposeScene3DResourceResolver(resolver);
56
+ });
57
+ });
58
+
59
+ describe('enableScene3DResourceFailureGuards', () => {
60
+ it('warns once per failed attempt, names recovery, and does not change resolver settlement', () => {
61
+ const sink = createMemoryLogSink(4);
62
+ sinks.push(sink);
63
+ addLogSink(sink.sink);
64
+ const resolver = createScene3DResourceResolver();
65
+ const signals = enableScene3DResourceSignals(resolver);
66
+ const texture = createTexture({ resource: failedRef() });
67
+ const dispose = enableScene3DResourceFailureGuards(resolver);
68
+ const event = { ref: texture.resource!, texture };
69
+
70
+ expect(() => emitSignal(signals.onResourceFailed, event)).not.toThrow();
71
+ emitSignal(signals.onResourceFailed, event);
72
+ const entries = getMemoryLogSinkEntries(sink);
73
+ expect(entries).toHaveLength(1);
74
+ expect(entries[0]).toMatchObject({ channel: 'scene-resources', level: LogLevel.Warn });
75
+ expect(entries[0]?.data).toMatchObject({
76
+ failureKind: ImageResourceFailureKind.Error,
77
+ failureMessage: 'bad image',
78
+ failureName: 'CodecError',
79
+ message:
80
+ 'resolveScene3DResources: image resource resolution failed — call retryFailedScene3DResources to request it again',
81
+ });
82
+ expect(texture.resource?.state).toBe(ResourceResolutionState.Failed);
83
+
84
+ emitSignal(signals.onResourceResolved, event);
85
+ texture.resource!.failure = {
86
+ kind: ImageResourceFailureKind.Unavailable,
87
+ message: 'still unavailable',
88
+ name: null,
89
+ };
90
+ emitSignal(signals.onResourceFailed, event);
91
+ expect(getMemoryLogSinkEntries(sink)).toHaveLength(2);
92
+ dispose();
93
+ emitSignal(signals.onResourceResolved, event);
94
+ emitSignal(signals.onResourceFailed, event);
95
+ expect(getMemoryLogSinkEntries(sink)).toHaveLength(2);
96
+ disposeScene3DResourceResolver(resolver);
97
+ });
98
+
99
+ it('is idempotent for one resolver', () => {
100
+ const sink = createMemoryLogSink(4);
101
+ sinks.push(sink);
102
+ addLogSink(sink.sink);
103
+ const resolver = createScene3DResourceResolver();
104
+ const signals = enableScene3DResourceSignals(resolver);
105
+ const texture = createTexture({ resource: failedRef() });
106
+ const firstDispose = enableScene3DResourceFailureGuards(resolver);
107
+ const secondDispose = enableScene3DResourceFailureGuards(resolver);
108
+
109
+ emitSignal(signals.onResourceFailed, { ref: texture.resource!, texture });
110
+ expect(getMemoryLogSinkEntries(sink)).toHaveLength(1);
111
+ secondDispose();
112
+ expect(areScene3DResourceFailureGuardsEnabled(resolver)).toBe(false);
113
+ firstDispose();
114
+ disposeScene3DResourceResolver(resolver);
115
+ });
116
+ });
@@ -0,0 +1,101 @@
1
+ import { createStandardPbrMaterial, createUnlitMaterial } from '@flighthq/materials';
2
+ import { createBoxMeshGeometry } from '@flighthq/mesh';
3
+ import { addNodeChild } from '@flighthq/node';
4
+ import { createMesh, createScene3D } from '@flighthq/scene3d';
5
+ import { createTexture } from '@flighthq/texture';
6
+ import type { ImageResourceReference, Texture } from '@flighthq/types';
7
+ import { ResourceResolutionState, ImageResourceReferenceKind } from '@flighthq/types';
8
+ import { describe, expect, it } from 'vitest';
9
+
10
+ import { getScene3DResourceTextures } from './getScene3DResourceTextures';
11
+ import {
12
+ createScene3DMaterialTextureRegistry,
13
+ registerBuiltInScene3DMaterialTextures,
14
+ } from './sceneMaterialTextureRegistry';
15
+
16
+ function embeddedRef(state: ResourceResolutionState = ResourceResolutionState.Unresolved): ImageResourceReference {
17
+ return {
18
+ bytes: new Uint8Array([1, 2, 3]),
19
+ failure: null,
20
+ kind: ImageResourceReferenceKind.Embedded,
21
+ mimeType: 'image/png',
22
+ state,
23
+ };
24
+ }
25
+
26
+ function registry() {
27
+ const r = createScene3DMaterialTextureRegistry();
28
+ registerBuiltInScene3DMaterialTextures(r);
29
+ return r;
30
+ }
31
+
32
+ describe('getScene3DResourceTextures', () => {
33
+ it('finds pending textures on the root mesh and descendants', () => {
34
+ const rootMap = createTexture({ resource: embeddedRef() });
35
+ const childMap = createTexture({ resource: embeddedRef() });
36
+ const root = createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: rootMap })]);
37
+ const child = createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: childMap })]);
38
+ addNodeChild(root, child);
39
+
40
+ const out: Texture[] = [];
41
+ getScene3DResourceTextures(root, registry(), out);
42
+ expect(out).toContain(rootMap);
43
+ expect(out).toContain(childMap);
44
+ expect(out).toHaveLength(2);
45
+ });
46
+
47
+ it('walks a group scene root that is not itself a mesh', () => {
48
+ const map = createTexture({ resource: embeddedRef() });
49
+ const scene = createScene3D();
50
+ const mesh = createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: map })]);
51
+ addNodeChild(scene.root, mesh);
52
+
53
+ const out: Texture[] = [];
54
+ getScene3DResourceTextures(scene.root, registry(), out);
55
+ expect(out).toEqual([map]);
56
+ });
57
+
58
+ it('dedupes a texture shared across meshes and materials', () => {
59
+ const shared = createTexture({ resource: embeddedRef() });
60
+ const scene = createScene3D();
61
+ const a = createMesh(createBoxMeshGeometry(), [
62
+ createUnlitMaterial({ baseColorMap: shared }),
63
+ createStandardPbrMaterial({ baseColorMap: shared }),
64
+ ]);
65
+ const b = createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: shared })]);
66
+ addNodeChild(scene.root, a);
67
+ addNodeChild(scene.root, b);
68
+
69
+ const out: Texture[] = [];
70
+ getScene3DResourceTextures(scene.root, registry(), out);
71
+ expect(out).toEqual([shared]);
72
+ });
73
+
74
+ it('ignores a texture that carries no resource ref', () => {
75
+ const bound = createTexture();
76
+ const pending = createTexture({ resource: embeddedRef() });
77
+ const mesh = createMesh(createBoxMeshGeometry(), [
78
+ createStandardPbrMaterial({ baseColorMap: bound, normalMap: pending }),
79
+ ]);
80
+
81
+ const out: Texture[] = [];
82
+ getScene3DResourceTextures(mesh, registry(), out);
83
+ expect(out).toEqual([pending]);
84
+ });
85
+
86
+ it('skips null material slots', () => {
87
+ const map = createTexture({ resource: embeddedRef() });
88
+ const mesh = createMesh(createBoxMeshGeometry(), [null, createUnlitMaterial({ baseColorMap: map })]);
89
+ const out: Texture[] = [];
90
+ getScene3DResourceTextures(mesh, registry(), out);
91
+ expect(out).toEqual([map]);
92
+ });
93
+
94
+ it('clears out before filling it', () => {
95
+ const map = createTexture({ resource: embeddedRef() });
96
+ const mesh = createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: map })]);
97
+ const out: Texture[] = [createTexture(), createTexture()];
98
+ getScene3DResourceTextures(mesh, registry(), out);
99
+ expect(out).toEqual([map]);
100
+ });
101
+ });
@@ -0,0 +1,118 @@
1
+ import type * as NetModule from '@flighthq/net';
2
+ import type * as Scene3DFormatsModule from '@flighthq/scene3d-formats';
3
+ import type { NetResponse, Scene3DDocument } from '@flighthq/types';
4
+ import type { Mock } from 'vitest';
5
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
6
+
7
+ import type * as LoadGltfModule from './gltfLoad';
8
+
9
+ let loadScene3DDocumentFromGlbUrl: typeof LoadGltfModule.loadScene3DDocumentFromGlbUrl;
10
+ let loadScene3DDocumentFromGltfUrl: typeof LoadGltfModule.loadScene3DDocumentFromGltfUrl;
11
+ let parseGlb: Mock<typeof Scene3DFormatsModule.parseGlb>;
12
+ let parseGltf: Mock<typeof Scene3DFormatsModule.parseGltf>;
13
+ let sendNetRequest: Mock<typeof NetModule.sendNetRequest>;
14
+
15
+ function emptyDocument(): Scene3DDocument {
16
+ return {
17
+ animations: [],
18
+ cameras: [],
19
+ lights: [],
20
+ materials: [],
21
+ meshes: [],
22
+ metadata: null,
23
+ nodes: [],
24
+ resources: [],
25
+ scenes: [],
26
+ skins: [],
27
+ };
28
+ }
29
+
30
+ function response(body: string | ArrayBuffer, url = 'u'): NetResponse {
31
+ return { body, headers: {}, ok: true, status: 200, statusText: 'OK', url };
32
+ }
33
+
34
+ beforeAll(async () => {
35
+ vi.resetModules();
36
+ parseGlb = vi.fn<typeof Scene3DFormatsModule.parseGlb>();
37
+ parseGltf = vi.fn<typeof Scene3DFormatsModule.parseGltf>();
38
+ sendNetRequest = vi.fn<typeof NetModule.sendNetRequest>();
39
+ vi.doMock('@flighthq/net', () => ({ sendNetRequest }));
40
+ vi.doMock('@flighthq/scene3d-formats', () => ({ parseGlb, parseGltf }));
41
+ ({ loadScene3DDocumentFromGlbUrl, loadScene3DDocumentFromGltfUrl } = await import('./gltfLoad'));
42
+ });
43
+
44
+ afterAll(() => {
45
+ vi.doUnmock('@flighthq/net');
46
+ vi.doUnmock('@flighthq/scene3d-formats');
47
+ vi.resetModules();
48
+ });
49
+
50
+ afterEach(() => {
51
+ parseGlb.mockReset();
52
+ parseGltf.mockReset();
53
+ sendNetRequest.mockReset();
54
+ });
55
+
56
+ describe('loadScene3DDocumentFromGlbUrl', () => {
57
+ it('fetches bytes, carries the source base path, and returns a CPU document', async () => {
58
+ const document = emptyDocument();
59
+ parseGlb.mockReturnValue(document);
60
+ sendNetRequest.mockResolvedValue(response(new Uint8Array([1, 2, 3]).buffer));
61
+
62
+ const loaded = await loadScene3DDocumentFromGlbUrl('models/ship.glb');
63
+
64
+ expect(Array.from(parseGlb.mock.calls[0][0])).toEqual([1, 2, 3]);
65
+ expect(parseGlb.mock.calls[0][2]).toEqual({ basePath: 'models' });
66
+ expect(loaded).toBe(document);
67
+ });
68
+
69
+ it('returns null rather than an empty document on transport failure', async () => {
70
+ sendNetRequest.mockResolvedValue({
71
+ body: null,
72
+ headers: {},
73
+ ok: false,
74
+ status: 404,
75
+ statusText: 'x',
76
+ url: 'u',
77
+ });
78
+
79
+ await expect(loadScene3DDocumentFromGlbUrl('missing.glb')).resolves.toBeNull();
80
+ expect(parseGlb).not.toHaveBeenCalled();
81
+ });
82
+ });
83
+
84
+ describe('loadScene3DDocumentFromGltfUrl', () => {
85
+ it('fetches external geometry buffers and supplies the image base path to parsing', async () => {
86
+ const document = emptyDocument();
87
+ parseGltf.mockReturnValue(document);
88
+ const requested: string[] = [];
89
+ sendNetRequest.mockImplementation(async (request) => {
90
+ requested.push(request.url);
91
+ return request.url.endsWith('.gltf')
92
+ ? response('{"asset":{"version":"2.0"},"buffers":[{"byteLength":2,"uri":"mesh.bin"}]}')
93
+ : response(new Uint8Array([8, 9]).buffer);
94
+ });
95
+
96
+ const loaded = await loadScene3DDocumentFromGltfUrl('models/ship.gltf');
97
+
98
+ expect(requested).toEqual(['models/ship.gltf', 'models/mesh.bin']);
99
+ expect(parseGltf.mock.calls[0][2]).toEqual({
100
+ basePath: 'models',
101
+ externalBuffers: { 'mesh.bin': new Uint8Array([8, 9]) },
102
+ });
103
+ expect(loaded).toBe(document);
104
+ });
105
+
106
+ it('returns null when JSON or a required external buffer cannot load', async () => {
107
+ sendNetRequest.mockResolvedValue(response('{'));
108
+ await expect(loadScene3DDocumentFromGltfUrl('broken.gltf')).resolves.toBeNull();
109
+
110
+ sendNetRequest.mockImplementation(async (request) =>
111
+ request.url.endsWith('.gltf')
112
+ ? response('{"asset":{"version":"2.0"},"buffers":[{"byteLength":2,"uri":"missing.bin"}]}')
113
+ : { body: null, headers: {}, ok: false, status: 404, statusText: 'x', url: request.url },
114
+ );
115
+ await expect(loadScene3DDocumentFromGltfUrl('models/ship.gltf')).resolves.toBeNull();
116
+ expect(parseGltf).not.toHaveBeenCalled();
117
+ });
118
+ });
@@ -0,0 +1,95 @@
1
+ import type * as ImageModule from '@flighthq/image';
2
+ import type { ExternalImageResourceReference, ImageResource } from '@flighthq/types';
3
+ import { ResourceResolutionState, ImageResourceReferenceKind } from '@flighthq/types';
4
+ import type { Mock } from 'vitest';
5
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
6
+
7
+ import type * as ImageResourceFetchModule from './imageResourceFetch';
8
+
9
+ const fakeImage = { height: 1, width: 1 } as unknown as ImageResource;
10
+ type LoadImageResourceFromUrl = typeof ImageModule.loadImageResourceFromUrl;
11
+
12
+ let fetchWebImageResource: typeof ImageResourceFetchModule.fetchWebImageResource;
13
+ let loadFromUrl: Mock<LoadImageResourceFromUrl>;
14
+ let resolveImageResourceUri: typeof ImageResourceFetchModule.resolveImageResourceUri;
15
+
16
+ function externalRef(uri: string, basePath: string | null): ExternalImageResourceReference {
17
+ return {
18
+ basePath,
19
+ failure: null,
20
+ kind: ImageResourceReferenceKind.External,
21
+ mimeType: null,
22
+ state: ResourceResolutionState.Unresolved,
23
+ uri,
24
+ };
25
+ }
26
+
27
+ beforeAll(async () => {
28
+ vi.resetModules();
29
+ loadFromUrl = vi.fn<LoadImageResourceFromUrl>();
30
+ vi.doMock('@flighthq/image', () => ({
31
+ loadImageResourceFromBytes: vi.fn(),
32
+ loadImageResourceFromUrl: loadFromUrl,
33
+ }));
34
+ ({ fetchWebImageResource, resolveImageResourceUri } = await import('./imageResourceFetch'));
35
+ });
36
+
37
+ afterAll(() => {
38
+ vi.doUnmock('@flighthq/image');
39
+ vi.resetModules();
40
+ });
41
+
42
+ afterEach(() => {
43
+ loadFromUrl.mockReset();
44
+ });
45
+
46
+ describe('fetchWebImageResource', () => {
47
+ it('fetches the resolved URL and returns the decoded image', async () => {
48
+ loadFromUrl.mockResolvedValue(fakeImage);
49
+ const result = await fetchWebImageResource(
50
+ externalRef('leaf.png', 'assets/textures'),
51
+ new AbortController().signal,
52
+ );
53
+ expect(loadFromUrl).toHaveBeenCalledWith('assets/textures/leaf.png', undefined, expect.anything());
54
+ expect(result).toBe(fakeImage);
55
+ });
56
+
57
+ it('returns null on a non-abort failure', async () => {
58
+ loadFromUrl.mockRejectedValue(new Error('404'));
59
+ const result = await fetchWebImageResource(externalRef('missing.png', null), new AbortController().signal);
60
+ expect(result).toBeNull();
61
+ });
62
+
63
+ it('rethrows when the signal aborted (a cancellation, not a failure)', async () => {
64
+ const controller = new AbortController();
65
+ controller.abort();
66
+ loadFromUrl.mockRejectedValue(new Error('aborted'));
67
+ await expect(fetchWebImageResource(externalRef('x.png', null), controller.signal)).rejects.toThrow();
68
+ });
69
+ });
70
+
71
+ describe('resolveImageResourceUri', () => {
72
+ it('joins a relative uri to a base path', () => {
73
+ expect(resolveImageResourceUri('leaf.png', 'assets/tex')).toBe('assets/tex/leaf.png');
74
+ });
75
+
76
+ it('does not double the separator when the base ends with a slash', () => {
77
+ expect(resolveImageResourceUri('leaf.png', 'assets/tex/')).toBe('assets/tex/leaf.png');
78
+ });
79
+
80
+ it('keeps a scheme-absolute uri verbatim', () => {
81
+ expect(resolveImageResourceUri('https://cdn.test/leaf.png', 'assets/tex')).toBe('https://cdn.test/leaf.png');
82
+ });
83
+
84
+ it('keeps a data uri verbatim', () => {
85
+ expect(resolveImageResourceUri('data:image/png;base64,AAAA', 'assets/tex')).toBe('data:image/png;base64,AAAA');
86
+ });
87
+
88
+ it('keeps a root-absolute uri verbatim', () => {
89
+ expect(resolveImageResourceUri('/textures/leaf.png', 'assets/tex')).toBe('/textures/leaf.png');
90
+ });
91
+
92
+ it('returns a relative uri unchanged when the base path is null', () => {
93
+ expect(resolveImageResourceUri('leaf.png', null)).toBe('leaf.png');
94
+ });
95
+ });
@@ -0,0 +1,63 @@
1
+ import type * as NetModule from '@flighthq/net';
2
+ import type * as Scene3DFormatsModule from '@flighthq/scene3d-formats';
3
+ import type { NetResponse, Scene3DDocument } from '@flighthq/types';
4
+ import type { Mock } from 'vitest';
5
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
6
+
7
+ import type * as LoadMd2Module from './md2Load';
8
+
9
+ let loadScene3DDocumentFromMd2Url: typeof LoadMd2Module.loadScene3DDocumentFromMd2Url;
10
+ let parseMd2: Mock<typeof Scene3DFormatsModule.parseMd2>;
11
+ let sendNetRequest: Mock<typeof NetModule.sendNetRequest>;
12
+
13
+ function emptyDocument(): Scene3DDocument {
14
+ return {
15
+ animations: [],
16
+ cameras: [],
17
+ lights: [],
18
+ materials: [],
19
+ meshes: [],
20
+ metadata: null,
21
+ nodes: [],
22
+ resources: [],
23
+ scenes: [],
24
+ skins: [],
25
+ };
26
+ }
27
+
28
+ function okResponse(body: ArrayBuffer): NetResponse {
29
+ return { body, headers: {}, ok: true, status: 200, statusText: 'OK', url: 'u' };
30
+ }
31
+
32
+ beforeAll(async () => {
33
+ vi.resetModules();
34
+ parseMd2 = vi.fn<typeof Scene3DFormatsModule.parseMd2>();
35
+ sendNetRequest = vi.fn<typeof NetModule.sendNetRequest>();
36
+ vi.doMock('@flighthq/net', () => ({ sendNetRequest }));
37
+ vi.doMock('@flighthq/scene3d-formats', () => ({ parseMd2 }));
38
+ ({ loadScene3DDocumentFromMd2Url } = await import('./md2Load'));
39
+ });
40
+
41
+ afterAll(() => {
42
+ vi.doUnmock('@flighthq/net');
43
+ vi.doUnmock('@flighthq/scene3d-formats');
44
+ vi.resetModules();
45
+ });
46
+
47
+ afterEach(() => {
48
+ parseMd2.mockReset();
49
+ sendNetRequest.mockReset();
50
+ });
51
+
52
+ describe('loadScene3DDocumentFromMd2Url', () => {
53
+ it('fetches bytes and returns the parsed CPU document without resolving resources', async () => {
54
+ const document = emptyDocument();
55
+ parseMd2.mockReturnValue(document);
56
+ sendNetRequest.mockResolvedValue(okResponse(new Uint8Array([7]).buffer));
57
+
58
+ const loaded = await loadScene3DDocumentFromMd2Url('model.md2');
59
+
60
+ expect(Array.from(parseMd2.mock.calls[0][0])).toEqual([7]);
61
+ expect(loaded).toBe(document);
62
+ });
63
+ });
@@ -0,0 +1,63 @@
1
+ import type * as NetModule from '@flighthq/net';
2
+ import type * as Scene3DFormatsModule from '@flighthq/scene3d-formats';
3
+ import type { NetResponse, Scene3DDocument } from '@flighthq/types';
4
+ import type { Mock } from 'vitest';
5
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
6
+
7
+ import type * as LoadMd5Module from './md5Load';
8
+
9
+ let loadScene3DDocumentFromMd5MeshUrl: typeof LoadMd5Module.loadScene3DDocumentFromMd5MeshUrl;
10
+ let parseMd5Mesh: Mock<typeof Scene3DFormatsModule.parseMd5Mesh>;
11
+ let sendNetRequest: Mock<typeof NetModule.sendNetRequest>;
12
+
13
+ function emptyDocument(): Scene3DDocument {
14
+ return {
15
+ animations: [],
16
+ cameras: [],
17
+ lights: [],
18
+ materials: [],
19
+ meshes: [],
20
+ metadata: null,
21
+ nodes: [],
22
+ resources: [],
23
+ scenes: [],
24
+ skins: [],
25
+ };
26
+ }
27
+
28
+ function okResponse(body: string): NetResponse {
29
+ return { body, headers: {}, ok: true, status: 200, statusText: 'OK', url: 'u' };
30
+ }
31
+
32
+ beforeAll(async () => {
33
+ vi.resetModules();
34
+ parseMd5Mesh = vi.fn<typeof Scene3DFormatsModule.parseMd5Mesh>();
35
+ sendNetRequest = vi.fn<typeof NetModule.sendNetRequest>();
36
+ vi.doMock('@flighthq/net', () => ({ sendNetRequest }));
37
+ vi.doMock('@flighthq/scene3d-formats', () => ({ parseMd5Mesh }));
38
+ ({ loadScene3DDocumentFromMd5MeshUrl } = await import('./md5Load'));
39
+ });
40
+
41
+ afterAll(() => {
42
+ vi.doUnmock('@flighthq/net');
43
+ vi.doUnmock('@flighthq/scene3d-formats');
44
+ vi.resetModules();
45
+ });
46
+
47
+ afterEach(() => {
48
+ parseMd5Mesh.mockReset();
49
+ sendNetRequest.mockReset();
50
+ });
51
+
52
+ describe('loadScene3DDocumentFromMd5MeshUrl', () => {
53
+ it('fetches text and returns the parsed CPU document without resolving resources', async () => {
54
+ const document = emptyDocument();
55
+ parseMd5Mesh.mockReturnValue(document);
56
+ sendNetRequest.mockResolvedValue(okResponse('MD5Version 10'));
57
+
58
+ const loaded = await loadScene3DDocumentFromMd5MeshUrl('model.md5mesh');
59
+
60
+ expect(parseMd5Mesh).toHaveBeenCalledWith('MD5Version 10');
61
+ expect(loaded).toBe(document);
62
+ });
63
+ });
@@ -0,0 +1,63 @@
1
+ import type * as NetModule from '@flighthq/net';
2
+ import type * as Scene3DFormatsModule from '@flighthq/scene3d-formats';
3
+ import type { NetResponse, Scene3DDocument } from '@flighthq/types';
4
+ import type { Mock } from 'vitest';
5
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
6
+
7
+ import type * as LoadObjModule from './objLoad';
8
+
9
+ let loadScene3DDocumentFromObjUrl: typeof LoadObjModule.loadScene3DDocumentFromObjUrl;
10
+ let parseObj: Mock<typeof Scene3DFormatsModule.parseObj>;
11
+ let sendNetRequest: Mock<typeof NetModule.sendNetRequest>;
12
+
13
+ function emptyDocument(): Scene3DDocument {
14
+ return {
15
+ animations: [],
16
+ cameras: [],
17
+ lights: [],
18
+ materials: [],
19
+ meshes: [],
20
+ metadata: null,
21
+ nodes: [],
22
+ resources: [],
23
+ scenes: [],
24
+ skins: [],
25
+ };
26
+ }
27
+
28
+ function okResponse(body: string): NetResponse {
29
+ return { body, headers: {}, ok: true, status: 200, statusText: 'OK', url: 'u' };
30
+ }
31
+
32
+ beforeAll(async () => {
33
+ vi.resetModules();
34
+ parseObj = vi.fn<typeof Scene3DFormatsModule.parseObj>();
35
+ sendNetRequest = vi.fn<typeof NetModule.sendNetRequest>();
36
+ vi.doMock('@flighthq/net', () => ({ sendNetRequest }));
37
+ vi.doMock('@flighthq/scene3d-formats', () => ({ parseObj }));
38
+ ({ loadScene3DDocumentFromObjUrl } = await import('./objLoad'));
39
+ });
40
+
41
+ afterAll(() => {
42
+ vi.doUnmock('@flighthq/net');
43
+ vi.doUnmock('@flighthq/scene3d-formats');
44
+ vi.resetModules();
45
+ });
46
+
47
+ afterEach(() => {
48
+ parseObj.mockReset();
49
+ sendNetRequest.mockReset();
50
+ });
51
+
52
+ describe('loadScene3DDocumentFromObjUrl', () => {
53
+ it('fetches text and returns the parsed CPU document without resolving resources', async () => {
54
+ const document = emptyDocument();
55
+ parseObj.mockReturnValue(document);
56
+ sendNetRequest.mockResolvedValue(okResponse('v 0 0 0'));
57
+
58
+ const loaded = await loadScene3DDocumentFromObjUrl('model.obj');
59
+
60
+ expect(parseObj).toHaveBeenCalledWith('v 0 0 0', undefined);
61
+ expect(loaded).toBe(document);
62
+ });
63
+ });