Skip to content

Commit e238e35

Browse files
committed
feat: add r3f stuffs
1 parent 52d487c commit e238e35

File tree

7 files changed

+682
-0
lines changed

7 files changed

+682
-0
lines changed

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

Lines changed: 400 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ export interface NgtInstanceLocalState {
272272
objects: BehaviorSubject<NgtInstanceNode[]>;
273273
// shortcut to add/remove object to list
274274
add: (instance: NgtInstanceNode, type: 'objects' | 'nonObjects') => void;
275+
remove: (instance: NgtInstanceNode, type: 'objects' | 'nonObjects') => void;
275276
// parent based on attach three instance
276277
parent: NgtInstanceNode | null;
277278
// if this THREE instance is a ngt-primitive
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { BehaviorSubject } from 'rxjs';
2+
import type { NgtAnyRecord, NgtInstanceLocalState, NgtInstanceNode } from '../types';
3+
import { checkUpdate } from './update';
4+
5+
export function getLocalState<TInstance extends object = NgtAnyRecord>(
6+
obj: TInstance | undefined
7+
): NgtInstanceLocalState {
8+
if (!obj) return {} as unknown as NgtInstanceLocalState;
9+
return (obj as NgtAnyRecord)['__ngt__'] as NgtInstanceLocalState;
10+
}
11+
12+
export function invalidateInstance<TInstance extends object>(instance: TInstance) {
13+
const state = getLocalState(instance).store?.get();
14+
if (state && state.internal.frames === 0) state.invalidate();
15+
checkUpdate(instance);
16+
}
17+
18+
export function prepare<TInstance extends object = NgtAnyRecord>(
19+
object: TInstance,
20+
localState?: Partial<NgtInstanceLocalState>
21+
): NgtInstanceNode<TInstance> {
22+
const instance = object as unknown as NgtInstanceNode<TInstance>;
23+
24+
if (localState?.primitive || !instance.__ngt__) {
25+
const {
26+
objects = new BehaviorSubject<NgtInstanceNode[]>([]),
27+
nonObjects = new BehaviorSubject<NgtInstanceNode[]>([]),
28+
...rest
29+
} = localState || {};
30+
31+
instance.__ngt__ = {
32+
previousAttach: null,
33+
store: null,
34+
parent: null,
35+
memoized: {},
36+
eventCount: 0,
37+
handlers: {},
38+
objects,
39+
nonObjects,
40+
add: (object, type) => {
41+
instance.__ngt__[type].next([...instance.__ngt__[type].value, object]);
42+
notifyAncestors(instance.__ngt__.parent);
43+
},
44+
remove: (object, type) => {
45+
instance.__ngt__[type].next(instance.__ngt__[type].value.filter((o) => o !== object));
46+
notifyAncestors(instance.__ngt__.parent);
47+
},
48+
...rest,
49+
} as NgtInstanceLocalState;
50+
}
51+
52+
return instance;
53+
}
54+
55+
function notifyAncestors(instance: NgtInstanceNode | null) {
56+
if (!instance) return;
57+
const localState = getLocalState(instance);
58+
if (localState.objects) localState.objects.next(localState.objects.value);
59+
if (localState.nonObjects) localState.nonObjects.next(localState.nonObjects.value);
60+
notifyAncestors(localState.parent);
61+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ElementRef } from '@angular/core';
2+
import * as THREE from 'three';
3+
import type { NgtAnyRecord, NgtEquConfig, NgtInstanceNode } from '../types';
4+
5+
export const is = {
6+
obj: (a: unknown): a is object => a === Object(a) && !Array.isArray(a) && typeof a !== 'function',
7+
material: (a: unknown): a is THREE.Material => !!a && (a as THREE.Material).isMaterial,
8+
geometry: (a: unknown): a is THREE.BufferGeometry => !!a && (a as THREE.BufferGeometry).isBufferGeometry,
9+
orthographicCamera: (a: unknown): a is THREE.OrthographicCamera =>
10+
!!a && (a as THREE.OrthographicCamera).isOrthographicCamera,
11+
perspectiveCamera: (a: unknown): a is THREE.PerspectiveCamera =>
12+
!!a && (a as THREE.PerspectiveCamera).isPerspectiveCamera,
13+
camera: (a: unknown): a is THREE.Camera => !!a && (a as THREE.Camera).isCamera,
14+
renderer: (a: unknown): a is THREE.WebGLRenderer => !!a && a instanceof THREE.WebGLRenderer,
15+
scene: (a: unknown): a is THREE.Scene => !!a && (a as THREE.Scene).isScene,
16+
object3D: (a: unknown): a is THREE.Object3D => !!a && (a as THREE.Object3D).isObject3D,
17+
instance: (a: unknown): a is NgtInstanceNode => !!a && !!(a as NgtAnyRecord)['__ngt__'],
18+
ref: (a: unknown): a is ElementRef => a instanceof ElementRef,
19+
equ(a: any, b: any, { arrays = 'shallow', objects = 'reference', strict = true }: NgtEquConfig = {}) {
20+
// Wrong type or one of the two undefined, doesn't match
21+
if (typeof a !== typeof b || !!a !== !!b) return false;
22+
// Atomic, just compare a against b
23+
if (typeof a === 'string' || typeof a === 'number') return a === b;
24+
const isObj = is.obj(a);
25+
if (isObj && objects === 'reference') return a === b;
26+
const isArr = Array.isArray(a);
27+
if (isArr && arrays === 'reference') return a === b;
28+
// Array or Object, shallow compare first to see if it's a match
29+
if ((isArr || isObj) && a === b) return true;
30+
// Last resort, go through keys
31+
let i;
32+
for (i in a) if (!(i in b)) return false;
33+
for (i in strict ? b : a) if (a[i] !== b[i]) return false;
34+
if (i === void 0) {
35+
if (isArr && a.length === 0 && b.length === 0) return true;
36+
if (isObj && Object.keys(a).length === 0 && Object.keys(b).length === 0) return true;
37+
if (a !== b) return false;
38+
}
39+
return true;
40+
},
41+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as THREE from 'three';
2+
import type { NgtDpr, NgtGLOptions, NgtIntersection, NgtObjectMap, NgtSize } from '../types';
3+
4+
const idCache: { [id: string]: boolean | undefined } = {};
5+
export function makeId(event?: NgtIntersection): string {
6+
if (event) {
7+
return (event.eventObject || event.object).uuid + '/' + event.index + event.instanceId;
8+
}
9+
10+
const newId = THREE.MathUtils.generateUUID();
11+
// ensure not already used
12+
if (!idCache[newId]) {
13+
idCache[newId] = true;
14+
return newId;
15+
}
16+
return makeId();
17+
}
18+
19+
export function makeDpr(dpr: NgtDpr, window?: Window) {
20+
const target = window?.devicePixelRatio || 1;
21+
return Array.isArray(dpr) ? Math.min(Math.max(dpr[0], target), dpr[1]) : dpr;
22+
}
23+
24+
export function makeDefaultCamera(isOrthographic: boolean, size: NgtSize) {
25+
if (isOrthographic) return new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000);
26+
return new THREE.PerspectiveCamera(75, size.width / size.height, 0.1, 1000);
27+
}
28+
29+
export function makeDefaultRenderer(glOptions: NgtGLOptions, canvasElement: HTMLCanvasElement): THREE.WebGLRenderer {
30+
const customRenderer = (
31+
typeof glOptions === 'function' ? glOptions(canvasElement) : glOptions
32+
) as THREE.WebGLRenderer;
33+
34+
if (customRenderer?.render != null) return customRenderer;
35+
36+
return new THREE.WebGLRenderer({
37+
powerPreference: 'high-performance',
38+
canvas: canvasElement,
39+
antialias: true,
40+
alpha: true,
41+
...(glOptions || {}),
42+
});
43+
}
44+
45+
export function makeObjectGraph(object: THREE.Object3D): NgtObjectMap {
46+
const data: NgtObjectMap = { nodes: {}, materials: {} };
47+
48+
if (object) {
49+
object.traverse((child: THREE.Object3D) => {
50+
if (child.name) data.nodes[child.name] = child;
51+
if ('material' in child && !data.materials[((child as THREE.Mesh).material as THREE.Material).name]) {
52+
data.materials[((child as THREE.Mesh).material as THREE.Material).name] = (child as THREE.Mesh)
53+
.material as THREE.Material;
54+
}
55+
});
56+
}
57+
return data;
58+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { NgtAnyRecord, NgtCameraManual, NgtSize } from '../types';
2+
import { is } from './is';
3+
4+
export function checkNeedsUpdate(value: unknown) {
5+
if (value !== null && is.obj(value) && 'needsUpdate' in (value as NgtAnyRecord)) {
6+
(value as NgtAnyRecord)['needsUpdate'] = true;
7+
8+
if ('uniformsNeedUpdate' in (value as NgtAnyRecord)) {
9+
(value as NgtAnyRecord)['uniformsNeedUpdate'] = true;
10+
}
11+
}
12+
}
13+
14+
export function checkUpdate(value: unknown) {
15+
if (is.object3D(value)) {
16+
value.updateMatrix();
17+
}
18+
19+
if (is.camera(value)) {
20+
if (is.perspectiveCamera(value) || is.orthographicCamera(value)) {
21+
value.updateProjectionMatrix();
22+
}
23+
value.updateMatrixWorld();
24+
}
25+
26+
checkNeedsUpdate(value);
27+
}
28+
29+
export function updateCamera(camera: NgtCameraManual, size: NgtSize) {
30+
if (!camera.manual) {
31+
if (is.orthographicCamera(camera)) {
32+
camera.left = size.width / -2;
33+
camera.right = size.width / 2;
34+
camera.top = size.height / 2;
35+
camera.bottom = size.height / -2;
36+
} else {
37+
camera.aspect = size.width / size.height;
38+
}
39+
40+
camera.updateProjectionMatrix();
41+
camera.updateMatrixWorld();
42+
}
43+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { createEvents } from '../events';
2+
import { NgtRxStore } from '../stores/rx-store';
3+
import { NgtAnyRecord, NgtDomEvent, NgtEventManager, NgtEvents, NgtState } from '../types';
4+
5+
const DOM_EVENTS = {
6+
click: false,
7+
contextmenu: false,
8+
dblclick: false,
9+
wheel: false, // passive wheel errors with OrbitControls
10+
pointerdown: true,
11+
pointerup: true,
12+
pointerleave: true,
13+
pointermove: true,
14+
pointercancel: true,
15+
lostpointercapture: true,
16+
} as const;
17+
18+
export const supportedEvents = [
19+
'click',
20+
'contextmenu',
21+
'dblclick',
22+
'pointerup',
23+
'pointerdown',
24+
'pointerover',
25+
'pointerout',
26+
'pointerenter',
27+
'pointerleave',
28+
'pointermove',
29+
'pointermissed',
30+
'pointercancel',
31+
'wheel',
32+
] as const;
33+
34+
export function createPointerEvents(store: NgtRxStore<NgtState>): NgtEventManager<HTMLElement> {
35+
const { handlePointer } = createEvents(store);
36+
37+
return {
38+
priority: 1,
39+
enabled: true,
40+
compute: (event: NgtDomEvent, root: NgtRxStore<NgtState>) => {
41+
const state = root.get();
42+
// https://github.com/pmndrs/react-three-fiber/pull/782
43+
// Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides
44+
state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
45+
state.raycaster.setFromCamera(state.pointer, state.camera);
46+
},
47+
connected: undefined,
48+
handlers: Object.keys(DOM_EVENTS).reduce((handlers: NgtAnyRecord, supportedEventName) => {
49+
handlers[supportedEventName] = handlePointer(supportedEventName);
50+
return handlers;
51+
}, {}) as NgtEvents,
52+
connect: (target: HTMLElement) => {
53+
const state = store.get();
54+
state.events.disconnect?.();
55+
56+
state.setEvents({ connected: target });
57+
58+
Object.entries(state.events.handlers ?? {}).forEach(
59+
([eventName, eventHandler]: [string, EventListener]) => {
60+
const passive = DOM_EVENTS[eventName as keyof typeof DOM_EVENTS];
61+
target.addEventListener(eventName, eventHandler, { passive });
62+
}
63+
);
64+
},
65+
disconnect: () => {
66+
const { events, setEvents } = store.get();
67+
if (events.connected) {
68+
Object.entries(events.handlers ?? {}).forEach(([eventName, eventHandler]: [string, EventListener]) => {
69+
if (events.connected instanceof HTMLElement) {
70+
events.connected.removeEventListener(eventName, eventHandler);
71+
}
72+
});
73+
74+
setEvents({ connected: undefined });
75+
}
76+
},
77+
};
78+
}

0 commit comments

Comments
 (0)