Untitled
typescript
a month ago
20 kB
1
Indexable
Never
// canvas.component.ts import { Component, ElementRef, AfterViewInit, ViewChild, EventEmitter, Output, Input, OnChanges, SimpleChanges, } from '@angular/core'; import { Rectangle } from '../models/rectangle'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ selector: 'app-canvas', templateUrl: './canvas.component.html', styleUrls: ['./canvas.component.css'], }) export class CanvasComponent implements AfterViewInit { //----------------- Canvas -----------------// @ViewChild('canvas', { static: true }) canvasElementRef!: ElementRef<HTMLCanvasElement>; @ViewChild('container', { static: true }) containerRef!: ElementRef<HTMLDivElement>; //----- Input/Output ------// @Output() rectangleDrawn = new EventEmitter<Rectangle>(); @Input() deletedRectangle = new EventEmitter<number>(); @Input() editRectangle = new EventEmitter<number | null>(); @Input() editInProgress= new EventEmitter<boolean>(); @Input() onMap = new EventEmitter<boolean>(); //----- Rectangle ------// currentRectangle: Rectangle | null = null; private ctx!: CanvasRenderingContext2D; crossClickIndex: number | null = null; editingInProgress: boolean = false; editingIndex: number | null = null; private canvas!: HTMLCanvasElement; pdfSrc: string = 'assets/test.pdf'; lockedRectangles: Rectangle[] = []; // Array to store locked rectangles showMarkedRegion: boolean = true; // rectangles: Rectangle[] = []; currentPage: number = 1; rectangleColor = 'red'; index: number = 0; isDrawing = false; numPages = 0; //----- Rectangle Selector ------// private selectedRectangleIndex: number | null = null; private dragging = false; private resizing = false; private resizingEdge: number | null = null; private resizingStartX = 0; private resizingStartY = 0; private resizingStartWidth = 0; private resizingStartHeight = 0; private crossSize: number = 12; constructor(private snackBar: MatSnackBar) {} //----- Angular Lifecycle Hooks ------// ngOnInit(): void { this.ngOnDeleteRectangle(); this.ngOnEditRectangle(); this.ngOnEditComplete(); } ngAfterViewInit(): void { this.canvas = this.canvasElementRef.nativeElement; this.ctx = this.canvas.getContext('2d')!; this.drawRectangles(); this.ctx.lineWidth = 10; this.adjustCanvasSize(); this.canvas.addEventListener('mousedown', (event) => this.onMouseDown(event) ); this.canvas.addEventListener('mousemove', (event) => this.onMouseMove(event) ); this.canvas.addEventListener('mouseup', () => this.onMouseUp()); this.canvas.addEventListener('mouseleave', () => this.onMouseLeave(this.ctx) ); } ngOnDeleteRectangle(): void { this.deletedRectangle.subscribe((index) => { this.lockedRectangles.splice(index, 1); this.drawRectangles(); }); } ngOnEditComplete(): void { this.editInProgress.subscribe((inProgress) => { this.saveCurrentRectangle(); this.editingInProgress = inProgress; if (!inProgress) { this.editingIndex = null; this.currentRectangle = null; this.drawRectangles(); } }); } ngOnMap(): void { this.onMap.subscribe((onMap) => { this.showMarkedRegion = !onMap; this.drawRectangles(); }); } ngOnEditRectangle(): void { this.editRectangle.subscribe((index) => { this.editingIndex = index; if (index !== null) { this.currentRectangle = this.lockedRectangles[index]; this.unlockRectangle(index); } this.drawRectangles(); }); } //-----Canvas Events------// changePage(pageChange: number): void { const newPage = this.currentPage + pageChange; this.currentPage = newPage; } afterLoadComplete(pdfData: any): void { this.numPages = pdfData.numPages; } private clearCanvas(ctx: CanvasRenderingContext2D): void { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); this.drawAllRectangles(); } private adjustCanvasSize(): void { const container = this.containerRef.nativeElement; const canvas = this.canvasElementRef.nativeElement; // Set the canvas size to match its container size canvas.width = container.clientWidth; canvas.height = container.clientHeight; } onFileSelected() { let $img: any = document.querySelector('#file'); if (typeof FileReader !== 'undefined') { let reader = new FileReader(); reader.onload = (e: any) => { this.pdfSrc = e.target.result; }; reader.readAsArrayBuffer($img.files[0]); } } //-------------------Mouse Events--------------------// onMouseLeave(ctx: CanvasRenderingContext2D): void { if (this.isDrawing) { this.clearCanvas(ctx); this.isDrawing = false; } else if (this.dragging) { this.clearCanvas(ctx); this.dragging = false; this.selectedRectangleIndex = null; } } onMouseDown(event: MouseEvent): void { const { offsetX, offsetY } = event; if (this.editingIndex === null) { this.selectedRectangleIndex = this.findSelectedRectangle( offsetX, offsetY ); if (this.selectedRectangleIndex !== null) { this.resizingEdge = this.getSegment( offsetX, offsetY, this.rectangles[this.selectedRectangleIndex] ); if (this.resizingEdge === 5) { this.dragging = true; this.resizing = false; this.resizingStartX = this.rectangles[this.selectedRectangleIndex].x; this.resizingStartY = this.rectangles[this.selectedRectangleIndex].y; this.resizingStartWidth = this.rectangles[this.selectedRectangleIndex].width; this.resizingStartHeight = this.rectangles[this.selectedRectangleIndex].height; } else { this.dragging = false; this.resizing = true; this.resizingStartX = this.rectangles[this.selectedRectangleIndex].x; this.resizingStartY = this.rectangles[this.selectedRectangleIndex].y; this.resizingStartWidth = this.rectangles[this.selectedRectangleIndex].width; this.resizingStartHeight = this.rectangles[this.selectedRectangleIndex].height; } } else { // this.crossClickIndex = this.onCrossClick(event); // if (this.crossClickIndex !== null) { // this.rectangles.splice(this.crossClickIndex, 1); // Delete the rectangle // this.drawRectangles(); // Redraw the canvas // } else { // if (this.editingInProgress) { // this.showSnackbar( // 'Please save or cancel the current rectangle before drawing a new one.' // ); // return; // } this.dragging = false; this.resizing = false; this.currentRectangle = { x: offsetX, y: offsetY, width: 0, height: 0, }; this.isDrawing = true; // } } } else { //Edit or resize the edge this.resizingEdge = this.getSegment( offsetX, offsetY, this.rectangles[this.editingIndex!] ); if (this.resizingEdge === 5) { this.dragging = true; this.resizing = false; this.resizingStartX = this.rectangles[this.editingIndex!].x; this.resizingStartY = this.rectangles[this.editingIndex!].y; this.resizingStartWidth = this.rectangles[this.editingIndex!].width; this.resizingStartHeight = this.rectangles[this.editingIndex!].height; } else { this.dragging = false; this.resizing = true; this.resizingStartX = this.rectangles[this.editingIndex!].x; this.resizingStartY = this.rectangles[this.editingIndex!].y; this.resizingStartWidth = this.rectangles[this.editingIndex!].width; this.resizingStartHeight = this.rectangles[this.editingIndex!].height; } } } onMouseMove(event: MouseEvent): void { const { offsetX, offsetY } = event; if (this.editingIndex === null) { if (this.isDrawing) { this.canvas.style.cursor = 'crosshair'; this.currentRectangle!.width = offsetX - this.currentRectangle!.x; this.currentRectangle!.height = offsetY - this.currentRectangle!.y; this.drawRectangles(); } else if (this.dragging) { this.dragRectangle( event, this.rectangles[this.selectedRectangleIndex!] ); } else if (this.resizing) { this.resizeRectangle(offsetX, offsetY); } else { this.selectedRectangleIndex = this.findSelectedRectangle( offsetX, offsetY ); if (this.selectedRectangleIndex !== null) { this.resizingEdge = this.getSegment( offsetX, offsetY, this.rectangles[this.selectedRectangleIndex] ); this.updateMouseCursor(this.resizingEdge); } } } else { if (this.dragging) { this.dragRectangle(event, this.rectangles[this.editingIndex!]); } else if (this.resizing) { this.resizeRectangle(offsetX, offsetY); } else { this.selectedRectangleIndex = this.editingIndex; if (this.selectedRectangleIndex !== null) { this.resizingEdge = this.getSegment( offsetX, offsetY, this.rectangles[this.editingIndex!] ); this.updateMouseCursor(this.resizingEdge); } } } } onMouseUp(): void { this.isDrawing = false; this.dragging = false; this.resizing = false; this.resizingEdge = null; if (!this.currentRectangle) { return; } this.editingInProgress = true; if (this.editingIndex !== null) { this.rectangles[this.editingIndex] = { ...this.currentRectangle }; } else { // this.rectangles.push({ ...this.currentRectangle }); this.rectangleDrawn.emit({ ...this.currentRectangle }); // Emit the rectangle coordinates } // this.currentRectangle = null; this.editingIndex = null; // this.drawRectangles(); } //---------------Rectangle Events--------------------// private isPointInsideRectangle( x: number, y: number, rectangle: Rectangle ): boolean { return ( x >= rectangle.x && x <= rectangle.x + rectangle.width && y >= rectangle.y && y <= rectangle.y + rectangle.height ); } private findSelectedRectangle(x: number, y: number): number | null { for (let i = this.rectangles.length - 1; i >= 0; i--) { if (this.isPointInsideRectangle(x, y, this.rectangles[i])) { return i; } } return null; } lockRectangle(): void { //update with current rectangle this.lockedRectangles[this.selectedRectangleIndex!] = this.currentRectangle!; this.currentRectangle = null; this.drawRectangles(); } unlockRectangle(index: number): void { this.rectangles.push(this.lockedRectangles[index]); this.selectedRectangleIndex = this.rectangles.length - 1; this.editingIndex = this.selectedRectangleIndex; this.lockedRectangles.splice(index, 1); } private isRectangleLocked(rectangle: Rectangle): boolean { return this.lockedRectangles.some( (r) => r.x === rectangle.x && r.y === rectangle.y ); } private resizeRectangle(x: number, y: number): void { if (this.selectedRectangleIndex !== null && this.resizingEdge !== null) { const selectedRectangle = this.rectangles[this.selectedRectangleIndex]; const deltaX = x - this.resizingStartX; const deltaY = y - this.resizingStartY; switch (this.resizingEdge) { case 1: selectedRectangle.x = this.resizingStartX + deltaX; selectedRectangle.y = this.resizingStartY + deltaY; selectedRectangle.width = this.resizingStartWidth - deltaX; selectedRectangle.height = this.resizingStartHeight - deltaY; break; case 2: selectedRectangle.y = this.resizingStartY + deltaY; selectedRectangle.height = this.resizingStartHeight - deltaY; break; case 3: selectedRectangle.y = this.resizingStartY + deltaY; selectedRectangle.width = this.resizingStartWidth + deltaX; selectedRectangle.height = this.resizingStartHeight - deltaY; break; case 4: selectedRectangle.x = this.resizingStartX + deltaX; selectedRectangle.width = this.resizingStartWidth - deltaX; break; case 5: selectedRectangle.x = this.resizingStartX + deltaX; selectedRectangle.y = this.resizingStartY + deltaY; break; case 6: selectedRectangle.width = this.resizingStartWidth + deltaX; break; case 7: selectedRectangle.x = this.resizingStartX + deltaX; selectedRectangle.width = this.resizingStartWidth - deltaX; selectedRectangle.height = this.resizingStartHeight + deltaY; break; case 8: selectedRectangle.height = this.resizingStartHeight + deltaY; break; case 9: selectedRectangle.width = this.resizingStartWidth + deltaX; selectedRectangle.height = this.resizingStartHeight + deltaY; break; default: break; } // Emit the updated coordinates, width, and height during resizing this.rectangleDrawn.emit({ ...selectedRectangle }); this.drawRectangles(); } } dragRectangle(event: MouseEvent, rectangle: Rectangle): void { const { offsetX, offsetY } = event; const deltaX = offsetX - this.resizingStartX; const deltaY = offsetY - this.resizingStartY; rectangle.x = this.resizingStartX + deltaX; rectangle.y = this.resizingStartY + deltaY; // Emit the updated coordinates, width, and height during dragging this.rectangleDrawn.emit({ ...rectangle }); this.drawRectangles(); } private drawRectangles(): void { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.drawAllRectangles(); //todo: check if adding && this.isDrawing will break anything if (this.currentRectangle) { this.ctx.beginPath(); this.ctx.rect( this.currentRectangle.x, this.currentRectangle.y, this.currentRectangle.width, this.currentRectangle.height ); this.ctx.stroke(); } } drawAllRectangles(): void { this.canvas.style.strokeWidth = '4.5'; this.lockedRectangles.forEach((rectangle, index) => { this.ctx.strokeStyle = 'blue'; this.ctx.beginPath(); this.ctx.rect( rectangle.x, rectangle.y, rectangle.width, rectangle.height ); this.ctx.stroke(); // this.drawCrossMark(rectangle); }); this.canvas.style.strokeWidth = '4.5'; this.rectangles.forEach((rectangle, index) => { this.ctx.strokeStyle = 'red'; this.ctx.beginPath(); this.ctx.rect( rectangle.x, rectangle.y, rectangle.width, rectangle.height ); this.ctx.stroke(); // this.drawCrossMark(rectangle); }); } getSegment(x: number, y: number, rectangle: Rectangle): number | null { const segmentWidth = rectangle.width / 3; const segmentHeight = rectangle.height / 3; // Calculate which segment the cursor is in const column = Math.floor((x - rectangle.x) / segmentWidth); const row = Math.floor((y - rectangle.y) / segmentHeight); // Check if the cursor is within the rectangle if (column < 0 || column > 2 || row < 0 || row > 2) { return null; // Cursor is outside the rectangle } // Calculate the corresponding segment number const segment = row * 3 + column + 1; return segment; } updateMouseCursor(segment: number | null): void { switch (segment) { case 1: this.canvas.style.cursor = 'nwse-resize'; break; case 2: this.canvas.style.cursor = 'ns-resize'; break; case 3: this.canvas.style.cursor = 'nesw-resize'; break; case 4: this.canvas.style.cursor = 'ew-resize'; break; case 5: this.canvas.style.cursor = 'move'; break; case 6: this.canvas.style.cursor = 'ew-resize'; break; case 7: this.canvas.style.cursor = 'nesw-resize'; break; case 8: this.canvas.style.cursor = 'ns-resize'; break; case 9: this.canvas.style.cursor = 'nwse-resize'; break; default: this.canvas.style.cursor = 'crosshair'; break; } } saveCurrentRectangle(): void { if (this.editingIndex !== null) { // Editing an existing rectangle console.log('editing existing rectangle'); console.log(this.editingIndex); console.log(this.currentRectangle); if (this.currentRectangle) { this.lockRectangle(this.editingIndex); } } else { // Adding a new rectangle if (this.currentRectangle) { this.lockedRectangles.push({ ...this.currentRectangle }); this.rectangleDrawn.emit({ ...this.currentRectangle }); this.currentRectangle = null; } } // Reset the editing flag this.editingInProgress = false; this.drawRectangles(); } //------------Utility functions----------------// showSnackbar(message: string): void { const snackBarRef = this.snackBar.open(message, 'OK'); snackBarRef.onAction().subscribe(() => { console.log('Saved changes'); }); //display snackbar for 3 seconds setTimeout(() => { snackBarRef.dismiss(); }, 3000); } toggleShowMarkedRegions(): void { this.showMarkedRegion = !this.showMarkedRegion; //Reset editing flag when toggling marked regions this.editingInProgress = false; this.drawRectangles(); } private drawCrossMark(rectangle: Rectangle): void { // Draw the cross mark on the top right edge const crossX = rectangle.x + rectangle.width - this.crossSize / 2; const crossY = rectangle.y - this.crossSize / 2; this.ctx.strokeStyle = 'black'; this.ctx.beginPath(); this.ctx.moveTo(crossX, crossY); this.ctx.lineTo(crossX + this.crossSize, crossY + this.crossSize); this.ctx.moveTo(crossX, crossY + this.crossSize); this.ctx.lineTo(crossX + this.crossSize, crossY); this.ctx.stroke(); } private onCrossClick(event: MouseEvent): number | null { const { offsetX, offsetY } = event; // Check if the click is on any cross mark for (let i = 0; i < this.rectangles.length; i++) { const rectangle = this.rectangles[i]; const crossX = rectangle.x + rectangle.width; const crossY = rectangle.y; if ( offsetX >= crossX - this.crossSize && offsetX <= crossX + this.crossSize && offsetY >= crossY - this.crossSize && offsetY <= crossY + this.crossSize ) { this.editingInProgress = false; return i; // Return the index of the deleted rectangle } } return null; } }