@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,116 @@
1
+ import { setNetBackend } from '@flighthq/net';
2
+ import { connectSignal, createSignal, emitSignal } from '@flighthq/signals';
3
+ import type { NetResponse, Scene3DDocument, Scene3DDocumentLoadProgress } from '@flighthq/types';
4
+ import { ImageResourceReferenceKind, ResourceResolutionState } from '@flighthq/types';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+
7
+ import {
8
+ getScene3DDocumentBasePathFromUrl,
9
+ loadScene3DDocumentBytesFromUrl,
10
+ loadScene3DDocumentTextFromUrl,
11
+ setScene3DDocumentResourceBasePathFromUrl,
12
+ } from './sceneDocumentSource';
13
+
14
+ function okResponse(body: string | ArrayBuffer): NetResponse {
15
+ return { body, headers: {}, ok: true, status: 200, statusText: 'OK', url: 'u' };
16
+ }
17
+
18
+ function failResponse(): NetResponse {
19
+ return { body: null, headers: {}, ok: false, status: 404, statusText: 'Not Found', url: 'u' };
20
+ }
21
+
22
+ afterEach(() => {
23
+ setNetBackend(null);
24
+ });
25
+
26
+ describe('getScene3DDocumentBasePathFromUrl', () => {
27
+ it('returns the containing path without query or fragment data', () => {
28
+ expect(getScene3DDocumentBasePathFromUrl('models/ship.gltf?cache=1')).toBe('models');
29
+ expect(getScene3DDocumentBasePathFromUrl('ship.gltf')).toBeNull();
30
+ });
31
+ });
32
+
33
+ describe('loadScene3DDocumentBytesFromUrl', () => {
34
+ it('forwards cancellation and identifies byte-progress events by URL', async () => {
35
+ const controller = new AbortController();
36
+ const events: Scene3DDocumentLoadProgress[] = [];
37
+ const progress = createSignal<(event: Readonly<Scene3DDocumentLoadProgress>) => void>();
38
+ connectSignal(progress, (event) => events.push({ ...event }));
39
+ setNetBackend({
40
+ sendNetRequest: async (_request, options) => {
41
+ expect(options?.signal).toBe(controller.signal);
42
+ emitSignal(options!.progress!, { loaded: 2, phase: 'download', total: 3 });
43
+ return okResponse(new Uint8Array([1, 2, 3]).buffer);
44
+ },
45
+ });
46
+
47
+ const bytes = await loadScene3DDocumentBytesFromUrl('model.bin', { progress, signal: controller.signal });
48
+
49
+ expect(Array.from(bytes!)).toEqual([1, 2, 3]);
50
+ expect(events).toEqual([{ loaded: 2, phase: 'download', total: 3, url: 'model.bin' }]);
51
+ });
52
+
53
+ it('returns null on an expected transport failure', async () => {
54
+ setNetBackend({ sendNetRequest: async () => failResponse() });
55
+ await expect(loadScene3DDocumentBytesFromUrl('missing.bin')).resolves.toBeNull();
56
+ });
57
+ });
58
+
59
+ describe('loadScene3DDocumentTextFromUrl', () => {
60
+ it('fetches the URL as text and returns the string', async () => {
61
+ let requestedType: string | undefined;
62
+ setNetBackend({
63
+ sendNetRequest: async (request) => {
64
+ requestedType = request.responseType;
65
+ return okResponse('v 0 0 0');
66
+ },
67
+ });
68
+
69
+ await expect(loadScene3DDocumentTextFromUrl('model.obj')).resolves.toBe('v 0 0 0');
70
+ expect(requestedType).toBe('text');
71
+ });
72
+
73
+ it('returns null on an expected transport failure', async () => {
74
+ setNetBackend({ sendNetRequest: async () => failResponse() });
75
+ await expect(loadScene3DDocumentTextFromUrl('missing.obj')).resolves.toBeNull();
76
+ });
77
+ });
78
+
79
+ describe('setScene3DDocumentResourceBasePathFromUrl', () => {
80
+ it('sets only unresolved relative resources whose parser did not already supply a base path', () => {
81
+ const document = {
82
+ animations: [],
83
+ cameras: [],
84
+ lights: [],
85
+ materials: [],
86
+ meshes: [],
87
+ metadata: null,
88
+ nodes: [],
89
+ scenes: [],
90
+ skins: [],
91
+ resources: [
92
+ {
93
+ basePath: null,
94
+ failure: null,
95
+ kind: ImageResourceReferenceKind.External,
96
+ mimeType: null,
97
+ state: ResourceResolutionState.Unresolved,
98
+ uri: 'skin.png',
99
+ },
100
+ {
101
+ basePath: 'authored',
102
+ failure: null,
103
+ kind: ImageResourceReferenceKind.External,
104
+ mimeType: null,
105
+ state: ResourceResolutionState.Unresolved,
106
+ uri: 'fixed.png',
107
+ },
108
+ ],
109
+ } satisfies Scene3DDocument;
110
+
111
+ setScene3DDocumentResourceBasePathFromUrl(document, 'models/ship.obj');
112
+
113
+ expect(document.resources[0].basePath).toBe('models');
114
+ expect(document.resources[1].basePath).toBe('authored');
115
+ });
116
+ });
@@ -0,0 +1,95 @@
1
+ import { createStandardPbrMaterial, createUnlitMaterial } from '@flighthq/materials';
2
+ import { createTexture } from '@flighthq/texture';
3
+ import type { Material, Texture } from '@flighthq/types';
4
+ import { EntityRuntimeKey, StandardPbrMaterialKind, UnlitMaterialKind } from '@flighthq/types';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import {
8
+ createScene3DMaterialTextureRegistry,
9
+ getScene3DMaterialTextures,
10
+ registerBuiltInScene3DMaterialTextures,
11
+ registerScene3DMaterialTextures,
12
+ } from './sceneMaterialTextureRegistry';
13
+
14
+ describe('createScene3DMaterialTextureRegistry', () => {
15
+ it('creates an empty registry', () => {
16
+ const registry = createScene3DMaterialTextureRegistry();
17
+ expect(EntityRuntimeKey in registry).toBe(true);
18
+ expect(registry.listers.size).toBe(0);
19
+ });
20
+ });
21
+
22
+ describe('getScene3DMaterialTextures', () => {
23
+ it('appends nothing for an unregistered kind', () => {
24
+ const registry = createScene3DMaterialTextureRegistry();
25
+ const out: Texture[] = [];
26
+ getScene3DMaterialTextures(registry, createUnlitMaterial(), out);
27
+ expect(out).toHaveLength(0);
28
+ });
29
+
30
+ it('accumulates across calls without clearing out', () => {
31
+ const registry = createScene3DMaterialTextureRegistry();
32
+ registerBuiltInScene3DMaterialTextures(registry);
33
+ const a = createTexture();
34
+ const b = createTexture();
35
+ const out: Texture[] = [];
36
+ getScene3DMaterialTextures(registry, createUnlitMaterial({ baseColorMap: a }), out);
37
+ getScene3DMaterialTextures(registry, createUnlitMaterial({ baseColorMap: b }), out);
38
+ expect(out).toEqual([a, b]);
39
+ });
40
+ });
41
+
42
+ describe('registerBuiltInScene3DMaterialTextures', () => {
43
+ it('registers the standard-pbr and unlit listers', () => {
44
+ const registry = createScene3DMaterialTextureRegistry();
45
+ registerBuiltInScene3DMaterialTextures(registry);
46
+ expect(registry.listers.has(StandardPbrMaterialKind)).toBe(true);
47
+ expect(registry.listers.has(UnlitMaterialKind)).toBe(true);
48
+ });
49
+
50
+ it('lists every non-null standard-pbr texture slot and skips null ones', () => {
51
+ const registry = createScene3DMaterialTextureRegistry();
52
+ registerBuiltInScene3DMaterialTextures(registry);
53
+ const baseColorMap = createTexture();
54
+ const normalMap = createTexture();
55
+ const material = createStandardPbrMaterial({ baseColorMap, normalMap });
56
+ const out: Texture[] = [];
57
+ getScene3DMaterialTextures(registry, material, out);
58
+ expect(out).toContain(baseColorMap);
59
+ expect(out).toContain(normalMap);
60
+ expect(out).toHaveLength(2);
61
+ });
62
+
63
+ it('lists the unlit base-color slot', () => {
64
+ const registry = createScene3DMaterialTextureRegistry();
65
+ registerBuiltInScene3DMaterialTextures(registry);
66
+ const baseColorMap = createTexture();
67
+ const out: Texture[] = [];
68
+ getScene3DMaterialTextures(registry, createUnlitMaterial({ baseColorMap }), out);
69
+ expect(out).toEqual([baseColorMap]);
70
+ });
71
+ });
72
+
73
+ describe('registerScene3DMaterialTextures', () => {
74
+ it('binds a lister for a custom kind', () => {
75
+ const registry = createScene3DMaterialTextureRegistry();
76
+ const custom = createTexture();
77
+ registerScene3DMaterialTextures(registry, 'acme.Custom', (_material: Readonly<Material>, out: Texture[]) => {
78
+ out.push(custom);
79
+ });
80
+ const out: Texture[] = [];
81
+ getScene3DMaterialTextures(registry, { kind: 'acme.Custom' } as unknown as Material, out);
82
+ expect(out).toEqual([custom]);
83
+ });
84
+
85
+ it('is last-write-wins with no guard', () => {
86
+ const registry = createScene3DMaterialTextureRegistry();
87
+ const first = createTexture();
88
+ const second = createTexture();
89
+ registerScene3DMaterialTextures(registry, UnlitMaterialKind, (_m, out) => out.push(first));
90
+ registerScene3DMaterialTextures(registry, UnlitMaterialKind, (_m, out) => out.push(second));
91
+ const out: Texture[] = [];
92
+ getScene3DMaterialTextures(registry, createUnlitMaterial(), out);
93
+ expect(out).toEqual([second]);
94
+ });
95
+ });
@@ -0,0 +1,98 @@
1
+ import { 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 { ImageResource, ImageResourceReference } from '@flighthq/types';
7
+ import { ImageResourceFailureKind, ImageResourceReferenceKind, ResourceResolutionState } from '@flighthq/types';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+
10
+ import { loadScene3DResources } from './resolveScene3DResourcesAndWait';
11
+ import { waitForScene3DResourceResolver } from './resolveScene3DResourcesAndWait';
12
+ import {
13
+ explainImageResourceReferenceResolution,
14
+ resetFailedImageResourceReference,
15
+ retryFailedScene3DResources,
16
+ } from './sceneResourceRecovery';
17
+ import { createBuiltInScene3DResourceResolver, disposeScene3DResourceResolver } from './sceneResourceResolver';
18
+
19
+ const fakeImage = { height: 1, width: 1 } as ImageResource;
20
+
21
+ function externalRef(state: ResourceResolutionState = ResourceResolutionState.Unresolved): ImageResourceReference {
22
+ return {
23
+ basePath: null,
24
+ failure: null,
25
+ kind: ImageResourceReferenceKind.External,
26
+ mimeType: null,
27
+ state,
28
+ uri: 'leaf.png',
29
+ };
30
+ }
31
+
32
+ describe('explainImageResourceReferenceResolution', () => {
33
+ it('returns detached plain data with retry eligibility and the preserved cause', () => {
34
+ const ref = externalRef(ResourceResolutionState.Failed);
35
+ ref.failure = { kind: ImageResourceFailureKind.Error, message: 'decode failed', name: 'CodecError' };
36
+ const explanation = explainImageResourceReferenceResolution(ref);
37
+ expect(explanation).toEqual({
38
+ failure: { kind: ImageResourceFailureKind.Error, message: 'decode failed', name: 'CodecError' },
39
+ kind: ImageResourceReferenceKind.External,
40
+ retryable: true,
41
+ state: ResourceResolutionState.Failed,
42
+ });
43
+ expect(explanation.failure).not.toBe(ref.failure);
44
+ });
45
+ });
46
+
47
+ describe('resetFailedImageResourceReference', () => {
48
+ it('clears a failed cause and returns the reference to Unresolved', () => {
49
+ const ref = externalRef(ResourceResolutionState.Failed);
50
+ ref.failure = { kind: ImageResourceFailureKind.Unavailable, message: 'missing', name: null };
51
+ expect(resetFailedImageResourceReference(ref)).toBe(true);
52
+ expect(ref.state).toBe(ResourceResolutionState.Unresolved);
53
+ expect(ref.failure).toBeNull();
54
+ });
55
+
56
+ it('does not invalidate a non-failed reference', () => {
57
+ const ref = externalRef(ResourceResolutionState.Resolved);
58
+ expect(resetFailedImageResourceReference(ref)).toBe(false);
59
+ expect(ref.state).toBe(ResourceResolutionState.Resolved);
60
+ });
61
+ });
62
+
63
+ describe('retryFailedScene3DResources', () => {
64
+ it('resets one shared failed identity and re-requests it for every subscriber', async () => {
65
+ const fetch = vi.fn<() => Promise<ImageResource | null>>().mockResolvedValueOnce(null).mockResolvedValue(fakeImage);
66
+ const ref = externalRef();
67
+ const a = createTexture({ resource: ref });
68
+ const b = createTexture({ resource: ref });
69
+ const scene = createScene3D();
70
+ addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: a })]));
71
+ addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: b })]));
72
+ const resolver = createBuiltInScene3DResourceResolver({ fetch });
73
+
74
+ await loadScene3DResources(scene.root, resolver);
75
+ expect(ref.state).toBe(ResourceResolutionState.Failed);
76
+ expect(ref.failure?.kind).toBe(ImageResourceFailureKind.Unavailable);
77
+
78
+ expect(retryFailedScene3DResources(scene.root, resolver)).toBe(1);
79
+ await waitForScene3DResourceResolver(resolver);
80
+ expect(fetch).toHaveBeenCalledTimes(2);
81
+ expect(ref.failure).toBeNull();
82
+ expect(a.image).toBe(fakeImage);
83
+ expect(b.image).toBe(fakeImage);
84
+ disposeScene3DResourceResolver(resolver);
85
+ });
86
+
87
+ it('honors selection and leaves an excluded failure untouched', () => {
88
+ const ref = externalRef(ResourceResolutionState.Failed);
89
+ ref.failure = { kind: ImageResourceFailureKind.Unavailable, message: 'missing', name: null };
90
+ const texture = createTexture({ resource: ref });
91
+ const scene = createScene3D();
92
+ addNodeChild(scene.root, createMesh(createBoxMeshGeometry(), [createUnlitMaterial({ baseColorMap: texture })]));
93
+ const resolver = createBuiltInScene3DResourceResolver();
94
+ expect(retryFailedScene3DResources(scene.root, resolver, { select: () => false })).toBe(0);
95
+ expect(ref.state).toBe(ResourceResolutionState.Failed);
96
+ disposeScene3DResourceResolver(resolver);
97
+ });
98
+ });
@@ -0,0 +1,80 @@
1
+ import { createTexture } from '@flighthq/texture';
2
+ import {
3
+ ResourceResolutionState,
4
+ EntityRuntimeKey,
5
+ ImageResourceReferenceKind,
6
+ StandardPbrMaterialKind,
7
+ UnlitMaterialKind,
8
+ } from '@flighthq/types';
9
+ import { Scene3DResourceResolverRuntimeKey } from '@flighthq/types';
10
+ import type { Scene3DResourceInFlight, Scene3DResourceResolverWithRuntime } from '@flighthq/types';
11
+ import { describe, expect, it } from 'vitest';
12
+
13
+ import { createScene3DMaterialTextureRegistry } from './sceneMaterialTextureRegistry';
14
+ import {
15
+ createBuiltInScene3DResourceResolver,
16
+ createScene3DResourceResolver,
17
+ disposeScene3DResourceResolver,
18
+ } from './sceneResourceResolver';
19
+
20
+ describe('createBuiltInScene3DResourceResolver', () => {
21
+ it('assembles Standard PBR and Unlit discovery only through the explicit built-in constructor', () => {
22
+ const resolver = createBuiltInScene3DResourceResolver();
23
+ expect(resolver.registry.listers.has(StandardPbrMaterialKind)).toBe(true);
24
+ expect(resolver.registry.listers.has(UnlitMaterialKind)).toBe(true);
25
+ disposeScene3DResourceResolver(resolver);
26
+ });
27
+ });
28
+
29
+ describe('createScene3DResourceResolver', () => {
30
+ it('creates an Entity with an empty registry and private runtime machinery', () => {
31
+ const resolver = createScene3DResourceResolver();
32
+ const runtime = (resolver as Scene3DResourceResolverWithRuntime)[Scene3DResourceResolverRuntimeKey];
33
+ expect(EntityRuntimeKey in resolver).toBe(true);
34
+ expect(resolver.registry.listers.size).toBe(0);
35
+ expect(typeof resolver.fetch).toBe('function');
36
+ expect(runtime.inFlight.size).toBe(0);
37
+ expect(runtime.signals).toBeNull();
38
+ expect(runtime.loader).toBeDefined();
39
+ disposeScene3DResourceResolver(resolver);
40
+ });
41
+ });
42
+
43
+ describe('createScene3DResourceResolver options', () => {
44
+ it('uses a caller-supplied registry and fetch wholesale', () => {
45
+ const registry = createScene3DMaterialTextureRegistry();
46
+ const fetch = async () => null;
47
+ const resolver = createScene3DResourceResolver({ fetch, registry });
48
+ expect(resolver.registry).toBe(registry);
49
+ expect(resolver.registry.listers.has(UnlitMaterialKind)).toBe(false);
50
+ expect(resolver.fetch).toBe(fetch);
51
+ disposeScene3DResourceResolver(resolver);
52
+ });
53
+ });
54
+
55
+ describe('disposeScene3DResourceResolver', () => {
56
+ it('aborts every in-flight controller and clears the map', () => {
57
+ const resolver = createScene3DResourceResolver();
58
+ const controller = new AbortController();
59
+ const texture = createTexture({
60
+ resource: {
61
+ bytes: new Uint8Array(0),
62
+ failure: null,
63
+ kind: ImageResourceReferenceKind.Embedded,
64
+ mimeType: null,
65
+ state: ResourceResolutionState.Loading,
66
+ },
67
+ });
68
+ const entry: Scene3DResourceInFlight = {
69
+ controller,
70
+ promise: Promise.resolve(),
71
+ subscribers: new Set([texture]),
72
+ };
73
+ const runtime = (resolver as Scene3DResourceResolverWithRuntime)[Scene3DResourceResolverRuntimeKey];
74
+ runtime.inFlight.set(texture.resource!, entry);
75
+
76
+ disposeScene3DResourceResolver(resolver);
77
+ expect(controller.signal.aborted).toBe(true);
78
+ expect(runtime.inFlight.size).toBe(0);
79
+ });
80
+ });
@@ -0,0 +1,51 @@
1
+ // @vitest-environment node
2
+
3
+ import { build } from 'esbuild';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ const resolveDir = getFileUrlDirectory(import.meta.url);
7
+
8
+ async function bundleScene3DResourceResolverExport(name: string): Promise<string> {
9
+ const result = await build({
10
+ bundle: true,
11
+ format: 'esm',
12
+ logLevel: 'silent',
13
+ minify: true,
14
+ packages: 'external',
15
+ stdin: {
16
+ contents: `export { ${name} } from './index.ts';`,
17
+ resolveDir,
18
+ sourcefile: `tree-shake-${name}.ts`,
19
+ },
20
+ treeShaking: true,
21
+ write: false,
22
+ });
23
+ return result.outputFiles[0].text;
24
+ }
25
+
26
+ describe('scene resource resolver tree shaking', () => {
27
+ it('keeps the primitive resolver independent of built-in material listers', async () => {
28
+ const output = await bundleScene3DResourceResolverExport('createScene3DResourceResolver');
29
+ expect(output).not.toContain('StandardPbrMaterial');
30
+ expect(output).not.toContain('UnlitMaterial');
31
+ expect(output).not.toContain('image resource resolution failed');
32
+ });
33
+
34
+ it('includes built-in material listers only through the named assembly', async () => {
35
+ const output = await bundleScene3DResourceResolverExport('createBuiltInScene3DResourceResolver');
36
+ expect(output).toContain('StandardPbrMaterial');
37
+ expect(output).toContain('UnlitMaterial');
38
+ });
39
+
40
+ it('includes failure logging only through the separately imported guard', async () => {
41
+ const output = await bundleScene3DResourceResolverExport('enableScene3DResourceFailureGuards');
42
+ expect(output).toContain('image resource resolution failed');
43
+ });
44
+ });
45
+
46
+ function getFileUrlDirectory(url: string): string {
47
+ const directory = new URL('.', url);
48
+ const pathname = decodeURIComponent(directory.pathname);
49
+ if (directory.hostname !== '') return `//${directory.hostname}${pathname}`;
50
+ return /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
51
+ }
@@ -0,0 +1,51 @@
1
+ import { connectSignal, emitSignal } from '@flighthq/signals';
2
+ import type { ImageResourceReference, Texture } from '@flighthq/types';
3
+ import { EntityRuntimeKey, ResourceResolutionState, ImageResourceReferenceKind } from '@flighthq/types';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { createScene3DResourceResolver, disposeScene3DResourceResolver } from './sceneResourceResolver';
7
+ import {
8
+ createScene3DResourceSignals,
9
+ enableScene3DResourceSignals,
10
+ getScene3DResourceSignals,
11
+ } from './sceneResourceSignals';
12
+
13
+ const ref: ImageResourceReference = {
14
+ bytes: new Uint8Array(),
15
+ failure: null,
16
+ kind: ImageResourceReferenceKind.Embedded,
17
+ mimeType: null,
18
+ state: ResourceResolutionState.Resolved,
19
+ };
20
+
21
+ describe('createScene3DResourceSignals', () => {
22
+ it('creates a connectable, emittable signal group', () => {
23
+ const signals = createScene3DResourceSignals();
24
+ expect(EntityRuntimeKey in signals).toBe(true);
25
+ let received = 0;
26
+ connectSignal(signals.onResourceResolved, () => received++);
27
+ emitSignal(signals.onResourceResolved, { ref, texture: {} as Texture });
28
+ expect(received).toBe(1);
29
+ });
30
+ });
31
+
32
+ describe('enableScene3DResourceSignals', () => {
33
+ it('stores the group on the resolver and is idempotent', () => {
34
+ const resolver = createScene3DResourceResolver();
35
+ const first = enableScene3DResourceSignals(resolver);
36
+ const second = enableScene3DResourceSignals(resolver);
37
+ expect(first).toBe(second);
38
+ expect(getScene3DResourceSignals(resolver)).toBe(first);
39
+ disposeScene3DResourceResolver(resolver);
40
+ });
41
+ });
42
+
43
+ describe('getScene3DResourceSignals', () => {
44
+ it('returns null until enabled, then the enabled group', () => {
45
+ const resolver = createScene3DResourceResolver();
46
+ expect(getScene3DResourceSignals(resolver)).toBeNull();
47
+ const signals = enableScene3DResourceSignals(resolver);
48
+ expect(getScene3DResourceSignals(resolver)).toBe(signals);
49
+ disposeScene3DResourceResolver(resolver);
50
+ });
51
+ });
@@ -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 Load3dsModule from './threeDsLoad';
8
+
9
+ let loadScene3DDocumentFrom3dsUrl: typeof Load3dsModule.loadScene3DDocumentFrom3dsUrl;
10
+ let parse3ds: Mock<typeof Scene3DFormatsModule.parse3ds>;
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
+ parse3ds = vi.fn<typeof Scene3DFormatsModule.parse3ds>();
35
+ sendNetRequest = vi.fn<typeof NetModule.sendNetRequest>();
36
+ vi.doMock('@flighthq/net', () => ({ sendNetRequest }));
37
+ vi.doMock('@flighthq/scene3d-formats', () => ({ parse3ds }));
38
+ ({ loadScene3DDocumentFrom3dsUrl } = await import('./threeDsLoad'));
39
+ });
40
+
41
+ afterAll(() => {
42
+ vi.doUnmock('@flighthq/net');
43
+ vi.doUnmock('@flighthq/scene3d-formats');
44
+ vi.resetModules();
45
+ });
46
+
47
+ afterEach(() => {
48
+ parse3ds.mockReset();
49
+ sendNetRequest.mockReset();
50
+ });
51
+
52
+ describe('loadScene3DDocumentFrom3dsUrl', () => {
53
+ it('fetches bytes and returns the parsed CPU document without resolving resources', async () => {
54
+ const document = emptyDocument();
55
+ parse3ds.mockReturnValue(document);
56
+ sendNetRequest.mockResolvedValue(okResponse(new Uint8Array([9, 8]).buffer));
57
+
58
+ const loaded = await loadScene3DDocumentFrom3dsUrl('model.3ds');
59
+
60
+ expect(Array.from(parse3ds.mock.calls[0][0])).toEqual([9, 8]);
61
+ expect(loaded).toBe(document);
62
+ });
63
+ });