Untitled

mail@pastecode.io avatarunknown
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;
  }
}