diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.html index c94f1cf..185995a 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.html +++ b/src/app/pages/algorithms/conway-gol/conway-gol.html @@ -33,8 +33,7 @@ [(ngModel)]="gridRows" [min]="MIN_GRID_SIZE" [max]="MAX_GRID_SIZE" - (blur)="applyGridSize()" - (keyup.enter)="applyGridSize()" + (ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()" /> @@ -45,8 +44,7 @@ [(ngModel)]="gridCols" [min]="MIN_GRID_SIZE" [max]="MAX_GRID_SIZE" - (blur)="applyGridSize()" - (keyup.enter)="applyGridSize()" + (ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()" /> @@ -67,6 +65,16 @@ {{ 'GOL.DEAD' | translate }} - + diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts index 4f8d05b..5d6d524 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -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 {TranslatePipe} from "@ngx-translate/core"; import {UrlConstants} from '../../../constants/UrlConstants'; @@ -9,8 +9,7 @@ import {MatIcon} from '@angular/material/icon'; import {MatFormField, MatInput, MatLabel} from '@angular/material/input'; 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'; - -interface GridPos { row: number; col: number } +import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid'; @Component({ selector: 'app-conway-gol', @@ -27,7 +26,8 @@ interface GridPos { row: number; col: number } MatInput, MatLabel, ReactiveFormsModule, - FormsModule + FormsModule, + GenericGridComponent ], templateUrl: './conway-gol.html', }) @@ -52,45 +52,68 @@ export class ConwayGol implements AfterViewInit { protected lifeSpeed = DEFAULT_TIME_PER_GENERATION; protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE; protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE; - nodeSize = 10; + protected readonly MAX_GRID_PX = MAX_GRID_PX; + grid: Node[][] = []; currentScenario: Scenario = 0; - @ViewChild('gridCanvas', { static: true }) - canvas!: ElementRef; - private ctx!: CanvasRenderingContext2D; - private lastCell: GridPos | null = null; - isDrawing = false; + @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent; ngAfterViewInit(): void { - this.ctx = this.getContextOrThrow(); - this.applyGridSize(); - 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(); - }); + if (this.genericGridComponent) { + this.genericGridComponent.initializationFn = this.initializeConwayGrid; + 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.minGridSize = this.MIN_GRID_SIZE; + this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE; + this.genericGridComponent.maxGridPx = this.MAX_GRID_PX; + this.genericGridComponent.initializeGrid(); + } } generate(scene: Scenario): void { 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 { for (let row = 0; row < this.gridRows; row++) { for (let col = 0; col < this.gridCols; col++) { @@ -99,189 +122,7 @@ export class ConwayGol implements AfterViewInit { } } - applyGridSize(): void { - 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); - } - + // --- Other methods --- protected readonly Scenario = Scenario; protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION; protected readonly MAX_TIME_PER_GENERATION = MAX_TIME_PER_GENERATION; diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index ed364a0..36d9ccd 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -29,29 +29,25 @@
{{ 'ALGORITHM.GRID_HEIGHT' | translate }} - - + {{ 'ALGORITHM.GRID_WIDTH' | translate }} - - +
@@ -68,6 +64,16 @@ - + diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts index 37837e1..98e99ca 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts @@ -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 {FormsModule} from '@angular/forms'; @@ -15,6 +15,7 @@ import {UrlConstants} from '../../../constants/UrlConstants'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {Information} from '../information/information'; import {AlgorithmInformation} from '../information/information.models'; +import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid'; enum NodeType { Start = 'start', @@ -23,8 +24,6 @@ enum NodeType { None = 'none' } -interface GridPos { row: number; col: number } - @Component({ selector: 'app-pathfinding', standalone: true, @@ -40,7 +39,8 @@ interface GridPos { row: number; col: number } MatCardHeader, MatCardTitle, MatCardContent, - Information + Information, + GenericGridComponent ], templateUrl: './pathfinding.component.html', }) @@ -51,6 +51,7 @@ export class PathfindingComponent implements AfterViewInit { readonly NodeType = NodeType; readonly MIN_GRID_SIZE = MIN_GRID_SIZE; readonly MAX_GRID_SIZE = MAX_GRID_SIZE; + readonly MAX_GRID_PX = MAX_GRID_PX; algoInformation: AlgorithmInformation = { title: 'PATHFINDING.EXPLANATION.TITLE', @@ -71,24 +72,15 @@ export class PathfindingComponent implements AfterViewInit { disclaimerListEntry: [] }; - @ViewChild('gridCanvas', { static: true }) - canvas!: ElementRef; - - private ctx!: CanvasRenderingContext2D; - gridRows = DEFAULT_GRID_ROWS; gridCols = DEFAULT_GRID_COLS; - nodeSize = 10; grid: Node[][] = []; startNode: Node | null = null; endNode: Node | null = null; selectedNodeType: NodeType = NodeType.None; - - isDrawing = false; - private lastCell: GridPos | null = null; - private shouldAddWall = true; + private shouldAddWall = true; // Moved here animationSpeed = 3; pathLength = "0"; @@ -96,58 +88,79 @@ export class PathfindingComponent implements AfterViewInit { private timeoutIds: number[] = []; + @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent; + ngAfterViewInit(): void { - this.ctx = this.getContextOrThrow(); - this.applyGridSize(true); - - 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(); - }); + // Canvas logic is now handled by GenericGridComponent + // Ensure genericGridComponent is initialized + if (this.genericGridComponent) { + this.genericGridComponent.initializationFn = this.initializePathfindingGrid; + 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.minGridSize = this.MIN_GRID_SIZE; + this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE; + this.genericGridComponent.maxGridPx = MAX_GRID_PX; + this.genericGridComponent.applyGridSize(); // Trigger initial grid setup + } + this.createCase({withWalls: true, scenario: "normal"}); } - applyGridSize(skipReset?: boolean): void { - 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(); + // --- Callbacks for GenericGridComponent --- + createPathfindingNode = (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, + }; + }; - if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length) - { - this.drawGrid(); - return; + getPathfindingNodeColor = (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'; + }; + + 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) { - this.initializeGrid({withWalls: true, scenario: 'normal'}); - this.drawGrid(); - return; + 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.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 { if (!this.ensureStartAndEnd()) { @@ -166,13 +179,13 @@ export class PathfindingComponent implements AfterViewInit { this.grid[this.startNode!.row][this.startNode!.col], this.grid[this.endNode!.row][this.endNode!.col] ); - break; + break; case 'astar': result = this.pathfindingService.aStar( this.grid, this.grid[this.startNode!.row][this.startNode!.col], this.grid[this.endNode!.row][this.endNode!.col] ); - break; + break; } if (!result) @@ -195,320 +208,19 @@ export class PathfindingComponent implements AfterViewInit { this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder); } - // Mouse interactions - private onMouseDown(event: MouseEvent): void { - this.stopAnimations(); - 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); + initializePathfindingGrid = (grid: Node[][]): void => { + this.grid = grid; // Update the component's grid reference + const {start, end} = this.getScenarioStartEnd('normal'); // Default 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; - if (withWalls) { - this.placeDefaultDiagonalWall(scenario); - } - } + this.placeDefaultDiagonalWall('normal'); + }; - 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)); - } - 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) + // --- Helper methods for node manipulation (kept local) --- private trySetStart(node: Node): void { if (!this.canBeStart(node)) { return; @@ -516,7 +228,7 @@ export class PathfindingComponent implements AfterViewInit { if (this.startNode) { this.startNode.isStart = false; - this.drawNode(this.startNode); + this.genericGridComponent.drawNode(this.startNode); // Redraw old start node } node.isStart = true; @@ -530,7 +242,7 @@ export class PathfindingComponent implements AfterViewInit { if (this.endNode) { this.endNode.isEnd = false; - this.drawNode(this.endNode); + this.genericGridComponent.drawNode(this.endNode); // Redraw old end node } node.isEnd = true; @@ -574,16 +286,197 @@ export class PathfindingComponent implements AfterViewInit { return !node.isStart && !node.isEnd; } - private shouldStartWallStroke(pos: GridPos): boolean { - if (this.selectedNodeType !== NodeType.Wall) { - return true; - } + // --- Grid manipulation for scenarios (kept local) --- + createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void { + 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]; - return !node.isWall; + if (withWalls) { + 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 { if (this.startNode && this.endNode) { return true; @@ -593,73 +486,7 @@ export class PathfindingComponent implements AfterViewInit { return false; } - // Grid sizing - 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; - } - + // --- Utility --- private randomIntFromInterval(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1) + min); } diff --git a/src/app/shared/components/generic-grid/generic-grid.html b/src/app/shared/components/generic-grid/generic-grid.html new file mode 100644 index 0000000..d0e9eb5 --- /dev/null +++ b/src/app/shared/components/generic-grid/generic-grid.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/shared/components/generic-grid/generic-grid.scss b/src/app/shared/components/generic-grid/generic-grid.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/components/generic-grid/generic-grid.ts b/src/app/shared/components/generic-grid/generic-grid.ts new file mode 100644 index 0000000..55c8035 --- /dev/null +++ b/src/app/shared/components/generic-grid/generic-grid.ts @@ -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; + + @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(); + @Output() nodeClick = new EventEmitter(); + + 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; + } +}