Extract generic grid component and refactor uses
Add a reusable GenericGridComponent (canvas + input handling) and migrate Conway Game of Life and Pathfinding pages to use it. New files: shared/components/generic-grid/{html,scss,ts} implement canvas rendering, resizing, input listeners and a callback API (createNodeFn, getNodeColorFn, applySelectionFn, initializationFn) plus gridChange events. Updated templates to replace raw <canvas> with <app-generic-grid> and switched grid size inputs to ngModelChange bindings. Conway and Pathfinding components: remove direct canvas/mouse handling, wire the generic grid callbacks, keep algorithm-specific logic (node creation, coloring, selection, scenarios and animations) but delegate drawing and interaction to GenericGridComponent. This centralizes grid rendering/interaction and simplifies per-algorithm components.
This commit is contained in:
@@ -33,8 +33,7 @@
|
|||||||
[(ngModel)]="gridRows"
|
[(ngModel)]="gridRows"
|
||||||
[min]="MIN_GRID_SIZE"
|
[min]="MIN_GRID_SIZE"
|
||||||
[max]="MAX_GRID_SIZE"
|
[max]="MAX_GRID_SIZE"
|
||||||
(blur)="applyGridSize()"
|
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||||
(keyup.enter)="applyGridSize()"
|
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="grid-field">
|
||||||
@@ -45,8 +44,7 @@
|
|||||||
[(ngModel)]="gridCols"
|
[(ngModel)]="gridCols"
|
||||||
[min]="MIN_GRID_SIZE"
|
[min]="MIN_GRID_SIZE"
|
||||||
[max]="MAX_GRID_SIZE"
|
[max]="MAX_GRID_SIZE"
|
||||||
(blur)="applyGridSize()"
|
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||||
(keyup.enter)="applyGridSize()"
|
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="grid-field">
|
||||||
@@ -67,6 +65,16 @@
|
|||||||
<span><span class="legend-color empty"></span> {{ 'GOL.DEAD' | translate }}</span>
|
<span><span class="legend-color empty"></span> {{ 'GOL.DEAD' | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<canvas #gridCanvas></canvas>
|
<app-generic-grid
|
||||||
|
[gridRows]="gridRows"
|
||||||
|
[gridCols]="gridCols"
|
||||||
|
[minGridSize]="MIN_GRID_SIZE"
|
||||||
|
[maxGridSize]="MAX_GRID_SIZE"
|
||||||
|
[maxGridPx]="MAX_GRID_PX"
|
||||||
|
[createNodeFn]="createConwayNode"
|
||||||
|
[getNodeColorFn]="getConwayNodeColor"
|
||||||
|
[applySelectionFn]="applyConwaySelection"
|
||||||
|
(gridChange)="grid = $event"
|
||||||
|
></app-generic-grid>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
|
import {AfterViewInit, Component, ViewChild} from '@angular/core';
|
||||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||||
import {TranslatePipe} from "@ngx-translate/core";
|
import {TranslatePipe} from "@ngx-translate/core";
|
||||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||||
@@ -9,8 +9,7 @@ import {MatIcon} from '@angular/material/icon';
|
|||||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_SIZE, MIN_GRID_SIZE, MAX_GRID_PX, Node, LIVE_SPAWN_PROBABILITY, Scenario, MAX_TIME_PER_GENERATION, MIN_TIME_PER_GENERATION, DEFAULT_TIME_PER_GENERATION} from './conway-gol.models';
|
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_SIZE, MIN_GRID_SIZE, MAX_GRID_PX, Node, LIVE_SPAWN_PROBABILITY, Scenario, MAX_TIME_PER_GENERATION, MIN_TIME_PER_GENERATION, DEFAULT_TIME_PER_GENERATION} from './conway-gol.models';
|
||||||
|
import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||||
interface GridPos { row: number; col: number }
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-conway-gol',
|
selector: 'app-conway-gol',
|
||||||
@@ -27,7 +26,8 @@ interface GridPos { row: number; col: number }
|
|||||||
MatInput,
|
MatInput,
|
||||||
MatLabel,
|
MatLabel,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
GenericGridComponent
|
||||||
],
|
],
|
||||||
templateUrl: './conway-gol.html',
|
templateUrl: './conway-gol.html',
|
||||||
})
|
})
|
||||||
@@ -52,45 +52,68 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
protected lifeSpeed = DEFAULT_TIME_PER_GENERATION;
|
protected lifeSpeed = DEFAULT_TIME_PER_GENERATION;
|
||||||
protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||||
protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||||
nodeSize = 10;
|
protected readonly MAX_GRID_PX = MAX_GRID_PX;
|
||||||
|
|
||||||
grid: Node[][] = [];
|
grid: Node[][] = [];
|
||||||
currentScenario: Scenario = 0;
|
currentScenario: Scenario = 0;
|
||||||
|
|
||||||
@ViewChild('gridCanvas', { static: true })
|
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
|
||||||
canvas!: ElementRef<HTMLCanvasElement>;
|
|
||||||
private ctx!: CanvasRenderingContext2D;
|
|
||||||
private lastCell: GridPos | null = null;
|
|
||||||
isDrawing = false;
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.ctx = this.getContextOrThrow();
|
if (this.genericGridComponent) {
|
||||||
this.applyGridSize();
|
this.genericGridComponent.initializationFn = this.initializeConwayGrid;
|
||||||
const el = this.canvas.nativeElement;
|
this.genericGridComponent.createNodeFn = this.createConwayNode;
|
||||||
el.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
this.genericGridComponent.getNodeColorFn = this.getConwayNodeColor;
|
||||||
el.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
this.genericGridComponent.applySelectionFn = this.applyConwaySelection;
|
||||||
el.addEventListener('mouseup', () => this.onMouseUp());
|
this.genericGridComponent.gridRows = this.gridRows;
|
||||||
el.addEventListener('mouseleave', () => this.onMouseUp());
|
this.genericGridComponent.gridCols = this.gridCols;
|
||||||
|
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
|
||||||
el.addEventListener('touchstart', (e) => {
|
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
|
||||||
if(e.cancelable) e.preventDefault();
|
this.genericGridComponent.maxGridPx = this.MAX_GRID_PX;
|
||||||
this.onMouseDown(e as never);
|
this.genericGridComponent.initializeGrid();
|
||||||
}, { passive: false });
|
}
|
||||||
|
|
||||||
el.addEventListener('touchmove', (e) => {
|
|
||||||
if(e.cancelable) e.preventDefault();
|
|
||||||
this.onMouseMove(e as never);
|
|
||||||
}, { passive: false });
|
|
||||||
|
|
||||||
el.addEventListener('touchend', () => {
|
|
||||||
this.onMouseUp();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(scene: Scenario): void {
|
generate(scene: Scenario): void {
|
||||||
this.currentScenario = scene;
|
this.currentScenario = scene;
|
||||||
this.initializeGrid();
|
this.genericGridComponent.initializationFn = this.initializeConwayGrid;
|
||||||
|
this.genericGridComponent.initializeGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applySpeed(): void {
|
||||||
|
this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Callbacks for GenericGridComponent ---
|
||||||
|
createConwayNode = (row: number, col: number): Node => {
|
||||||
|
return {
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
alive: false
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getConwayNodeColor = (node: Node): string => {
|
||||||
|
if (node.alive) {
|
||||||
|
return 'black';
|
||||||
|
}
|
||||||
|
return 'lightgray';
|
||||||
|
};
|
||||||
|
|
||||||
|
applyConwaySelection = (pos: GridPos, grid: Node[][]): void => {
|
||||||
|
this.grid = grid; // Keep internal grid in sync
|
||||||
|
const node = grid[pos.row][pos.col];
|
||||||
|
node.alive = !node.alive; // Toggle alive status
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeConwayGrid = (grid: Node[][]): void => {
|
||||||
|
this.grid = grid;
|
||||||
|
if (this.currentScenario === Scenario.RANDOM) {
|
||||||
|
this.setupRandomLives();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Conway-specific logic (kept local) ---
|
||||||
setupRandomLives(): void {
|
setupRandomLives(): void {
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
@@ -99,189 +122,7 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyGridSize(): void {
|
// --- Other methods ---
|
||||||
this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
|
|
||||||
this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
|
|
||||||
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
|
|
||||||
this.resizeCanvas();
|
|
||||||
|
|
||||||
if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length)
|
|
||||||
{
|
|
||||||
this.drawGrid();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.initializeGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
applySpeed(): void {
|
|
||||||
this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private initializeGrid(): void {
|
|
||||||
this.grid = this.createEmptyGrid();
|
|
||||||
if (this.currentScenario === Scenario.RANDOM) {
|
|
||||||
this.setupRandomLives();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.drawGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
private createEmptyGrid(): Node[][] {
|
|
||||||
const grid: Node[][] = [];
|
|
||||||
|
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
|
||||||
const currentRow: Node[] = [];
|
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
|
||||||
currentRow.push(this.createNode(row, col, false));
|
|
||||||
}
|
|
||||||
grid.push(currentRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
return grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createNode(row: number, col: number, alive: boolean): Node {
|
|
||||||
return {
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
alive
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawGrid(): void {
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
|
||||||
|
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
|
||||||
this.drawNode(this.grid[row][col]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawNode(node: Node): void {
|
|
||||||
this.ctx.fillStyle = this.getNodeColor(node);
|
|
||||||
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
|
||||||
|
|
||||||
this.ctx.strokeStyle = '#ccc';
|
|
||||||
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNodeColor(node: Node): string {
|
|
||||||
if (node.alive)
|
|
||||||
{
|
|
||||||
return 'black';
|
|
||||||
}
|
|
||||||
return 'lightgray';
|
|
||||||
}
|
|
||||||
|
|
||||||
private getContextOrThrow(): CanvasRenderingContext2D {
|
|
||||||
const ctx = this.canvas.nativeElement.getContext('2d');
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('CanvasRenderingContext2D not available.');
|
|
||||||
}
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
private clampGridSize(value: number, fallback: number): number {
|
|
||||||
const parsed = Math.floor(Number(value));
|
|
||||||
const safe = Number.isFinite(parsed) ? parsed : fallback;
|
|
||||||
return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeNodeSize(rows: number, cols: number): number {
|
|
||||||
const sizeByWidth = Math.floor(MAX_GRID_PX / cols);
|
|
||||||
const sizeByHeight = Math.floor(MAX_GRID_PX / rows);
|
|
||||||
return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
|
|
||||||
}
|
|
||||||
|
|
||||||
private resizeCanvas(): void {
|
|
||||||
const el = this.canvas.nativeElement;
|
|
||||||
el.width = this.gridCols * this.nodeSize;
|
|
||||||
el.height = this.gridRows * this.nodeSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
//mouse listener
|
|
||||||
private onMouseDown(event: MouseEvent): void {
|
|
||||||
const pos = this.getGridPosition(event);
|
|
||||||
if (!pos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isDrawing = true;
|
|
||||||
this.lastCell = null;
|
|
||||||
this.applySelectionAt(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseMove(event: MouseEvent): void {
|
|
||||||
if (!this.isDrawing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = this.getGridPosition(event);
|
|
||||||
if (!pos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isSameCell(pos, this.lastCell)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.applySelectionAt(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseUp(): void {
|
|
||||||
this.isDrawing = false;
|
|
||||||
this.lastCell = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mouse -> grid cell
|
|
||||||
private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
|
|
||||||
const canvas = this.canvas.nativeElement;
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
let clientX, clientY;
|
|
||||||
if (event instanceof MouseEvent) {
|
|
||||||
clientX = event.clientX;
|
|
||||||
clientY = event.clientY;
|
|
||||||
} else if (event instanceof TouchEvent && event.touches.length > 0) {
|
|
||||||
clientX = event.touches[0].clientX;
|
|
||||||
clientY = event.touches[0].clientY;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scaleX = canvas.width / rect.width;
|
|
||||||
const scaleY = canvas.height / rect.height;
|
|
||||||
|
|
||||||
const x = (clientX - rect.left) * scaleX;
|
|
||||||
const y = (clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
const col = Math.floor(x / this.nodeSize);
|
|
||||||
const row = Math.floor(y / this.nodeSize);
|
|
||||||
|
|
||||||
if (!this.isValidPosition(row, col)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { row, col };
|
|
||||||
}
|
|
||||||
|
|
||||||
private isValidPosition(row: number, col: number): boolean {
|
|
||||||
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSameCell(a: GridPos, b: GridPos | null): boolean {
|
|
||||||
return !!b && a.row === b.row && a.col === b.col;
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySelectionAt(pos: GridPos): void {
|
|
||||||
const node = this.grid[pos.row][pos.col];
|
|
||||||
node.alive = !node.alive;
|
|
||||||
this.lastCell = pos;
|
|
||||||
this.drawNode(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly Scenario = Scenario;
|
protected readonly Scenario = Scenario;
|
||||||
protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION;
|
protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION;
|
||||||
protected readonly MAX_TIME_PER_GENERATION = MAX_TIME_PER_GENERATION;
|
protected readonly MAX_TIME_PER_GENERATION = MAX_TIME_PER_GENERATION;
|
||||||
|
|||||||
@@ -29,29 +29,25 @@
|
|||||||
<div class="grid-size">
|
<div class="grid-size">
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="grid-field">
|
||||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
[min]="MIN_GRID_SIZE"
|
[min]="MIN_GRID_SIZE"
|
||||||
[max]="MAX_GRID_SIZE"
|
[max]="MAX_GRID_SIZE"
|
||||||
[(ngModel)]="gridRows"
|
[(ngModel)]="gridRows"
|
||||||
(blur)="applyGridSize()"
|
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||||
(keyup.enter)="applyGridSize()"
|
/> </mat-form-field>
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="grid-field">
|
||||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
[min]="MIN_GRID_SIZE"
|
[min]="MIN_GRID_SIZE"
|
||||||
[max]="MAX_GRID_SIZE"
|
[max]="MAX_GRID_SIZE"
|
||||||
[(ngModel)]="gridCols"
|
[(ngModel)]="gridCols"
|
||||||
(blur)="applyGridSize()"
|
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||||
(keyup.enter)="applyGridSize()"
|
/> </mat-form-field>
|
||||||
/>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,6 +64,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<canvas #gridCanvas></canvas>
|
<app-generic-grid
|
||||||
|
[gridRows]="gridRows"
|
||||||
|
[gridCols]="gridCols"
|
||||||
|
[minGridSize]="MIN_GRID_SIZE"
|
||||||
|
[maxGridSize]="MAX_GRID_SIZE"
|
||||||
|
[maxGridPx]="MAX_GRID_PX"
|
||||||
|
[createNodeFn]="createPathfindingNode"
|
||||||
|
[getNodeColorFn]="getPathfindingNodeColor"
|
||||||
|
[applySelectionFn]="applyPathfindingSelection"
|
||||||
|
(gridChange)="grid = $event"
|
||||||
|
></app-generic-grid>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
|
import {AfterViewInit, Component, inject, ViewChild} from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {FormsModule} from '@angular/forms';
|
import {FormsModule} from '@angular/forms';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import {UrlConstants} from '../../../constants/UrlConstants';
|
|||||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
import {Information} from '../information/information';
|
import {Information} from '../information/information';
|
||||||
import {AlgorithmInformation} from '../information/information.models';
|
import {AlgorithmInformation} from '../information/information.models';
|
||||||
|
import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||||
|
|
||||||
enum NodeType {
|
enum NodeType {
|
||||||
Start = 'start',
|
Start = 'start',
|
||||||
@@ -23,8 +24,6 @@ enum NodeType {
|
|||||||
None = 'none'
|
None = 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GridPos { row: number; col: number }
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pathfinding',
|
selector: 'app-pathfinding',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -40,7 +39,8 @@ interface GridPos { row: number; col: number }
|
|||||||
MatCardHeader,
|
MatCardHeader,
|
||||||
MatCardTitle,
|
MatCardTitle,
|
||||||
MatCardContent,
|
MatCardContent,
|
||||||
Information
|
Information,
|
||||||
|
GenericGridComponent
|
||||||
],
|
],
|
||||||
templateUrl: './pathfinding.component.html',
|
templateUrl: './pathfinding.component.html',
|
||||||
})
|
})
|
||||||
@@ -51,6 +51,7 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
readonly NodeType = NodeType;
|
readonly NodeType = NodeType;
|
||||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||||
readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||||
|
readonly MAX_GRID_PX = MAX_GRID_PX;
|
||||||
|
|
||||||
algoInformation: AlgorithmInformation = {
|
algoInformation: AlgorithmInformation = {
|
||||||
title: 'PATHFINDING.EXPLANATION.TITLE',
|
title: 'PATHFINDING.EXPLANATION.TITLE',
|
||||||
@@ -71,24 +72,15 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
disclaimerListEntry: []
|
disclaimerListEntry: []
|
||||||
};
|
};
|
||||||
|
|
||||||
@ViewChild('gridCanvas', { static: true })
|
|
||||||
canvas!: ElementRef<HTMLCanvasElement>;
|
|
||||||
|
|
||||||
private ctx!: CanvasRenderingContext2D;
|
|
||||||
|
|
||||||
gridRows = DEFAULT_GRID_ROWS;
|
gridRows = DEFAULT_GRID_ROWS;
|
||||||
gridCols = DEFAULT_GRID_COLS;
|
gridCols = DEFAULT_GRID_COLS;
|
||||||
nodeSize = 10;
|
|
||||||
|
|
||||||
grid: Node[][] = [];
|
grid: Node[][] = [];
|
||||||
startNode: Node | null = null;
|
startNode: Node | null = null;
|
||||||
endNode: Node | null = null;
|
endNode: Node | null = null;
|
||||||
|
|
||||||
selectedNodeType: NodeType = NodeType.None;
|
selectedNodeType: NodeType = NodeType.None;
|
||||||
|
private shouldAddWall = true; // Moved here
|
||||||
isDrawing = false;
|
|
||||||
private lastCell: GridPos | null = null;
|
|
||||||
private shouldAddWall = true;
|
|
||||||
|
|
||||||
animationSpeed = 3;
|
animationSpeed = 3;
|
||||||
pathLength = "0";
|
pathLength = "0";
|
||||||
@@ -96,58 +88,79 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
|
|
||||||
private timeoutIds: number[] = [];
|
private timeoutIds: number[] = [];
|
||||||
|
|
||||||
|
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.ctx = this.getContextOrThrow();
|
// Canvas logic is now handled by GenericGridComponent
|
||||||
this.applyGridSize(true);
|
// Ensure genericGridComponent is initialized
|
||||||
|
if (this.genericGridComponent) {
|
||||||
const el = this.canvas.nativeElement;
|
this.genericGridComponent.initializationFn = this.initializePathfindingGrid;
|
||||||
el.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
this.genericGridComponent.createNodeFn = this.createPathfindingNode;
|
||||||
el.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
this.genericGridComponent.getNodeColorFn = this.getPathfindingNodeColor;
|
||||||
el.addEventListener('mouseup', () => this.onMouseUp());
|
this.genericGridComponent.applySelectionFn = this.applyPathfindingSelection;
|
||||||
el.addEventListener('mouseleave', () => this.onMouseUp());
|
this.genericGridComponent.gridRows = this.gridRows;
|
||||||
|
this.genericGridComponent.gridCols = this.gridCols;
|
||||||
el.addEventListener('touchstart', (e) => {
|
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
|
||||||
if(e.cancelable) e.preventDefault();
|
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
|
||||||
this.onMouseDown(e as never);
|
this.genericGridComponent.maxGridPx = MAX_GRID_PX;
|
||||||
}, { passive: false });
|
this.genericGridComponent.applyGridSize(); // Trigger initial grid setup
|
||||||
|
}
|
||||||
el.addEventListener('touchmove', (e) => {
|
this.createCase({withWalls: true, scenario: "normal"});
|
||||||
if(e.cancelable) e.preventDefault();
|
|
||||||
this.onMouseMove(e as never);
|
|
||||||
}, { passive: false });
|
|
||||||
|
|
||||||
el.addEventListener('touchend', () => {
|
|
||||||
this.onMouseUp();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyGridSize(skipReset?: boolean): void {
|
// --- Callbacks for GenericGridComponent ---
|
||||||
this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
|
createPathfindingNode = (row: number, col: number): Node => {
|
||||||
this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
|
return {
|
||||||
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
|
row,
|
||||||
this.resizeCanvas();
|
col,
|
||||||
|
isStart: false,
|
||||||
|
isEnd: false,
|
||||||
|
isWall: false,
|
||||||
|
isVisited: false,
|
||||||
|
isPath: false,
|
||||||
|
distance: Infinity,
|
||||||
|
previousNode: null,
|
||||||
|
hScore: 0,
|
||||||
|
fScore: Infinity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length)
|
getPathfindingNodeColor = (node: Node): string => {
|
||||||
{
|
if (node.isStart) return 'green';
|
||||||
this.drawGrid();
|
if (node.isEnd) return 'red';
|
||||||
return;
|
if (node.isPath) return 'gold';
|
||||||
|
if (node.isVisited) return 'skyblue';
|
||||||
|
if (node.isWall) return 'black';
|
||||||
|
return 'lightgray';
|
||||||
|
};
|
||||||
|
|
||||||
|
applyPathfindingSelection = (pos: GridPos, grid: Node[][]): void => {
|
||||||
|
this.grid = grid; // Keep internal grid in sync
|
||||||
|
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) {
|
||||||
|
this.shouldAddWall = !node.isWall;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipReset) {
|
switch (this.selectedNodeType) {
|
||||||
this.initializeGrid({withWalls: true, scenario: 'normal'});
|
case NodeType.Start:
|
||||||
this.drawGrid();
|
this.trySetStart(node);
|
||||||
return;
|
break;
|
||||||
|
|
||||||
|
case NodeType.End:
|
||||||
|
this.trySetEnd(node);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NodeType.Wall:
|
||||||
|
this.tryToggleWall(node, this.shouldAddWall);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NodeType.None:
|
||||||
|
this.tryClearNode(node);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
this.createCase({withWalls: true, scenario: 'normal'});
|
|
||||||
}
|
|
||||||
|
|
||||||
createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void
|
|
||||||
{
|
|
||||||
this.stopAnimations();
|
|
||||||
this.initializeGrid({withWalls, scenario});
|
|
||||||
this.drawGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
visualize(algorithm: string): void {
|
visualize(algorithm: string): void {
|
||||||
if (!this.ensureStartAndEnd()) {
|
if (!this.ensureStartAndEnd()) {
|
||||||
@@ -166,13 +179,13 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
this.grid[this.startNode!.row][this.startNode!.col],
|
this.grid[this.startNode!.row][this.startNode!.col],
|
||||||
this.grid[this.endNode!.row][this.endNode!.col]
|
this.grid[this.endNode!.row][this.endNode!.col]
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'astar': result = this.pathfindingService.aStar(
|
case 'astar': result = this.pathfindingService.aStar(
|
||||||
this.grid,
|
this.grid,
|
||||||
this.grid[this.startNode!.row][this.startNode!.col],
|
this.grid[this.startNode!.row][this.startNode!.col],
|
||||||
this.grid[this.endNode!.row][this.endNode!.col]
|
this.grid[this.endNode!.row][this.endNode!.col]
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
@@ -195,320 +208,19 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse interactions
|
initializePathfindingGrid = (grid: Node[][]): void => {
|
||||||
private onMouseDown(event: MouseEvent): void {
|
this.grid = grid; // Update the component's grid reference
|
||||||
this.stopAnimations();
|
const {start, end} = this.getScenarioStartEnd('normal'); // Default scenario
|
||||||
this.clearPath();
|
|
||||||
const pos = this.getGridPosition(event);
|
|
||||||
if (!pos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shouldAddWall = this.shouldStartWallStroke(pos);
|
|
||||||
|
|
||||||
this.isDrawing = true;
|
|
||||||
this.lastCell = null;
|
|
||||||
this.applySelectionAt(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseMove(event: MouseEvent): void {
|
|
||||||
if (!this.isDrawing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = this.getGridPosition(event);
|
|
||||||
if (!pos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isSameCell(pos, this.lastCell)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.applySelectionAt(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseUp(): void {
|
|
||||||
this.isDrawing = false;
|
|
||||||
this.lastCell = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySelectionAt(pos: GridPos): void {
|
|
||||||
const node = this.grid[pos.row][pos.col];
|
|
||||||
|
|
||||||
switch (this.selectedNodeType) {
|
|
||||||
case NodeType.Start:
|
|
||||||
this.trySetStart(node);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NodeType.End:
|
|
||||||
this.trySetEnd(node);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NodeType.Wall:
|
|
||||||
this.tryToggleWall(node, this.shouldAddWall);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NodeType.None:
|
|
||||||
this.tryClearNode(node);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastCell = pos;
|
|
||||||
this.drawNode(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grid init
|
|
||||||
private initializeGrid({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void {
|
|
||||||
this.grid = this.createEmptyGrid();
|
|
||||||
|
|
||||||
const { start, end } = this.getScenarioStartEnd(scenario);
|
|
||||||
this.startNode = this.grid[start.row][start.col];
|
this.startNode = this.grid[start.row][start.col];
|
||||||
this.endNode = this.grid[end.row][end.col];
|
this.endNode = this.grid[end.row][end.col];
|
||||||
|
|
||||||
this.startNode.isStart = true;
|
this.startNode.isStart = true;
|
||||||
this.endNode.isEnd = true;
|
this.endNode.isEnd = true;
|
||||||
|
|
||||||
if (withWalls) {
|
this.placeDefaultDiagonalWall('normal');
|
||||||
this.placeDefaultDiagonalWall(scenario);
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createEmptyGrid(): Node[][] {
|
// --- Helper methods for node manipulation (kept local) ---
|
||||||
const grid: Node[][] = [];
|
|
||||||
|
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
|
||||||
const currentRow: Node[] = [];
|
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
|
||||||
currentRow.push(this.createNode(row, col));
|
|
||||||
}
|
|
||||||
grid.push(currentRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
return grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createNode(row: number, col: number): Node {
|
|
||||||
return {
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
isStart: false,
|
|
||||||
isEnd: false,
|
|
||||||
isWall: false,
|
|
||||||
isVisited: false,
|
|
||||||
isPath: false,
|
|
||||||
distance: Infinity,
|
|
||||||
previousNode: null,
|
|
||||||
hScore: 0,
|
|
||||||
fScore: Infinity,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } {
|
|
||||||
if (scenario === 'edge') {
|
|
||||||
return {
|
|
||||||
start: { row: 0, col: 0 },
|
|
||||||
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);
|
|
||||||
return {
|
|
||||||
start: { row: midRow, col: 0 },
|
|
||||||
end: { row: midRow, col: this.gridCols - 1 }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createRandomStartEndPosition() {
|
|
||||||
const midCol = Math.floor(this.gridCols / 2);
|
|
||||||
|
|
||||||
const startRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
|
||||||
const startCol: number = this.randomIntFromInterval(0, this.gridCols - 1);
|
|
||||||
|
|
||||||
const endRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
|
||||||
let endCol: number;
|
|
||||||
|
|
||||||
if (startCol <= midCol) {
|
|
||||||
endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1);
|
|
||||||
} else {
|
|
||||||
endCol = this.randomIntFromInterval(0, midCol);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start: {row: startRow, col: startCol},
|
|
||||||
end: {row: endRow, col: endCol}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private placeDefaultDiagonalWall(scenario: 'normal' | 'edge' | 'random'): void {
|
|
||||||
if (scenario === 'edge') {
|
|
||||||
this.createDiagonalWall();
|
|
||||||
}
|
|
||||||
else if (scenario === 'normal') {
|
|
||||||
this.createVerticalWall();
|
|
||||||
}
|
|
||||||
else if (scenario === 'random') {
|
|
||||||
this.createRandomWalls();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createRandomWalls(){
|
|
||||||
const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows);
|
|
||||||
|
|
||||||
for (let wall = 0; wall < maxNumberOfWalls; wall++) {
|
|
||||||
|
|
||||||
const row: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
|
||||||
const col: number = this.randomIntFromInterval(0, this.gridCols - 1);
|
|
||||||
|
|
||||||
if (!this.isValidPosition(row, col)) {
|
|
||||||
wall--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = this.grid[row][col];
|
|
||||||
if (node.isStart || node.isEnd) {
|
|
||||||
wall--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
node.isWall = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private createVerticalWall() {
|
|
||||||
const height = this.gridRows;
|
|
||||||
const startCol = Math.floor(this.gridCols / 2);
|
|
||||||
|
|
||||||
for (let i = 5; i < (height - 5); i++) {
|
|
||||||
const row = i;
|
|
||||||
|
|
||||||
if (!this.isValidPosition(row, startCol)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = this.grid[row][startCol];
|
|
||||||
if (node.isStart || node.isEnd) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
node.isWall = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private createDiagonalWall() {
|
|
||||||
// Diagonal-ish wall; avoids start/end
|
|
||||||
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;
|
|
||||||
const col = startCol + i;
|
|
||||||
|
|
||||||
if (!this.isValidPosition(row, col)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = this.grid[row][col];
|
|
||||||
if (node.isStart || node.isEnd) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
node.isWall = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path state
|
|
||||||
private clearPath(): void {
|
|
||||||
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;
|
|
||||||
node.distance = Infinity;
|
|
||||||
node.previousNode = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.drawGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation
|
|
||||||
private stopAnimations(): void {
|
|
||||||
for (const id of this.timeoutIds) {
|
|
||||||
clearTimeout(id);
|
|
||||||
}
|
|
||||||
this.timeoutIds = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private animateAlgorithm(visited: Node[], path: Node[]): void {
|
|
||||||
for (let i = 0; i <= visited.length; i++) {
|
|
||||||
if (i === visited.length) {
|
|
||||||
const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
|
|
||||||
this.timeoutIds.push(id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = visited[i];
|
|
||||||
const id = globalThis.setTimeout(() => {
|
|
||||||
if (!node.isStart && !node.isEnd) {
|
|
||||||
node.isVisited = true;
|
|
||||||
this.drawNode(node);
|
|
||||||
}
|
|
||||||
}, this.animationSpeed * i);
|
|
||||||
|
|
||||||
this.timeoutIds.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private animateShortestPath(path: Node[]): void {
|
|
||||||
for (let i = 0; i < path.length; i++) {
|
|
||||||
const node = path[i];
|
|
||||||
const id = globalThis.setTimeout(() => {
|
|
||||||
if (!node.isStart && !node.isEnd) {
|
|
||||||
node.isPath = true;
|
|
||||||
this.drawNode(node);
|
|
||||||
}
|
|
||||||
}, this.animationSpeed * i);
|
|
||||||
|
|
||||||
this.timeoutIds.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drawing
|
|
||||||
private drawGrid(): void {
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
|
||||||
|
|
||||||
for (let row = 0; row < this.gridRows; row++) {
|
|
||||||
for (let col = 0; col < this.gridCols; col++) {
|
|
||||||
this.drawNode(this.grid[row][col]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawNode(node: Node): void {
|
|
||||||
this.ctx.fillStyle = this.getNodeColor(node);
|
|
||||||
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
|
||||||
|
|
||||||
this.ctx.strokeStyle = '#ccc';
|
|
||||||
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNodeColor(node: Node): string {
|
|
||||||
if (node.isStart) return 'green';
|
|
||||||
if (node.isEnd) return 'red';
|
|
||||||
if (node.isPath) return 'gold';
|
|
||||||
if (node.isVisited) return 'skyblue';
|
|
||||||
if (node.isWall) return 'black';
|
|
||||||
return 'lightgray';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placement rules (readability helpers)
|
|
||||||
private trySetStart(node: Node): void {
|
private trySetStart(node: Node): void {
|
||||||
if (!this.canBeStart(node)) {
|
if (!this.canBeStart(node)) {
|
||||||
return;
|
return;
|
||||||
@@ -516,7 +228,7 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
|
|
||||||
if (this.startNode) {
|
if (this.startNode) {
|
||||||
this.startNode.isStart = false;
|
this.startNode.isStart = false;
|
||||||
this.drawNode(this.startNode);
|
this.genericGridComponent.drawNode(this.startNode); // Redraw old start node
|
||||||
}
|
}
|
||||||
|
|
||||||
node.isStart = true;
|
node.isStart = true;
|
||||||
@@ -530,7 +242,7 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
|
|
||||||
if (this.endNode) {
|
if (this.endNode) {
|
||||||
this.endNode.isEnd = false;
|
this.endNode.isEnd = false;
|
||||||
this.drawNode(this.endNode);
|
this.genericGridComponent.drawNode(this.endNode); // Redraw old end node
|
||||||
}
|
}
|
||||||
|
|
||||||
node.isEnd = true;
|
node.isEnd = true;
|
||||||
@@ -574,16 +286,197 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
return !node.isStart && !node.isEnd;
|
return !node.isStart && !node.isEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldStartWallStroke(pos: GridPos): boolean {
|
// --- Grid manipulation for scenarios (kept local) ---
|
||||||
if (this.selectedNodeType !== NodeType.Wall) {
|
createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void {
|
||||||
return true;
|
this.stopAnimations();
|
||||||
}
|
// Reinitialize grid through the generic component
|
||||||
|
this.genericGridComponent.initializationFn = (grid) => {
|
||||||
|
this.grid = grid;
|
||||||
|
const {start, end} = this.getScenarioStartEnd(scenario);
|
||||||
|
this.startNode = this.grid[start.row][start.col];
|
||||||
|
this.endNode = this.grid[end.row][end.col];
|
||||||
|
this.startNode.isStart = true;
|
||||||
|
this.endNode.isEnd = true;
|
||||||
|
|
||||||
const node = this.grid[pos.row][pos.col];
|
if (withWalls) {
|
||||||
return !node.isWall;
|
this.placeDefaultDiagonalWall(scenario);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.genericGridComponent.initializeGrid(); // Trigger re-initialization and redraw
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation
|
private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } {
|
||||||
|
if (scenario === 'edge') {
|
||||||
|
return {
|
||||||
|
start: {row: 0, col: 0},
|
||||||
|
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);
|
||||||
|
return {
|
||||||
|
start: {row: midRow, col: 0},
|
||||||
|
end: {row: midRow, col: this.gridCols - 1}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRandomStartEndPosition(): { start: GridPos; end: GridPos } {
|
||||||
|
const midCol = Math.floor(this.gridCols / 2);
|
||||||
|
|
||||||
|
const startRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
||||||
|
const startCol: number = this.randomIntFromInterval(0, this.gridCols - 1);
|
||||||
|
|
||||||
|
const endRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
||||||
|
let endCol: number;
|
||||||
|
|
||||||
|
if (startCol <= midCol) {
|
||||||
|
endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1);
|
||||||
|
} else {
|
||||||
|
endCol = this.randomIntFromInterval(0, midCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: {row: startRow, col: startCol},
|
||||||
|
end: {row: endRow, col: endCol}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private placeDefaultDiagonalWall(scenario: 'normal' | 'edge' | 'random'): void {
|
||||||
|
if (scenario === 'edge') {
|
||||||
|
this.createDiagonalWall();
|
||||||
|
} else if (scenario === 'normal') {
|
||||||
|
this.createVerticalWall();
|
||||||
|
} else if (scenario === 'random') {
|
||||||
|
this.createRandomWalls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRandomWalls() {
|
||||||
|
const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows);
|
||||||
|
|
||||||
|
for (let wall = 0; wall < maxNumberOfWalls; wall++) {
|
||||||
|
|
||||||
|
const row: number = this.randomIntFromInterval(0, this.gridRows - 1);
|
||||||
|
const col: number = this.randomIntFromInterval(0, this.gridCols - 1);
|
||||||
|
|
||||||
|
if (!this.grid[row][col]) { // Use the grid passed from GenericGrid
|
||||||
|
wall--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = this.grid[row][col];
|
||||||
|
if (node.isStart || node.isEnd) {
|
||||||
|
wall--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.isWall = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private createVerticalWall() {
|
||||||
|
const height = this.gridRows;
|
||||||
|
const startCol = Math.floor(this.gridCols / 2);
|
||||||
|
|
||||||
|
for (let i = 5; i < (height - 5); i++) {
|
||||||
|
const row = i;
|
||||||
|
|
||||||
|
if (!this.grid[row]?.[startCol]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = this.grid[row][startCol];
|
||||||
|
if (node.isStart || node.isEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.isWall = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private createDiagonalWall() {
|
||||||
|
// Diagonal-ish wall; avoids start/end
|
||||||
|
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;
|
||||||
|
const col = startCol + i;
|
||||||
|
|
||||||
|
if (!this.grid[row]?.[col]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = this.grid[row][col];
|
||||||
|
if (node.isStart || node.isEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.isWall = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Animation (adapted to use genericGridComponent for redraw) ---
|
||||||
|
private stopAnimations(): void {
|
||||||
|
for (const id of this.timeoutIds) {
|
||||||
|
clearTimeout(id);
|
||||||
|
}
|
||||||
|
this.timeoutIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearPath(): void {
|
||||||
|
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;
|
||||||
|
node.distance = Infinity;
|
||||||
|
node.previousNode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component
|
||||||
|
}
|
||||||
|
|
||||||
|
private animateAlgorithm(visited: Node[], path: Node[]): void {
|
||||||
|
for (let i = 0; i <= visited.length; i++) {
|
||||||
|
if (i === visited.length) {
|
||||||
|
const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
|
||||||
|
this.timeoutIds.push(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = visited[i];
|
||||||
|
const id = globalThis.setTimeout(() => {
|
||||||
|
if (!node.isStart && !node.isEnd) {
|
||||||
|
node.isVisited = true;
|
||||||
|
this.genericGridComponent?.drawNode(node); // Redraw single node
|
||||||
|
}
|
||||||
|
}, this.animationSpeed * i);
|
||||||
|
|
||||||
|
this.timeoutIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private animateShortestPath(path: Node[]): void {
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
const node = path[i];
|
||||||
|
const id = globalThis.setTimeout(() => {
|
||||||
|
if (!node.isStart && !node.isEnd) {
|
||||||
|
node.isPath = true;
|
||||||
|
this.genericGridComponent?.drawNode(node); // Redraw single node
|
||||||
|
}
|
||||||
|
}, this.animationSpeed * i);
|
||||||
|
|
||||||
|
this.timeoutIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validation ---
|
||||||
private ensureStartAndEnd(): boolean {
|
private ensureStartAndEnd(): boolean {
|
||||||
if (this.startNode && this.endNode) {
|
if (this.startNode && this.endNode) {
|
||||||
return true;
|
return true;
|
||||||
@@ -593,73 +486,7 @@ export class PathfindingComponent implements AfterViewInit {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid sizing
|
// --- Utility ---
|
||||||
private clampGridSize(value: number, fallback: number): number {
|
|
||||||
const parsed = Math.floor(Number(value));
|
|
||||||
const safe = Number.isFinite(parsed) ? parsed : fallback;
|
|
||||||
return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeNodeSize(rows: number, cols: number): number {
|
|
||||||
const sizeByWidth = Math.floor(MAX_GRID_PX / cols);
|
|
||||||
const sizeByHeight = Math.floor(MAX_GRID_PX / rows);
|
|
||||||
return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
|
|
||||||
}
|
|
||||||
|
|
||||||
private resizeCanvas(): void {
|
|
||||||
const el = this.canvas.nativeElement;
|
|
||||||
el.width = this.gridCols * this.nodeSize;
|
|
||||||
el.height = this.gridRows * this.nodeSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mouse -> grid cell
|
|
||||||
private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
|
|
||||||
const canvas = this.canvas.nativeElement;
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
let clientX, clientY;
|
|
||||||
if (event instanceof MouseEvent) {
|
|
||||||
clientX = event.clientX;
|
|
||||||
clientY = event.clientY;
|
|
||||||
} else if (event instanceof TouchEvent && event.touches.length > 0) {
|
|
||||||
clientX = event.touches[0].clientX;
|
|
||||||
clientY = event.touches[0].clientY;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scaleX = canvas.width / rect.width;
|
|
||||||
const scaleY = canvas.height / rect.height;
|
|
||||||
|
|
||||||
const x = (clientX - rect.left) * scaleX;
|
|
||||||
const y = (clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
const col = Math.floor(x / this.nodeSize);
|
|
||||||
const row = Math.floor(y / this.nodeSize);
|
|
||||||
|
|
||||||
if (!this.isValidPosition(row, col)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { row, col };
|
|
||||||
}
|
|
||||||
|
|
||||||
private isValidPosition(row: number, col: number): boolean {
|
|
||||||
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSameCell(a: GridPos, b: GridPos | null): boolean {
|
|
||||||
return !!b && a.row === b.row && a.col === b.col;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getContextOrThrow(): CanvasRenderingContext2D {
|
|
||||||
const ctx = this.canvas.nativeElement.getContext('2d');
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('CanvasRenderingContext2D not available.');
|
|
||||||
}
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
private randomIntFromInterval(min: number, max: number): number {
|
private randomIntFromInterval(min: number, max: number): number {
|
||||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/app/shared/components/generic-grid/generic-grid.html
Normal file
1
src/app/shared/components/generic-grid/generic-grid.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<canvas #gridCanvas></canvas>
|
||||||
213
src/app/shared/components/generic-grid/generic-grid.ts
Normal file
213
src/app/shared/components/generic-grid/generic-grid.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
|
||||||
|
export interface GridPos { row: number; col: number }
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-generic-grid',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './generic-grid.html',
|
||||||
|
styleUrl: './generic-grid.scss',
|
||||||
|
})
|
||||||
|
export class GenericGridComponent implements AfterViewInit {
|
||||||
|
@ViewChild('gridCanvas', { static: true })
|
||||||
|
canvas!: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
@Input() gridRows: number = 10;
|
||||||
|
@Input() gridCols: number = 10;
|
||||||
|
@Input() nodeSize: number = 10; // Default node size, can be overridden by computeNodeSize
|
||||||
|
@Input() maxGridPx: number = 500; // Max pixels for grid dimension
|
||||||
|
@Input() minGridSize: number = 5;
|
||||||
|
@Input() maxGridSize: number = 50;
|
||||||
|
@Input() drawNodeBorderColor: string = '#ccc';
|
||||||
|
|
||||||
|
// Callbacks from parent component
|
||||||
|
@Input() createNodeFn!: (row: number, col: number) => any;
|
||||||
|
@Input() getNodeColorFn!: (node: any) => string;
|
||||||
|
@Input() applySelectionFn!: (pos: GridPos, grid: any[][]) => void;
|
||||||
|
@Input() initializationFn!: (grid: any[][]) => void;
|
||||||
|
|
||||||
|
@Output() gridChange = new EventEmitter<any[][]>();
|
||||||
|
@Output() nodeClick = new EventEmitter<GridPos>();
|
||||||
|
|
||||||
|
private ctx!: CanvasRenderingContext2D;
|
||||||
|
grid: any[][] = [];
|
||||||
|
|
||||||
|
isDrawing = false;
|
||||||
|
private lastCell: GridPos | null = null;
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.ctx = this.getContextOrThrow();
|
||||||
|
this.setupCanvasListeners();
|
||||||
|
this.applyGridSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCanvasListeners(): void {
|
||||||
|
const el = this.canvas.nativeElement;
|
||||||
|
el.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
||||||
|
el.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||||
|
el.addEventListener('mouseup', () => this.onMouseUp());
|
||||||
|
el.addEventListener('mouseleave', () => this.onMouseUp());
|
||||||
|
|
||||||
|
el.addEventListener('touchstart', (e) => {
|
||||||
|
if (e.cancelable) e.preventDefault();
|
||||||
|
this.onMouseDown(e as never);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
el.addEventListener('touchmove', (e) => {
|
||||||
|
if (e.cancelable) e.preventDefault();
|
||||||
|
this.onMouseMove(e as never);
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
el.addEventListener('touchend', () => {
|
||||||
|
this.onMouseUp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyGridSize(): void {
|
||||||
|
this.gridRows = this.clampGridSize(this.gridRows);
|
||||||
|
this.gridCols = this.clampGridSize(this.gridCols);
|
||||||
|
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
|
||||||
|
this.resizeCanvas();
|
||||||
|
if (this.gridRows === this.grid.length && this.gridCols === this.grid[0]?.length) {
|
||||||
|
this.drawGrid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.initializeGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeGrid(): void {
|
||||||
|
this.grid = this.createEmptyGrid();
|
||||||
|
if (this.initializationFn) {
|
||||||
|
this.initializationFn(this.grid);
|
||||||
|
}
|
||||||
|
this.drawGrid();
|
||||||
|
this.gridChange.emit(this.grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmptyGrid(): any[][] {
|
||||||
|
const grid: any[][] = [];
|
||||||
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
|
const currentRow: any[] = [];
|
||||||
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
|
currentRow.push(this.createNodeFn(row, col));
|
||||||
|
}
|
||||||
|
grid.push(currentRow);
|
||||||
|
}
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrid(): void {
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
||||||
|
for (let row = 0; row < this.gridRows; row++) {
|
||||||
|
for (let col = 0; col < this.gridCols; col++) {
|
||||||
|
this.drawNode(this.grid[row][col]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawNode(node: any): void {
|
||||||
|
this.ctx.fillStyle = this.getNodeColorFn(node);
|
||||||
|
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||||
|
this.ctx.strokeStyle = this.drawNodeBorderColor;
|
||||||
|
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContextOrThrow(): CanvasRenderingContext2D {
|
||||||
|
const ctx = this.canvas.nativeElement.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('CanvasRenderingContext2D not available.');
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clampGridSize(value: number): number {
|
||||||
|
const parsed = Math.floor(Number(value));
|
||||||
|
const safe = Number.isFinite(parsed) ? parsed : this.minGridSize; // Use minGridSize as fallback
|
||||||
|
return Math.min(Math.max(this.minGridSize, safe), this.maxGridSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeNodeSize(rows: number, cols: number): number {
|
||||||
|
const sizeByWidth = Math.floor(this.maxGridPx / cols);
|
||||||
|
const sizeByHeight = Math.floor(this.maxGridPx / rows);
|
||||||
|
return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeCanvas(): void {
|
||||||
|
const el = this.canvas.nativeElement;
|
||||||
|
el.width = this.gridCols * this.nodeSize;
|
||||||
|
el.height = this.gridRows * this.nodeSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(event: MouseEvent | TouchEvent): void {
|
||||||
|
this.isDrawing = true;
|
||||||
|
this.lastCell = null;
|
||||||
|
const pos = this.getGridPosition(event);
|
||||||
|
if (pos) {
|
||||||
|
this.handleInteraction(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event: MouseEvent | TouchEvent): void {
|
||||||
|
if (!this.isDrawing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pos = this.getGridPosition(event);
|
||||||
|
if (pos && !this.isSameCell(pos, this.lastCell)) {
|
||||||
|
this.handleInteraction(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp(): void {
|
||||||
|
this.isDrawing = false;
|
||||||
|
this.lastCell = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInteraction(pos: GridPos): void {
|
||||||
|
this.applySelectionFn(pos, this.grid);
|
||||||
|
this.drawNode(this.grid[pos.row][pos.col]);
|
||||||
|
this.lastCell = pos;
|
||||||
|
this.nodeClick.emit(pos);
|
||||||
|
this.gridChange.emit(this.grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
|
||||||
|
const canvas = this.canvas.nativeElement;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
let clientX, clientY;
|
||||||
|
if (event instanceof MouseEvent) {
|
||||||
|
clientX = event.clientX;
|
||||||
|
clientY = event.clientY;
|
||||||
|
} else if (event instanceof TouchEvent && event.touches.length > 0) {
|
||||||
|
clientX = event.touches[0].clientX;
|
||||||
|
clientY = event.touches[0].clientY;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
|
||||||
|
const x = (clientX - rect.left) * scaleX;
|
||||||
|
const y = (clientY - rect.top) * scaleY;
|
||||||
|
|
||||||
|
const col = Math.floor(x / this.nodeSize);
|
||||||
|
const row = Math.floor(y / this.nodeSize);
|
||||||
|
|
||||||
|
if (!this.isValidPosition(row, col)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { row, col };
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidPosition(row: number, col: number): boolean {
|
||||||
|
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSameCell(a: GridPos, b: GridPos | null): boolean {
|
||||||
|
return !!b && a.row === b.row && a.col === b.col;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user