Skip to content

Commit 3cb1ed0

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
feat(soba): start migrating billboard and its story
1 parent 10a1fa1 commit 3cb1ed0

File tree

21 files changed

+739
-16
lines changed

21 files changed

+739
-16
lines changed

libs/angular-three/src/lib/di/ref.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ import {
88
computed,
99
inject,
1010
runInInjectionContext,
11-
signal,
1211
untracked,
1312
} from '@angular/core';
1413
import { NgtInstanceNode } from '../types';
1514
import { getLocalState } from '../utils/instance';
1615
import { is } from '../utils/is';
1716
import { safeDetectChanges } from '../utils/safe-detect-changes';
17+
import { createSignal } from '../utils/signal';
1818

1919
export type NgtInjectedRef<TElement> = ElementRef<TElement> & {
2020
children: (type?: 'objects' | 'nonObjects' | 'both') => Signal<NgtInstanceNode[]>;
21+
untracked: TElement;
2122
};
2223

2324
export function injectNgtRef<TElement>(
@@ -30,7 +31,7 @@ export function injectNgtRef<TElement>(
3031
const cdr = inject(ChangeDetectorRef);
3132

3233
const ref = is.ref(initial) ? initial : new ElementRef<TElement>(initial as TElement);
33-
const signalRef = signal(ref.nativeElement);
34+
const signalRef = createSignal(ref.nativeElement);
3435
const readonlySignal = signalRef.asReadonly();
3536
const cached = new Map();
3637

@@ -57,13 +58,21 @@ export function injectNgtRef<TElement>(
5758
Object.defineProperty(ref, 'nativeElement', {
5859
set: (newElement) => {
5960
if (newElement !== untracked(signalRef)) {
60-
signalRef.set(newElement);
61+
try {
62+
signalRef.set(newElement);
63+
} catch {
64+
requestAnimationFrame(() => signalRef.set(newElement));
65+
}
6166
safeDetectChanges(cdr);
6267
}
6368
},
6469
get: () => readonlySignal(),
6570
});
6671

67-
return Object.assign(ref, { children });
72+
Object.defineProperty(ref, 'untracked', {
73+
get: () => untracked(readonlySignal),
74+
});
75+
76+
return Object.assign(ref, { children }) as NgtInjectedRef<TElement>;
6877
});
6978
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { NgtStore } from '../stores/store';
1818
import type { NgtAnyRecord } from '../types';
1919
import { getLocalState, prepare } from '../utils/instance';
2020
import { is } from '../utils/is';
21+
import { createSignal } from '../utils/signal';
2122
import { NGT_COMPOUND_PREFIXES } from './di';
2223
import { NgtRendererClassId } from './enums';
2324
import { NgtRendererStore, type NgtRendererNode, type NgtRendererState } from './store';
@@ -112,7 +113,10 @@ export class NgtRenderer implements Renderer2 {
112113
if (name === SPECIAL_DOM_TAG.NGT_VALUE) {
113114
return this.store.createNode(
114115
'three',
115-
Object.assign({ __ngt_renderer__: { rawValue: undefined } }, { __ngt__: { isRaw: true } })
116+
Object.assign(
117+
{ __ngt_renderer__: { rawValue: undefined } },
118+
{ __ngt__: { isRaw: true, parent: createSignal(null) } }
119+
)
116120
);
117121
}
118122

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ export function attachThreeChild(parent: NgtInstanceNode, child: NgtInstanceNode
7474

7575
// attach
7676
if (cLS.isRaw) {
77-
cLS.parent.set(parent);
77+
if (cLS.parent) {
78+
cLS.parent.set(parent);
79+
}
7880
// at this point we don't have rawValue yet, so we bail and wait until the Renderer recalls attach
7981
if (child.__ngt_renderer__[NgtRendererClassId.rawValue] === undefined) return;
8082
attach(parent, child.__ngt_renderer__[NgtRendererClassId.rawValue], attachProp);
@@ -91,7 +93,9 @@ export function attachThreeChild(parent: NgtInstanceNode, child: NgtInstanceNode
9193

9294
pLS.add(child, added ? 'objects' : 'nonObjects');
9395

94-
cLS.parent.set(parent);
96+
if (cLS.parent) {
97+
cLS.parent.set(parent);
98+
}
9599

96100
if (cLS.afterAttach) cLS.afterAttach.emit({ parent, node: child });
97101

libs/angular-three/src/lib/stores/signal.store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import {
33
Injectable,
44
Optional,
55
computed,
6-
signal,
76
untracked,
87
type CreateComputedOptions,
98
type Signal,
109
type WritableSignal,
1110
} from '@angular/core';
1211
import type { NgtAnyRecord } from '../types';
12+
import { createSignal } from '../utils/signal';
1313

1414
const STORE_COMPUTED_KEY = '__ngt_store_computed__' as const;
1515

@@ -24,7 +24,7 @@ export class NgtSignalStore<TState extends object> {
2424
initialState: Partial<TState> = {} as unknown as Partial<TState>
2525
) {
2626
initialState ??= {};
27-
this.#state = signal(Object.assign(initialState, { __ngt_dummy_state__: Date.now() }) as TState);
27+
this.#state = createSignal(Object.assign(initialState, { __ngt_dummy_state__: Date.now() }) as TState);
2828
}
2929

3030
select<

libs/angular-three/src/lib/utils/instance.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { signal, untracked } from '@angular/core';
1+
import { untracked } from '@angular/core';
22
import type { NgtAnyRecord, NgtInstanceLocalState, NgtInstanceNode } from '../types';
3+
import { createSignal } from './signal';
34
import { checkUpdate } from './update';
45

56
export function getLocalState<TInstance extends object = NgtAnyRecord>(
@@ -23,15 +24,15 @@ export function prepare<TInstance extends object = NgtAnyRecord>(
2324

2425
if (localState?.primitive || !instance.__ngt__) {
2526
const {
26-
objects = signal<NgtInstanceNode[]>([]),
27-
nonObjects = signal<NgtInstanceNode[]>([]),
27+
objects = createSignal<NgtInstanceNode[]>([]),
28+
nonObjects = createSignal<NgtInstanceNode[]>([]),
2829
...rest
2930
} = localState || {};
3031

3132
instance.__ngt__ = {
3233
previousAttach: null,
3334
store: null,
34-
parent: signal(null),
35+
parent: createSignal(null),
3536
memoized: {},
3637
eventCount: 0,
3738
handlers: {},
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { signal, type CreateSignalOptions, type WritableSignal } from '@angular/core';
2+
3+
export function createSignal<TValue>(initialValue: TValue, options: CreateSignalOptions<TValue> = {}) {
4+
const original = signal(initialValue, options);
5+
6+
const originalSet = original.set.bind(original);
7+
const originalUpdate = original.update.bind(original);
8+
9+
original.set = (...args: Parameters<WritableSignal<TValue>['set']>) => {
10+
try {
11+
originalSet(...args);
12+
} catch {
13+
requestAnimationFrame(() => originalSet(...args));
14+
}
15+
};
16+
17+
original.update = (...args: Parameters<WritableSignal<TValue>['update']>) => {
18+
try {
19+
originalUpdate(...args);
20+
} catch {
21+
requestAnimationFrame(() => originalUpdate(...args));
22+
}
23+
};
24+
25+
return original;
26+
}

libs/soba/.storybook/main.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { resolve } from 'path';
2+
13
const config = {
24
stories: ['../**/*.stories.@(js|jsx|ts|tsx|mdx)'],
35
addons: ['@storybook/addon-essentials'],

libs/soba/abstractions/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# angular-three-soba/abstractions
2+
3+
Secondary entry point of `angular-three-soba`. It can be used by importing from `angular-three-soba/abstractions`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "src/index.ts"
4+
}
5+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { CUSTOM_ELEMENTS_SCHEMA, Component, Input } from '@angular/core';
2+
import { NgtGroup, NgtSignalStore, extend, injectBeforeRender, injectNgtRef } from 'angular-three';
3+
import { Group } from 'three';
4+
5+
extend({ Group });
6+
7+
export type NgtsBillboardState = {
8+
follow?: boolean;
9+
lockX?: boolean;
10+
lockY?: boolean;
11+
lockZ?: boolean;
12+
};
13+
14+
declare global {
15+
interface HTMLElementTagNameMap {
16+
'ngts-billboard': NgtsBillboardState & NgtGroup;
17+
}
18+
}
19+
20+
@Component({
21+
selector: 'ngts-billboard',
22+
standalone: true,
23+
template: `
24+
<ngt-group ngtCompound [ref]="billboardRef">
25+
<ng-content />
26+
</ngt-group>
27+
`,
28+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
29+
})
30+
export class NgtsBillboard extends NgtSignalStore<NgtsBillboardState> {
31+
@Input() billboardRef = injectNgtRef<Group>();
32+
33+
@Input() set follow(follow: boolean) {
34+
this.set({ follow });
35+
}
36+
@Input() set lockX(lockX: boolean) {
37+
this.set({ lockX });
38+
}
39+
@Input() set lockY(lockY: boolean) {
40+
this.set({ lockY });
41+
}
42+
@Input() set lockZ(lockZ: boolean) {
43+
this.set({ lockZ });
44+
}
45+
46+
constructor() {
47+
super({ follow: true, lockX: false, lockY: false, lockZ: false });
48+
injectBeforeRender(({ camera }) => {
49+
const ref = this.billboardRef.untracked;
50+
const { follow, lockX, lockY, lockZ } = this.get();
51+
if (!ref || !follow) return;
52+
// save previous rotation in case we're locking an axis
53+
const prevRotation = ref.rotation.clone();
54+
55+
// always face the camera
56+
camera.getWorldQuaternion(ref.quaternion);
57+
58+
// re-adjust any axis that is locked
59+
if (lockX) ref.rotation.x = prevRotation.x;
60+
if (lockY) ref.rotation.y = prevRotation.y;
61+
if (lockZ) ref.rotation.z = prevRotation.z;
62+
});
63+
}
64+
}

libs/soba/abstractions/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './billboard/billboard';

libs/soba/controls/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# angular-three-soba/controls
2+
3+
Secondary entry point of `angular-three-soba`. It can be used by importing from `angular-three-soba/controls`.

libs/soba/controls/ng-package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "src/index.ts"
4+
}
5+
}

libs/soba/controls/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './orbit-controls/orbit-controls';

0 commit comments

Comments
 (0)