Skip to content

Commit 70f66e8

Browse files
committed
feat: finish the renderer
1 parent 59d3423 commit 70f66e8

File tree

6 files changed

+849
-34
lines changed

6 files changed

+849
-34
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { InjectionToken } from '@angular/core';
2+
3+
const catalogue: Record<string, new (...args: any[]) => any> = {};
4+
5+
export function extend(objects: object): void {
6+
Object.assign(catalogue, objects);
7+
}
8+
9+
export const NGT_CATALOGUE = new InjectionToken<Record<string, new (...args: any[]) => any>>(
10+
'THREE Constructors Catalogue',
11+
{ factory: () => catalogue }
12+
);

libs/angular-three/src/lib/renderer/enums.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const enum NgtRendererClassId {
22
type,
33
parent,
44
children,
5-
removed,
5+
destroyed,
66
compound,
77
compoundParent,
88
compounded,
@@ -12,6 +12,7 @@ export const enum NgtRendererClassId {
1212
rawValue,
1313
ref,
1414
portalContainer,
15+
injectorFactory,
1516
}
1617

1718
export const enum NgtCompoundClassId {

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

Lines changed: 299 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,332 @@
1-
import { inject, Injectable, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2 } from '@angular/core';
1+
import { ChangeDetectorRef, inject, Injectable, Renderer2, RendererFactory2, RendererType2 } from '@angular/core';
22
import { ɵDomRendererFactory2 as DomRendererFactory2 } from '@angular/platform-browser';
3+
import { NGT_CATALOGUE } from '../di/catalogue';
4+
import { NgtStore } from '../stores/store';
5+
import { getLocalState, prepare } from '../utils/instance';
6+
import { is } from '../utils/is';
7+
import { NGT_COMPOUND_PREFIXES } from './di';
8+
import { NgtRendererClassId } from './enums';
9+
import { NgtRendererNode, NgtRendererStore } from './state';
10+
import { attachThreeChild, kebabToPascal, processThreeEvent, removeThreeChild, SPECIAL_DOM_TAG } from './utils';
311

412
@Injectable()
513
export class NgtRendererFactory implements RendererFactory2 {
614
private readonly domRendererFactory = inject(DomRendererFactory2);
15+
private readonly cdr = inject(ChangeDetectorRef);
16+
private readonly store = inject(NgtStore);
17+
private readonly catalogue = inject(NGT_CATALOGUE);
18+
private readonly compoundPrefixes = inject(NGT_COMPOUND_PREFIXES);
19+
20+
private readonly rendererStore = new NgtRendererStore({
21+
store: this.store,
22+
cdr: this.cdr,
23+
compoundPrefixes: this.compoundPrefixes,
24+
});
25+
26+
private renderer?: NgtRenderer;
727

828
createRenderer(hostElement: any, type: RendererType2 | null): Renderer2 {
9-
const domRenderer = this.domRendererFactory.createRenderer(hostElement, type);
10-
const renderer = new NgtRenderer(domRenderer);
11-
return renderer;
29+
// TODO we might need to check on "type" to return DomRenderer for that particular type to support HTML
30+
if (!this.renderer) {
31+
const domRenderer = this.domRendererFactory.createRenderer(hostElement, type);
32+
this.renderer = new NgtRenderer(domRenderer, this.rendererStore, this.catalogue);
33+
}
34+
return this.renderer;
1235
}
1336
}
1437

1538
export class NgtRenderer implements Renderer2 {
16-
constructor(private domRenderer: Renderer2) {}
39+
private first = false;
40+
41+
constructor(
42+
private readonly domRenderer: Renderer2,
43+
private readonly store: NgtRendererStore,
44+
private readonly catalogue: Record<string, new (...args: any[]) => any>
45+
) {}
1746

1847
createElement(name: string, namespace?: string | null | undefined) {
19-
throw new Error('Method not implemented.');
48+
const element = this.domRenderer.createElement(name, namespace);
49+
50+
// on first pass, we return the Root Scene as the root node
51+
if (!this.first) {
52+
this.first = true;
53+
return this.store.createNode('three', this.store.rootScene);
54+
}
55+
56+
// handle compound
57+
if (this.store.isCompound(name)) return this.store.createNode('compound', element);
58+
59+
// handle portal
60+
if (name === SPECIAL_DOM_TAG.NGT_PORTAL) {
61+
return this.store.createNode('portal', element);
62+
}
63+
64+
// handle raw value
65+
if (name === SPECIAL_DOM_TAG.NGT_VALUE) {
66+
return this.store.createNode(
67+
'three',
68+
Object.assign({ __ngt_renderer__: { rawValue: undefined } }, { __ngt__: { isRaw: true } })
69+
);
70+
}
71+
72+
const { injectedArgs, store } = this.store.getCreationState();
73+
74+
// handle primitive
75+
if (name === SPECIAL_DOM_TAG.NGT_PRIMITIVE) {
76+
if (!injectedArgs[0]) throw new Error(`[NGT] ngt-primitive without args is invalid`);
77+
const object = injectedArgs[0];
78+
let localState = getLocalState(object);
79+
if (!Object.keys(localState).length) {
80+
prepare(object, { store, args: injectedArgs, primitive: true });
81+
localState = getLocalState(object);
82+
}
83+
if (!localState.store) localState.store = store;
84+
return this.store.createNode('three', object);
85+
}
86+
87+
const threeTag = name.startsWith('ngt') ? name.slice(4) : name;
88+
const threeName = kebabToPascal(threeTag);
89+
const threeTarget = this.catalogue[threeName];
90+
// we have the THREE constructor here, handle it
91+
if (threeTarget) {
92+
const instance = prepare(new threeTarget(...injectedArgs), { store, args: injectedArgs });
93+
const node = this.store.createNode('three', instance);
94+
const localState = getLocalState(instance);
95+
if (is.geometry(instance)) {
96+
localState.attach = ['geometry'];
97+
} else if (is.material(instance)) {
98+
localState.attach = ['material'];
99+
}
100+
101+
return node;
102+
}
103+
104+
return this.store.createNode('dom', element);
20105
}
21106

22107
createComment(value: string) {
23-
throw new Error('Method not implemented.');
108+
const comment = this.domRenderer.createComment(value);
109+
return this.store.createNode('comment', comment);
24110
}
25111

26-
appendChild(parent: any, newChild: any): void {
27-
throw new Error('Method not implemented.');
112+
appendChild(parent: NgtRendererNode, newChild: NgtRendererNode): void {
113+
// TODO: just ignore text node for now
114+
if (newChild instanceof Text) return;
115+
116+
if (newChild.__ngt_renderer__[NgtRendererClassId.type] === 'comment') {
117+
this.store.setParent(newChild, parent);
118+
return;
119+
}
120+
121+
this.store.setParent(newChild, parent);
122+
this.store.addChild(parent, newChild);
123+
124+
// if new chlid is a portal
125+
if (newChild.__ngt_renderer__[NgtRendererClassId.type] === 'portal') {
126+
this.store.processPortalContainer(newChild);
127+
if (newChild.__ngt_renderer__[NgtRendererClassId.portalContainer]) {
128+
this.appendChild(parent, newChild.__ngt_renderer__[NgtRendererClassId.portalContainer]);
129+
}
130+
return;
131+
}
132+
133+
// if parent is a portal
134+
if (parent.__ngt_renderer__[NgtRendererClassId.type] === 'portal') {
135+
this.store.processPortalContainer(parent);
136+
if (parent.__ngt_renderer__[NgtRendererClassId.portalContainer]) {
137+
this.appendChild(parent.__ngt_renderer__[NgtRendererClassId.portalContainer], newChild);
138+
}
139+
return;
140+
}
141+
142+
// if both are three instances, straightforward case
143+
if (
144+
parent.__ngt_renderer__[NgtRendererClassId.type] === 'three' &&
145+
newChild.__ngt_renderer__[NgtRendererClassId.type] === 'three'
146+
) {
147+
attachThreeChild(parent, newChild);
148+
// here, we handle the special case of if the parent has a compoundParent, which means this child is part of a compound parent template
149+
if (!newChild.__ngt_renderer__[NgtRendererClassId.compound]) return;
150+
const closestGrandparentWithCompound = this.store.getClosestParentWithCompound(parent);
151+
if (!closestGrandparentWithCompound) return;
152+
this.appendChild(closestGrandparentWithCompound, newChild);
153+
return;
154+
}
155+
156+
// if only the parent is the THREE instance
157+
if (parent.__ngt_renderer__[NgtRendererClassId.type] === 'three') {
158+
if (newChild.__ngt_renderer__[NgtRendererClassId.children].length) {
159+
for (const renderChild of newChild.__ngt_renderer__[NgtRendererClassId.children]) {
160+
this.appendChild(parent, renderChild);
161+
}
162+
}
163+
}
164+
165+
// if parent is a compound
166+
if (parent.__ngt_renderer__[NgtRendererClassId.type] === 'compound') {
167+
// if compound doesn't have a THREE instance set yet
168+
if (
169+
!parent.__ngt_renderer__[NgtRendererClassId.compounded] &&
170+
newChild.__ngt_renderer__[NgtRendererClassId.type] === 'three'
171+
) {
172+
// if child is indeed an ngtCompound
173+
if (newChild.__ngt_renderer__[NgtRendererClassId.compound]) {
174+
this.store.setCompound(parent, newChild);
175+
} else {
176+
// if not, we track the parent (that is supposedly the compound component) on this three instance
177+
if (!newChild.__ngt_renderer__[NgtRendererClassId.compoundParent]) {
178+
newChild.__ngt_renderer__[NgtRendererClassId.compoundParent] = parent;
179+
}
180+
}
181+
}
182+
183+
// reset the compound if it's changed
184+
if (
185+
parent.__ngt_renderer__[NgtRendererClassId.compounded] &&
186+
newChild.__ngt_renderer__[NgtRendererClassId.type] === 'three' &&
187+
newChild.__ngt_renderer__[NgtRendererClassId.compound] &&
188+
parent.__ngt_renderer__[NgtRendererClassId.compounded] !== newChild
189+
) {
190+
this.store.setCompound(parent, newChild);
191+
}
192+
}
193+
194+
if (newChild.__ngt_renderer__[NgtRendererClassId.type] === 'three' && !getLocalState(newChild).parent) {
195+
// we'll try to get the grandparent instance here so that we can run appendChild with both instances
196+
const closestGrandparentInstance = this.store.getClosestParentWithInstance(parent);
197+
if (closestGrandparentInstance) {
198+
this.appendChild(closestGrandparentInstance, newChild);
199+
}
200+
return;
201+
}
202+
203+
if (
204+
parent.__ngt_renderer__[NgtRendererClassId.type] === 'dom' &&
205+
newChild.__ngt_renderer__[NgtRendererClassId.type] === 'dom'
206+
) {
207+
const closestGrandparentInstance = this.store.getClosestParentWithInstance(parent);
208+
if (closestGrandparentInstance) {
209+
this.appendChild(closestGrandparentInstance, newChild);
210+
}
211+
}
28212
}
29213

30-
insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean | undefined): void {
31-
throw new Error('Method not implemented.');
214+
insertBefore(
215+
parent: NgtRendererNode,
216+
newChild: NgtRendererNode
217+
// TODO we might need these?
218+
// refChild: NgtRendererNode,
219+
// isMove?: boolean | undefined
220+
): void {
221+
if (!parent.__ngt_renderer__) return;
222+
this.appendChild(parent, newChild);
32223
}
33224

34-
removeChild(parent: any, oldChild: any, isHostElement?: boolean | undefined): void {
35-
throw new Error('Method not implemented.');
225+
removeChild(parent: NgtRendererNode, oldChild: NgtRendererNode, isHostElement?: boolean | undefined): void {
226+
if (
227+
parent.__ngt_renderer__[NgtRendererClassId.type] === 'three' &&
228+
oldChild.__ngt_renderer__[NgtRendererClassId.type] === 'three'
229+
) {
230+
removeThreeChild(parent, oldChild, true);
231+
this.store.destroy(oldChild, parent);
232+
return;
233+
}
234+
235+
if (
236+
parent.__ngt_renderer__[NgtRendererClassId.type] === 'compound' &&
237+
parent.__ngt_renderer__[NgtRendererClassId.parent]
238+
) {
239+
this.removeChild(parent.__ngt_renderer__[NgtRendererClassId.parent], oldChild, isHostElement);
240+
return;
241+
}
242+
243+
if (parent.__ngt_renderer__[NgtRendererClassId.type] === 'three') {
244+
this.store.destroy(oldChild, parent);
245+
return;
246+
}
247+
248+
const closestGrandparentInstance = this.store.getClosestParentWithInstance(parent);
249+
if (closestGrandparentInstance) {
250+
this.removeChild(closestGrandparentInstance, oldChild, isHostElement);
251+
}
252+
this.store.destroy(oldChild, closestGrandparentInstance as NgtRendererNode);
36253
}
37254

38-
parentNode(node: any) {
39-
throw new Error('Method not implemented.');
255+
parentNode(node: NgtRendererNode) {
256+
if (node.__ngt_renderer__?.[NgtRendererClassId.parent]) return node.__ngt_renderer__[NgtRendererClassId.parent];
257+
return this.domRenderer.parentNode(node);
40258
}
41259

42-
setAttribute(el: any, name: string, value: string, namespace?: string | null | undefined): void {
43-
throw new Error('Method not implemented.');
260+
setAttribute(el: NgtRendererNode, name: string, value: string, namespace?: string | null | undefined): void {
261+
if (el.__ngt_renderer__[NgtRendererClassId.type] === 'compound') {
262+
// we don't have the compound instance yet
263+
el.__ngt_renderer__[NgtRendererClassId.attributes][name] = value;
264+
if (!el.__ngt_renderer__[NgtRendererClassId.compounded]) {
265+
this.store.queueOperation(el, ['op', () => this.setAttribute(el, name, value, namespace)]);
266+
return;
267+
}
268+
269+
this.setAttribute(el.__ngt_renderer__[NgtRendererClassId.compounded], name, value, namespace);
270+
return;
271+
}
272+
273+
if (el.__ngt_renderer__[NgtRendererClassId.type] === 'three') {
274+
this.store.applyAttribute(el, name, value);
275+
}
44276
}
45277

46-
setProperty(el: any, name: string, value: any): void {
47-
throw new Error('Method not implemented.');
278+
setProperty(el: NgtRendererNode, name: string, value: any): void {
279+
if (el.__ngt_renderer__[NgtRendererClassId.type] === 'compound') {
280+
// we don't have the compound instance yet
281+
el.__ngt_renderer__[NgtRendererClassId.properties][name] = value;
282+
if (!el.__ngt_renderer__[NgtRendererClassId.compounded]) {
283+
this.store.queueOperation(el, ['op', () => this.setProperty(el, name, value)]);
284+
return;
285+
}
286+
287+
if (el.__ngt_renderer__[NgtRendererClassId.compounded].__ngt_renderer__[NgtRendererClassId.compound]) {
288+
Object.assign(
289+
el.__ngt_renderer__[NgtRendererClassId.compounded].__ngt_renderer__[NgtRendererClassId.compound],
290+
{
291+
props: {
292+
...el.__ngt_renderer__[NgtRendererClassId.compounded].__ngt_renderer__[
293+
NgtRendererClassId.compound
294+
],
295+
[name]: value,
296+
},
297+
}
298+
);
299+
}
300+
this.setProperty(el.__ngt_renderer__[NgtRendererClassId.compounded], name, value);
301+
return;
302+
}
303+
304+
if (el.__ngt_renderer__[NgtRendererClassId.type] === 'three') {
305+
this.store.applyProperty(el, name, value);
306+
}
48307
}
49308

50-
listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void {
51-
throw new Error('Method not implemented.');
309+
listen(target: NgtRendererNode, eventName: string, callback: (event: any) => boolean | void): () => void {
310+
if (
311+
target.__ngt_renderer__[NgtRendererClassId.type] === 'three' ||
312+
(target.__ngt_renderer__[NgtRendererClassId.type] === 'compound' &&
313+
target.__ngt_renderer__[NgtRendererClassId.compounded])
314+
) {
315+
const instance = target.__ngt_renderer__[NgtRendererClassId.compounded] || target;
316+
const priority = getLocalState(target).priority;
317+
return processThreeEvent(instance, priority || 0, eventName, callback, this.store.rootCdr);
318+
}
319+
320+
if (
321+
target.__ngt_renderer__[NgtRendererClassId.type] === 'compound' &&
322+
!target.__ngt_renderer__[NgtRendererClassId.compounded]
323+
) {
324+
this.store.queueOperation(target, [
325+
'op',
326+
() => this.store.queueOperation(target, ['cleanUp', this.listen(target, eventName, callback)]),
327+
]);
328+
}
329+
return () => {};
52330
}
53331

54332
get data(): { [key: string]: any } {

0 commit comments

Comments
 (0)