Skip to content

Commit 79184dc

Browse files
committed
feat(core,soba/loaders,soba/staging): implement resources
1 parent 486917e commit 79184dc

File tree

16 files changed

+729
-28
lines changed

16 files changed

+729
-28
lines changed

apps/examples/src/app/soba/basic/scene.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { NgtArgs } from 'angular-three';
1616
import { NgtpBloom, NgtpEffectComposer, NgtpGlitch } from 'angular-three-postprocessing';
1717
import { NgtsOrbitControls } from 'angular-three-soba/controls';
18-
import { injectGLTF } from 'angular-three-soba/loaders';
18+
import { gltfResource, injectGLTF } from 'angular-three-soba/loaders';
1919
import { NgtsAnimation, injectAnimations } from 'angular-three-soba/misc';
2020
import { injectMatcapTexture } from 'angular-three-soba/staging';
2121
import {
@@ -68,7 +68,7 @@ export class BotAnimations {
6868
template: `
6969
<ngt-group [position]="[0, -1, 0]">
7070
<ngt-grid-helper *args="[10, 20]" />
71-
@if (gltf(); as gltf) {
71+
@if (resource.value(); as gltf) {
7272
<ngt-group [dispose]="null" [animations]="gltf" [referenceRef]="boneRef()">
7373
<ngt-group [rotation]="[Math.PI / 2, 0, 0]" [scale]="0.01">
7474
<ngt-primitive #bone *args="[gltf.nodes.mixamorigHips]" />
@@ -97,6 +97,9 @@ export class Bot {
9797
protected Math = Math;
9898

9999
protected gltf = injectGLTF<BotGLTF>(() => './ybot.glb');
100+
101+
protected resource = gltfResource<BotGLTF>(() => './ybot.glb');
102+
100103
protected matcapBody = injectMatcapTexture(() => '293534_B2BFC5_738289_8A9AA7', {
101104
onLoad: (textures) => {
102105
textures[0].colorSpace = SRGBColorSpace;

libs/core/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
export * from './lib/directives/args';
22
export * from './lib/directives/parent';
33
export * from './lib/directives/selection';
4+
export * from './lib/events';
45
export * from './lib/html';
56
export * from './lib/instance';
67
export * from './lib/loader';
8+
export * from './lib/loader-resource';
79
export * from './lib/loop';
810
export * from './lib/pipes/hexify';
911
export * from './lib/portal';
10-
// export * from './lib/renderer-old';
11-
export * from './lib/events';
1212
export * from './lib/renderer/catalogue';
1313
export * from './lib/renderer/constants';
1414
export * from './lib/renderer/renderer';

libs/core/src/lib/loader-resource.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { type Injector, resource, type ResourceRef } from '@angular/core';
2+
import { assertInjector } from 'ngxtension/assert-injector';
3+
import * as THREE from 'three';
4+
import type {
5+
NgtBranchingReturn,
6+
NgtGLTFLike,
7+
NgtLoaderExtensions,
8+
NgtLoaderProto,
9+
NgtLoaderResults,
10+
NgtLoaderReturnType,
11+
} from './loader';
12+
import type { NgtAnyRecord } from './types';
13+
import { makeObjectGraph, type NgtObjectMap } from './utils/make';
14+
15+
function normalizeInputs(input: string | string[] | Record<string, string>) {
16+
let urls: string[] = [];
17+
if (Array.isArray(input)) {
18+
urls = input;
19+
} else if (typeof input === 'string') {
20+
urls = [input];
21+
} else {
22+
urls = Object.values(input);
23+
}
24+
25+
return urls.map((url) => (url.includes('undefined') || url.includes('null') || !url ? '' : url));
26+
}
27+
28+
const cached = new Map();
29+
const memoizedLoaders = new WeakMap();
30+
31+
function getLoaderRequestParams<
32+
TData,
33+
TUrl extends string | string[] | Record<string, string>,
34+
TLoaderConstructor extends NgtLoaderProto<TData>,
35+
>(
36+
input: { (): TUrl },
37+
loaderConstructorFactory: {
38+
(url: TUrl): TLoaderConstructor;
39+
},
40+
extensions: NgtLoaderExtensions<TLoaderConstructor> | undefined,
41+
) {
42+
const urls = input();
43+
const LoaderConstructor = loaderConstructorFactory(urls);
44+
const normalizedUrls = normalizeInputs(urls);
45+
let loader: THREE.Loader<TData> = memoizedLoaders.get(LoaderConstructor);
46+
if (!loader) {
47+
loader = new LoaderConstructor();
48+
memoizedLoaders.set(LoaderConstructor, loader);
49+
}
50+
51+
if (extensions) extensions(loader);
52+
53+
return { urls, normalizedUrls, loader };
54+
}
55+
56+
function getLoaderPromises<TData, TUrl extends string | string[] | Record<string, string>>(
57+
request: { loader: THREE.Loader<TData>; normalizedUrls: string[]; urls: TUrl },
58+
onProgress?: { (event: ProgressEvent<EventTarget>): void },
59+
) {
60+
return request.normalizedUrls.map((url) => {
61+
if (url === '') return Promise.resolve(null);
62+
const cachedPromise = cached.get(url);
63+
if (cachedPromise) return cachedPromise;
64+
65+
const promise = new Promise<TData>((res, rej) => {
66+
request.loader.load(
67+
url,
68+
(data) => {
69+
if ('scene' in (data as NgtAnyRecord)) {
70+
Object.assign(data as NgtAnyRecord, makeObjectGraph((data as NgtAnyRecord)['scene']));
71+
}
72+
73+
res(data);
74+
},
75+
onProgress,
76+
(error) => rej(new Error(`[NGT] Could not load ${url}: ${(error as ErrorEvent)?.message}`)),
77+
);
78+
});
79+
80+
cached.set(url, promise);
81+
82+
return promise;
83+
});
84+
}
85+
86+
export function loaderResource<
87+
TData,
88+
TUrl extends string | string[] | Record<string, string>,
89+
TLoaderConstructor extends NgtLoaderProto<TData>,
90+
TReturn = NgtLoaderReturnType<TData, TLoaderConstructor>,
91+
>(
92+
loaderConstructorFactory: (url: TUrl) => TLoaderConstructor,
93+
input: () => TUrl,
94+
{
95+
extensions,
96+
onLoad,
97+
onProgress,
98+
injector,
99+
}: {
100+
extensions?: NgtLoaderExtensions<TLoaderConstructor>;
101+
onLoad?: (
102+
data: NoInfer<NgtLoaderResults<TUrl, NgtBranchingReturn<TReturn, NgtGLTFLike, NgtGLTFLike & NgtObjectMap>>>,
103+
) => void;
104+
onProgress?: (event: ProgressEvent) => void;
105+
injector?: Injector;
106+
} = {},
107+
): ResourceRef<
108+
NgtLoaderResults<TUrl, NgtBranchingReturn<TReturn, NgtGLTFLike, NgtGLTFLike & NgtObjectMap>> | undefined
109+
> {
110+
return assertInjector(loaderResource, injector, () => {
111+
return resource({
112+
request: () => getLoaderRequestParams(input, loaderConstructorFactory, extensions),
113+
loader: async ({ request }) => {
114+
const loadedResults = await Promise.all(getLoaderPromises(request, onProgress));
115+
116+
let results: NgtLoaderResults<
117+
TUrl,
118+
NgtBranchingReturn<TReturn, NgtGLTFLike, NgtGLTFLike & NgtObjectMap>
119+
>;
120+
121+
if (Array.isArray(request.urls)) {
122+
results = loadedResults as NgtLoaderResults<
123+
TUrl,
124+
NgtBranchingReturn<TReturn, NgtGLTFLike, NgtGLTFLike & NgtObjectMap>
125+
>;
126+
} else if (typeof request.urls === 'string') {
127+
results = loadedResults[0] as NgtLoaderResults<
128+
TUrl,
129+
NgtBranchingReturn<TReturn, NgtGLTFLike, NgtGLTFLike & NgtObjectMap>
130+
>;
131+
} else {
132+
const keys = Object.keys(request.urls);
133+
results = keys.reduce(
134+
(result, key) => {
135+
// @ts-ignore
136+
(result as NgtAnyRecord)[key] = loadedResults[keys.indexOf(key)];
137+
return result;
138+
},
139+
{} as {
140+
[key in keyof TUrl]: NgtBranchingReturn<TReturn, NgtGLTFLike, NgtGLTFLike & NgtObjectMap>;
141+
},
142+
) as NgtLoaderResults<TUrl, NgtBranchingReturn<TReturn, NgtGLTFLike, NgtGLTFLike & NgtObjectMap>>;
143+
}
144+
145+
if (onLoad) {
146+
onLoad(results);
147+
}
148+
149+
return results;
150+
},
151+
});
152+
});
153+
}
154+
155+
loaderResource.preload = <
156+
TData,
157+
TUrl extends string | string[] | Record<string, string>,
158+
TLoaderConstructor extends NgtLoaderProto<TData>,
159+
>(
160+
loaderConstructor: TLoaderConstructor,
161+
inputs: TUrl,
162+
extensions?: NgtLoaderExtensions<TLoaderConstructor>,
163+
) => {
164+
const params = getLoaderRequestParams(
165+
() => inputs,
166+
() => loaderConstructor,
167+
extensions,
168+
);
169+
void Promise.all(getLoaderPromises(params));
170+
};
171+
172+
loaderResource.destroy = () => {
173+
cached.clear();
174+
};
175+
176+
loaderResource.clear = (urls: string | string[]) => {
177+
const urlToClear = Array.isArray(urls) ? urls : [urls];
178+
urlToClear.forEach((url) => {
179+
cached.delete(url);
180+
});
181+
};

libs/core/src/lib/loader.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Injector, Signal, effect, signal } from '@angular/core';
22
import { assertInjector } from 'ngxtension/assert-injector';
3-
import { Loader, Object3D } from 'three';
3+
import * as THREE from 'three';
44
import type { NgtAnyRecord } from './types';
55
import { NgtObjectMap, makeObjectGraph } from './utils/make';
66

7-
export type NgtGLTFLike = { scene: Object3D };
7+
export type NgtGLTFLike = { scene: THREE.Object3D };
88

9-
export interface NgtLoader<T> extends Loader {
9+
export interface NgtLoader<T> extends THREE.Loader {
1010
load(
1111
url: string,
1212
onLoad?: (result: T) => void,
@@ -24,7 +24,6 @@ export type NgtLoaderReturnType<T, L extends NgtLoaderProto<T>> = T extends unkn
2424
export type NgtLoaderExtensions<T extends { prototype: NgtLoaderProto<any> }> = (loader: T['prototype']) => void;
2525
export type NgtConditionalType<Child, Parent, Truthy, Falsy> = Child extends Parent ? Truthy : Falsy;
2626
export type NgtBranchingReturn<T, Parent, Coerced> = NgtConditionalType<T, Parent, Coerced, T>;
27-
2827
export type NgtLoaderResults<
2928
TInput extends string | string[] | Record<string, string>,
3029
TReturn,
@@ -67,7 +66,7 @@ function load<
6766
return (): Array<Promise<any>> => {
6867
const urls = normalizeInputs(inputs());
6968

70-
let loader: Loader<TData> = memoizedLoaders.get(loaderConstructorFactory(urls));
69+
let loader: THREE.Loader<TData> = memoizedLoaders.get(loaderConstructorFactory(urls));
7170
if (!loader) {
7271
loader = new (loaderConstructorFactory(urls))();
7372
memoizedLoaders.set(loaderConstructorFactory(urls), loader);
@@ -111,6 +110,10 @@ function load<
111110
};
112111
}
113112

113+
/**
114+
* @deprecated Use loaderResource instead. Will be removed in v5.0.0
115+
* @since v4.0.0~
116+
*/
114117
function _injectLoader<
115118
TData,
116119
TUrl extends string | string[] | Record<string, string>,
@@ -197,4 +200,9 @@ _injectLoader.clear = (urls: string | string[]) => {
197200
};
198201

199202
export type NgtInjectedLoader = typeof _injectLoader;
203+
204+
/**
205+
* @deprecated Use loaderResource instead. Will be removed in v5.0.0
206+
* @since v4.0.0~
207+
*/
200208
export const injectLoader: NgtInjectedLoader = _injectLoader;

libs/soba/loaders/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
export * from './lib/fbx-loader';
2+
export * from './lib/fbx-resource';
23
export * from './lib/font-loader';
4+
export * from './lib/font-resource';
35
export * from './lib/gltf-loader';
6+
export * from './lib/gltf-resource';
47
export * from './lib/loader';
58
export * from './lib/progress';
69
export * from './lib/texture-loader';
10+
export * from './lib/texture-resource';

libs/soba/loaders/src/lib/fbx-loader.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { injectLoader } from 'angular-three';
33
import { assertInjector } from 'ngxtension/assert-injector';
44
import { FBXLoader } from 'three-stdlib';
55

6+
/**
7+
* @deprecated Use fbxResource instead. Will be removed in v5.0.0
8+
* @since v4.0.0
9+
*/
610
function _injectFBX<TUrl extends string | string[] | Record<string, string>>(
711
input: () => TUrl,
812
{ injector }: { injector?: Injector } = {},
@@ -17,4 +21,9 @@ _injectFBX.preload = <TUrl extends string | string[] | Record<string, string>>(i
1721
};
1822

1923
export type NgtsFBXLoader = typeof _injectFBX;
24+
25+
/**
26+
* @deprecated Use fbxResource instead. Will be removed in v5.0.0
27+
* @since v4.0.0
28+
*/
2029
export const injectFBX: NgtsFBXLoader = _injectFBX;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Injector } from '@angular/core';
2+
import { loaderResource } from 'angular-three';
3+
import { assertInjector } from 'ngxtension/assert-injector';
4+
import { FBXLoader } from 'three-stdlib';
5+
6+
export function fbxResource<TUrl extends string | string[] | Record<string, string>>(
7+
input: () => TUrl,
8+
{ injector }: { injector?: Injector } = {},
9+
) {
10+
return assertInjector(fbxResource, injector, () => {
11+
return loaderResource(() => FBXLoader, input);
12+
});
13+
}
14+
15+
fbxResource.preload = <TUrl extends string | string[] | Record<string, string>>(input: TUrl) => {
16+
loaderResource.preload(FBXLoader, input);
17+
};

libs/soba/loaders/src/lib/font-loader.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ export type NgtsFontInput = string | FontData;
2525

2626
let fontLoader: FontLoader | null = null;
2727

28-
async function loadFontData(font: NgtsFontInput): Promise<FontData> {
28+
export async function loadFontData(font: NgtsFontInput): Promise<FontData> {
2929
return typeof font === 'string' ? await (await fetch(font)).json() : font;
3030
}
3131

32-
function parseFontData(fontData: FontData) {
32+
export function parseFontData(fontData: FontData) {
3333
if (!fontLoader) {
3434
fontLoader = new FontLoader();
3535
}
@@ -38,6 +38,10 @@ function parseFontData(fontData: FontData) {
3838

3939
const cache = new Map<NgtsFontInput, Font>();
4040

41+
/**
42+
* @deprecated Use fontResource instead. Will be removed in v5.0.0
43+
* @since v4.0.0
44+
*/
4145
export function injectFont(input: () => NgtsFontInput, { injector }: { injector?: Injector } = {}) {
4246
return assertInjector(injectFont, injector, () => {
4347
const font = signal<Font | null>(null);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Injector, resource } from '@angular/core';
2+
import { assertInjector } from 'ngxtension/assert-injector';
3+
import { Font } from 'three-stdlib';
4+
import { loadFontData, NgtsFontInput, parseFontData } from './font-loader';
5+
6+
const cache = new Map<NgtsFontInput, Font>();
7+
8+
export function fontResource(input: () => NgtsFontInput, { injector }: { injector?: Injector } = {}) {
9+
return assertInjector(fontResource, injector, () => {
10+
return resource({
11+
request: input,
12+
loader: async ({ request }) => {
13+
if (cache.has(request)) {
14+
return cache.get(request) as Font;
15+
}
16+
17+
const fontData = await loadFontData(request);
18+
const parsed = parseFontData(fontData);
19+
cache.set(request, parsed);
20+
return parsed;
21+
},
22+
});
23+
});
24+
}
25+
26+
fontResource.preload = (input: NgtsFontInput) => {
27+
loadFontData(input).then((data) => {
28+
const parsed = parseFontData(data);
29+
cache.set(input, parsed);
30+
});
31+
};
32+
33+
fontResource.clear = (input?: NgtsFontInput) => {
34+
if (input) {
35+
cache.delete(input);
36+
} else {
37+
cache.clear();
38+
}
39+
};

0 commit comments

Comments
 (0)