Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1b764ca31 | ||
|
|
6fee084d77 | ||
|
|
2965706b7e | ||
|
|
243aa3bbce | ||
|
|
7983a6875f | ||
|
|
6507da1610 | ||
|
|
a47b5d3d92 | ||
|
|
bd90b4a21f | ||
|
|
d0344d5d17 | ||
|
|
deb840e9bd | ||
|
|
d2b0cc5008 |
@@ -1,4 +1,6 @@
|
|||||||
# Editor configuration, see https://editorconfig.org
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
# Source: @avalon-vanguard/config. EditorConfig cannot extend a file, so each repo commits a copy:
|
||||||
|
# cp node_modules/@avalon-vanguard/config/.editorconfig .editorconfig
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
# Gitea port of .github/workflows/ci.yml, through the organisation's shared workflow
|
||||||
|
# (avalon-vanguard/ci, node.yml): lint, typecheck, unit tests, production build, then the
|
||||||
|
# Playwright suite. Same triggers: pull requests and the branches they merge into, plus manual
|
||||||
|
# dispatch.
|
||||||
|
#
|
||||||
|
# A .gitea/workflows directory makes Gitea ignore .github/workflows, so pages.yml,
|
||||||
|
# data-refresh.yml and junie-review.yml now run on GitHub only, until they are ported.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# A second push to the same branch makes the first run's answer irrelevant.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
uses: avalon-vanguard/ci/.gitea/workflows/node.yml@v1
|
||||||
|
# Pas d'e2e pour l'instant : la carte 3D rendue en logiciel par Chromium, sur ng serve,
|
||||||
|
# dépasse le runner du VPS (17 tests, 7 en échec par délai dépassé en 16,8 min). À
|
||||||
|
# remettre (e2e: true) une fois le runner capable de les passer.
|
||||||
|
secrets: inherit
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@avalon-vanguard:registry=https://git.avalonvanguard.com/api/packages/avalon-vanguard/npm/
|
||||||
+1
-12
@@ -1,12 +1 @@
|
|||||||
{
|
"@avalon-vanguard/config/prettier/angular"
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true,
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.html",
|
|
||||||
"options": {
|
|
||||||
"parser": "angular"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// @ts-check
|
||||||
|
const { defineConfig } = require('eslint/config');
|
||||||
|
const avalonBase = require('@avalon-vanguard/config/eslint');
|
||||||
|
const avalonAngular = require('@avalon-vanguard/config/eslint/angular');
|
||||||
|
|
||||||
|
module.exports = defineConfig([
|
||||||
|
{
|
||||||
|
// Build, test and Playwright output (see .gitignore): ESLint does not read .gitignore.
|
||||||
|
ignores: [
|
||||||
|
'dist/**',
|
||||||
|
'coverage/**',
|
||||||
|
'.angular/**',
|
||||||
|
'node_modules/**',
|
||||||
|
'out-tsc/**',
|
||||||
|
'tools/etl/.cache/**',
|
||||||
|
'e2e/test-output/**',
|
||||||
|
'test-results/**',
|
||||||
|
'playwright-report/**',
|
||||||
|
'blob-report/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
extends: [avalonBase, avalonAngular.typescript],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{ type: 'attribute', prefix: 'app', style: 'camelCase' },
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{ type: 'element', prefix: 'app', style: 'kebab-case' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
avalonAngular.templates,
|
||||||
|
]);
|
||||||
Generated
+1433
-841
File diff suppressed because it is too large
Load Diff
+8
-1
@@ -7,11 +7,13 @@
|
|||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
|
"lint": "eslint .",
|
||||||
"etl": "tsx tools/etl/build.ts",
|
"etl": "tsx tools/etl/build.ts",
|
||||||
"etl:typecheck": "tsc -p tools/etl/tsconfig.json --noEmit",
|
"etl:typecheck": "tsc -p tools/etl/tsconfig.json --noEmit",
|
||||||
"e2e": "playwright test",
|
"e2e": "playwright test",
|
||||||
"e2e:typecheck": "tsc -p e2e/tsconfig.json --noEmit",
|
"e2e:typecheck": "tsc -p e2e/tsconfig.json --noEmit",
|
||||||
"worker:typecheck": "tsc -p tsconfig.worker.json --noEmit"
|
"worker:typecheck": "tsc -p tsconfig.worker.json --noEmit",
|
||||||
|
"typecheck": "npm run etl:typecheck && npm run e2e:typecheck && npm run worker:typecheck"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "npm@11.12.1",
|
"packageManager": "npm@11.12.1",
|
||||||
@@ -33,16 +35,21 @@
|
|||||||
"@angular/build": "^22.0.6",
|
"@angular/build": "^22.0.6",
|
||||||
"@angular/cli": "^22.0.6",
|
"@angular/cli": "^22.0.6",
|
||||||
"@angular/compiler-cli": "^22.0.0",
|
"@angular/compiler-cli": "^22.0.0",
|
||||||
|
"@avalon-vanguard/config": "^1.0.0",
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@playwright/test": "^1.61.1",
|
"@playwright/test": "^1.61.1",
|
||||||
"@tailwindcss/postcss": "^4.3.2",
|
"@tailwindcss/postcss": "^4.3.2",
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^26.1.1",
|
||||||
"@types/three": "^0.185.1",
|
"@types/three": "^0.185.1",
|
||||||
|
"angular-eslint": "^22.5.0",
|
||||||
|
"eslint": "^10.10.0",
|
||||||
"jsdom": "^28.0.0",
|
"jsdom": "^28.0.0",
|
||||||
"postcss": "^8.5.19",
|
"postcss": "^8.5.19",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"tailwindcss": "^4.3.2",
|
"tailwindcss": "^4.3.2",
|
||||||
"tsx": "^4.23.1",
|
"tsx": "^4.23.1",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
|
"typescript-eslint": "^8.70.0",
|
||||||
"vitest": "^4.0.8"
|
"vitest": "^4.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"local>avalon-vanguard/renovate-config",
|
||||||
|
"local>avalon-vanguard/renovate-config:angular"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgZone } from '@angular/core';
|
import { Injector, NgZone, runInInjectionContext } from '@angular/core';
|
||||||
import * as THREE from 'three/webgpu';
|
import * as THREE from 'three/webgpu';
|
||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -10,7 +10,8 @@ import { EngineService } from './engine.service';
|
|||||||
* arithmetic that keeps them showing the same thing.
|
* arithmetic that keeps them showing the same thing.
|
||||||
*/
|
*/
|
||||||
function engineWithCameras(): { engine: EngineService; perspective: THREE.PerspectiveCamera; orthographic: THREE.OrthographicCamera } {
|
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 perspective = new THREE.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
|
||||||
const orthographic = new THREE.OrthographicCamera(-1, 1, 1, -1, 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.
|
// The two cameras are private, because nothing outside should choose between them by hand.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, NgZone } from '@angular/core';
|
import { inject, Injectable, NgZone } from '@angular/core';
|
||||||
import * as THREE from 'three/webgpu';
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
export type EngineTickCallback = (deltaSeconds: number, elapsedSeconds: number) => void;
|
export type EngineTickCallback = (deltaSeconds: number, elapsedSeconds: number) => void;
|
||||||
@@ -20,6 +20,7 @@ export type SceneCamera = THREE.PerspectiveCamera | THREE.OrthographicCamera;
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EngineService {
|
export class EngineService {
|
||||||
|
private readonly ngZone = inject(NgZone);
|
||||||
private readonly clock = new THREE.Clock(false);
|
private readonly clock = new THREE.Clock(false);
|
||||||
private readonly tickCallbacks = new Set<EngineTickCallback>();
|
private readonly tickCallbacks = new Set<EngineTickCallback>();
|
||||||
|
|
||||||
@@ -36,8 +37,6 @@ export class EngineService {
|
|||||||
private projection: Projection = 'perspective';
|
private projection: Projection = 'perspective';
|
||||||
private running = false;
|
private running = false;
|
||||||
|
|
||||||
constructor(private readonly ngZone: NgZone) {}
|
|
||||||
|
|
||||||
get isInitialized(): boolean {
|
get isInitialized(): boolean {
|
||||||
return !!this.renderer;
|
return !!this.renderer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import * as THREE from 'three/webgpu';
|
import * as THREE from 'three/webgpu';
|
||||||
@@ -65,6 +65,12 @@ const GLOW_SCALE = 2.6;
|
|||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy {
|
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. */
|
/** A kept place, revisited from this page: a star means leaving it for the map. */
|
||||||
goToBookmark(bookmark: Bookmark): void {
|
goToBookmark(bookmark: Bookmark): void {
|
||||||
if (bookmark.kind === 'star') {
|
if (bookmark.kind === 'star') {
|
||||||
@@ -95,14 +101,6 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
readonly viewModel = signal<BodyDetailViewModel | undefined>(undefined);
|
readonly viewModel = signal<BodyDetailViewModel | undefined>(undefined);
|
||||||
readonly notFound = signal(false);
|
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 {
|
ngAfterViewInit(): void {
|
||||||
void this.bootstrap();
|
void this.bootstrap();
|
||||||
}
|
}
|
||||||
@@ -273,7 +271,6 @@ export class BodyDetailSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
scene.add(sunLight);
|
scene.add(sunLight);
|
||||||
|
|
||||||
const geometry = new THREE.SphereGeometry(1, 64, 48);
|
const geometry = new THREE.SphereGeometry(1, 64, 48);
|
||||||
const viewModel = this.viewModel();
|
|
||||||
this.planetMaterial = new THREE.MeshStandardMaterial({
|
this.planetMaterial = new THREE.MeshStandardMaterial({
|
||||||
// White, always: the map that arrives a moment later carries the colour, whether it is a
|
// White, always: the map that arrives a moment later carries the colour, whether it is a
|
||||||
// photograph or a surface derived from the body's own measurements.
|
// photograph or a surface derived from the body's own measurements.
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ import { ReadoutSectionsComponent } from './readout-sections.component';
|
|||||||
export class InfoPanelComponent {
|
export class InfoPanelComponent {
|
||||||
readonly body = input.required<BodyDetailViewModel>();
|
readonly body = input.required<BodyDetailViewModel>();
|
||||||
|
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
readonly bookmarks = inject(BookmarksStore);
|
readonly bookmarks = inject(BookmarksStore);
|
||||||
|
|
||||||
private readonly articles = inject(ArticleService);
|
private readonly articles = inject(ArticleService);
|
||||||
@@ -105,7 +107,7 @@ export class InfoPanelComponent {
|
|||||||
readonly article = signal<Article | null>(null);
|
readonly article = signal<Article | null>(null);
|
||||||
readonly aboutState = signal<'idle' | 'loading' | 'none' | 'unavailable'>('idle');
|
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
|
// 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.
|
// must not still be showing under the next one's name.
|
||||||
effect(() => {
|
effect(() => {
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('links the stars being drawn, and asks again once a new set of them holds still', async () => {
|
it('links the stars being drawn, and asks again once a new set of them holds still', async () => {
|
||||||
const links = vi.fn((_rangePc: number, _drawn: Uint32Array) => Promise.resolve(new Float32Array(0)));
|
const links = vi.fn<LinkScene['routing']['links']>(() => Promise.resolve(new Float32Array(0)));
|
||||||
const component = linkScene(links);
|
const component = linkScene(links);
|
||||||
await settle();
|
await settle();
|
||||||
expect(links).toHaveBeenCalledTimes(1);
|
expect(links).toHaveBeenCalledTimes(1);
|
||||||
@@ -625,7 +625,7 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('asks for as much of the graph as a million pixels of line make, around where the view is centred', async () => {
|
it('asks for as much of the graph as a million pixels of line make, around where the view is centred', async () => {
|
||||||
const links = vi.fn((_rangePc: number, _drawn: Uint32Array, _budget?: LinkBudget) => Promise.resolve(new Float32Array(0)));
|
const links = vi.fn<LinkScene['routing']['links']>(() => Promise.resolve(new Float32Array(0)));
|
||||||
Object.defineProperty((fixture.nativeElement as HTMLElement).querySelector('canvas')!, 'clientHeight', { value: 1080 });
|
Object.defineProperty((fixture.nativeElement as HTMLElement).querySelector('canvas')!, 'clientHeight', { value: 1080 });
|
||||||
// A screen scaled to 200%: 1080 CSS pixels are 2160 drawn ones, and the lines are drawn in those.
|
// A screen scaled to 200%: 1080 CSS pixels are 2160 drawn ones, and the lines are drawn in those.
|
||||||
engine.pixelRatio = 2;
|
engine.pixelRatio = 2;
|
||||||
@@ -641,7 +641,7 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
|||||||
|
|
||||||
it('asks again once the view has zoomed past the budget it asked with, though the drawn stars are the same', async () => {
|
it('asks again once the view has zoomed past the budget it asked with, though the drawn stars are the same', async () => {
|
||||||
// All three stars fit the star budget, so the drawn set never changes: only the budget can.
|
// All three stars fit the star budget, so the drawn set never changes: only the budget can.
|
||||||
const links = vi.fn((_rangePc: number, _drawn: Uint32Array, _budget?: LinkBudget) => Promise.resolve(new Float32Array(0)));
|
const links = vi.fn<LinkScene['routing']['links']>(() => Promise.resolve(new Float32Array(0)));
|
||||||
Object.defineProperty((fixture.nativeElement as HTMLElement).querySelector('canvas')!, 'clientHeight', { value: 1080 });
|
Object.defineProperty((fixture.nativeElement as HTMLElement).querySelector('canvas')!, 'clientHeight', { value: 1080 });
|
||||||
const component = linkScene(links);
|
const component = linkScene(links);
|
||||||
await advanceFrames(engine, 0.3);
|
await advanceFrames(engine, 0.3);
|
||||||
@@ -658,7 +658,7 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('asks for no graph from inside a system, where distances are in astronomical units', async () => {
|
it('asks for no graph from inside a system, where distances are in astronomical units', async () => {
|
||||||
const links = vi.fn((_rangePc: number, _drawn: Uint32Array, _budget?: LinkBudget) => Promise.resolve(new Float32Array(0)));
|
const links = vi.fn<LinkScene['routing']['links']>(() => Promise.resolve(new Float32Array(0)));
|
||||||
navigationStore.selectStar(SUN.id);
|
navigationStore.selectStar(SUN.id);
|
||||||
await flushAsync();
|
await flushAsync();
|
||||||
await advanceFrames(engine, 2.5);
|
await advanceFrames(engine, 2.5);
|
||||||
@@ -695,7 +695,7 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('gives a view on the move a new graph at least every quarter second, rather than waiting for it to stop', async () => {
|
it('gives a view on the move a new graph at least every quarter second, rather than waiting for it to stop', async () => {
|
||||||
const links = vi.fn((_rangePc: number, _drawn: Uint32Array) => Promise.resolve(new Float32Array(0)));
|
const links = vi.fn<LinkScene['routing']['links']>(() => Promise.resolve(new Float32Array(0)));
|
||||||
const component = linkScene(links);
|
const component = linkScene(links);
|
||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
|
|||||||
@@ -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 { Router } from '@angular/router';
|
||||||
import * as THREE from 'three/webgpu';
|
import * as THREE from 'three/webgpu';
|
||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
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 {
|
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<ElementRef<HTMLCanvasElement>>('canvas');
|
private readonly canvasRef = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
|
||||||
private readonly labelHostRef = viewChild.required<ElementRef<HTMLDivElement>>('labelHost');
|
private readonly labelHostRef = viewChild.required<ElementRef<HTMLDivElement>>('labelHost');
|
||||||
private readonly leaderRef = viewChild.required<ElementRef<SVGLineElement>>('leader');
|
private readonly leaderRef = viewChild.required<ElementRef<SVGLineElement>>('leader');
|
||||||
@@ -467,12 +472,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
private starMarker?: THREE.Mesh;
|
private starMarker?: THREE.Mesh;
|
||||||
private starGlow?: THREE.Sprite;
|
private starGlow?: THREE.Sprite;
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
private readonly engine: EngineService,
|
|
||||||
private readonly dataLoader: DataLoaderService,
|
|
||||||
private readonly router: Router,
|
|
||||||
readonly navigationStore: NavigationStore
|
|
||||||
) {
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const selectedStarId = this.navigationStore.selectedStarId();
|
const selectedStarId = this.navigationStore.selectedStarId();
|
||||||
if (this.ready) {
|
if (this.ready) {
|
||||||
@@ -484,7 +484,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
// Reads both signals, so flipping the layer on and dragging the range each land here.
|
// Reads both signals, so flipping the layer on and dragging the range each land here.
|
||||||
effect(() => {
|
effect(() => {
|
||||||
this.jumpRangePc();
|
this.jumpRangePc();
|
||||||
this.display().jumpLinks;
|
void this.display().jumpLinks;
|
||||||
this.scheduleJumpLinks();
|
this.scheduleJumpLinks();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1804,8 +1804,6 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private swapToSystemSpace(star: StarRecord, direction: THREE.Vector3, onComplete: () => void): void {
|
private swapToSystemSpace(star: StarRecord, direction: THREE.Vector3, onComplete: () => void): void {
|
||||||
const camera = this.engine.getCamera();
|
|
||||||
|
|
||||||
this.systemRenderer?.dispose();
|
this.systemRenderer?.dispose();
|
||||||
if (this.starMarker) {
|
if (this.starMarker) {
|
||||||
this.systemGroup.remove(this.starMarker);
|
this.systemGroup.remove(this.starMarker);
|
||||||
@@ -1912,7 +1910,6 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private swapToGalaxySpace(exitingStarId: number, direction: THREE.Vector3, isSwitchingSystems: boolean, onComplete: () => void): void {
|
private swapToGalaxySpace(exitingStarId: number, direction: THREE.Vector3, isSwitchingSystems: boolean, onComplete: () => void): void {
|
||||||
const camera = this.engine.getCamera();
|
|
||||||
const star = this.starsById.get(exitingStarId);
|
const star = this.starsById.get(exitingStarId);
|
||||||
const starPc = star ? new THREE.Vector3(star.x, star.y, star.z) : GALAXY_OVERVIEW_TARGET.clone();
|
const starPc = star ? new THREE.Vector3(star.x, star.y, star.z) : GALAXY_OVERVIEW_TARGET.clone();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as THREE from 'three/webgpu';
|
import * as THREE from 'three/webgpu';
|
||||||
|
|
||||||
import { GALACTIC_BASIS_EQUATORIAL, SUN_HEIGHT_ABOVE_MIDPLANE_PC } from '../../shared/astro/galaxy';
|
import { GALACTIC_BASIS_EQUATORIAL } from '../../shared/astro/galaxy';
|
||||||
|
|
||||||
const SEGMENTS_PER_RING = 180;
|
const SEGMENTS_PER_RING = 180;
|
||||||
|
|
||||||
|
|||||||
@@ -87,9 +87,9 @@ export function createRandom(seed: number): () => number {
|
|||||||
|
|
||||||
/** Standard normal sample, by the polar form of Box-Muller. */
|
/** Standard normal sample, by the polar form of Box-Muller. */
|
||||||
function gaussian(random: () => number): number {
|
function gaussian(random: () => number): number {
|
||||||
let u = 0;
|
let u: number;
|
||||||
let v = 0;
|
let v: number;
|
||||||
let s = 0;
|
let s: number;
|
||||||
do {
|
do {
|
||||||
u = random() * 2 - 1;
|
u = random() * 2 - 1;
|
||||||
v = random() * 2 - 1;
|
v = random() * 2 - 1;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { BrightnessIndex, brightnessIndex, Positioned } from '../../shared/astro
|
|||||||
import { spectralTypeToColorIndex } from '../../shared/astro/spectral';
|
import { spectralTypeToColorIndex } from '../../shared/astro/spectral';
|
||||||
import { SceneCamera } from '../../core/engine/engine.service';
|
import { SceneCamera } from '../../core/engine/engine.service';
|
||||||
import { StarRecord } from '../../shared/models/star.model';
|
import { StarRecord } from '../../shared/models/star.model';
|
||||||
import { PIXELS_TO_ANGULAR_SIZE, REFERENCE_FOV_DEGREES, REFERENCE_VIEWPORT_HEIGHT_PX } from './angular-size';
|
import { PIXELS_TO_ANGULAR_SIZE, REFERENCE_FOV_DEGREES } from './angular-size';
|
||||||
|
|
||||||
/** Apparent star diameters, in pixels at {@link REFERENCE_VIEWPORT_HEIGHT_PX}. */
|
/** Apparent star diameters, in pixels at {@link REFERENCE_VIEWPORT_HEIGHT_PX}. */
|
||||||
const MIN_POINT_SIZE = 1.5;
|
const MIN_POINT_SIZE = 1.5;
|
||||||
|
|||||||
@@ -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 { Router } from '@angular/router';
|
||||||
|
|
||||||
import { DataLoaderService } from '../../core/data/data-loader.service';
|
import { DataLoaderService } from '../../core/data/data-loader.service';
|
||||||
@@ -80,6 +80,10 @@ const KIND_LABELS: Record<SearchResultKind, string> = {
|
|||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class SearchComponent {
|
export class SearchComponent {
|
||||||
|
private readonly dataLoader = inject(DataLoaderService);
|
||||||
|
private readonly navigationStore = inject(NavigationStore);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
readonly query = signal('');
|
readonly query = signal('');
|
||||||
/** Fires once a result has been chosen and navigation kicked off — the dock uses it to hand
|
/** 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. */
|
* 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 matchTotal = computed(() => this.matches().length);
|
||||||
readonly results = computed(() => this.matches().slice(0, MAX_RESULTS));
|
readonly results = computed(() => this.matches().slice(0, MAX_RESULTS));
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
private readonly dataLoader: DataLoaderService,
|
|
||||||
private readonly navigationStore: NavigationStore,
|
|
||||||
private readonly router: Router
|
|
||||||
) {
|
|
||||||
void this.buildIndex();
|
void this.buildIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,8 @@ describe('minimumRangeBetween', () => {
|
|||||||
expect(minimumRangeBetween(knotAndChain(), 0, CHAIN_END, 8).rangePc).toBeCloseTo(1, 1);
|
expect(minimumRangeBetween(knotAndChain(), 0, CHAIN_END, 8).rangePc).toBeCloseTo(1, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stops bisecting where a search gave up, and hands back a range that does work', { timeout: 30_000 }, () => {
|
// 7,5 s sur un poste récent, autour de 30 s sur le runner Gitea du VPS : 90 s de marge.
|
||||||
|
it('stops bisecting where a search gave up, and hands back a range that does work', { timeout: 90_000 }, () => {
|
||||||
// Below the chain's own hop of a parsec, the crowd is still one connected piece and larger than
|
// Below the chain's own hop of a parsec, the crowd is still one connected piece and larger than
|
||||||
// the budget, so those probes give up. Reading a give-up as "no chain at this range" is what
|
// the budget, so those probes give up. Reading a give-up as "no chain at this range" is what
|
||||||
// used to report ranges up to 29% wider than needed, and went on paying for probes whose
|
// used to report ranges up to 29% wider than needed, and went on paying for probes whose
|
||||||
|
|||||||
@@ -115,11 +115,12 @@ describe('renderPlanetTexture', () => {
|
|||||||
it('produces no NaN or out-of-range bytes for any class', () => {
|
it('produces no NaN or out-of-range bytes for any class', () => {
|
||||||
for (const planetClass of ALL_CLASSES) {
|
for (const planetClass of ALL_CLASSES) {
|
||||||
const pixels = renderPlanetTexture(appearanceOf(planetClass, { polarCapExtentDeg: 30 }), SIZE);
|
const pixels = renderPlanetTexture(appearanceOf(planetClass, { polarCapExtentDeg: 30 }), SIZE);
|
||||||
for (const value of pixels) {
|
// One assertion per class, not three per byte: 245 760 expect() calls ran past the
|
||||||
expect(Number.isInteger(value)).toBe(true);
|
// default 5 s timeout on a slower CI machine.
|
||||||
expect(value).toBeGreaterThanOrEqual(0);
|
const firstBad = pixels.findIndex(
|
||||||
expect(value).toBeLessThanOrEqual(255);
|
(value) => !Number.isInteger(value) || value < 0 || value > 255,
|
||||||
}
|
);
|
||||||
|
expect(firstBad, planetClass).toBe(-1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class BookmarksStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private read(): Bookmark[] {
|
private read(): Bookmark[] {
|
||||||
let raw: string | null = null;
|
let raw: string | null;
|
||||||
try {
|
try {
|
||||||
raw = localStorage.getItem(STORAGE_KEY);
|
raw = localStorage.getItem(STORAGE_KEY);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+2
-17
@@ -1,24 +1,9 @@
|
|||||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
/* Compiler and Angular options come from @avalon-vanguard/config (tsconfig/angular.json). */
|
||||||
{
|
{
|
||||||
|
"extends": "@avalon-vanguard/config/tsconfig/angular.json",
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"importHelpers": true,
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "preserve"
|
|
||||||
},
|
|
||||||
"angularCompilerOptions": {
|
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
|
||||||
"strictInjectionParameters": true,
|
|
||||||
"strictInputAccessModifiers": true
|
|
||||||
},
|
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user