Skip to content

Commit 7f78a4c

Browse files
committed
feat(core): add parent directive for special attach case
1 parent f52de02 commit 7f78a4c

File tree

5 files changed

+217
-5
lines changed

5 files changed

+217
-5
lines changed

libs/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './lib/canvas';
22
export * from './lib/directives/args';
3+
export * from './lib/directives/parent';
34
export * from './lib/directives/selection';
45
export * from './lib/html';
56
export * from './lib/instance';
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {
2+
computed,
3+
DestroyRef,
4+
Directive,
5+
effect,
6+
ElementRef,
7+
EmbeddedViewRef,
8+
inject,
9+
input,
10+
TemplateRef,
11+
ViewContainerRef,
12+
} from '@angular/core';
13+
import { Object3D } from 'three';
14+
import { SPECIAL_INTERNAL_ADD_COMMENT, SPECIAL_INTERNAL_SET_PARENT_COMMENT } from '../renderer/constants';
15+
import { NgtNullish } from '../types';
16+
17+
@Directive({ selector: 'ng-template[parent]' })
18+
export class NgtParent {
19+
parent = input.required<
20+
string | Object3D | ElementRef<Object3D> | (() => NgtNullish<ElementRef<Object3D> | Object3D | string>)
21+
>();
22+
23+
private vcr = inject(ViewContainerRef);
24+
private template = inject(TemplateRef);
25+
26+
protected injected = false;
27+
protected injectedParent: NgtNullish<ElementRef<Object3D> | Object3D | string> = null;
28+
private view?: EmbeddedViewRef<unknown>;
29+
30+
private _parent = computed(() => {
31+
const parent = this.parent();
32+
if (typeof parent === 'function') {
33+
return parent();
34+
}
35+
return parent;
36+
});
37+
38+
constructor() {
39+
const commentNode = this.vcr.element.nativeElement;
40+
if (commentNode[SPECIAL_INTERNAL_ADD_COMMENT]) {
41+
commentNode[SPECIAL_INTERNAL_ADD_COMMENT]('parent');
42+
delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT];
43+
}
44+
45+
effect(() => {
46+
const parent = this._parent();
47+
if (!parent) return;
48+
49+
this.injected = false;
50+
this.injectedParent = parent;
51+
this.createView();
52+
});
53+
54+
inject(DestroyRef).onDestroy(() => {
55+
this.view?.destroy();
56+
});
57+
}
58+
59+
get value() {
60+
if (this.validate()) {
61+
this.injected = true;
62+
return this.injectedParent;
63+
}
64+
return null;
65+
}
66+
67+
validate() {
68+
return !this.injected && !!this.injectedParent;
69+
}
70+
71+
private createView() {
72+
if (this.view && !this.view.destroyed) this.view.destroy();
73+
74+
const comment = this.vcr.element.nativeElement;
75+
if (comment[SPECIAL_INTERNAL_SET_PARENT_COMMENT]) {
76+
comment[SPECIAL_INTERNAL_SET_PARENT_COMMENT](this.injectedParent);
77+
}
78+
79+
this.view = this.vcr.createEmbeddedView(this.template);
80+
this.view.detectChanges();
81+
}
82+
}

libs/core/src/lib/renderer/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const ROUTED_SCENE = '__ngt_renderer_is_routed_scene__';
22
export const HTML = '__ngt_renderer_is_html';
33
export const NON_ROOT = '__ngt_renderer_is_non_root__';
44
export const SPECIAL_INTERNAL_ADD_COMMENT = '__ngt_renderer_add_comment__';
5+
export const SPECIAL_INTERNAL_SET_PARENT_COMMENT = '__ngt_renderer_set_parent_comment__';
56
export const DOM_PARENT = '__ngt_dom_parent__';
67

78
export const SPECIAL_DOM_TAG = {

libs/core/src/lib/renderer/index.ts

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import { DOCUMENT } from '@angular/common';
22
import {
33
DebugNode,
4+
ElementRef,
45
Injectable,
56
Renderer2,
67
RendererFactory2,
78
RendererType2,
9+
Type,
810
inject,
911
makeEnvironmentProviders,
1012
untracked,
1113
} from '@angular/core';
14+
import { Object3D } from 'three';
1215
import { NgtArgs } from '../directives/args';
16+
import { NgtParent } from '../directives/parent';
1317
import { getLocalState, prepare } from '../instance';
1418
import { NGT_STORE, injectStore, provideStore } from '../store';
1519
import { NgtAnyRecord, NgtLocalState, NgtState } from '../types';
@@ -24,6 +28,7 @@ import {
2428
ROUTED_SCENE,
2529
SPECIAL_DOM_TAG,
2630
SPECIAL_INTERNAL_ADD_COMMENT,
31+
SPECIAL_INTERNAL_SET_PARENT_COMMENT,
2732
SPECIAL_PROPERTIES,
2833
} from './constants';
2934
import {
@@ -87,6 +92,7 @@ export class NgtRendererFactory implements RendererFactory2 {
8792

8893
export class NgtRenderer implements Renderer2 {
8994
private argsCommentNodes: Array<NgtRendererNode> = [];
95+
private parentCommentNodes: Array<NgtRendererNode> = [];
9096

9197
constructor(
9298
private delegate: Renderer2,
@@ -130,17 +136,31 @@ export class NgtRenderer implements Renderer2 {
130136
);
131137
}
132138

133-
const [injectedArgs] = [this.getNgtArgs()?.value || []];
139+
const [injectedArgs, injectedParent] = [
140+
this.getNgtDirective(NgtArgs, this.argsCommentNodes)?.value || [],
141+
this.getNgtDirective(NgtParent, this.parentCommentNodes)?.value,
142+
];
134143

135144
if (name === SPECIAL_DOM_TAG.NGT_PRIMITIVE) {
136145
if (!injectedArgs[0]) throw new Error(`[NGT] ngt-primitive without args is invalid`);
137146
const object = injectedArgs[0];
138-
const localState = getLocalState(object);
147+
let localState = getLocalState(object);
139148
if (!localState) {
140149
// NOTE: if an object isn't already "prepared", we prepare it
141150
prepare(object, { store: this.rootStore, primitive: true });
151+
localState = getLocalState(object);
142152
}
143-
return createNode('three', object, this.document);
153+
154+
const primitiveNode = createNode('three', object, this.document);
155+
156+
if (injectedParent) {
157+
const resolvedParent = this.getParentFromNgtParent(injectedParent, this.rootStore);
158+
if (resolvedParent) {
159+
primitiveNode.__ngt_renderer__[NgtRendererClassId.parent] = resolvedParent as unknown as NgtRendererNode;
160+
}
161+
}
162+
163+
return primitiveNode;
144164
}
145165

146166
const threeName = kebabToPascal(name.startsWith('ngt-') ? name.slice(4) : name);
@@ -159,6 +179,13 @@ export class NgtRenderer implements Renderer2 {
159179
localState.attach = ['material'];
160180
}
161181

182+
if (injectedParent) {
183+
const resolvedParent = this.getParentFromNgtParent(injectedParent, this.rootStore);
184+
if (resolvedParent) {
185+
node.__ngt_renderer__[NgtRendererClassId.parent] = resolvedParent as unknown as NgtRendererNode;
186+
}
187+
}
188+
162189
return node;
163190
}
164191

@@ -167,19 +194,28 @@ export class NgtRenderer implements Renderer2 {
167194

168195
createComment(value: string) {
169196
const comment = this.delegate.createComment(value);
197+
const commentNode = createNode('comment', comment, this.document);
170198

171199
// NOTE: we attach an arrow function to the Comment node
172200
// In our directives, we can call this function to then start tracking the RendererNode
173201
// this is done to limit the amount of Nodes we need to process for getCreationState
174-
comment[SPECIAL_INTERNAL_ADD_COMMENT] = (node: NgtRendererNode | 'args') => {
202+
comment[SPECIAL_INTERNAL_ADD_COMMENT] = (node: NgtRendererNode | 'args' | 'parent') => {
175203
if (node === 'args') {
176204
this.argsCommentNodes.push(comment);
205+
} else if (node === 'parent') {
206+
this.parentCommentNodes.push(comment);
207+
comment[SPECIAL_INTERNAL_SET_PARENT_COMMENT] = (ngtParent: Object3D | ElementRef<Object3D> | string) => {
208+
commentNode.__ngt_renderer__[NgtRendererClassId.parent] = this.getParentFromNgtParent(
209+
ngtParent,
210+
this.rootStore,
211+
) as unknown as NgtRendererNode;
212+
};
177213
} else if (typeof node === 'object') {
178214
this.portalCommentsNodes.push(node);
179215
}
180216
};
181217

182-
return createNode('comment', comment, this.document);
218+
return commentNode;
183219
}
184220

185221
appendChild(parent: NgtRendererNode, newChild: NgtRendererNode): void {
@@ -627,6 +663,97 @@ export class NgtRenderer implements Renderer2 {
627663
return isDanglingThreeChild || (isParentStillDOM && isChildStillDOM) || isParentStillDOM;
628664
}
629665

666+
private getNgtDirective<TDirective>(directive: Type<TDirective>, commentNodes: Array<NgtRendererNode>) {
667+
let directiveInstance: TDirective | undefined;
668+
669+
const destroyed = [];
670+
671+
let i = commentNodes.length - 1;
672+
while (i >= 0) {
673+
const comment = commentNodes[i];
674+
if (comment.__ngt_renderer__[NgtRendererClassId.destroyed]) {
675+
destroyed.push(i);
676+
i--;
677+
continue;
678+
}
679+
const injector = comment.__ngt_renderer__[NgtRendererClassId.debugNodeFactory]?.()?.injector;
680+
if (!injector) {
681+
i--;
682+
continue;
683+
}
684+
const instance = injector.get(directive, null);
685+
if (
686+
instance &&
687+
typeof instance === 'object' &&
688+
'validate' in instance &&
689+
typeof instance.validate === 'function' &&
690+
instance.validate()
691+
) {
692+
directiveInstance = instance;
693+
break;
694+
}
695+
i--;
696+
}
697+
destroyed.forEach((index) => {
698+
commentNodes.splice(index, 1);
699+
});
700+
return directiveInstance;
701+
}
702+
703+
private getParentFromNgtParent(ngtParent: Object3D | ElementRef<Object3D> | string, store: NgtSignalStore<NgtState>) {
704+
let topMostStore = store;
705+
706+
while (topMostStore.snapshot.previousRoot) {
707+
topMostStore = topMostStore.snapshot.previousRoot;
708+
}
709+
710+
const scene = topMostStore.snapshot.scene;
711+
712+
if (typeof ngtParent === 'string') {
713+
return scene.getObjectByName(ngtParent);
714+
}
715+
716+
if ('nativeElement' in ngtParent) {
717+
return ngtParent.nativeElement;
718+
}
719+
720+
return ngtParent;
721+
}
722+
723+
private getNgtParent() {
724+
let directive: NgtParent | undefined;
725+
726+
const destroyed = [];
727+
728+
let i = this.parentCommentNodes.length - 1;
729+
while (i >= 0) {
730+
const comment = this.parentCommentNodes[i];
731+
if (comment.__ngt_renderer__[NgtRendererClassId.destroyed]) {
732+
destroyed.push(i);
733+
i--;
734+
continue;
735+
}
736+
const injector = comment.__ngt_renderer__[NgtRendererClassId.debugNodeFactory]?.()?.injector;
737+
if (!injector) {
738+
i--;
739+
continue;
740+
}
741+
const instance = injector.get(NgtParent, null);
742+
if (instance && instance.validate()) {
743+
directive = instance;
744+
break;
745+
}
746+
747+
i--;
748+
}
749+
750+
destroyed.forEach((index) => {
751+
this.parentCommentNodes.splice(index, 1);
752+
});
753+
754+
return directive;
755+
}
756+
630757
private getNgtArgs() {
631758
let directive: NgtArgs | undefined;
632759

libs/core/src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { NgtSignalStore } from './utils/signal-store';
2222

2323
export type NgtProperties<T> = { [K in keyof T as T[K] extends (...args: Array<any>) => any ? never : K]: T[K] };
2424
export type NgtAnyRecord = Record<string, any>;
25+
export type NgtNullish<T> = T | null | undefined;
2526

2627
export type NgtEquConfig = {
2728
/** Compare arrays by reference equality a === b (default), or by shallow equality */

0 commit comments

Comments
 (0)