Skip to content

Commit 6aa6f29

Browse files
committed
loaders
1 parent ddeacf1 commit 6aa6f29

File tree

6 files changed

+284
-4
lines changed

6 files changed

+284
-4
lines changed

libs/soba/loaders/src/gltf-loader/gltf-loader.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import type { Injector, Signal } from '@angular/core';
22
import { injectNgtLoader, type NgtLoaderResults, type NgtObjectMap } from 'angular-three';
3-
// @ts-ignore
4-
import { MeshoptDecoder } from 'three-stdlib';
5-
import { DRACOLoader } from 'three-stdlib/loaders/DRACOLoader';
6-
import { GLTF, GLTFLoader } from 'three-stdlib/loaders/GLTFLoader';
3+
import { DRACOLoader, GLTF, GLTFLoader, MeshoptDecoder } from 'three-stdlib';
74

85
let dracoLoader: DRACOLoader | null = null;
96

libs/soba/loaders/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
export * from './gltf-loader/gltf-loader';
2+
export * from './loader/loader';
3+
export * from './progress/progress';
4+
export * from './texture-loader/texture-loader';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
.ngts-loader-container {
2+
--ngts-loader-container-opacity: 0;
3+
position: absolute;
4+
top: 0;
5+
left: 0;
6+
width: 100%;
7+
height: 100%;
8+
background: #171717;
9+
display: flex;
10+
align-items: center;
11+
justify-content: center;
12+
transition: opacity 300ms ease;
13+
z-index: 1000;
14+
opacity: var(--ngts-loader-container-opacity);
15+
}
16+
17+
.ngts-loader-inner {
18+
width: 100px;
19+
height: 3px;
20+
background: #272727;
21+
text-align: center;
22+
}
23+
24+
.ngts-loader-bar {
25+
--ngts-loader-bar-scale: 0;
26+
height: 3px;
27+
width: 100px;
28+
background: white;
29+
transition: transform 200ms;
30+
transform-origin: left center;
31+
transform: scaleX(var(--ngts-loader-bar-scale));
32+
}
33+
34+
.ngts-loader-data {
35+
display: inline-block;
36+
position: relative;
37+
font-variant-numeric: tabular-nums;
38+
margin-top: 0.8em;
39+
color: #f0f0f0;
40+
font-size: 0.6em;
41+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, Roboto,
42+
Ubuntu, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
43+
white-space: nowrap;
44+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { NgIf } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
ElementRef,
6+
Input,
7+
ViewChild,
8+
computed,
9+
effect,
10+
signal,
11+
untracked,
12+
} from '@angular/core';
13+
import { signalStore } from 'angular-three';
14+
import { injectNgtsProgress } from '../progress/progress';
15+
16+
const defaultDataInterpolation = (p: number) => `Loading ${p.toFixed(2)}%`;
17+
18+
export interface NgtsLoaderState {
19+
containerClass?: string;
20+
innerClass?: string;
21+
barClass?: string;
22+
dataClass?: string;
23+
dataInterpolation: (value: number) => string;
24+
initialState: (value: boolean) => boolean;
25+
}
26+
27+
@Component({
28+
selector: 'ngts-loader',
29+
standalone: true,
30+
template: `
31+
<div
32+
*ngIf="shown()"
33+
class="ngts-loader-container"
34+
[class]="container() || ''"
35+
[style.--ngts-loader-container-opacity]="active() ? 1 : 0"
36+
>
37+
<div>
38+
<div class="ngts-loader-inner" [class]="inner() || ''">
39+
<div
40+
class="ngts-loader-bar"
41+
[class]="bar() || ''"
42+
[style.--ngts-loader-bar-scale]="progress() / 100"
43+
></div>
44+
<span #progressSpanRef class="ngts-loader-data" [class]="data() || ''"></span>
45+
</div>
46+
</div>
47+
</div>
48+
`,
49+
styleUrls: ['./loader.css'],
50+
imports: [NgIf],
51+
changeDetection: ChangeDetectionStrategy.OnPush,
52+
})
53+
export class NgtsLoader {
54+
private inputs = signalStore<NgtsLoaderState>({
55+
dataInterpolation: defaultDataInterpolation,
56+
initialState: (active: boolean) => active,
57+
});
58+
59+
private _progress = injectNgtsProgress();
60+
61+
active = computed(() => this._progress().active);
62+
progress = computed(() => this._progress().progress);
63+
64+
container = this.inputs.select('containerClass');
65+
inner = this.inputs.select('innerClass');
66+
bar = this.inputs.select('barClass');
67+
data = this.inputs.select('dataClass');
68+
69+
@Input() set containerClass(containerClass: string) {
70+
this.inputs.set({ containerClass });
71+
}
72+
73+
@Input() set innerClass(innerClass: string) {
74+
this.inputs.set({ innerClass });
75+
}
76+
77+
@Input() set barClass(barClass: string) {
78+
this.inputs.set({ barClass });
79+
}
80+
81+
@Input() set dataClass(dataClass: string) {
82+
this.inputs.set({ dataClass });
83+
}
84+
85+
@Input() set dataInterpolation(dataInterpolation: (value: number) => string) {
86+
this.inputs.set({ dataInterpolation });
87+
}
88+
89+
@Input() set initialState(initialState: (value: boolean) => boolean) {
90+
this.inputs.set({ initialState });
91+
}
92+
93+
@ViewChild('progressSpanRef') progressSpanRef?: ElementRef<HTMLSpanElement>;
94+
95+
shown = signal(this.inputs.get('initialState')(this.active()));
96+
97+
constructor() {
98+
this.setShown();
99+
this.updateProgress();
100+
}
101+
102+
private setShown() {
103+
effect((onCleanup) => {
104+
const active = this.active();
105+
const lastShown = untracked(this.shown);
106+
if (lastShown !== active) {
107+
const timeoutId = setTimeout(() => {
108+
this.shown.set(active);
109+
}, 300);
110+
onCleanup(() => clearTimeout(timeoutId));
111+
}
112+
});
113+
}
114+
115+
private updateProgress() {
116+
let progressRef = 0;
117+
let rafId: ReturnType<typeof requestAnimationFrame>;
118+
119+
const dataInterpolation = this.inputs.select('dataInterpolation');
120+
const trigger = computed(() => ({ dataInterpolation: dataInterpolation(), progress: this.progress() }));
121+
122+
effect((onCleanup) => {
123+
const { dataInterpolation, progress } = trigger();
124+
125+
const updateProgress = () => {
126+
if (!this.progressSpanRef?.nativeElement) return;
127+
progressRef += (progress - progressRef) / 2;
128+
if (progressRef > 0.95 * progress || progress === 100) progressRef = progress;
129+
this.progressSpanRef.nativeElement.innerText = dataInterpolation(progressRef);
130+
if (progressRef < progress) {
131+
rafId = requestAnimationFrame(updateProgress);
132+
}
133+
};
134+
updateProgress();
135+
onCleanup(() => cancelAnimationFrame(rafId));
136+
});
137+
}
138+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ChangeDetectorRef, inject, runInInjectionContext, signal, type Injector } from '@angular/core';
2+
import { assertInjectionContext, safeDetectChanges } from 'angular-three';
3+
import * as THREE from 'three';
4+
5+
export function injectNgtsProgress(injector?: Injector) {
6+
injector = assertInjectionContext(injectNgtsProgress, injector);
7+
return runInInjectionContext(injector, () => {
8+
const cdr = inject(ChangeDetectorRef);
9+
10+
const progress = signal<{
11+
errors: string[];
12+
active: boolean;
13+
progress: number;
14+
item: string;
15+
loaded: number;
16+
total: number;
17+
}>({ errors: [], active: false, progress: 0, item: '', loaded: 0, total: 0 });
18+
19+
let saveLastTotalLoaded = 0;
20+
21+
THREE.DefaultLoadingManager.onStart = (item, loaded, total) => {
22+
progress.update((prev) => ({
23+
...prev,
24+
active: true,
25+
item,
26+
loaded,
27+
total,
28+
progress: ((loaded - saveLastTotalLoaded) / (total - saveLastTotalLoaded)) * 100,
29+
}));
30+
safeDetectChanges(cdr);
31+
};
32+
33+
THREE.DefaultLoadingManager.onLoad = () => {
34+
progress.update((prev) => ({ ...prev, active: false }));
35+
safeDetectChanges(cdr);
36+
cdr.detectChanges();
37+
};
38+
39+
THREE.DefaultLoadingManager.onError = (url) => {
40+
progress.update((prev) => ({ ...prev, errors: [...prev.errors, url] }));
41+
safeDetectChanges(cdr);
42+
};
43+
44+
THREE.DefaultLoadingManager.onProgress = (item, loaded, total) => {
45+
if (loaded === total) saveLastTotalLoaded = total;
46+
progress.update((prev) => ({
47+
...prev,
48+
item,
49+
loaded,
50+
total,
51+
progress: ((loaded - saveLastTotalLoaded) / (total - saveLastTotalLoaded)) * 100 || 100,
52+
}));
53+
safeDetectChanges(cdr);
54+
};
55+
56+
return progress.asReadonly();
57+
});
58+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { effect, runInInjectionContext, type Injector, type Signal } from '@angular/core';
2+
import { assertInjectionContext, injectNgtLoader, injectNgtStore, type NgtLoaderResults } from 'angular-three';
3+
import * as THREE from 'three';
4+
5+
export function injectNgtsTextureLoader<TInput extends string[] | string | Record<string, string>>(
6+
input: () => TInput,
7+
{
8+
onLoad,
9+
injector,
10+
}: {
11+
onLoad?: (texture: THREE.Texture | THREE.Texture[]) => void;
12+
injector?: Injector;
13+
} = {},
14+
): Signal<NgtLoaderResults<TInput, THREE.Texture> | null> {
15+
injector = assertInjectionContext(injectNgtsTextureLoader, injector);
16+
return runInInjectionContext(injector, () => {
17+
const store = injectNgtStore();
18+
const result = injectNgtLoader(() => THREE.TextureLoader, input, { injector });
19+
20+
effect(() => {
21+
const textures = result();
22+
if (!textures) return;
23+
const array = Array.isArray(textures)
24+
? textures
25+
: textures instanceof THREE.Texture
26+
? [textures]
27+
: Object.values(textures);
28+
if (onLoad) onLoad(array);
29+
array.forEach(store.get('gl').initTexture);
30+
});
31+
32+
return result;
33+
});
34+
}
35+
36+
injectNgtsTextureLoader['preload'] = <TInput extends string[] | string | Record<string, string>>(
37+
input: () => TInput,
38+
) => {
39+
(injectNgtLoader as any).preload(() => THREE.TextureLoader, input);
40+
};

0 commit comments

Comments
 (0)