Skip to content

Commit 52d487c

Browse files
committed
feat: add loop
1 parent 8cbda45 commit 52d487c

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

libs/angular-three/src/lib/loop.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { NgtRxStore } from './stores/rx-store';
2+
import { NgtState } from './types';
3+
4+
export type NgtGlobalRenderCallback = (timestamp: number) => void;
5+
type NgtSubItem = { callback: NgtGlobalRenderCallback };
6+
7+
function createSubs(callback: NgtGlobalRenderCallback, subs: Set<NgtSubItem>): () => void {
8+
const sub = { callback };
9+
subs.add(sub);
10+
return () => void subs.delete(sub);
11+
}
12+
13+
const globalEffects: Set<NgtSubItem> = new Set();
14+
const globalAfterEffects: Set<NgtSubItem> = new Set();
15+
const globalTailEffects: Set<NgtSubItem> = new Set();
16+
17+
/**
18+
* Adds a global render callback which is called each frame.
19+
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addEffect
20+
*/
21+
export const addEffect = (callback: NgtGlobalRenderCallback) => createSubs(callback, globalEffects);
22+
23+
/**
24+
* Adds a global after-render callback which is called each frame.
25+
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addAfterEffect
26+
*/
27+
export const addAfterEffect = (callback: NgtGlobalRenderCallback) => createSubs(callback, globalAfterEffects);
28+
29+
/**
30+
* Adds a global callback which is called when rendering stops.
31+
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addTail
32+
*/
33+
export const addTail = (callback: NgtGlobalRenderCallback) => createSubs(callback, globalTailEffects);
34+
35+
function run(effects: Set<NgtSubItem>, timestamp: number) {
36+
if (!effects.size) return;
37+
for (const { callback } of effects.values()) {
38+
callback(timestamp);
39+
}
40+
}
41+
42+
export type GlobalEffectType = 'before' | 'after' | 'tail';
43+
44+
export function flushGlobalEffects(type: GlobalEffectType, timestamp: number): void {
45+
switch (type) {
46+
case 'before':
47+
return run(globalEffects, timestamp);
48+
case 'after':
49+
return run(globalAfterEffects, timestamp);
50+
case 'tail':
51+
return run(globalTailEffects, timestamp);
52+
}
53+
}
54+
55+
function render(timestamp: number, store: NgtRxStore<NgtState>, frame?: XRFrame) {
56+
const state = store.get();
57+
// Run local effects
58+
let delta = state.clock.getDelta();
59+
// In frameloop='never' mode, clock times are updated using the provided timestamp
60+
if (state.frameloop === 'never' && typeof timestamp === 'number') {
61+
delta = timestamp - state.clock.elapsedTime;
62+
state.clock.oldTime = state.clock.elapsedTime;
63+
state.clock.elapsedTime = timestamp;
64+
}
65+
// Call subscribers (useFrame)
66+
// subscribers = state.internal.subscribers;
67+
for (let i = 0; i < state.internal.subscribers.length; i++) {
68+
const subscriber = state.internal.subscribers[i];
69+
subscriber.callback({ ...state, delta, frame });
70+
}
71+
// Render content
72+
if (!state.internal.priority && state.gl.render) {
73+
state.gl.render(state.scene, state.camera);
74+
}
75+
// Decrease frame count
76+
state.internal.frames = Math.max(0, state.internal.frames - 1);
77+
return state.frameloop === 'always' ? 1 : state.internal.frames;
78+
}
79+
80+
export function createLoop<TCanvas>(roots: Map<TCanvas, NgtRxStore<NgtState>>) {
81+
let running = false;
82+
let repeat: number;
83+
let frame: number;
84+
85+
function loop(timestamp: number): void {
86+
frame = requestAnimationFrame(loop);
87+
running = true;
88+
repeat = 0;
89+
90+
// Run effects
91+
flushGlobalEffects('before', timestamp);
92+
93+
// Render all roots
94+
for (const root of roots.values()) {
95+
const state = root.get();
96+
// If the frameloop is invalidated, do not run another frame
97+
if (
98+
state.internal.active &&
99+
(state.frameloop === 'always' || state.internal.frames > 0) &&
100+
!state.gl.xr?.isPresenting
101+
) {
102+
repeat += render(timestamp, root);
103+
}
104+
}
105+
106+
// Run after-effects
107+
flushGlobalEffects('after', timestamp);
108+
109+
// Stop the loop if nothing invalidates it
110+
if (repeat === 0) {
111+
// Tail call effects, they are called when rendering stops
112+
flushGlobalEffects('tail', timestamp);
113+
114+
// Flag end of operation
115+
running = false;
116+
return cancelAnimationFrame(frame);
117+
}
118+
}
119+
120+
function invalidate(store?: NgtRxStore<NgtState>, frames = 1): void {
121+
const state = store?.get();
122+
if (!state) return roots.forEach((root) => invalidate(root, frames));
123+
if (state.gl.xr?.isPresenting || !state.internal.active || state.frameloop === 'never') return;
124+
// Increase frames, do not go higher than 60
125+
state.internal.frames = Math.min(60, state.internal.frames + frames);
126+
// If the render-loop isn't active, start it
127+
if (!running) {
128+
running = true;
129+
requestAnimationFrame(loop);
130+
}
131+
}
132+
133+
function advance(
134+
timestamp: number,
135+
runGlobalEffects: boolean = true,
136+
store?: NgtRxStore<NgtState>,
137+
frame?: XRFrame
138+
): void {
139+
const state = store?.get();
140+
if (runGlobalEffects) flushGlobalEffects('before', timestamp);
141+
if (!state) for (const root of roots.values()) render(timestamp, root);
142+
// safe to assume store is available here
143+
else render(timestamp, store!, frame);
144+
if (runGlobalEffects) flushGlobalEffects('after', timestamp);
145+
}
146+
147+
return {
148+
loop,
149+
/**
150+
* Invalidates the view, requesting a frame to be rendered. Will globally invalidate unless passed a root's state.
151+
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#invalidate
152+
*/
153+
invalidate,
154+
/**
155+
* Advances the frameloop and runs render effects, useful for when manually rendering via `frameloop="never"`.
156+
* @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#advance
157+
*/
158+
advance,
159+
};
160+
}

libs/angular-three/src/lib/utils/is.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)