feat(algorithms): add how-it-works and source links

Add idea, complexity, data structures, GPU use to 10 algorithm pages.
Link logic files on Gitea; a spec fails on a missing path.
Fix wrong texts: cloth phases, pendulum dt, fractal lighting, typos.
This commit is contained in:
lobo committed 2026-10-21 11:51:15 +02:00
1 parent 0857ce482b
commit 4b19245c6c
20 files changed
+318 -27

No files matched your search

+1
View File
@@ -26,5 +26,6 @@
static readonly FOUR_COLOR_THEOREM = 'https://de.wikipedia.org/wiki/Vier-Farben-Satz'
static readonly FOURIER_SERIES_WIKI = 'https://de.wikipedia.org/wiki/Fourierreihe'
static readonly GITEA = 'https://git.andreas-dahm.eu'
static readonly PLAYGROUND_SOURCE_BASE = `${UrlConstants.GITEA}/Lobo_Corp/playground-frontend/src/branch/main/`
}
@@ -8,6 +8,7 @@ import {MatButton} from '@angular/material/button';
import {ClothConfig} from './cloth.model';
import {Information} from '../information/information';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {UrlConstants} from '../../../constants/UrlConstants';
import {ClothSimulationStrategy} from './strategies/cloth-simulation.strategy';
import {ClothGpuStrategy} from './strategies/cloth-gpu.strategy';
@@ -75,7 +76,9 @@ export class ClothComponent {
],
disclaimer: 'CLOTH.EXPLANATION.DISCLAIMER',
disclaimerBottom: '',
disclaimerListEntry: ['CLOTH.EXPLANATION.DISCLAIMER_1', 'CLOTH.EXPLANATION.DISCLAIMER_2', 'CLOTH.EXPLANATION.DISCLAIMER_3', 'CLOTH.EXPLANATION.DISCLAIMER_4']
disclaimerListEntry: ['CLOTH.EXPLANATION.DISCLAIMER_1', 'CLOTH.EXPLANATION.DISCLAIMER_2', 'CLOTH.EXPLANATION.DISCLAIMER_3', 'CLOTH.EXPLANATION.DISCLAIMER_4'],
howItWorks: {intro: 'CLOTH.HOW_IT_WORKS.INTRO', items: ['CLOTH.HOW_IT_WORKS.ITEM_1', 'CLOTH.HOW_IT_WORKS.ITEM_2', 'CLOTH.HOW_IT_WORKS.ITEM_3', 'CLOTH.HOW_IT_WORKS.ITEM_4', 'CLOTH.HOW_IT_WORKS.ITEM_5']},
sources: SOURCE_PATHS.CLOTH
};
public onSceneReady(event: SceneEventData): void {
@@ -4,6 +4,7 @@ import {TranslatePipe} from "@ngx-translate/core";
import {UrlConstants} from '../../../constants/UrlConstants';
import {Information} from '../information/information';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {MatButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon';
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
@@ -46,7 +47,9 @@ export class ConwayGolComponent implements AfterViewInit {
],
disclaimer: 'GOL.EXPLANATION.DISCLAIMER',
disclaimerBottom: '',
disclaimerListEntry: ['GOL.EXPLANATION.DISCLAIMER_1', 'GOL.EXPLANATION.DISCLAIMER_2', 'GOL.EXPLANATION.DISCLAIMER_3', 'GOL.EXPLANATION.DISCLAIMER_4']
disclaimerListEntry: ['GOL.EXPLANATION.DISCLAIMER_1', 'GOL.EXPLANATION.DISCLAIMER_2', 'GOL.EXPLANATION.DISCLAIMER_3', 'GOL.EXPLANATION.DISCLAIMER_4'],
howItWorks: {intro: 'GOL.HOW_IT_WORKS.INTRO', items: ['GOL.HOW_IT_WORKS.ITEM_1', 'GOL.HOW_IT_WORKS.ITEM_2', 'GOL.HOW_IT_WORKS.ITEM_3', 'GOL.HOW_IT_WORKS.ITEM_4']},
sources: SOURCE_PATHS.GOL
};
protected readonly gridCols = signal(DEFAULT_GRID_COLS);
@@ -10,6 +10,7 @@ import {MatSnackBar} from '@angular/material/snack-bar';
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, FourColorMap, FourColorNode, SolutionStatus} from './four-color.models';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {Information} from '../information/information';
import {GridPos} from '../../../shared/components/generic-grid/generic-grid';
import {FourColorService} from './service/four-color.service';
@@ -57,7 +58,9 @@ export class FourColorComponent implements AfterViewInit {
'FOUR_COLOR.EXPLANATION.DISCLAIMER_2',
'FOUR_COLOR.EXPLANATION.DISCLAIMER_3',
'FOUR_COLOR.EXPLANATION.DISCLAIMER_4'
]
],
howItWorks: {intro: 'FOUR_COLOR.HOW_IT_WORKS.INTRO', items: ['FOUR_COLOR.HOW_IT_WORKS.ITEM_1', 'FOUR_COLOR.HOW_IT_WORKS.ITEM_2', 'FOUR_COLOR.HOW_IT_WORKS.ITEM_3', 'FOUR_COLOR.HOW_IT_WORKS.ITEM_4']},
sources: SOURCE_PATHS.FOUR_COLOR
};
readonly gridRows = signal(DEFAULT_GRID_ROWS);
@@ -8,6 +8,7 @@ import {MatSliderModule} from '@angular/material/slider';
import {TranslatePipe} from '@ngx-translate/core';
import {Information} from '../information/information';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {UrlConstants} from '../../../constants/UrlConstants';
import {ThemeService} from '../../../service/theme.service';
import {FourierService} from './service/fourier.service';
@@ -62,7 +63,9 @@ export class FourierComponent implements AfterViewInit, OnDestroy {
'FOURIER.EXPLANATION.DISCLAIMER_1',
'FOURIER.EXPLANATION.DISCLAIMER_2',
'FOURIER.EXPLANATION.DISCLAIMER_3'
]
],
howItWorks: {intro: 'FOURIER.HOW_IT_WORKS.INTRO', items: ['FOURIER.HOW_IT_WORKS.ITEM_1', 'FOURIER.HOW_IT_WORKS.ITEM_2', 'FOURIER.HOW_IT_WORKS.ITEM_3', 'FOURIER.HOW_IT_WORKS.ITEM_4', 'FOURIER.HOW_IT_WORKS.ITEM_5']},
sources: SOURCE_PATHS.FOURIER
};
readonly terms = signal<EpicycleTerm[]>([]);
@@ -6,6 +6,7 @@ import {MatFormField, MatLabel} from '@angular/material/input';
import {MatOption} from '@angular/material/core';
import {MatSelect} from '@angular/material/select';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {UrlConstants} from '../../../constants/UrlConstants';
import {FormsModule} from '@angular/forms';
import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
@@ -77,7 +78,9 @@ export class FractalComponent implements OnInit {
'FRACTAL.EXPLANATION.DISCLAIMER_2',
'FRACTAL.EXPLANATION.DISCLAIMER_3',
'FRACTAL.EXPLANATION.DISCLAIMER_4'
]
],
howItWorks: {intro: 'FRACTAL.HOW_IT_WORKS.INTRO', items: ['FRACTAL.HOW_IT_WORKS.ITEM_1', 'FRACTAL.HOW_IT_WORKS.ITEM_2', 'FRACTAL.HOW_IT_WORKS.ITEM_3', 'FRACTAL.HOW_IT_WORKS.ITEM_4', 'FRACTAL.HOW_IT_WORKS.ITEM_5']},
sources: SOURCE_PATHS.FRACTAL
};
renderConfig: RenderConfig = {
@@ -5,6 +5,7 @@ import {Information} from '../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {UrlConstants} from '../../../constants/UrlConstants';
import {MatButton} from '@angular/material/button';
import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/components/render-canvas/babylon-canvas.component';
@@ -47,7 +48,9 @@ export class Fractal3dComponent {
],
disclaimer: 'FRACTAL3D.EXPLANATION.DISCLAIMER',
disclaimerBottom: '',
disclaimerListEntry: ['FRACTAL3D.EXPLANATION.DISCLAIMER_1', 'FRACTAL3D.EXPLANATION.DISCLAIMER_2', 'FRACTAL3D.EXPLANATION.DISCLAIMER_3', 'FRACTAL3D.EXPLANATION.DISCLAIMER_4']
disclaimerListEntry: ['FRACTAL3D.EXPLANATION.DISCLAIMER_1', 'FRACTAL3D.EXPLANATION.DISCLAIMER_2', 'FRACTAL3D.EXPLANATION.DISCLAIMER_3', 'FRACTAL3D.EXPLANATION.DISCLAIMER_4'],
howItWorks: {intro: 'FRACTAL3D.HOW_IT_WORKS.INTRO', items: ['FRACTAL3D.HOW_IT_WORKS.ITEM_1', 'FRACTAL3D.HOW_IT_WORKS.ITEM_2', 'FRACTAL3D.HOW_IT_WORKS.ITEM_3', 'FRACTAL3D.HOW_IT_WORKS.ITEM_4', 'FRACTAL3D.HOW_IT_WORKS.ITEM_5']},
sources: SOURCE_PATHS.FRACTAL3D
};
fractalConfig: RenderConfig = {
@@ -19,6 +19,20 @@
}
}
<h4 class="m-0 mt-3 mb-1">{{ 'ALGORITHM.HOW_IT_WORKS' | translate }}</h4>
<p class="my-2">{{ info.howItWorks.intro | translate }}</p>
<ul>
@for (item of info.howItWorks.items; track item) {
<li>{{ item | translate }}</li>
}
</ul>
<p class="my-2">
<strong class="mr-2">{{ 'ALGORITHM.SOURCE' | translate }}:</strong>
@for (path of info.sources; track path) {
<a [href]="sourceUrl(path)" target="_blank" rel="noopener noreferrer" class="mr-3">{{ fileName(path) }}</a>
}
</p>
@if (info.disclaimer !== '')
{
<p class="my-2">
@@ -4,6 +4,13 @@
disclaimer: string;
disclaimerBottom: string;
disclaimerListEntry: string[];
howItWorks: HowItWorks;
sources: readonly string[];
}
export interface HowItWorks {
intro: string;
items: string[];
}
export interface AlgorithmEntry {
@@ -0,0 +1,43 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {provideZonelessChangeDetection} from '@angular/core';
import {provideTranslateService} from '@ngx-translate/core';
import {Information} from './information';
import {AlgorithmInformation} from './information.models';
import {UrlConstants} from '../../../constants/UrlConstants';
describe('Information', () => {
let fixture: ComponentFixture<Information>;
const info: AlgorithmInformation = {
title: 'T',
entries: [],
disclaimer: '',
disclaimerBottom: '',
disclaimerListEntry: [],
howItWorks: {intro: 'INTRO_KEY', items: ['ITEM_A', 'ITEM_B']},
sources: ['src/app/a/one.ts', 'src/app/b/two.ts']
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideZonelessChangeDetection(), provideTranslateService({lang: 'en', fallbackLang: 'en'})]
});
fixture = TestBed.createComponent(Information);
fixture.componentRef.setInput('algorithmInformation', info);
fixture.detectChanges();
});
it('renders one list entry per how-it-works item', () => {
const items = fixture.nativeElement.querySelectorAll('ul li');
expect(items.length).toBe(2);
});
it('renders one source link per path with the Gitea base URL and the file name', () => {
const links: HTMLAnchorElement[] = Array.from(fixture.nativeElement.querySelectorAll('a'));
expect(links.map(link => link.getAttribute('href'))).toEqual([
UrlConstants.PLAYGROUND_SOURCE_BASE + 'src/app/a/one.ts',
UrlConstants.PLAYGROUND_SOURCE_BASE + 'src/app/b/two.ts'
]);
expect(links.map(link => link.textContent?.trim())).toEqual(['one.ts', 'two.ts']);
});
});
@@ -1,6 +1,7 @@
import {Component, input} from '@angular/core';
import {TranslatePipe} from "@ngx-translate/core";
import {AlgorithmInformation} from './information.models';
import {UrlConstants} from '../../../constants/UrlConstants';
@Component({
selector: 'app-information',
@@ -13,4 +14,12 @@ import {AlgorithmInformation} from './information.models';
export class Information {
readonly algorithmInformation = input.required<AlgorithmInformation>();
protected sourceUrl(path: string): string {
return UrlConstants.PLAYGROUND_SOURCE_BASE + path;
}
protected fileName(path: string): string {
return path.slice(path.lastIndexOf('/') + 1);
}
}
+4
View File
@@ -0,0 +1,4 @@
// No Node types in tsconfig.spec: they would change the type of setTimeout in the whole app.
declare module 'node:fs' {
export function existsSync(path: string): boolean;
}
@@ -0,0 +1,14 @@
import {existsSync} from 'node:fs';
import {SOURCE_PATHS} from './source-paths';
// Paths are relative to the repo root, which is the working directory of `ng test`.
describe('SOURCE_PATHS', () => {
it('every linked source file exists in the repo', () => {
const missing = Object.values(SOURCE_PATHS).flat().filter(path => !existsSync(path));
expect(missing).toEqual([]);
});
it('detects a path that does not exist', () => {
expect(existsSync('src/app/does-not-exist.ts')).toBe(false);
});
});
@@ -0,0 +1,13 @@
/** Repo-relative paths of the files that hold the algorithm logic. Checked by source-paths.spec.ts. */
export const SOURCE_PATHS = {
PATHFINDING: ['src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts', 'src/app/pages/algorithms/pathfinding/pathfinding-animator.ts'],
LABYRINTH: ['src/app/pages/algorithms/pathfinding/labyrinth/service/maze-generator.service.ts'],
SORTING: ['src/app/pages/algorithms/sorting/service/sorting.service.ts'],
GOL: ['src/app/pages/algorithms/conway-gol/service/conway-gol.service.ts'],
FOUR_COLOR: ['src/app/pages/algorithms/four-color/service/four-color.service.ts'],
FOURIER: ['src/app/pages/algorithms/fourier/service/fourier.service.ts'],
FRACTAL: ['src/app/pages/algorithms/fractal/fractal.shader.ts', 'src/app/pages/algorithms/fractal/fractal-view.ts'],
FRACTAL3D: ['src/app/pages/algorithms/fractal3d/fractal3d.shader.ts'],
PENDULUM: ['src/app/pages/algorithms/pendulum/pendulum.shader.ts', 'src/app/pages/algorithms/pendulum/strategies/pendulum-gpu.strategy.ts', 'src/app/pages/algorithms/pendulum/strategies/pendulum-cpu-physics.ts'],
CLOTH: ['src/app/pages/algorithms/cloth/cloth.shader.ts', 'src/app/pages/algorithms/cloth/strategies/cloth-gpu.strategy.ts', 'src/app/pages/algorithms/cloth/strategies/cloth-cpu-physics.ts'],
} as const satisfies Record<string, readonly string[]>;
@@ -4,6 +4,7 @@ import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/mat
import {TranslatePipe} from '@ngx-translate/core';
import {GenericGridComponent, GridPos} from '../../../../shared/components/generic-grid/generic-grid';
import {AlgorithmInformation} from '../../information/information.models';
import {SOURCE_PATHS} from '../../information/source-paths';
import {UrlConstants} from '../../../../constants/UrlConstants';
import {createPathfindingNode, Node, PathfindingAlgorithm} from '../pathfinding.models';
import {MatButton} from '@angular/material/button';
@@ -54,7 +55,9 @@ export class LabyrinthComponent implements AfterViewInit {
],
disclaimer: 'LABYRINTH.EXPLANATION.DISCLAIMER',
disclaimerBottom: '',
disclaimerListEntry: ['LABYRINTH.EXPLANATION.DISCLAIMER_1', 'LABYRINTH.EXPLANATION.DISCLAIMER_2', 'LABYRINTH.EXPLANATION.DISCLAIMER_3', 'LABYRINTH.EXPLANATION.DISCLAIMER_4']
disclaimerListEntry: ['LABYRINTH.EXPLANATION.DISCLAIMER_1', 'LABYRINTH.EXPLANATION.DISCLAIMER_2', 'LABYRINTH.EXPLANATION.DISCLAIMER_3', 'LABYRINTH.EXPLANATION.DISCLAIMER_4'],
howItWorks: {intro: 'LABYRINTH.HOW_IT_WORKS.INTRO', items: ['LABYRINTH.HOW_IT_WORKS.ITEM_1', 'LABYRINTH.HOW_IT_WORKS.ITEM_2', 'LABYRINTH.HOW_IT_WORKS.ITEM_3', 'LABYRINTH.HOW_IT_WORKS.ITEM_4']},
sources: SOURCE_PATHS.LABYRINTH
};
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
@@ -18,6 +18,7 @@ import {UrlConstants} from '../../../constants/UrlConstants';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {Information} from '../information/information';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
enum NodeType {
@@ -74,7 +75,9 @@ export class PathfindingComponent implements AfterViewInit {
],
disclaimer: 'PATHFINDING.EXPLANATION.DISCLAIMER',
disclaimerBottom: '',
disclaimerListEntry: []
disclaimerListEntry: [],
howItWorks: {intro: 'PATHFINDING.HOW_IT_WORKS.INTRO', items: ['PATHFINDING.HOW_IT_WORKS.ITEM_1', 'PATHFINDING.HOW_IT_WORKS.ITEM_2', 'PATHFINDING.HOW_IT_WORKS.ITEM_3', 'PATHFINDING.HOW_IT_WORKS.ITEM_4', 'PATHFINDING.HOW_IT_WORKS.ITEM_5']},
sources: SOURCE_PATHS.PATHFINDING
};
readonly gridRows = signal(DEFAULT_GRID_ROWS);
@@ -9,6 +9,7 @@ import {TranslatePipe} from '@ngx-translate/core';
import {MatButton} from '@angular/material/button';
import {Information} from '../information/information';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {UrlConstants} from '../../../constants/UrlConstants';
import {PendulumSimulationStrategy} from './strategies/pendulum-simulation.strategy';
import {PendulumGpuStrategy} from './strategies/pendulum-gpu.strategy';
@@ -51,7 +52,9 @@ class PendulumComponent {
],
disclaimer: 'PENDULUM.EXPLANATION.DISCLAIMER',
disclaimerBottom: 'PENDULUM.EXPLANATION.DISCLAIMER_BOTTOM',
disclaimerListEntry: ['PENDULUM.EXPLANATION.DISCLAIMER_1', 'PENDULUM.EXPLANATION.DISCLAIMER_2', 'PENDULUM.EXPLANATION.DISCLAIMER_3', 'PENDULUM.EXPLANATION.DISCLAIMER_4']
disclaimerListEntry: ['PENDULUM.EXPLANATION.DISCLAIMER_1', 'PENDULUM.EXPLANATION.DISCLAIMER_2', 'PENDULUM.EXPLANATION.DISCLAIMER_3', 'PENDULUM.EXPLANATION.DISCLAIMER_4'],
howItWorks: {intro: 'PENDULUM.HOW_IT_WORKS.INTRO', items: ['PENDULUM.HOW_IT_WORKS.ITEM_1', 'PENDULUM.HOW_IT_WORKS.ITEM_2', 'PENDULUM.HOW_IT_WORKS.ITEM_3', 'PENDULUM.HOW_IT_WORKS.ITEM_4']},
sources: SOURCE_PATHS.PENDULUM
};
renderConfig: RenderConfig = {
@@ -13,6 +13,7 @@ import { FormsModule } from '@angular/forms';
import {MatInput} from '@angular/material/input';
import {UrlConstants} from '../../../constants/UrlConstants';
import {AlgorithmInformation} from '../information/information.models';
import {SOURCE_PATHS} from '../information/source-paths';
import {Information} from '../information/information';
@Component({
selector: 'app-sorting',
@@ -63,7 +64,9 @@ export class SortingComponent implements OnInit {
'SORTING.EXPLANATION.DISCLAIMER_1',
'SORTING.EXPLANATION.DISCLAIMER_2',
'SORTING.EXPLANATION.DISCLAIMER_3'
]
],
howItWorks: {intro: 'SORTING.HOW_IT_WORKS.INTRO', items: ['SORTING.HOW_IT_WORKS.ITEM_1', 'SORTING.HOW_IT_WORKS.ITEM_2', 'SORTING.HOW_IT_WORKS.ITEM_3', 'SORTING.HOW_IT_WORKS.ITEM_4', 'SORTING.HOW_IT_WORKS.ITEM_5']},
sources: SOURCE_PATHS.SORTING
};
private timeoutIds: number[] = [];