refactor(components): use OnPush and signals in components
Angular 22 defaults to OnPush, so Eager is removed. Template state is now signals, so sorting needs no detectChanges. Game of Life execution time now updates live. Remove the unused ImageDialogComponent and turn the OnPush lint rule back on.
This commit is contained in:
1 parent
2abc7c31e2
commit
dc9f09bede
33 files changed
+336
-336
No files matched your search
@@ -18,8 +18,6 @@ module.exports = defineConfig([
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/prefer-for-of": "off",
|
||||
// ng update set ChangeDetectionStrategy.Eager everywhere; OnPush is a separate refactor.
|
||||
"@angular-eslint/prefer-on-push-component-change-detection": "off",
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterLink, RouterOutlet } from '@angular/router';
|
||||
import {TopbarComponent} from '../topbar/topbar.component';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
@@ -10,7 +10,6 @@ import {SeoService} from '../../service/seo.service';
|
||||
selector: 'app-root',
|
||||
imports: [RouterLink, RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
|
||||
templateUrl: './app.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
export class AppComponent {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
imports: [MatDialogModule, MatButtonModule, MatIconModule],
|
||||
template: `
|
||||
<div class="dialog">
|
||||
<div class="topbar">
|
||||
<div class="title">{{ data.title }}</div>
|
||||
<button mat-icon-button mat-dialog-close aria-label="Schließen">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
<img class="full" [src]="data.src" [alt]="data.title" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styles: [`
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
max-width: calc(100% - 56px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stage {
|
||||
flex: 1 1 auto;
|
||||
padding: 0 16px 16px;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class ImageDialogComponent {
|
||||
data = inject<{
|
||||
title: string;
|
||||
src: string;
|
||||
}>(MAT_DIALOG_DATA);
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -21,7 +21,6 @@ import {RouterConstants} from '../../constants/RouterConstants';
|
||||
TranslatePipe, MatDivider, NgOptimizedImage
|
||||
],
|
||||
templateUrl: './topbar.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './topbar.component.scss'
|
||||
})
|
||||
export class TopbarComponent {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, computed, signal, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, computed, signal } from '@angular/core';
|
||||
import { NgOptimizedImage } from '@angular/common';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
@@ -37,7 +37,6 @@ interface SkillGroup {
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './about.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './about.component.scss'
|
||||
})
|
||||
export class AboutComponent {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AlgorithmsService } from './algorithms.service';
|
||||
import { AlgorithmCategory } from './algorithm-category';
|
||||
import { RouterLink } from '@angular/router';
|
||||
@@ -10,7 +10,6 @@ import {TranslatePipe} from '@ngx-translate/core';
|
||||
selector: 'app-algorithms',
|
||||
templateUrl: './algorithms.component.html',
|
||||
styleUrl: './algorithms.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe],
|
||||
})
|
||||
export class AlgorithmsComponent {
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
<div class="flex flex-col mb-4">
|
||||
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
|
||||
<button mat-raised-button color="primary" (click)="toggleWind()">
|
||||
{{ isWindActive ? ('CLOTH.WIND_OFF' | translate) : ('CLOTH.WIND_ON' | translate) }}
|
||||
{{ isWindActive() ? ('CLOTH.WIND_OFF' | translate) : ('CLOTH.WIND_ON' | translate) }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="toggleMesh()">
|
||||
{{ isOutlineActive ? ('CLOTH.OUTLINE_OFF' | translate) : ('CLOTH.OUTLINE_ON' | translate) }}
|
||||
{{ isOutlineActive() ? ('CLOTH.OUTLINE_OFF' | translate) : ('CLOTH.OUTLINE_ON' | translate) }}
|
||||
</button>
|
||||
<button mat-raised-button color="accent" (click)="restartSimulation()">
|
||||
{{ 'CLOTH.RESTART_SIMULATION' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span>{{ 'CLOTH.ELONGATION' | translate }}: {{ elongation }}</span>
|
||||
<span>{{ 'CLOTH.ELONGATION' | translate }}: {{ elongation() }}</span>
|
||||
<mat-slider min="0.5" max="2.0" step="0.1">
|
||||
<input matSliderThumb [(ngModel)]="elongation">
|
||||
</mat-slider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
@@ -28,7 +28,6 @@ import {ClothCpuStrategy} from './strategies/cloth-cpu.strategy';
|
||||
Information
|
||||
],
|
||||
templateUrl: './cloth.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './cloth.component.scss',
|
||||
})
|
||||
export class ClothComponent {
|
||||
@@ -36,10 +35,10 @@ export class ClothComponent {
|
||||
private simulationTime: number = 0;
|
||||
private strategy: ClothSimulationStrategy | null = null;
|
||||
|
||||
public isWindActive: boolean = false;
|
||||
public isOutlineActive: boolean = false;
|
||||
public readonly isWindActive = signal(false);
|
||||
public readonly isOutlineActive = signal(false);
|
||||
public stiffness: number = 80;
|
||||
public elongation: number = 1.0;
|
||||
public readonly elongation = signal(1.0);
|
||||
|
||||
public renderConfig: RenderConfig = {
|
||||
mode: '3D',
|
||||
@@ -85,16 +84,16 @@ export class ClothComponent {
|
||||
}
|
||||
|
||||
public toggleWind(): void {
|
||||
this.isWindActive = !this.isWindActive;
|
||||
this.isWindActive.update(v => !v);
|
||||
}
|
||||
|
||||
public toggleMesh(): void {
|
||||
this.isOutlineActive = !this.isOutlineActive;
|
||||
this.isOutlineActive.update(v => !v);
|
||||
const mesh = this.strategy?.getMesh();
|
||||
if (!mesh?.material) {
|
||||
return;
|
||||
}
|
||||
mesh.material.wireframe = this.isOutlineActive;
|
||||
mesh.material.wireframe = this.isOutlineActive();
|
||||
}
|
||||
|
||||
public restartSimulation(): void {
|
||||
@@ -130,8 +129,8 @@ export class ClothComponent {
|
||||
if (this.strategy) {
|
||||
this.strategy.updateParams({
|
||||
stiffness: this.stiffness,
|
||||
elongation: this.elongation,
|
||||
isWindActive: this.isWindActive,
|
||||
elongation: this.elongation(),
|
||||
isWindActive: this.isWindActive(),
|
||||
simulationTime: this.simulationTime,
|
||||
deltaTime: engine.getDeltaTime() / 1000.0
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
|
||||
</button>
|
||||
}
|
||||
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms</p>
|
||||
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime() }} ms</p>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center flex-wrap">
|
||||
<mat-form-field appearance="outline" class="w-[150px]">
|
||||
@@ -44,7 +44,7 @@
|
||||
[(ngModel)]="gridRows"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
(ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||
(ngModelChange)="applyGridRows($event)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="w-[150px]">
|
||||
@@ -55,7 +55,7 @@
|
||||
[(ngModel)]="gridCols"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
(ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||
(ngModelChange)="applyGridCols($event)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="w-[150px]">
|
||||
@@ -78,8 +78,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<app-generic-grid
|
||||
[gridRows]="gridRows"
|
||||
[gridCols]="gridCols"
|
||||
[gridRows]="gridRows()"
|
||||
[gridCols]="gridCols()"
|
||||
[minGridSize]="MIN_GRID_SIZE"
|
||||
[maxGridSize]="MAX_GRID_SIZE"
|
||||
[maxGridPx]="MAX_GRID_PX"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, signal, ViewChild, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {AfterViewInit, Component, signal, ViewChild} from '@angular/core';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||
import {TranslatePipe} from "@ngx-translate/core";
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
@@ -29,7 +29,6 @@ import {GenericGridComponent, GridPos} from '../../../shared/components/generic-
|
||||
FormsModule,
|
||||
GenericGridComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
templateUrl: './conway-gol.component.html',
|
||||
})
|
||||
export class ConwayGolComponent implements AfterViewInit {
|
||||
@@ -48,16 +47,16 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
disclaimerListEntry: ['GOL.EXPLANATION.DISCLAIMER_1', 'GOL.EXPLANATION.DISCLAIMER_2', 'GOL.EXPLANATION.DISCLAIMER_3', 'GOL.EXPLANATION.DISCLAIMER_4']
|
||||
};
|
||||
|
||||
protected gridCols = DEFAULT_GRID_COLS;
|
||||
protected gridRows = DEFAULT_GRID_ROWS;
|
||||
protected lifeSpeed = DEFAULT_TIME_PER_GENERATION;
|
||||
protected readonly gridCols = signal(DEFAULT_GRID_COLS);
|
||||
protected readonly gridRows = signal(DEFAULT_GRID_ROWS);
|
||||
protected readonly lifeSpeed = signal(DEFAULT_TIME_PER_GENERATION);
|
||||
protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||
protected readonly MAX_GRID_PX = MAX_GRID_PX;
|
||||
|
||||
readGrid: Node[][] = [];
|
||||
writeGrid: Node[][] = [];
|
||||
executionTime = 0;
|
||||
readonly executionTime = signal(0);
|
||||
currentScenario: Scenario = Scenario.SIMPLE;
|
||||
readonly gameStarted = signal(false);
|
||||
|
||||
@@ -69,8 +68,8 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
this.genericGridComponent.createNodeFn = this.createConwayNode;
|
||||
this.genericGridComponent.getNodeColorFn = this.getConwayNodeColor;
|
||||
this.genericGridComponent.applySelectionFn = this.applyConwaySelection;
|
||||
this.genericGridComponent.gridRows = this.gridRows;
|
||||
this.genericGridComponent.gridCols = this.gridCols;
|
||||
this.genericGridComponent.gridRows = this.gridRows();
|
||||
this.genericGridComponent.gridCols = this.gridCols();
|
||||
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
|
||||
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
|
||||
this.genericGridComponent.maxGridPx = this.MAX_GRID_PX;
|
||||
@@ -86,7 +85,19 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
applySpeed(): void {
|
||||
this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION);
|
||||
this.lifeSpeed.set(Math.min(Math.max(this.lifeSpeed(), MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION));
|
||||
}
|
||||
|
||||
protected applyGridRows(rows: number): void {
|
||||
this.pauseGame();
|
||||
this.genericGridComponent.gridRows = rows;
|
||||
this.genericGridComponent.applyGridSize();
|
||||
}
|
||||
|
||||
protected applyGridCols(cols: number): void {
|
||||
this.pauseGame();
|
||||
this.genericGridComponent.gridCols = cols;
|
||||
this.genericGridComponent.applyGridSize();
|
||||
}
|
||||
|
||||
// --- Callbacks for GenericGridComponent ---
|
||||
@@ -122,8 +133,10 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
|
||||
// --- Conway-specific logic (kept local) ---
|
||||
setupRandomLives(): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
const rows = this.gridRows();
|
||||
const cols = this.gridCols();
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
this.readGrid[row][col].alive = Math.random() <= LIVE_SPAWN_PROBABILITY;
|
||||
}
|
||||
}
|
||||
@@ -138,8 +151,8 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
setupPulsar(): void {
|
||||
const centerRow = Math.floor(this.gridRows / 2);
|
||||
const centerCol = Math.floor(this.gridCols / 2);
|
||||
const centerRow = Math.floor(this.gridRows() / 2);
|
||||
const centerCol = Math.floor(this.gridCols() / 2);
|
||||
|
||||
const rows = [-6, -1, 1, 6];
|
||||
const offsets = [2, 3, 4];
|
||||
@@ -182,22 +195,25 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
while (this.gameStarted()){
|
||||
const startTime = performance.now();
|
||||
let lifeIsDead = true;
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
const rows = this.gridRows();
|
||||
const cols = this.gridCols();
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
lifeIsDead = this.checkLifeRules(row, col, this.writeGrid) && lifeIsDead;
|
||||
}
|
||||
}
|
||||
|
||||
this.swapGrids();
|
||||
const endTime = performance.now();
|
||||
this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4));
|
||||
const elapsed = Number.parseFloat((endTime - startTime).toFixed(4));
|
||||
this.executionTime.set(elapsed);
|
||||
if (lifeIsDead){
|
||||
this.gameStarted.set(false);
|
||||
}
|
||||
const delta = Math.max(this.lifeSpeed - this.executionTime, 0);
|
||||
const delta = Math.max(this.lifeSpeed() - elapsed, 0);
|
||||
await this.delay(delta);
|
||||
}
|
||||
this.executionTime = 0;
|
||||
this.executionTime.set(0);
|
||||
}
|
||||
|
||||
private checkLifeRules(row: number, col: number, writeGrid: Node[][]): boolean {
|
||||
@@ -227,8 +243,8 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
let aliveNeighborCount = 0;
|
||||
const minRow = Math.max(row - 1, 0);
|
||||
const minCol = Math.max(col - 1, 0);
|
||||
const maxRow = Math.min(row + 1, this.gridRows - 1);
|
||||
const maxCol = Math.min(col + 1, this.gridCols - 1);
|
||||
const maxRow = Math.min(row + 1, this.gridRows() - 1);
|
||||
const maxCol = Math.min(col + 1, this.gridCols() - 1);
|
||||
|
||||
for (let nRow = minRow; nRow <= maxRow; nRow++) {
|
||||
for (let nCol = minCol; nCol <= maxCol; nCol++) {
|
||||
@@ -253,7 +269,7 @@ export class ConwayGolComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
private setAlive(r: number, c: number): void {
|
||||
if (r >= 0 && r < this.gridRows && c >= 0 && c < this.gridCols) {
|
||||
if (r >= 0 && r < this.gridRows() && c >= 0 && c < this.gridCols()) {
|
||||
this.readGrid[r][c].alive = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@
|
||||
|
||||
<div class="mt-5 flex gap-2.5 py-2.5 px-4 rounded-sm border-l-[5px] font-medium min-w-[300px] items-center"
|
||||
[ngClass]="{
|
||||
'border-l-gray-500 bg-app-bg': solutionStatus === 'INCOMPLETE',
|
||||
'border-l-green-500 bg-green-50 text-green-800': solutionStatus === 'SOLVED',
|
||||
'border-l-orange-500 bg-orange-50 text-orange-700': solutionStatus === 'CONFLICTS',
|
||||
'border-l-red-500 bg-red-50 text-red-800': solutionStatus === 'INVALID'
|
||||
'border-l-gray-500 bg-app-bg': solutionStatus() === 'INCOMPLETE',
|
||||
'border-l-green-500 bg-green-50 text-green-800': solutionStatus() === 'SOLVED',
|
||||
'border-l-orange-500 bg-orange-50 text-orange-700': solutionStatus() === 'CONFLICTS',
|
||||
'border-l-red-500 bg-red-50 text-red-800': solutionStatus() === 'INVALID'
|
||||
}">
|
||||
<span class="uppercase text-[0.8em] opacity-70">{{ 'FOUR_COLOR.STATUS.LABEL' | translate }}:</span>
|
||||
<span>{{ 'FOUR_COLOR.STATUS.' + solutionStatus | translate }}</span>
|
||||
<span>{{ 'FOUR_COLOR.STATUS.' + solutionStatus() | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, ElementRef, inject, ViewChild, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {AfterViewInit, Component, ElementRef, inject, signal, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
@@ -29,7 +29,6 @@ import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
Information
|
||||
],
|
||||
templateUrl: './four-color.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './four-color.component.scss'
|
||||
})
|
||||
export class FourColorComponent implements AfterViewInit {
|
||||
@@ -59,12 +58,12 @@ export class FourColorComponent implements AfterViewInit {
|
||||
]
|
||||
};
|
||||
|
||||
gridRows = DEFAULT_GRID_ROWS;
|
||||
gridCols = DEFAULT_GRID_COLS;
|
||||
readonly gridRows = signal(DEFAULT_GRID_ROWS);
|
||||
readonly gridCols = signal(DEFAULT_GRID_COLS);
|
||||
grid: FourColorNode[][] = [];
|
||||
regions: Region[] = [];
|
||||
executionTime = 0;
|
||||
solutionStatus: 'INCOMPLETE' | 'SOLVED' | 'CONFLICTS' | 'INVALID' = 'INCOMPLETE';
|
||||
readonly solutionStatus = signal<'INCOMPLETE' | 'SOLVED' | 'CONFLICTS' | 'INVALID'>('INCOMPLETE');
|
||||
|
||||
@ViewChild('fourColorCanvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
@@ -76,20 +75,18 @@ export class FourColorComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
applyGridSize(): void {
|
||||
if (this.gridRows < MIN_GRID_SIZE) this.gridRows = MIN_GRID_SIZE;
|
||||
if (this.gridRows > MAX_GRID_SIZE) this.gridRows = MAX_GRID_SIZE;
|
||||
if (this.gridCols < MIN_GRID_SIZE) this.gridCols = MIN_GRID_SIZE;
|
||||
if (this.gridCols > MAX_GRID_SIZE) this.gridCols = MAX_GRID_SIZE;
|
||||
this.gridRows.set(Math.min(Math.max(this.gridRows(), MIN_GRID_SIZE), MAX_GRID_SIZE));
|
||||
this.gridCols.set(Math.min(Math.max(this.gridCols(), MIN_GRID_SIZE), MAX_GRID_SIZE));
|
||||
|
||||
this.initializeGrid();
|
||||
}
|
||||
|
||||
initializeGrid(): void {
|
||||
this.grid = [];
|
||||
this.solutionStatus = 'INCOMPLETE';
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
this.solutionStatus.set('INCOMPLETE');
|
||||
for (let r = 0; r < this.gridRows(); r++) {
|
||||
const row: FourColorNode[] = [];
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
for (let c = 0; c < this.gridCols(); c++) {
|
||||
row.push({
|
||||
row: r,
|
||||
col: c,
|
||||
@@ -107,15 +104,15 @@ export class FourColorComponent implements AfterViewInit {
|
||||
|
||||
private resizeCanvas(): void {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
const maxDim = Math.max(this.gridRows, this.gridCols);
|
||||
const maxDim = Math.max(this.gridRows(), this.gridCols());
|
||||
this.nodeSize = Math.floor(MAX_GRID_PX / maxDim);
|
||||
|
||||
canvas.width = this.gridCols * this.nodeSize;
|
||||
canvas.height = this.gridRows * this.nodeSize;
|
||||
canvas.width = this.gridCols() * this.nodeSize;
|
||||
canvas.height = this.gridRows() * this.nodeSize;
|
||||
}
|
||||
|
||||
generateRegions(): void {
|
||||
const numRegions = Math.floor((this.gridRows * this.gridCols) / 30);
|
||||
const numRegions = Math.floor((this.gridRows() * this.gridCols()) / 30);
|
||||
this.regions = [];
|
||||
const seeds = this.determineSeeds(numRegions);
|
||||
this.regionGrowth(seeds);
|
||||
@@ -123,8 +120,8 @@ export class FourColorComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
private determineAdjacency() {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
for (let row = 0; row < this.gridRows(); row++) {
|
||||
for (let col = 0; col < this.gridCols(); col++) {
|
||||
const currentRegionId = this.grid[row][col].regionId;
|
||||
const neighbors = this.getNeighbors(row, col);
|
||||
for (const neighbor of neighbors) {
|
||||
@@ -157,11 +154,11 @@ export class FourColorComponent implements AfterViewInit {
|
||||
private determineSeeds(numRegions: number) {
|
||||
const seeds: GridPos[] = [];
|
||||
for (let i = 0; i < numRegions; i++) {
|
||||
let r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
let c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
let r = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
|
||||
let c = SharedFunctions.randomIntFromInterval(0, this.gridCols() - 1);
|
||||
while (this.grid[r][c].regionId !== -1) {
|
||||
r = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
c = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
r = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
|
||||
c = SharedFunctions.randomIntFromInterval(0, this.gridCols() - 1);
|
||||
}
|
||||
this.grid[r][c].regionId = i;
|
||||
seeds.push({row: r, col: c});
|
||||
@@ -173,9 +170,9 @@ export class FourColorComponent implements AfterViewInit {
|
||||
private getNeighbors(row: number, col: number): GridPos[] {
|
||||
const res: GridPos[] = [];
|
||||
if (row > 0) res.push({row: row - 1, col});
|
||||
if (row < this.gridRows - 1) res.push({row: row + 1, col});
|
||||
if (row < this.gridRows() - 1) res.push({row: row + 1, col});
|
||||
if (col > 0) res.push({row, col: col - 1});
|
||||
if (col < this.gridCols - 1) res.push({row, col: col + 1});
|
||||
if (col < this.gridCols() - 1) res.push({row, col: col + 1});
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -185,8 +182,8 @@ export class FourColorComponent implements AfterViewInit {
|
||||
this.ctx.clearRect(0, 0, this.canvasRef.nativeElement.width, this.canvasRef.nativeElement.height);
|
||||
|
||||
// 1. Draw Cell Backgrounds
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
for (let r = 0; r < this.gridRows(); r++) {
|
||||
for (let c = 0; c < this.gridCols(); c++) {
|
||||
const node = this.grid[r][c];
|
||||
this.ctx.fillStyle = this.getNodeColor(node);
|
||||
this.ctx.fillRect(c * this.nodeSize, r * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
@@ -197,18 +194,18 @@ export class FourColorComponent implements AfterViewInit {
|
||||
this.ctx.strokeStyle = '#000';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
for (let r = 0; r < this.gridRows; r++) {
|
||||
for (let c = 0; c < this.gridCols; c++) {
|
||||
for (let r = 0; r < this.gridRows(); r++) {
|
||||
for (let c = 0; c < this.gridCols(); c++) {
|
||||
const currentRegion = this.grid[r][c].regionId;
|
||||
|
||||
// Right border
|
||||
if (c < this.gridCols - 1 && this.grid[r][c+1].regionId !== currentRegion) {
|
||||
if (c < this.gridCols() - 1 && this.grid[r][c+1].regionId !== currentRegion) {
|
||||
this.ctx.moveTo((c + 1) * this.nodeSize, r * this.nodeSize);
|
||||
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
}
|
||||
|
||||
// Bottom border
|
||||
if (r < this.gridRows - 1 && this.grid[r+1][c].regionId !== currentRegion) {
|
||||
if (r < this.gridRows() - 1 && this.grid[r+1][c].regionId !== currentRegion) {
|
||||
this.ctx.moveTo(c * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
this.ctx.lineTo((c + 1) * this.nodeSize, (r + 1) * this.nodeSize);
|
||||
}
|
||||
@@ -219,7 +216,7 @@ export class FourColorComponent implements AfterViewInit {
|
||||
// 3. Draw Outer Border
|
||||
this.ctx.strokeStyle = '#000';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.strokeRect(0, 0, this.gridCols * this.nodeSize, this.gridRows * this.nodeSize);
|
||||
this.ctx.strokeRect(0, 0, this.gridCols() * this.nodeSize, this.gridRows() * this.nodeSize);
|
||||
}
|
||||
|
||||
private getNodeColor(node: FourColorNode): string {
|
||||
@@ -260,10 +257,10 @@ export class FourColorComponent implements AfterViewInit {
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
const col = Math.floor(x / (rect.width / this.gridCols));
|
||||
const row = Math.floor(y / (rect.height / this.gridRows));
|
||||
const col = Math.floor(x / (rect.width / this.gridCols()));
|
||||
const row = Math.floor(y / (rect.height / this.gridRows()));
|
||||
|
||||
if (row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols) {
|
||||
if (row >= 0 && row < this.gridRows() && col >= 0 && col < this.gridCols()) {
|
||||
return {row, col};
|
||||
}
|
||||
return null;
|
||||
@@ -283,8 +280,8 @@ export class FourColorComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
private updateRegionColors(region: Region): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
for (let row = 0; row < this.gridRows(); row++) {
|
||||
for (let col = 0; col < this.gridCols(); col++) {
|
||||
if (this.grid[row][col].regionId === region.id) {
|
||||
this.grid[row][col].color = region.color;
|
||||
}
|
||||
@@ -297,7 +294,7 @@ export class FourColorComponent implements AfterViewInit {
|
||||
region.color = 0;
|
||||
this.updateRegionColors(region);
|
||||
}
|
||||
this.solutionStatus = 'INCOMPLETE';
|
||||
this.solutionStatus.set('INCOMPLETE');
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
@@ -363,9 +360,9 @@ export class FourColorComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
if (hasConflicts) {
|
||||
this.solutionStatus = allColored ? 'INVALID' : 'CONFLICTS';
|
||||
this.solutionStatus.set(allColored ? 'INVALID' : 'CONFLICTS');
|
||||
} else {
|
||||
this.solutionStatus = allColored ? 'SOLVED' : 'INCOMPLETE';
|
||||
this.solutionStatus.set(allColored ? 'SOLVED' : 'INCOMPLETE');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, OnInit, signal, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {Component, OnInit, signal} from '@angular/core';
|
||||
import {Information} from '../information/information';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
@@ -35,7 +35,6 @@ import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
||||
NgxSliderModule
|
||||
],
|
||||
templateUrl: './fractal.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './fractal.component.scss',
|
||||
})
|
||||
export class FractalComponent implements OnInit {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {Component} from '@angular/core';
|
||||
import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core';
|
||||
import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal3d.shader';
|
||||
import {Information} from '../information/information';
|
||||
@@ -22,7 +22,6 @@ import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/compo
|
||||
BabylonCanvas
|
||||
],
|
||||
templateUrl: './fractal3d.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './fractal3d.component.scss',
|
||||
})
|
||||
export class Fractal3dComponent {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
@let info = algorithmInformation();
|
||||
<div class="mb-4 py-3 px-4 border border-[#ddd] rounded-lg">
|
||||
<h3 class="m-0 mb-2">{{ algorithmInformation.title | translate }}</h3>
|
||||
<h3 class="m-0 mb-2">{{ info.title | translate }}</h3>
|
||||
|
||||
@if(algorithmInformation.entries && algorithmInformation.entries.length > 0){
|
||||
@for (algo of algorithmInformation.entries; track algo)
|
||||
@if(info.entries && info.entries.length > 0){
|
||||
@for (algo of info.entries; track algo)
|
||||
{
|
||||
<p class="my-2">
|
||||
<strong>
|
||||
@@ -18,24 +19,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
@if (algorithmInformation.disclaimer !== '')
|
||||
@if (info.disclaimer !== '')
|
||||
{
|
||||
<p class="my-2">
|
||||
<strong>{{ 'ALGORITHM.NOTE' | translate}}</strong> {{ algorithmInformation.disclaimer | translate}}
|
||||
<strong>{{ 'ALGORITHM.NOTE' | translate}}</strong> {{ info.disclaimer | translate}}
|
||||
</p>
|
||||
@if (algorithmInformation.disclaimerListEntry && algorithmInformation.disclaimerListEntry.length > 0)
|
||||
@if (info.disclaimerListEntry && info.disclaimerListEntry.length > 0)
|
||||
{
|
||||
<ul>
|
||||
@for (entry of algorithmInformation.disclaimerListEntry; track entry)
|
||||
@for (entry of info.disclaimerListEntry; track entry)
|
||||
{
|
||||
<li>{{ entry | translate}}</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (algorithmInformation.disclaimerBottom !== '')
|
||||
@if (info.disclaimerBottom !== '')
|
||||
{
|
||||
<p>
|
||||
{{ algorithmInformation.disclaimerBottom | translate}}
|
||||
{{ info.disclaimerBottom | translate}}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, Input, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {Component, input} from '@angular/core';
|
||||
import {TranslatePipe} from "@ngx-translate/core";
|
||||
import {AlgorithmInformation} from './information.models';
|
||||
|
||||
@@ -8,10 +8,9 @@ import {AlgorithmInformation} from './information.models';
|
||||
TranslatePipe
|
||||
],
|
||||
templateUrl: './information.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './information.scss',
|
||||
})
|
||||
export class Information {
|
||||
|
||||
@Input({ required: true }) algorithmInformation!: AlgorithmInformation;
|
||||
readonly algorithmInformation = input.required<AlgorithmInformation>();
|
||||
}
|
||||
@@ -22,8 +22,8 @@
|
||||
<span><span class="legend-swatch bg-[gold]"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
|
||||
</div>
|
||||
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
|
||||
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p>
|
||||
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p>
|
||||
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength() }}</p>
|
||||
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime() | number:'1.2-2' }} ms</p>
|
||||
</div>
|
||||
</div>
|
||||
<app-generic-grid
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, inject, signal, ViewChild, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core';
|
||||
import {Information} from '../../information/information';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
@@ -25,7 +25,6 @@ import {PathfindingService} from '../service/pathfinding.service';
|
||||
DecimalPipe
|
||||
],
|
||||
templateUrl: './labyrinth.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './labyrinth.component.scss',
|
||||
})
|
||||
export class LabyrinthComponent implements AfterViewInit {
|
||||
@@ -64,8 +63,8 @@ export class LabyrinthComponent implements AfterViewInit {
|
||||
endNode: Node | null = null;
|
||||
animationSpeed = 3;
|
||||
mazeAnimationSpeed = 1;
|
||||
pathLength = "0";
|
||||
executionTime = 0;
|
||||
readonly pathLength = signal("0");
|
||||
readonly executionTime = signal(0);
|
||||
|
||||
private timeoutIds: number[] = [];
|
||||
protected mazeNodesInOrder: Node[] = [];
|
||||
@@ -309,13 +308,13 @@ export class LabyrinthComponent implements AfterViewInit {
|
||||
const lengthOfShortestPath = result.nodesInShortestPathOrder.length;
|
||||
if (lengthOfShortestPath === 0)
|
||||
{
|
||||
this.pathLength = "∞"
|
||||
this.pathLength.set("∞");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.pathLength = result.nodesInShortestPathOrder.length + "";
|
||||
this.pathLength.set(result.nodesInShortestPathOrder.length + "");
|
||||
}
|
||||
this.executionTime = endTime - startTime;
|
||||
this.executionTime.set(endTime - startTime);
|
||||
|
||||
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridRows"
|
||||
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||
(ngModelChange)="applyGridRows($event)"
|
||||
/> </mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="w-[150px]">
|
||||
@@ -46,7 +46,7 @@
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridCols"
|
||||
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||
(ngModelChange)="applyGridCols($event)"
|
||||
/> </mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,14 +59,14 @@
|
||||
<span><span class="legend-swatch bg-[gold]"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
|
||||
</div>
|
||||
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
|
||||
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p>
|
||||
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p>
|
||||
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength() }}</p>
|
||||
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime() | number:'1.2-2' }} ms</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-generic-grid
|
||||
[gridRows]="gridRows"
|
||||
[gridCols]="gridCols"
|
||||
[gridRows]="gridRows()"
|
||||
[gridCols]="gridCols()"
|
||||
[minGridSize]="MIN_GRID_SIZE"
|
||||
[maxGridSize]="MAX_GRID_SIZE"
|
||||
[maxGridPx]="MAX_GRID_PX"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, inject, ViewChild, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@@ -43,7 +43,6 @@ enum NodeType {
|
||||
Information,
|
||||
GenericGridComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
templateUrl: './pathfinding.component.html',
|
||||
})
|
||||
export class PathfindingComponent implements AfterViewInit {
|
||||
@@ -75,19 +74,19 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
disclaimerListEntry: []
|
||||
};
|
||||
|
||||
gridRows = DEFAULT_GRID_ROWS;
|
||||
gridCols = DEFAULT_GRID_COLS;
|
||||
readonly gridRows = signal(DEFAULT_GRID_ROWS);
|
||||
readonly gridCols = signal(DEFAULT_GRID_COLS);
|
||||
|
||||
grid: Node[][] = [];
|
||||
startNode: Node | null = null;
|
||||
endNode: Node | null = null;
|
||||
|
||||
selectedNodeType: NodeType = NodeType.None;
|
||||
readonly selectedNodeType = signal<NodeType>(NodeType.None);
|
||||
private shouldAddWall = true; // Moved here
|
||||
|
||||
animationSpeed = 3;
|
||||
pathLength = "0";
|
||||
executionTime = 0;
|
||||
readonly pathLength = signal("0");
|
||||
readonly executionTime = signal(0);
|
||||
|
||||
private timeoutIds: number[] = [];
|
||||
|
||||
@@ -101,8 +100,8 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
this.genericGridComponent.createNodeFn = this.createPathfindingNode;
|
||||
this.genericGridComponent.getNodeColorFn = this.getPathfindingNodeColor;
|
||||
this.genericGridComponent.applySelectionFn = this.applyPathfindingSelection;
|
||||
this.genericGridComponent.gridRows = this.gridRows;
|
||||
this.genericGridComponent.gridCols = this.gridCols;
|
||||
this.genericGridComponent.gridRows = this.gridRows();
|
||||
this.genericGridComponent.gridCols = this.gridCols();
|
||||
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
|
||||
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
|
||||
this.genericGridComponent.maxGridPx = MAX_GRID_PX;
|
||||
@@ -111,6 +110,16 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
this.createCase({withWalls: true, scenario: "normal"});
|
||||
}
|
||||
|
||||
protected applyGridRows(rows: number): void {
|
||||
this.genericGridComponent.gridRows = rows;
|
||||
this.genericGridComponent.applyGridSize();
|
||||
}
|
||||
|
||||
protected applyGridCols(cols: number): void {
|
||||
this.genericGridComponent.gridCols = cols;
|
||||
this.genericGridComponent.applyGridSize();
|
||||
}
|
||||
|
||||
// --- Callbacks for GenericGridComponent ---
|
||||
createPathfindingNode = (row: number, col: number): Node => {
|
||||
return {
|
||||
@@ -142,11 +151,11 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
const node = grid[pos.row][pos.col];
|
||||
|
||||
// Determine if we should add or remove a wall
|
||||
if (this.selectedNodeType === NodeType.Wall && this.genericGridComponent.isDrawing && this.genericGridComponent['lastCell'] === null) {
|
||||
if (this.selectedNodeType() === NodeType.Wall && this.genericGridComponent.isDrawing && this.genericGridComponent['lastCell'] === null) {
|
||||
this.shouldAddWall = !node.isWall;
|
||||
}
|
||||
|
||||
switch (this.selectedNodeType) {
|
||||
switch (this.selectedNodeType()) {
|
||||
case NodeType.Start:
|
||||
this.trySetStart(node);
|
||||
break;
|
||||
@@ -200,13 +209,13 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
const lengthOfShortestPath = result.nodesInShortestPathOrder.length;
|
||||
if (lengthOfShortestPath === 0)
|
||||
{
|
||||
this.pathLength = "∞"
|
||||
this.pathLength.set("∞");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.pathLength = result.nodesInShortestPathOrder.length + "";
|
||||
this.pathLength.set(result.nodesInShortestPathOrder.length + "");
|
||||
}
|
||||
this.executionTime = endTime - startTime;
|
||||
this.executionTime.set(endTime - startTime);
|
||||
|
||||
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
||||
}
|
||||
@@ -312,31 +321,31 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
if (scenario === 'edge') {
|
||||
return {
|
||||
start: {row: 0, col: 0},
|
||||
end: {row: this.gridRows - 1, col: this.gridCols - 1}
|
||||
end: {row: this.gridRows() - 1, col: this.gridCols() - 1}
|
||||
};
|
||||
} else if (scenario === 'random') {
|
||||
return this.createRandomStartEndPosition();
|
||||
} else {
|
||||
// normal: mid-left -> mid-right
|
||||
const midRow = Math.floor(this.gridRows / 2);
|
||||
const midRow = Math.floor(this.gridRows() / 2);
|
||||
return {
|
||||
start: {row: midRow, col: 0},
|
||||
end: {row: midRow, col: this.gridCols - 1}
|
||||
end: {row: midRow, col: this.gridCols() - 1}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private createRandomStartEndPosition(): { start: GridPos; end: GridPos } {
|
||||
const midCol = Math.floor(this.gridCols / 2);
|
||||
const midCol = Math.floor(this.gridCols() / 2);
|
||||
|
||||
const startRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
const startCol: number = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
const startRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
|
||||
const startCol: number = SharedFunctions.randomIntFromInterval(0, this.gridCols() - 1);
|
||||
|
||||
const endRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
const endRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
|
||||
let endCol: number;
|
||||
|
||||
if (startCol <= midCol) {
|
||||
endCol = SharedFunctions.randomIntFromInterval(midCol + 1, this.gridCols - 1);
|
||||
endCol = SharedFunctions.randomIntFromInterval(midCol + 1, this.gridCols() - 1);
|
||||
} else {
|
||||
endCol = SharedFunctions.randomIntFromInterval(0, midCol);
|
||||
}
|
||||
@@ -358,12 +367,12 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
private createRandomWalls() {
|
||||
const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows);
|
||||
const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols() * this.gridRows());
|
||||
|
||||
for (let wall = 0; wall < maxNumberOfWalls; wall++) {
|
||||
|
||||
const row: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1);
|
||||
const col: number = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1);
|
||||
const row: number = SharedFunctions.randomIntFromInterval(0, this.gridRows() - 1);
|
||||
const col: number = SharedFunctions.randomIntFromInterval(0, this.gridCols() - 1);
|
||||
|
||||
if (!this.grid[row][col]) { // Use the grid passed from GenericGrid
|
||||
wall--;
|
||||
@@ -382,8 +391,8 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
private createVerticalWall() {
|
||||
const height = this.gridRows;
|
||||
const startCol = Math.floor(this.gridCols / 2);
|
||||
const height = this.gridRows();
|
||||
const startCol = Math.floor(this.gridCols() / 2);
|
||||
|
||||
for (let i = 5; i < (height - 5); i++) {
|
||||
const row = i;
|
||||
@@ -404,8 +413,8 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
|
||||
private createDiagonalWall() {
|
||||
// Diagonal-ish wall; avoids start/end
|
||||
const len = Math.min(this.gridRows, this.gridCols);
|
||||
const startCol = Math.floor((this.gridCols - len) / 2);
|
||||
const len = Math.min(this.gridRows(), this.gridCols());
|
||||
const startCol = Math.floor((this.gridCols() - len) / 2);
|
||||
|
||||
for (let i = 0; i < Math.max(0, len - 10); i++) {
|
||||
const row = len - i - 1;
|
||||
@@ -433,8 +442,8 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
private clearPath(): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
for (let row = 0; row < this.gridRows(); row++) {
|
||||
for (let col = 0; col < this.gridCols(); col++) {
|
||||
const node = this.grid[row][col];
|
||||
node.isVisited = false;
|
||||
node.isPath = false;
|
||||
|
||||
@@ -8,34 +8,34 @@
|
||||
<div class="grid grid-cols-2 tablet:grid-cols-1 tablet:gap-4 mb-6">
|
||||
<div class="flex items-center gap-4 mr-4">
|
||||
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.TRAIL_DECAY_TIME' | translate }}</p>
|
||||
<ngx-slider class="grow" [(value)]="simParams.trailDecay" [options]="trailDecayOptions" ></ngx-slider>
|
||||
<ngx-slider class="grow" [(value)]="trailDecay" [options]="trailDecayOptions" ></ngx-slider>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 mr-4">
|
||||
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.ATTRACTION' | translate }}</p>
|
||||
<ngx-slider class="grow" [(value)]="simParams.g" [options]="gravityOptions" ></ngx-slider>
|
||||
<ngx-slider class="grow" [(value)]="g" [options]="gravityOptions" ></ngx-slider>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 mr-4">
|
||||
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L1_LENGTH' | translate }}</p>
|
||||
<ngx-slider class="grow" [(value)]="simParams.l1" [options]="lengthOptions" ></ngx-slider>
|
||||
<ngx-slider class="grow" [(value)]="l1" [options]="lengthOptions" ></ngx-slider>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 mr-4">
|
||||
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L2_LENGTH' | translate }}</p>
|
||||
<ngx-slider class="grow" [(value)]="simParams.l2" [options]="lengthOptions" ></ngx-slider>
|
||||
<ngx-slider class="grow" [(value)]="l2" [options]="lengthOptions" ></ngx-slider>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 mr-4">
|
||||
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M1_MASS' | translate }}</p>
|
||||
<ngx-slider class="grow" [(value)]="simParams.m1" [options]="massOptions" ></ngx-slider>
|
||||
<ngx-slider class="grow" [(value)]="m1" [options]="massOptions" ></ngx-slider>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 mr-4">
|
||||
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M2_MASS' | translate }}</p>
|
||||
<ngx-slider class="grow" [(value)]="simParams.m2" [options]="massOptions" ></ngx-slider>
|
||||
<ngx-slider class="grow" [(value)]="m2" [options]="massOptions" ></ngx-slider>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 mr-4 col-span-full">
|
||||
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.DAMPING' | translate }}</p>
|
||||
<ngx-slider class="grow" [(value)]="simParams.damping" [options]="dampingOptions" ></ngx-slider>
|
||||
<ngx-slider class="grow" [(value)]="damping" [options]="dampingOptions" ></ngx-slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
|
||||
@@ -29,7 +29,6 @@ import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
|
||||
Information,
|
||||
],
|
||||
templateUrl: './pendulum.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './pendulum.component.scss',
|
||||
})
|
||||
class PendulumComponent {
|
||||
@@ -103,16 +102,17 @@ class PendulumComponent {
|
||||
hidePointerLabels: false
|
||||
};
|
||||
|
||||
readonly simParams = {
|
||||
readonly trailDecay = signal(DEFAULT_TRAIL_DECAY);
|
||||
readonly g = signal(DEFAULT_G);
|
||||
readonly l1 = signal(DEFAULT_L1_LENGTH);
|
||||
readonly l2 = signal(DEFAULT_L2_LENGTH);
|
||||
readonly m1 = signal(DEFAULT_M1_MASS);
|
||||
readonly m2 = signal(DEFAULT_M2_MASS);
|
||||
readonly damping = signal(DEFAULT_DAMPING);
|
||||
|
||||
private readonly loop = {
|
||||
time: 0,
|
||||
dt: 0.015,
|
||||
g: DEFAULT_G,
|
||||
m1: DEFAULT_M1_MASS,
|
||||
m2: DEFAULT_M2_MASS,
|
||||
l1: DEFAULT_L1_LENGTH,
|
||||
l2: DEFAULT_L2_LENGTH,
|
||||
damping: DEFAULT_DAMPING,
|
||||
trailDecay: DEFAULT_TRAIL_DECAY,
|
||||
impulseM1: 0.0,
|
||||
impulseM2: 0.0,
|
||||
};
|
||||
@@ -147,10 +147,19 @@ class PendulumComponent {
|
||||
private startParamUpdateLoop(scene: any): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.simParams.time += this.simParams.dt;
|
||||
this.loop.time += this.loop.dt;
|
||||
|
||||
if (this.strategy) {
|
||||
this.strategy.updateParams({...this.simParams});
|
||||
this.strategy.updateParams({
|
||||
...this.loop,
|
||||
g: this.g(),
|
||||
m1: this.m1(),
|
||||
m2: this.m2(),
|
||||
l1: this.l1(),
|
||||
l2: this.l2(),
|
||||
damping: this.damping(),
|
||||
trailDecay: this.trailDecay()
|
||||
});
|
||||
}
|
||||
|
||||
this.resetImpulses();
|
||||
@@ -158,22 +167,22 @@ class PendulumComponent {
|
||||
}
|
||||
|
||||
private resetImpulses() {
|
||||
if (this.simParams.impulseM1 !== 0.0) {
|
||||
this.simParams.impulseM1 = 0;
|
||||
if (this.loop.impulseM1 !== 0.0) {
|
||||
this.loop.impulseM1 = 0;
|
||||
}
|
||||
|
||||
if (this.simParams.impulseM2 !== 0.0) {
|
||||
this.simParams.impulseM2 = 0;
|
||||
if (this.loop.impulseM2 !== 0.0) {
|
||||
this.loop.impulseM2 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pushPendulum(m1: boolean) {
|
||||
if (m1) {
|
||||
this.simParams.impulseM1 = IMPULSE_M1;
|
||||
this.loop.impulseM1 = IMPULSE_M1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.simParams.impulseM2 = IMPULSE_M2;
|
||||
this.loop.impulseM2 = IMPULSE_M2;
|
||||
}
|
||||
|
||||
resetPendulum() {
|
||||
|
||||
@@ -38,19 +38,20 @@
|
||||
<mat-icon>add_box</mat-icon> {{ 'SORTING.GENERATE_NEW_ARRAY' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button (click)="toggleSound()">
|
||||
<mat-icon>{{ isSoundEnabled ? 'volume_up' : 'volume_off' }}</mat-icon>
|
||||
{{ isSoundEnabled ? ('SORTING.SOUND_OFF' | translate) : ('SORTING.SOUND_ON' | translate) }}
|
||||
<mat-icon>{{ isSoundEnabled() ? 'volume_up' : 'volume_off' }}</mat-icon>
|
||||
{{ isSoundEnabled() ? ('SORTING.SOUND_OFF' | translate) : ('SORTING.SOUND_ON' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
|
||||
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms</p>
|
||||
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime() }} ms</p>
|
||||
</div>
|
||||
<div class="flex items-end h-[clamp(200px,40vh,400px)] border-b border-app-fg mb-[clamp(10px,3vw,20px)] gap-px bg-card-bg">
|
||||
@for (item of sortArray; track $index) {
|
||||
@for (item of sortArray(); track $index) {
|
||||
<div
|
||||
class="grow w-2.5 min-w-px transition-all duration-50"
|
||||
[style.height.px]="item.value * 3"
|
||||
data-testid="sort-bar"
|
||||
[ngClass]="{
|
||||
'bg-[#424242]': item.state === 'unsorted',
|
||||
'bg-[#ffeb3b]': item.state === 'comparing',
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { provideNoopAnimations } from '@angular/platform-browser/animations';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
import { SortingComponent } from './sorting.component';
|
||||
import { SortingAudioService } from './service/sorting-audio.service';
|
||||
|
||||
/**
|
||||
* The animation writes the bars from setTimeout callbacks. Under OnPush a bar
|
||||
* only repaints when the sortArray signal is replaced, so these tests fail if
|
||||
* the component goes back to mutating the array in place.
|
||||
*/
|
||||
describe('SortingComponent', () => {
|
||||
let fixture: ComponentFixture<SortingComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SortingComponent],
|
||||
providers: [
|
||||
provideZonelessChangeDetection(),
|
||||
provideNoopAnimations(),
|
||||
provideTranslateService({ lang: 'en', fallbackLang: 'en' }),
|
||||
{
|
||||
provide: SortingAudioService,
|
||||
useValue: { initOnUserGesture: vi.fn(), playTone: vi.fn(), playSortedSweep: vi.fn() },
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SortingComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function bars() {
|
||||
return fixture.debugElement.queryAll(By.css('[data-testid="sort-bar"]'));
|
||||
}
|
||||
|
||||
function heights(): number[] {
|
||||
return bars().map(bar => Number.parseFloat((bar.nativeElement as HTMLElement).style.height));
|
||||
}
|
||||
|
||||
it('shows every bar as sorted once the animation has run', () => {
|
||||
fixture.componentInstance.startSorting();
|
||||
vi.runAllTimers();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(bars()).toHaveLength(50);
|
||||
expect(bars().every(bar => bar.nativeElement.classList.contains('bg-[#4caf50]'))).toBe(true);
|
||||
const sorted = heights();
|
||||
expect(sorted).toEqual([...sorted].sort((a, b) => a - b));
|
||||
});
|
||||
|
||||
it('restores the unsorted array when reset during the animation', () => {
|
||||
const initial = heights();
|
||||
|
||||
fixture.componentInstance.startSorting();
|
||||
vi.advanceTimersByTime(300);
|
||||
fixture.componentInstance.resetSorting();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(heights()).toEqual(initial);
|
||||
expect(bars().some(bar => bar.nativeElement.classList.contains('bg-[#ffeb3b]'))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ChangeDetectorRef, Component, inject, OnInit, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {Component, inject, OnInit, signal} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
@@ -18,14 +18,12 @@ import {Information} from '../information/information';
|
||||
selector: 'app-sorting',
|
||||
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslatePipe, FormsModule, MatInput, Information],
|
||||
templateUrl: './sorting.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './sorting.component.scss'
|
||||
})
|
||||
export class SortingComponent implements OnInit {
|
||||
|
||||
private readonly sortingService: SortingService = inject(SortingService);
|
||||
private readonly audioService: SortingAudioService = inject(SortingAudioService);
|
||||
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
||||
|
||||
readonly MAX_ARRAY_SIZE: number = 200;
|
||||
readonly MIN_ARRAY_SIZE: number = 20;
|
||||
@@ -69,14 +67,14 @@ export class SortingComponent implements OnInit {
|
||||
};
|
||||
|
||||
private timeoutIds: number[] = [];
|
||||
sortArray: SortData[] = [];
|
||||
readonly sortArray = signal<SortData[]>([]);
|
||||
unsortedArrayCopy: SortData[] = [];
|
||||
arraySize = 50;
|
||||
readonly arraySize = signal(50);
|
||||
maxArrayValue = 100;
|
||||
animationSpeed = 100; // Milliseconds per step
|
||||
selectedAlgorithm: string = this.algoInformation.entries[0].name;
|
||||
executionTime = 0;
|
||||
isSoundEnabled = false;
|
||||
readonly selectedAlgorithm = signal(this.algoInformation.entries[0].name);
|
||||
readonly executionTime = signal(0);
|
||||
readonly isSoundEnabled = signal(false);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.generateNewArray();
|
||||
@@ -84,8 +82,9 @@ export class SortingComponent implements OnInit {
|
||||
|
||||
newArraySizeSet()
|
||||
{
|
||||
this.arraySize = Math.min(Math.max(this.arraySize, this.MIN_ARRAY_SIZE), this.MAX_ARRAY_SIZE);
|
||||
if (this.arraySize == this.sortArray.length)
|
||||
const clamped = Math.min(Math.max(this.arraySize(), this.MIN_ARRAY_SIZE), this.MAX_ARRAY_SIZE);
|
||||
this.arraySize.set(clamped);
|
||||
if (clamped == this.sortArray().length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -94,35 +93,27 @@ export class SortingComponent implements OnInit {
|
||||
|
||||
generateNewArray(): void {
|
||||
this.resetSorting();
|
||||
this.executionTime = 0;
|
||||
this.unsortedArrayCopy = [];
|
||||
this.sortArray = [];
|
||||
this.executionTime.set(0);
|
||||
const unsorted: SortData[] = [];
|
||||
|
||||
for (let i = 0; i < this.arraySize; i++) {
|
||||
for (let i = 0; i < this.arraySize(); i++) {
|
||||
const randomValue = Math.floor(Math.random() * this.maxArrayValue) + 1;
|
||||
this.sortArray.push({
|
||||
value: randomValue,
|
||||
state: 'unsorted'
|
||||
});
|
||||
|
||||
this.unsortedArrayCopy.push({
|
||||
unsorted.push({
|
||||
value: randomValue,
|
||||
state: 'unsorted'
|
||||
});
|
||||
}
|
||||
|
||||
this.unsortedArrayCopy = unsorted;
|
||||
this.resetSortState();
|
||||
}
|
||||
|
||||
private resetSortState() {
|
||||
for (let i = 0; i < this.sortArray.length; i++) {
|
||||
const element = this.sortArray[i];
|
||||
const unsortedElement = this.unsortedArrayCopy[i];
|
||||
element.value = unsortedElement.value;
|
||||
element.state = 'unsorted';
|
||||
}
|
||||
this.sortArray.set(this.unsortedArrayCopy.map(item => ({...item})));
|
||||
}
|
||||
|
||||
toggleSound(): void {
|
||||
this.isSoundEnabled = !this.isSoundEnabled;
|
||||
this.isSoundEnabled.update(v => !v);
|
||||
}
|
||||
|
||||
async startSorting(): Promise<void> {
|
||||
@@ -132,26 +123,26 @@ export class SortingComponent implements OnInit {
|
||||
const startTime = performance.now();
|
||||
let snapshots: SortSnapshot[] = [];
|
||||
|
||||
switch (this.selectedAlgorithm) {
|
||||
switch (this.selectedAlgorithm()) {
|
||||
case 'Bubble Sort':
|
||||
snapshots = this.sortingService.bubbleSort(this.sortArray);
|
||||
snapshots = this.sortingService.bubbleSort(this.sortArray());
|
||||
break;
|
||||
case 'Quick Sort':
|
||||
snapshots = this.sortingService.quickSort(this.sortArray);
|
||||
snapshots = this.sortingService.quickSort(this.sortArray());
|
||||
break;
|
||||
case 'Heap Sort':
|
||||
snapshots = this.sortingService.heapSort(this.sortArray);
|
||||
snapshots = this.sortingService.heapSort(this.sortArray());
|
||||
break;
|
||||
case 'Cocktail Sort':
|
||||
snapshots = this.sortingService.cocktailSort(this.sortArray);
|
||||
snapshots = this.sortingService.cocktailSort(this.sortArray());
|
||||
break;
|
||||
case 'Tim Sort':
|
||||
snapshots = this.sortingService.timSort(this.sortArray);
|
||||
snapshots = this.sortingService.timSort(this.sortArray());
|
||||
break;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4));
|
||||
this.executionTime.set(Number.parseFloat((endTime - startTime).toFixed(4)));
|
||||
|
||||
this.animateSorting(snapshots);
|
||||
}
|
||||
@@ -159,14 +150,9 @@ export class SortingComponent implements OnInit {
|
||||
private animateSorting(snapshots: SortSnapshot[]): void {
|
||||
snapshots.forEach((snapshot, index) => {
|
||||
const id = setTimeout(() => {
|
||||
for (let i = 0; i < this.sortArray.length; i++) {
|
||||
if (snapshot.array[i]) {
|
||||
this.sortArray[i].value = snapshot.array[i].value;
|
||||
this.sortArray[i].state = snapshot.array[i].state;
|
||||
}
|
||||
}
|
||||
this.sortArray.set(snapshot.array);
|
||||
|
||||
if (this.isSoundEnabled) {
|
||||
if (this.isSoundEnabled()) {
|
||||
// Play a tone for each comparing bar (max 2 at once to avoid noise)
|
||||
snapshot.array
|
||||
.filter(item => item.state === 'comparing')
|
||||
@@ -174,13 +160,10 @@ export class SortingComponent implements OnInit {
|
||||
.forEach(item => this.audioService.playTone(item.value, this.maxArrayValue, this.animationSpeed));
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
|
||||
if (index === snapshots.length - 1) {
|
||||
this.sortArray.forEach(item => item.state = 'sorted');
|
||||
this.cdr.detectChanges();
|
||||
if (this.isSoundEnabled) {
|
||||
this.audioService.playSortedSweep(this.sortArray.map(item => item.value), this.maxArrayValue);
|
||||
this.sortArray.set(snapshot.array.map(item => ({...item, state: 'sorted'})));
|
||||
if (this.isSoundEnabled()) {
|
||||
this.audioService.playSortedSweep(this.sortArray().map(item => item.value), this.maxArrayValue);
|
||||
}
|
||||
}
|
||||
}, index * this.animationSpeed);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import {MatCard} from '@angular/material/card';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
import {SharedFunctions} from '../../shared/SharedFunctions';
|
||||
@@ -10,7 +10,6 @@ import {SharedFunctions} from '../../shared/SharedFunctions';
|
||||
TranslatePipe
|
||||
],
|
||||
templateUrl: './imprint.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './imprint.component.scss',
|
||||
})
|
||||
export class ImprintComponent {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MatCard } from '@angular/material/card';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
@@ -8,7 +8,6 @@ import { RouterConstants } from '../../constants/RouterConstants';
|
||||
@Component({
|
||||
selector: 'app-not-found',
|
||||
imports: [RouterLink, MatCard, MatButton, TranslatePipe],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
templateUrl: './not-found.component.html',
|
||||
})
|
||||
export class NotFoundComponent {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, inject, CUSTOM_ELEMENTS_SCHEMA, ViewChild, ElementRef, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {AfterViewInit, Component, inject, CUSTOM_ELEMENTS_SCHEMA, ViewChild, ElementRef} from '@angular/core';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
@@ -30,7 +30,6 @@ register();
|
||||
MatButton,
|
||||
MatDialogClose
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class ProjectDialogComponent implements AfterViewInit {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Subscription } from "rxjs";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
@@ -43,7 +43,6 @@ export interface Projects {
|
||||
],
|
||||
templateUrl: './projects.component.html',
|
||||
styleUrl: './projects.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, computed, effect, inject, OnDestroy, signal, WritableSignal, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {Component, computed, effect, inject, OnDestroy, signal, WritableSignal} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
@@ -30,7 +30,6 @@ const SECOND_INTERVAL_FREQUENCY_HZ = 587;
|
||||
MatCheckboxModule,
|
||||
TranslatePipe
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
templateUrl: './stopwatch.component.html'
|
||||
})
|
||||
export class StopwatchComponent implements OnDestroy {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
export interface GridPos { row: number; col: number }
|
||||
@@ -7,7 +7,6 @@ export interface GridPos { row: number; col: number }
|
||||
selector: 'app-generic-grid',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './generic-grid.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './generic-grid.scss',
|
||||
})
|
||||
export class GenericGridComponent implements AfterViewInit {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {AfterViewInit, Component, ElementRef, HostListener, inject, NgZone, OnDestroy, PLATFORM_ID, ViewChild, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {AfterViewInit, Component, ElementRef, HostListener, inject, NgZone, OnDestroy, PLATFORM_ID, ViewChild} from '@angular/core';
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-particles-background',
|
||||
imports: [],
|
||||
templateUrl: './particles-background.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './particles-background.component.scss',
|
||||
})
|
||||
export class ParticleBackgroundComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
|
||||
@@ -26,7 +26,6 @@ export interface SceneEventData {
|
||||
selector: 'app-babylon-canvas',
|
||||
imports: [],
|
||||
templateUrl: './babylon-canvas.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './babylon-canvas.component.scss',
|
||||
})
|
||||
export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
@@ -37,8 +36,8 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
|
||||
@ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@Input({ required: true }) config!: RenderConfig;
|
||||
@Input() renderCallback?: RenderCallback;
|
||||
readonly config = input.required<RenderConfig>();
|
||||
readonly renderCallback = input<RenderCallback>();
|
||||
|
||||
@Output() sceneReady = new EventEmitter<SceneEventData>();
|
||||
@Output() sceneResized = new EventEmitter<SceneEventData>();
|
||||
@@ -131,7 +130,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
private setupCamera(canvas: HTMLCanvasElement) {
|
||||
if (this.config.mode === '3D') {
|
||||
if (this.config().mode === '3D') {
|
||||
this.setup3dCamera(canvas);
|
||||
return;
|
||||
}
|
||||
@@ -144,7 +143,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
cam.mode = Camera.ORTHOGRAPHIC_CAMERA;
|
||||
|
||||
const aspect = canvas.width / canvas.height;
|
||||
const viewSize = this.config?.initialViewSize ?? 10;
|
||||
const viewSize = this.config().initialViewSize ?? 10;
|
||||
cam.orthoLeft = -viewSize * aspect / 2;
|
||||
cam.orthoRight = viewSize * aspect / 2;
|
||||
cam.orthoTop = viewSize / 2;
|
||||
@@ -161,17 +160,18 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
cam.maxZ = 100;
|
||||
cam.lowerRadiusLimit = 1.5;
|
||||
cam.upperRadiusLimit = 20;
|
||||
cam.radius = this.config?.initialViewSize ?? 1;
|
||||
cam.radius = this.config().initialViewSize ?? 1;
|
||||
cam.attachControl(canvas, true);
|
||||
this.camera = cam;
|
||||
}
|
||||
|
||||
private createFullScreenRect() {
|
||||
if (!this.config.vertexShader || !this.config.fragmentShader) {
|
||||
const config = this.config();
|
||||
if (!config.vertexShader || !config.fragmentShader) {
|
||||
return;
|
||||
}
|
||||
const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
|
||||
if (this.config.mode === '3D') {
|
||||
if (config.mode === '3D') {
|
||||
plane.parent = this.camera;
|
||||
plane.position.z = 1;
|
||||
} else {
|
||||
@@ -183,7 +183,8 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
private createShaderMaterial() {
|
||||
if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) {
|
||||
const config = this.config();
|
||||
if (!config.vertexShader || !config.fragmentShader || !config.uniformNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -191,14 +192,14 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
"shaderMaterial",
|
||||
this.scene,
|
||||
{
|
||||
vertexSource: this.config.vertexShader,
|
||||
fragmentSource: this.config.fragmentShader
|
||||
vertexSource: config.vertexShader,
|
||||
fragmentSource: config.fragmentShader
|
||||
},
|
||||
{
|
||||
attributes: ["position", "uv"],
|
||||
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames],
|
||||
uniformBuffers: this.config.uniformBufferNames ?? [],
|
||||
shaderLanguage: this.config.shaderLanguage ?? ShaderLanguage.GLSL
|
||||
uniforms: ["resolution", "cameraPosition", ...config.uniformNames],
|
||||
uniformBuffers: config.uniformBufferNames ?? [],
|
||||
shaderLanguage: config.shaderLanguage ?? ShaderLanguage.GLSL
|
||||
}
|
||||
);
|
||||
this.shaderMaterial.disableDepthWrite = true;
|
||||
@@ -209,8 +210,9 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
this.engine.runRenderLoop(() => {
|
||||
|
||||
// callback call to call specific uniforms
|
||||
if (this.renderCallback) {
|
||||
this.renderCallback(this.shaderMaterial, this.camera, canvas, this.scene);
|
||||
const callback = this.renderCallback();
|
||||
if (callback) {
|
||||
callback(this.shaderMaterial, this.camera, canvas, this.scene);
|
||||
}
|
||||
|
||||
// default uniforms which maybe each scene has
|
||||
@@ -228,8 +230,9 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
this.engine.resize();
|
||||
}
|
||||
|
||||
if (this.config.mode === '2D' && this.camera instanceof ArcRotateCamera) {
|
||||
const viewSize = this.config?.initialViewSize ?? 10;
|
||||
const config = this.config();
|
||||
if (config.mode === '2D' && this.camera instanceof ArcRotateCamera) {
|
||||
const viewSize = config.initialViewSize ?? 10;
|
||||
this.camera.orthoLeft = -viewSize / 2;
|
||||
this.camera.orthoRight = viewSize / 2;
|
||||
this.camera.orthoTop = viewSize / 2;
|
||||
|
||||
Reference in new issue
Block a user