Source: core/sceneObjs/other/Drawing.js

/*
 * Copyright 2024 The Ray Optics Simulation authors and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import BaseSceneObj from '../BaseSceneObj.js';
import i18next from 'i18next';
import geometry from '../../geometry.js';

/**
 * Drawing tool
 * 
 * Tools -> Other -> Drawing
 * @class
 * @extends BaseSceneObj
 * @memberof sceneObjs
 * @property {Array<Array<number>>} strokes - The strokes of the drawing. Each element represents a stroke, which is an array of coordinates ordered as `[x1, y1, x2, y2, ...]`. The coordinates are rounded to reduce the size of the JSON data.
 * @property {boolean} isDrawing - Whether the user is drawing (before "stop drawing" is clicked).
 * @property {boolean} isMouseDown - Temperary indication of whether the mouse is down (during the drawing stage).
 */
class Drawing extends BaseSceneObj {
  static type = 'Drawing';
  static serializableDefaults = {
    strokes: [],
    isDrawing: false
  };

  populateObjBar(objBar) {
    objBar.setTitle(i18next.t('main:tools.Drawing.title'));
    if (this.isDrawing) {
      objBar.createButton(i18next.t('simulator:sceneObjs.Drawing.finishDrawing'), function (obj) {
        obj.isDrawing = false;
        if (obj.scene.editor.isConstructing) {
          obj.scene.editor.isConstructing = false;
        }
      }, true);
    }
  }

  draw(canvasRenderer, isAboveLight, isHovered) {
    const ctx = canvasRenderer.ctx;
    const ls = canvasRenderer.lengthScale;

    ctx.strokeStyle = isHovered ? 'cyan' : 'white';
    ctx.lineWidth = 1 * ls;
    ctx.beginPath();
    for (const stroke of this.strokes) {
      ctx.moveTo(stroke[0], stroke[1]);
      for (let i = 2; i < stroke.length; i += 2) {
        ctx.lineTo(stroke[i], stroke[i + 1]);
      }
    }
    ctx.stroke();
  }

  move(diffX, diffY) {
    let roundedDiffX = this.round(diffX);
    let roundedDiffY = this.round(diffY);
    for (const stroke of this.strokes) {
      for (let i = 0; i < stroke.length; i += 2) {
        stroke[i] += roundedDiffX;
        stroke[i + 1] += roundedDiffY;
      }
    }
  }

  onConstructMouseDown(mouse, ctrl, shift) {
    if (!this.isDrawing) {
      // Initialize the drawing
      this.isDrawing = true;
      this.strokes = [];
    }
    const mousePos = mouse.getPosSnappedToGrid();
    this.strokes.push([this.round(mousePos.x), this.round(mousePos.y)]);
    this.isMouseDown = true;
  }

  onConstructMouseMove(mouse, ctrl, shift) {
    const mousePos = mouse.getPosSnappedToGrid();
    if (!this.isMouseDown) return;
    if (this.strokes.length === 0 || this.strokes[this.strokes.length - 1].length < 2) return;

    const distanceSq = (this.strokes[this.strokes.length - 1][this.strokes[this.strokes.length - 1].length - 2] - mousePos.x) ** 2 + (this.strokes[this.strokes.length - 1][this.strokes[this.strokes.length - 1].length - 1] - mousePos.y) ** 2;
    
    if (distanceSq < 4 * this.scene.lengthScale * this.scene.lengthScale) return;

    this.strokes[this.strokes.length - 1].push(this.round(mousePos.x), this.round(mousePos.y));
  }

  onConstructMouseUp(mouse, ctrl, shift) {
    this.isMouseDown = false;
  }

  onConstructUndo() {
    if (this.strokes.length < 2) {
      return {
        isCancelled: true
      }
    } else {
      this.strokes.pop();
    }
  }

  checkMouseOver(mouse) {
    let dragContext = {};
    for (const stroke of this.strokes) {
      for (let i = 0; i < stroke.length - 2; i += 2) {
        if (mouse.isOnSegment(geometry.line(geometry.point(stroke[i], stroke[i + 1]), geometry.point(stroke[i + 2], stroke[i + 3])))) {
          const mousePos = mouse.getPosSnappedToGrid();
          const roundedMousePos = geometry.point(this.round(mousePos.x), this.round(mousePos.y));
          dragContext.part = 0;
          dragContext.mousePos0 = roundedMousePos; // Mouse position when the user starts dragging
          dragContext.mousePos1 = roundedMousePos; // Mouse position at the last moment during dragging
          dragContext.snapContext = {};
          return dragContext;
        }
      }
    }
  }

  onDrag(mouse, dragContext, ctrl, shift) {
    if (shift) {
      var mousePos = mouse.getPosSnappedToDirection(dragContext.mousePos0, [{ x: 1, y: 0 }, { x: 0, y: 1 }], dragContext.snapContext);
    } else {
      var mousePos = mouse.getPosSnappedToGrid();
      dragContext.snapContext = {}; // Unlock the dragging direction when the user release the shift key
    }

    const roundedMousePos = geometry.point(this.round(mousePos.x), this.round(mousePos.y));

    var mouseDiffX = dragContext.mousePos1.x - roundedMousePos.x; // The X difference between the mouse position now and at the previous moment
    var mouseDiffY = dragContext.mousePos1.y - roundedMousePos.y; // The Y difference between the mouse position now and at the previous moment

    if (dragContext.part == 0) {
      for (const stroke of this.strokes) {
        for (let i = 0; i < stroke.length; i += 2) {
          stroke[i] -= mouseDiffX;
          stroke[i + 1] -= mouseDiffY;
        }
      }
    }

    // Update the mouse position
    dragContext.mousePos1 = roundedMousePos;
  }

  /* Utility functions */

  /**
   * Round the coordinates of the strokes to integers times the length scale (to reduce the size of the JSON data).
   */
  round(num) {
    return Math.round(num / this.scene.lengthScale) * this.scene.lengthScale;
  }
};

export default Drawing;