diff --git a/src/app/core/engine/engine.service.spec.ts b/src/app/core/engine/engine.service.spec.ts index 60094d0..c14956e 100644 --- a/src/app/core/engine/engine.service.spec.ts +++ b/src/app/core/engine/engine.service.spec.ts @@ -1,4 +1,4 @@ -import { NgZone } from '@angular/core'; +import { Injector, NgZone, runInInjectionContext } from '@angular/core'; import * as THREE from 'three/webgpu'; import { beforeEach, describe, expect, it } from 'vitest'; @@ -10,7 +10,8 @@ import { EngineService } from './engine.service'; * arithmetic that keeps them showing the same thing. */ function engineWithCameras(): { engine: EngineService; perspective: THREE.PerspectiveCamera; orthographic: THREE.OrthographicCamera } { - const engine = new EngineService({ runOutsideAngular: (fn: () => unknown) => fn() } as unknown as NgZone); + const zone = { runOutsideAngular: (fn: () => unknown) => fn() } as unknown as NgZone; + const engine = runInInjectionContext(Injector.create({ providers: [{ provide: NgZone, useValue: zone }] }), () => new EngineService()); const perspective = new THREE.PerspectiveCamera(50, 16 / 9, 0.1, 1000); const orthographic = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000); // The two cameras are private, because nothing outside should choose between them by hand. diff --git a/src/app/core/engine/engine.service.ts b/src/app/core/engine/engine.service.ts index b68420b..0c9e81b 100644 --- a/src/app/core/engine/engine.service.ts +++ b/src/app/core/engine/engine.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NgZone } from '@angular/core'; +import { inject, Injectable, NgZone } from '@angular/core'; import * as THREE from 'three/webgpu'; export type EngineTickCallback = (deltaSeconds: number, elapsedSeconds: number) => void; @@ -20,6 +20,7 @@ export type SceneCamera = THREE.PerspectiveCamera | THREE.OrthographicCamera; */ @Injectable() export class EngineService { + private readonly ngZone = inject(NgZone); private readonly clock = new THREE.Clock(false); private readonly tickCallbacks = new Set(); @@ -36,8 +37,6 @@ export class EngineService { private projection: Projection = 'perspective'; private running = false; - constructor(private readonly ngZone: NgZone) {} - get isInitialized(): boolean { return !!this.renderer; } diff --git a/src/app/features/body-detail/body-detail-scene.component.ts b/src/app/features/body-detail/body-detail-scene.component.ts index 9252089..bf4bcb5 100644 --- a/src/app/features/body-detail/body-detail-scene.component.ts +++ b/src/app/features/body-detail/body-detail-scene.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, ElementRef, OnDestroy, signal, viewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, inject, OnDestroy, signal, viewChild } from '@angular/core'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { Subscription } from 'rxjs'; import * as THREE from 'three/webgpu'; @@ -65,6 +65,12 @@ const GLOW_SCALE = 2.6; ` }) export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy { + private readonly engine = inject(EngineService); + private readonly dataLoader = inject(DataLoaderService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly navigationStore = inject(NavigationStore); + /** A kept place, revisited from this page: a star means leaving it for the map. */ goToBookmark(bookmark: Bookmark): void { if (bookmark.kind === 'star') { @@ -95,14 +101,6 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy { readonly viewModel = signal(undefined); readonly notFound = signal(false); - constructor( - private readonly engine: EngineService, - private readonly dataLoader: DataLoaderService, - private readonly route: ActivatedRoute, - private readonly router: Router, - private readonly navigationStore: NavigationStore - ) {} - ngAfterViewInit(): void { void this.bootstrap(); } diff --git a/src/app/features/body-detail/info-panel.component.ts b/src/app/features/body-detail/info-panel.component.ts index 24180fc..387cdd7 100644 --- a/src/app/features/body-detail/info-panel.component.ts +++ b/src/app/features/body-detail/info-panel.component.ts @@ -98,6 +98,8 @@ import { ReadoutSectionsComponent } from './readout-sections.component'; export class InfoPanelComponent { readonly body = input.required(); + private readonly router = inject(Router); + readonly bookmarks = inject(BookmarksStore); private readonly articles = inject(ArticleService); @@ -105,7 +107,7 @@ export class InfoPanelComponent { readonly article = signal
(null); readonly aboutState = signal<'idle' | 'loading' | 'none' | 'unavailable'>('idle'); - constructor(private readonly router: Router) { + constructor() { // The panel is reused as the route's parameter changes, so what was asked about one body // must not still be showing under the next one's name. effect(() => { diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.ts index 51c8a40..05bf8ad 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, computed, effect, ElementRef, OnDestroy, signal, viewChild } from '@angular/core'; +import { AfterViewInit, Component, computed, effect, ElementRef, inject, OnDestroy, signal, viewChild } from '@angular/core'; import { Router } from '@angular/router'; import * as THREE from 'three/webgpu'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; @@ -323,6 +323,11 @@ function galacticOverviewPose(): { position: THREE.Vector3; target: THREE.Vector ` }) export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { + private readonly engine = inject(EngineService); + private readonly dataLoader = inject(DataLoaderService); + private readonly router = inject(Router); + readonly navigationStore = inject(NavigationStore); + private readonly canvasRef = viewChild.required>('canvas'); private readonly labelHostRef = viewChild.required>('labelHost'); private readonly leaderRef = viewChild.required>('leader'); @@ -467,12 +472,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { private starMarker?: THREE.Mesh; private starGlow?: THREE.Sprite; - constructor( - private readonly engine: EngineService, - private readonly dataLoader: DataLoaderService, - private readonly router: Router, - readonly navigationStore: NavigationStore - ) { + constructor() { effect(() => { const selectedStarId = this.navigationStore.selectedStarId(); if (this.ready) { diff --git a/src/app/features/search/search.component.ts b/src/app/features/search/search.component.ts index 08b052c..d1645a1 100644 --- a/src/app/features/search/search.component.ts +++ b/src/app/features/search/search.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, ElementRef, output, signal, viewChild } from '@angular/core'; +import { Component, computed, ElementRef, inject, output, signal, viewChild } from '@angular/core'; import { Router } from '@angular/router'; import { DataLoaderService } from '../../core/data/data-loader.service'; @@ -80,6 +80,10 @@ const KIND_LABELS: Record = { ` }) export class SearchComponent { + private readonly dataLoader = inject(DataLoaderService); + private readonly navigationStore = inject(NavigationStore); + private readonly router = inject(Router); + readonly query = signal(''); /** Fires once a result has been chosen and navigation kicked off — the dock uses it to hand * the view back to the readout, since the thing to look at is now the scene, not the box. */ @@ -109,11 +113,7 @@ export class SearchComponent { readonly matchTotal = computed(() => this.matches().length); readonly results = computed(() => this.matches().slice(0, MAX_RESULTS)); - constructor( - private readonly dataLoader: DataLoaderService, - private readonly navigationStore: NavigationStore, - private readonly router: Router - ) { + constructor() { void this.buildIndex(); }