Skip to content

Commit c7bec7e

Browse files
committed
feat(core): new renderer
1 parent ff53d1d commit c7bec7e

File tree

10 files changed

+782
-63
lines changed

10 files changed

+782
-63
lines changed
Lines changed: 164 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,175 @@
1-
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
2-
import { NgtArgs } from 'angular-three';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
CUSTOM_ELEMENTS_SCHEMA,
5+
ElementRef,
6+
input,
7+
signal,
8+
viewChild,
9+
} from '@angular/core';
10+
import { injectBeforeRender, NgtArgs, NgtPortalDeclarations, NgtVector3 } from 'angular-three';
11+
import * as THREE from 'three';
312

413
@Component({
5-
selector: 'app-scene',
14+
selector: 'app-condition-box',
15+
template: `
16+
@if (true) {
17+
<ngt-mesh [position]="position()">
18+
<ngt-box-geometry *args="[0.5, 0.5, 0.5]" />
19+
20+
<ng-content>
21+
<ngt-mesh-normal-material />
22+
</ng-content>
23+
</ngt-mesh>
24+
}
25+
`,
26+
imports: [NgtArgs],
27+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
28+
changeDetection: ChangeDetectionStrategy.OnPush,
29+
})
30+
export class ConditionBox {
31+
position = input<NgtVector3>(0);
32+
33+
onAttach(event: any) {
34+
console.log('in condition box', event);
35+
}
36+
}
37+
38+
@Component({
39+
selector: 'app-box',
640
template: `
7-
<ngt-mesh>
8-
<ngt-box-geometry *args="[2, 2, 2]" />
9-
<ngt-mesh-basic-material [color]="color()" />
41+
<ngt-mesh [position]="position()">
42+
<ngt-box-geometry *args="[0.5, 0.5, 0.5]" />
43+
44+
<ng-content>
45+
<ngt-mesh-basic-material [color]="color()" (attached)="onAttach($event)" />
46+
</ng-content>
47+
48+
<ng-content select="[data-children]" />
1049
</ngt-mesh>
1150
`,
1251
imports: [NgtArgs],
52+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
53+
changeDetection: ChangeDetectionStrategy.OnPush,
54+
host: {
55+
'(document:dblclick)': 'onDocumentDblClick($event)',
56+
},
57+
})
58+
export class Box {
59+
position = input<NgtVector3>(0);
60+
color = input('turquoise');
61+
62+
onDocumentDblClick(event: MouseEvent) {
63+
console.log('in box document dbl click', event);
64+
}
65+
66+
onAttach(event: any) {
67+
console.log('in box', event);
68+
}
69+
}
70+
71+
@Component({
72+
selector: 'app-scene',
73+
template: `
74+
<ngt-ambient-light [intensity]="Math.PI" />
75+
76+
<ngt-group #group>
77+
<ngt-mesh>
78+
<ngt-sphere-geometry *args="[0.5, 32, 32]" />
79+
<ngt-mesh-toon-material [color]="color()" (attached)="onAttach($event)" />
80+
</ngt-mesh>
81+
82+
@if (show()) {
83+
<ngt-mesh [position]="[3, 0, 0]">
84+
<ngt-icosahedron-geometry />
85+
<ngt-mesh-normal-material />
86+
</ngt-mesh>
87+
}
88+
89+
<app-box [position]="[1, 0, 0]" />
90+
<app-box [position]="[-1, 0, 0]" color="red" />
91+
<app-box [position]="[0, 1, 0]">
92+
<ngt-mesh-standard-material color="green" />
93+
</app-box>
94+
95+
<app-box [position]="[0, -1, 0]" color="purple" />
96+
97+
@if (show()) {
98+
<app-box [position]="[1, 1, 0]">
99+
@if (show()) {
100+
<ngt-mesh-phong-material color="yellow" />
101+
}
102+
</app-box>
103+
}
104+
105+
<app-box [position]="[-1, -1, 0]" color="brown">
106+
<app-box data-children [position]="[-0.5, -0.5, 0]" color="pink" />
107+
</app-box>
108+
109+
<app-box [position]="[-1, 1, 0]">
110+
<ngt-mesh-lambert-material color="orange" />
111+
<app-box data-children [position]="[-0.5, 0.5, 0]" color="skyblue" />
112+
</app-box>
113+
114+
<app-box [position]="[1, -1, 0]">
115+
@if (true) {
116+
<ngt-mesh-normal-material />
117+
}
118+
119+
@if (show()) {
120+
<app-box data-children [position]="[0.5, -0.5, 0]" color="black" />
121+
}
122+
</app-box>
123+
124+
<app-condition-box [position]="[0, 2, 0]" />
125+
@if (show()) {
126+
<app-condition-box [position]="[0, -2, 0]" />
127+
}
128+
</ngt-group>
129+
130+
<ngt-portal [container]="virtualScene">
131+
<ngt-group *portalContent>
132+
<app-box />
133+
<app-condition-box />
134+
</ngt-group>
135+
</ngt-portal>
136+
`,
137+
imports: [NgtArgs, Box, ConditionBox, NgtPortalDeclarations],
13138
changeDetection: ChangeDetectionStrategy.OnPush,
14139
schemas: [CUSTOM_ELEMENTS_SCHEMA],
140+
host: {
141+
'(document:click)': 'onDocumentClick($event)',
142+
},
15143
})
16144
export class Scene {
17-
color = signal('hotpink');
145+
protected readonly Math = Math;
146+
147+
protected show = signal(true);
148+
protected color = signal('hotpink');
149+
protected sphereArgs = signal([0.5, 32, 32]);
150+
151+
protected virtualScene = new THREE.Scene();
152+
153+
private groupRef = viewChild.required<ElementRef<THREE.Group>>('group');
154+
155+
constructor() {
156+
setInterval(() => {
157+
this.show.update((v) => !v);
158+
this.sphereArgs.update((v) => [v[0] === 0.5 ? 1 : 0.5, v[1], v[2]]);
159+
}, 2500);
160+
161+
injectBeforeRender(() => {
162+
const group = this.groupRef().nativeElement;
163+
group.rotation.x += 0.01;
164+
group.rotation.y += 0.01;
165+
});
166+
}
167+
168+
onDocumentClick(event: MouseEvent) {
169+
console.log('document', event);
170+
}
171+
172+
onAttach(event: any) {
173+
console.log('in scene', event);
174+
}
18175
}

libs/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export * from './lib/instance';
77
export * from './lib/loader';
88
export * from './lib/loop';
99
export * from './lib/pipes/hexify';
10-
// export * from './lib/portal';
10+
export * from './lib/portal';
1111
// export * from './lib/renderer-old';
1212
export * from './lib/renderer/catalogue';
1313
export * from './lib/renderer/renderer';

libs/core/src/lib/directives/args.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class NgtArgs {
2424

2525
constructor() {
2626
const commentNode = this.vcr.element.nativeElement;
27+
commentNode.data = 'args-container';
2728
if (commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG]) {
2829
commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG]('args');
2930
delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG];
@@ -44,6 +45,7 @@ export class NgtArgs {
4445
});
4546

4647
inject(DestroyRef).onDestroy(() => {
48+
console.log('destroy args');
4749
this.view?.destroy();
4850
});
4951
}

libs/core/src/lib/directives/parent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class NgtParent {
5151

5252
constructor() {
5353
const commentNode = this.vcr.element.nativeElement;
54+
commentNode.data = 'parent-container';
5455
if (commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG]) {
5556
commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG]('parent');
5657
delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG];

libs/core/src/lib/portal.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,183 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
contentChild,
5+
Directive,
6+
effect,
7+
EmbeddedViewRef,
8+
inject,
9+
Injector,
10+
input,
11+
signal,
12+
SkipSelf,
13+
TemplateRef,
14+
untracked,
15+
viewChild,
16+
ViewContainerRef,
17+
} from '@angular/core';
18+
import * as THREE from 'three';
19+
import { getInstanceState, prepare } from './instance';
20+
import { SPECIAL_INTERNAL_ADD_COMMENT_FLAG } from './renderer/constants';
21+
import { injectStore, NGT_STORE } from './store';
22+
import type { NgtComputeFunction, NgtEventManager, NgtSize, NgtState, NgtViewport } from './types';
23+
import { is } from './utils/is';
24+
import { omit, pick } from './utils/parameters';
25+
import { signalState, SignalState } from './utils/signal-state';
26+
import { updateCamera } from './utils/update';
27+
28+
@Directive({ selector: 'ng-template[portalContent]' })
29+
export class NgtPortalContent {
30+
static ngTemplateContextGuard(_: NgtPortalContent, ctx: unknown): ctx is { injector: Injector } {
31+
return true;
32+
}
33+
34+
constructor() {
35+
const { element } = inject(ViewContainerRef);
36+
const { element: parentComment } = inject(ViewContainerRef, { skipSelf: true });
37+
const commentNode = element.nativeElement;
38+
39+
commentNode.data = 'portal-content-container';
40+
41+
if (commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG]) {
42+
commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG](parentComment.nativeElement);
43+
delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT_FLAG];
44+
}
45+
}
46+
}
47+
48+
export interface NgtPortalState extends Omit<NgtState, 'events'> {
49+
events: {
50+
enabled?: boolean;
51+
priority?: number;
52+
compute?: NgtComputeFunction;
53+
connected?: any;
54+
};
55+
}
56+
57+
function mergeState(
58+
previousRoot: SignalState<NgtState>,
59+
store: SignalState<NgtState>,
60+
container: THREE.Object3D,
61+
pointer: THREE.Vector2,
62+
raycaster: THREE.Raycaster,
63+
events?: NgtPortalState['events'],
64+
size?: NgtSize,
65+
) {
66+
const previousState = previousRoot.snapshot;
67+
const state = store.snapshot;
68+
69+
let viewport: Omit<NgtViewport, 'dpr' | 'initialDpr'> | undefined = undefined;
70+
71+
if (state.camera && size) {
72+
const camera = state.camera;
73+
// calculate the override viewport, if present
74+
viewport = previousState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size);
75+
// update the portal camera, if it differs from the previous layer
76+
if (camera !== previousState.camera) updateCamera(camera, size);
77+
}
78+
79+
return {
80+
// the intersect consists of the previous root state
81+
...previousState,
82+
...state,
83+
// portals have their own scene, which forms the root, a raycaster and a pointer
84+
scene: container as THREE.Scene,
85+
pointer,
86+
raycaster,
87+
// their previous root is the layer before it
88+
previousRoot,
89+
events: { ...previousState.events, ...state.events, ...events },
90+
size: { ...previousState.size, ...size },
91+
viewport: { ...previousState.viewport, ...viewport },
92+
// layers are allowed to override events
93+
setEvents: (events: Partial<NgtEventManager<any>>) =>
94+
store.update((state) => ({ ...state, events: { ...state.events, ...events } })),
95+
} as NgtState;
96+
}
97+
98+
@Component({
99+
selector: 'ngt-portal',
100+
template: `
101+
<ng-container #anchor />
102+
`,
103+
changeDetection: ChangeDetectionStrategy.OnPush,
104+
providers: [
105+
{
106+
provide: NGT_STORE,
107+
useFactory: (previousStore: SignalState<NgtState>) => {
108+
const store = signalState({} as NgtState);
109+
store.update(mergeState(previousStore, store, null!, new THREE.Vector2(), new THREE.Raycaster()));
110+
return store;
111+
},
112+
deps: [[new SkipSelf(), NGT_STORE]],
113+
},
114+
],
115+
})
116+
export class NgtPortal {
117+
container = input.required<THREE.Object3D>();
118+
state = input<Partial<NgtPortalState>>({});
119+
120+
private contentRef = contentChild.required(NgtPortalContent, { read: TemplateRef });
121+
private anchorRef = viewChild.required('anchor', { read: ViewContainerRef });
122+
123+
private previousStore = injectStore({ skipSelf: true });
124+
private portalStore = injectStore();
125+
private injector = inject(Injector);
126+
127+
private size = pick(this.state, 'size');
128+
private events = pick(this.state, 'events');
129+
private restState = omit(this.state, ['size', 'events']);
130+
131+
protected portalContentRendered = signal(false);
132+
133+
private portalViewRef?: EmbeddedViewRef<unknown>;
134+
135+
constructor() {
136+
effect(() => {
137+
let [container, prevState] = [this.container(), this.previousStore()];
138+
139+
const [size, events, restState] = [untracked(this.size), untracked(this.events), untracked(this.restState)];
140+
141+
if (!is.instance(container)) {
142+
container = prepare(container, this.portalStore, 'ngt-portal');
143+
}
144+
145+
const instanceState = getInstanceState(container);
146+
if (instanceState && instanceState.store !== this.portalStore) {
147+
instanceState.store = this.portalStore;
148+
}
149+
150+
this.portalStore.update(
151+
restState,
152+
mergeState(
153+
this.previousStore,
154+
this.portalStore,
155+
container,
156+
this.portalStore.snapshot.pointer,
157+
this.portalStore.snapshot.raycaster,
158+
events,
159+
size,
160+
),
161+
);
162+
163+
if (this.portalViewRef) {
164+
this.portalViewRef.detectChanges();
165+
return;
166+
}
167+
168+
this.portalViewRef = untracked(this.anchorRef).createEmbeddedView(
169+
untracked(this.contentRef),
170+
{ injector: this.injector },
171+
{ injector: this.injector },
172+
);
173+
this.portalViewRef.detectChanges();
174+
this.portalContentRendered.set(true);
175+
});
176+
}
177+
}
178+
179+
export const NgtPortalDeclarations = [NgtPortal, NgtPortalContent] as const;
180+
1181
// import {
2182
// afterNextRender,
3183
// ChangeDetectionStrategy,

0 commit comments

Comments
 (0)