Skip to content

Commit aa3f5aa

Browse files
feat(angular-3d): implement state store and component registry services
1 parent d77c662 commit aa3f5aa

File tree

11 files changed

+2150
-6
lines changed

11 files changed

+2150
-6
lines changed

libs/angular-3d/eslint.config.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,15 @@ export default [
5858
'@typescript-eslint/no-explicit-any': 'warn',
5959
'@typescript-eslint/no-unused-vars': [
6060
'error',
61-
{ argsIgnorePattern: '^_' },
61+
{
62+
args: 'all',
63+
argsIgnorePattern: '^_',
64+
caughtErrors: 'all',
65+
caughtErrorsIgnorePattern: '^_',
66+
destructuredArrayIgnorePattern: '^_',
67+
varsIgnorePattern: '^_',
68+
ignoreRestSiblings: true,
69+
},
6270
],
6371
'@typescript-eslint/explicit-member-accessibility': [
6472
'error',

libs/angular-3d/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"peerDependencies": {
55
"@angular/core": "~20.3.0",
66
"three": "^0.182.0",
7-
"gsap": "^3.14.2"
7+
"gsap": "^3.14.2",
8+
"rxjs": "~7.8.0"
89
},
910
"sideEffects": false
1011
}
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import {
3+
Angular3DStateStore,
4+
SceneState,
5+
LightState,
6+
MaterialState,
7+
AnimationState,
8+
SceneObjectState,
9+
} from './angular-3d-state.store';
10+
11+
describe('Angular3DStateStore', () => {
12+
let store: Angular3DStateStore;
13+
14+
beforeEach(() => {
15+
TestBed.configureTestingModule({
16+
providers: [Angular3DStateStore],
17+
});
18+
store = TestBed.inject(Angular3DStateStore);
19+
});
20+
21+
afterEach(() => {
22+
store.reset();
23+
});
24+
25+
describe('initialization', () => {
26+
it('should create store instance', () => {
27+
expect(store).toBeTruthy();
28+
});
29+
30+
it('should have default state', () => {
31+
const state = store.state();
32+
expect(state.scenes).toEqual({});
33+
expect(state.activeSceneId).toBeNull();
34+
expect(state.camera.type).toBe('perspective');
35+
expect(state.camera.fov).toBe(75);
36+
expect(state.isDebugMode).toBe(false);
37+
});
38+
39+
it('should have null active scene initially', () => {
40+
expect(store.activeScene()).toBeNull();
41+
});
42+
});
43+
44+
describe('scene management', () => {
45+
it('should create a scene', () => {
46+
const scene = store.createScene('scene-1', 'Test Scene');
47+
48+
expect(scene.id).toBe('scene-1');
49+
expect(scene.name).toBe('Test Scene');
50+
expect(store.state().scenes['scene-1']).toBeDefined();
51+
});
52+
53+
it('should create a scene with custom config', () => {
54+
const scene = store.createScene('scene-1', 'Test Scene', {
55+
backgroundColor: 0xff0000,
56+
isActive: true,
57+
});
58+
59+
expect(scene.backgroundColor).toBe(0xff0000);
60+
expect(scene.isActive).toBe(true);
61+
});
62+
63+
it('should update a scene', () => {
64+
store.createScene('scene-1', 'Test Scene');
65+
store.updateScene('scene-1', { backgroundColor: 0x00ff00 });
66+
67+
expect(store.state().scenes['scene-1'].backgroundColor).toBe(0x00ff00);
68+
});
69+
70+
it('should not update non-existent scene', () => {
71+
const stateBefore = store.state();
72+
store.updateScene('nonexistent', { backgroundColor: 0x00ff00 });
73+
expect(store.state()).toBe(stateBefore);
74+
});
75+
76+
it('should remove a scene', () => {
77+
store.createScene('scene-1', 'Test Scene');
78+
store.removeScene('scene-1');
79+
80+
expect(store.state().scenes['scene-1']).toBeUndefined();
81+
});
82+
83+
it('should clear active scene when removed', () => {
84+
store.createScene('scene-1', 'Test Scene');
85+
store.setActiveScene('scene-1');
86+
store.removeScene('scene-1');
87+
88+
expect(store.state().activeSceneId).toBeNull();
89+
});
90+
91+
it('should set active scene', () => {
92+
store.createScene('scene-1', 'Test Scene');
93+
store.setActiveScene('scene-1');
94+
95+
expect(store.state().activeSceneId).toBe('scene-1');
96+
expect(store.activeScene()?.id).toBe('scene-1');
97+
});
98+
});
99+
100+
describe('scene object management', () => {
101+
const testObject: SceneObjectState = {
102+
id: 'obj-1',
103+
name: 'Test Object',
104+
type: 'mesh',
105+
visible: true,
106+
position: [0, 0, 0],
107+
rotation: [0, 0, 0],
108+
scale: [1, 1, 1],
109+
children: [],
110+
userData: {},
111+
};
112+
113+
beforeEach(() => {
114+
store.createScene('scene-1', 'Test Scene');
115+
});
116+
117+
it('should add object to scene', () => {
118+
store.addSceneObject('scene-1', testObject);
119+
120+
expect(store.state().scenes['scene-1'].objects['obj-1']).toBeDefined();
121+
});
122+
123+
it('should not add object to non-existent scene', () => {
124+
store.addSceneObject('nonexistent', testObject);
125+
126+
expect(store.state().scenes['scene-1'].objects['obj-1']).toBeUndefined();
127+
});
128+
129+
it('should update scene object', () => {
130+
store.addSceneObject('scene-1', testObject);
131+
store.updateSceneObject('scene-1', 'obj-1', { visible: false });
132+
133+
expect(store.state().scenes['scene-1'].objects['obj-1'].visible).toBe(
134+
false
135+
);
136+
});
137+
138+
it('should remove scene object', () => {
139+
store.addSceneObject('scene-1', testObject);
140+
store.removeSceneObject('scene-1', 'obj-1');
141+
142+
expect(store.state().scenes['scene-1'].objects['obj-1']).toBeUndefined();
143+
});
144+
145+
it('should return scene objects via computed signal', () => {
146+
store.addSceneObject('scene-1', testObject);
147+
store.setActiveScene('scene-1');
148+
149+
expect(store.sceneObjects()).toHaveLength(1);
150+
expect(store.sceneObjects()[0].id).toBe('obj-1');
151+
});
152+
});
153+
154+
describe('camera management', () => {
155+
it('should update camera position', () => {
156+
store.updateCamera({ position: [10, 20, 30] });
157+
158+
expect(store.state().camera.position).toEqual([10, 20, 30]);
159+
});
160+
161+
it('should update camera fov', () => {
162+
store.updateCamera({ fov: 90 });
163+
164+
expect(store.state().camera.fov).toBe(90);
165+
});
166+
167+
it('should preserve other camera properties on update', () => {
168+
store.updateCamera({ fov: 90 });
169+
170+
expect(store.state().camera.near).toBe(0.1);
171+
expect(store.state().camera.far).toBe(1000);
172+
});
173+
});
174+
175+
describe('light management', () => {
176+
const testLight: LightState = {
177+
id: 'light-1',
178+
type: 'directional',
179+
color: 0xffffff,
180+
intensity: 1,
181+
position: [0, 10, 0],
182+
castShadow: true,
183+
};
184+
185+
it('should add a light', () => {
186+
store.addLight(testLight);
187+
188+
expect(store.state().lights['light-1']).toBeDefined();
189+
expect(store.activeLights()).toHaveLength(1);
190+
});
191+
192+
it('should update a light', () => {
193+
store.addLight(testLight);
194+
store.updateLight('light-1', { intensity: 0.5 });
195+
196+
expect(store.state().lights['light-1'].intensity).toBe(0.5);
197+
});
198+
199+
it('should remove a light', () => {
200+
store.addLight(testLight);
201+
store.removeLight('light-1');
202+
203+
expect(store.state().lights['light-1']).toBeUndefined();
204+
expect(store.activeLights()).toHaveLength(0);
205+
});
206+
});
207+
208+
describe('material management', () => {
209+
const testMaterial: MaterialState = {
210+
id: 'mat-1',
211+
type: 'standard',
212+
color: 0xff0000,
213+
opacity: 1,
214+
transparent: false,
215+
wireframe: false,
216+
roughness: 0.5,
217+
metalness: 0.5,
218+
};
219+
220+
it('should add a material', () => {
221+
store.addMaterial(testMaterial);
222+
223+
expect(store.state().materials['mat-1']).toBeDefined();
224+
expect(store.activeMaterials()).toHaveLength(1);
225+
});
226+
227+
it('should update a material', () => {
228+
store.addMaterial(testMaterial);
229+
store.updateMaterial('mat-1', { roughness: 0.8 });
230+
231+
expect(store.state().materials['mat-1'].roughness).toBe(0.8);
232+
});
233+
234+
it('should remove a material', () => {
235+
store.addMaterial(testMaterial);
236+
store.removeMaterial('mat-1');
237+
238+
expect(store.state().materials['mat-1']).toBeUndefined();
239+
});
240+
});
241+
242+
describe('animation management', () => {
243+
const testAnimation: AnimationState = {
244+
id: 'anim-1',
245+
target: 'obj-1',
246+
isPlaying: true,
247+
duration: 1000,
248+
currentTime: 0,
249+
loop: true,
250+
timeScale: 1,
251+
};
252+
253+
it('should add an animation', () => {
254+
store.addAnimation(testAnimation);
255+
256+
expect(store.state().animations['anim-1']).toBeDefined();
257+
});
258+
259+
it('should report playing animations', () => {
260+
store.addAnimation(testAnimation);
261+
262+
expect(store.playingAnimations()).toHaveLength(1);
263+
});
264+
265+
it('should update an animation', () => {
266+
store.addAnimation(testAnimation);
267+
store.updateAnimation('anim-1', { isPlaying: false });
268+
269+
expect(store.playingAnimations()).toHaveLength(0);
270+
});
271+
272+
it('should remove an animation', () => {
273+
store.addAnimation(testAnimation);
274+
store.removeAnimation('anim-1');
275+
276+
expect(store.state().animations['anim-1']).toBeUndefined();
277+
});
278+
});
279+
280+
describe('performance monitoring', () => {
281+
it('should have default performance metrics', () => {
282+
const perf = store.performance();
283+
284+
expect(perf.fps).toBe(60);
285+
expect(perf.frameTime).toBe(16.67);
286+
});
287+
288+
it('should update performance metrics', () => {
289+
store.updatePerformance({ fps: 30, frameTime: 33.33 });
290+
291+
expect(store.performance().fps).toBe(30);
292+
});
293+
294+
it('should report healthy status for good fps', () => {
295+
store.updatePerformance({ fps: 60, frameTime: 16.67 });
296+
297+
expect(store.performanceStatus().isHealthy).toBe(true);
298+
});
299+
300+
it('should report unhealthy status for low fps', () => {
301+
store.updatePerformance({ fps: 20, frameTime: 50 });
302+
303+
expect(store.performanceStatus().isHealthy).toBe(false);
304+
});
305+
});
306+
307+
describe('debug mode', () => {
308+
it('should toggle debug mode', () => {
309+
expect(store.isDebugMode()).toBe(false);
310+
311+
store.toggleDebugMode();
312+
expect(store.isDebugMode()).toBe(true);
313+
314+
store.toggleDebugMode();
315+
expect(store.isDebugMode()).toBe(false);
316+
});
317+
318+
it('should set debug mode explicitly', () => {
319+
store.setDebugMode(true);
320+
expect(store.isDebugMode()).toBe(true);
321+
322+
store.setDebugMode(false);
323+
expect(store.isDebugMode()).toBe(false);
324+
});
325+
});
326+
327+
describe('reset', () => {
328+
it('should reset state to initial values', () => {
329+
store.createScene('scene-1', 'Test Scene');
330+
store.setActiveScene('scene-1');
331+
store.updateCamera({ fov: 90 });
332+
store.setDebugMode(true);
333+
334+
store.reset();
335+
336+
expect(store.state().scenes).toEqual({});
337+
expect(store.state().activeSceneId).toBeNull();
338+
expect(store.state().camera.fov).toBe(75);
339+
expect(store.isDebugMode()).toBe(false);
340+
});
341+
});
342+
343+
describe('lastUpdateTime', () => {
344+
it('should update timestamp on state changes', (done) => {
345+
const initialTime = store.lastUpdateTime();
346+
347+
setTimeout(() => {
348+
store.createScene('scene-1', 'Test Scene');
349+
expect(store.lastUpdateTime()).toBeGreaterThan(initialTime);
350+
done();
351+
}, 10);
352+
});
353+
});
354+
});

0 commit comments

Comments
 (0)