Skip to content

Commit baba2d8

Browse files
committed
feat: add portal
1 parent c8c3079 commit baba2d8

File tree

2 files changed

+260
-0
lines changed

2 files changed

+260
-0
lines changed

libs/angular-three/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './lib/directives/args';
77
export * from './lib/directives/repeat';
88
export * from './lib/loader';
99
export * from './lib/pipes/push';
10+
export * from './lib/portal';
1011
export * from './lib/stores/rx-store';
1112
export * from './lib/stores/store';
1213
export * from './lib/types';

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

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { NgIf } from '@angular/common';
2+
import {
3+
Component,
4+
ContentChild,
5+
Directive,
6+
ElementRef,
7+
EmbeddedViewRef,
8+
EventEmitter,
9+
inject,
10+
Input,
11+
OnDestroy,
12+
OnInit,
13+
Output,
14+
SkipSelf,
15+
TemplateRef,
16+
ViewChild,
17+
ViewContainerRef,
18+
} from '@angular/core';
19+
import * as THREE from 'three';
20+
import { injectNgtRef } from './di/ref';
21+
import { NgtRxStore } from './stores/rx-store';
22+
import { NgtStore } from './stores/store';
23+
import { NgtEventManager, NgtRenderState, NgtSize, NgtState } from './types';
24+
import { getLocalState, prepare } from './utils/instance';
25+
import { is } from './utils/is';
26+
import { updateCamera } from './utils/update';
27+
28+
const privateKeys = [
29+
'get',
30+
'set',
31+
'select',
32+
'setSize',
33+
'setDpr',
34+
'setFrameloop',
35+
'events',
36+
'invalidate',
37+
'advance',
38+
'size',
39+
'viewport',
40+
'addInteraction',
41+
'removeInteraction',
42+
] as const;
43+
type PrivateKeys = (typeof privateKeys)[number];
44+
45+
export interface NgtPortalInputs {
46+
container: ElementRef<THREE.Object3D> | THREE.Object3D;
47+
camera: ElementRef<THREE.Camera> | THREE.Camera;
48+
state: Partial<
49+
Omit<NgtState, PrivateKeys> & {
50+
events: Partial<Pick<NgtEventManager<any>, 'enabled' | 'priority' | 'compute' | 'connected'>>;
51+
size: NgtSize;
52+
}
53+
>;
54+
}
55+
56+
@Directive({
57+
selector: 'ngt-portal-before-render',
58+
standalone: true,
59+
})
60+
export class NgtPortalBeforeRender implements OnInit, OnDestroy {
61+
private readonly portalStore = inject(NgtStore);
62+
63+
@Input() renderPriority = 1;
64+
@Input() parentScene!: THREE.Scene;
65+
@Input() parentCamera!: THREE.Camera;
66+
67+
@Output() beforeRender = new EventEmitter<NgtRenderState>();
68+
69+
private subscription?: () => void;
70+
71+
ngOnInit() {
72+
let oldClear: boolean;
73+
this.subscription = this.portalStore.get('internal').subscribe(
74+
({ delta, frame }) => {
75+
this.beforeRender.emit({ ...this.portalStore.get(), delta, frame });
76+
const { gl, scene, camera } = this.portalStore.get();
77+
oldClear = gl.autoClear;
78+
if (this.renderPriority === 1) {
79+
// clear scene and render with default
80+
gl.autoClear = true;
81+
gl.render(this.parentScene, this.parentCamera);
82+
}
83+
// disable cleaning
84+
gl.autoClear = false;
85+
gl.clearDepth();
86+
gl.render(scene, camera);
87+
// restore
88+
gl.autoClear = oldClear;
89+
},
90+
this.renderPriority,
91+
this.portalStore
92+
);
93+
}
94+
95+
ngOnDestroy() {
96+
this.subscription?.();
97+
}
98+
}
99+
100+
@Directive({ selector: 'ng-template[ngtPortalContent]', standalone: true })
101+
export class NgtPortalContent {
102+
constructor(vcr: ViewContainerRef, @SkipSelf() parentVcr: ViewContainerRef) {
103+
const commentNode = vcr.element.nativeElement;
104+
if (commentNode['__ngt_renderer_add_comment__']) {
105+
commentNode['__ngt_renderer_add_comment__'](parentVcr.element.nativeElement);
106+
delete commentNode['__ngt_renderer_add_comment__'];
107+
}
108+
}
109+
}
110+
111+
@Component({
112+
selector: 'ngt-portal',
113+
standalone: true,
114+
template: `
115+
<ng-container #portalContentAnchor>
116+
<ngt-portal-before-render
117+
*ngIf="autoRender && portalContentRendered"
118+
[renderPriority]="autoRenderPriority"
119+
[parentScene]="parentScene"
120+
[parentCamera]="parentCamera"
121+
(beforeRender)="onBeforeRender($event)"
122+
/>
123+
</ng-container>
124+
`,
125+
imports: [NgIf, NgtPortalBeforeRender],
126+
providers: [NgtStore],
127+
})
128+
export class NgtPortal extends NgtRxStore<NgtPortalInputs> implements OnInit, OnDestroy {
129+
@Input() set container(container: NgtPortalInputs['container']) {
130+
this.set({ container });
131+
}
132+
133+
@Input() set state(state: NgtPortalInputs['state']) {
134+
this.set({ state });
135+
}
136+
137+
@Input() autoRender = true;
138+
@Input() autoRenderPriority = 1;
139+
140+
@Output() beforeRender = new EventEmitter<{ root: NgtRenderState; portal: NgtRenderState }>();
141+
142+
@ContentChild(NgtPortalContent, { read: TemplateRef, static: true })
143+
readonly portalContentTemplate!: TemplateRef<unknown>;
144+
145+
@ViewChild('portalContentAnchor', { read: ViewContainerRef, static: true })
146+
readonly portalContentAnchor!: ViewContainerRef;
147+
148+
private readonly parentStore = inject(NgtStore, { skipSelf: true });
149+
readonly parentScene = this.parentStore.get('scene');
150+
readonly parentCamera = this.parentStore.get('camera');
151+
152+
private readonly portalStore = inject(NgtStore, { self: true });
153+
154+
private readonly raycaster = new THREE.Raycaster();
155+
private readonly pointer = new THREE.Vector2();
156+
157+
portalContentRendered = false;
158+
private portalContentView?: EmbeddedViewRef<unknown>;
159+
160+
override initialize() {
161+
super.initialize();
162+
this.set({ container: injectNgtRef<THREE.Scene>(prepare(new THREE.Scene())) });
163+
}
164+
165+
ngOnInit() {
166+
const previousState = this.parentStore.get();
167+
const inputsState = this.get();
168+
169+
if (!inputsState.state && this.autoRender) {
170+
inputsState.state = { events: { priority: this.autoRenderPriority + 1 } };
171+
}
172+
173+
const { events, size, ...restInputsState } = inputsState.state || {};
174+
175+
const containerState = inputsState.container;
176+
const container = is.ref(containerState) ? containerState.nativeElement : containerState;
177+
178+
const localState = getLocalState(container);
179+
if (!localState.store) {
180+
localState.store = this.portalStore;
181+
}
182+
183+
this.portalStore.set({
184+
...previousState,
185+
scene: container as THREE.Scene,
186+
raycaster: this.raycaster,
187+
pointer: this.pointer,
188+
previousStore: this.parentStore,
189+
events: { ...previousState.events, ...(events || {}) },
190+
size: { ...previousState.size, ...(size || {}) },
191+
...restInputsState,
192+
get: this.portalStore.get.bind(this.portalStore),
193+
set: this.portalStore.set.bind(this.portalStore),
194+
select: this.portalStore.select.bind(this.portalStore),
195+
setEvents: (events) =>
196+
this.portalStore.set((state) => ({ ...state, events: { ...state.events, ...events } })),
197+
});
198+
199+
this.hold(this.parentStore.select(), (previous) =>
200+
this.portalStore.set((state) => this.#inject(previous, state))
201+
);
202+
203+
requestAnimationFrame(() => {
204+
this.portalStore.set((injectState) => this.#inject(this.parentStore.get(), injectState));
205+
});
206+
this.portalContentView = this.portalContentAnchor.createEmbeddedView(this.portalContentTemplate);
207+
this.portalContentView.detectChanges();
208+
this.portalContentRendered = true;
209+
}
210+
211+
onBeforeRender(portal: NgtRenderState) {
212+
this.beforeRender.emit({
213+
root: { ...this.parentStore.get(), delta: portal.delta, frame: portal.frame },
214+
portal,
215+
});
216+
}
217+
218+
override ngOnDestroy() {
219+
if (this.portalContentView && !this.portalContentView.destroyed) {
220+
this.portalContentView.destroy();
221+
}
222+
super.ngOnDestroy();
223+
}
224+
225+
#inject(rootState: NgtState, injectState: NgtState) {
226+
const intersect: Partial<NgtState> = { ...rootState };
227+
228+
Object.keys(intersect).forEach((key) => {
229+
if (
230+
privateKeys.includes(key as PrivateKeys) ||
231+
rootState[key as keyof NgtState] !== injectState[key as keyof NgtState]
232+
) {
233+
delete intersect[key as keyof NgtState];
234+
}
235+
});
236+
237+
const inputs = this.get();
238+
const { size, events, ...restInputsState } = inputs.state || {};
239+
240+
let viewport = undefined;
241+
if (injectState && size) {
242+
const camera = injectState.camera;
243+
viewport = rootState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size);
244+
if (camera !== rootState.camera) updateCamera(camera, size);
245+
}
246+
247+
return {
248+
...intersect,
249+
scene: is.ref(inputs.container) ? inputs.container.nativeElement : inputs.container,
250+
raycaster: this.raycaster,
251+
pointer: this.pointer,
252+
previousStore: this.parentStore,
253+
events: { ...rootState.events, ...(injectState?.events || {}), ...events },
254+
size: { ...rootState.size, ...size },
255+
viewport: { ...rootState.viewport, ...(viewport || {}) },
256+
...restInputsState,
257+
} as NgtState;
258+
}
259+
}

0 commit comments

Comments
 (0)