Source: core/sceneObjs/CurveObjMixin.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 i18next from 'i18next';
import geometry from '../geometry.js';
import Simulator from '../Simulator.js';
import Bezier from 'bezier-js';

/**
 * The mixin for the scene objects that are defined by a circle.
 * @template {typeof BaseSceneObj} T
 * @param {T} Base 
 * @returns {T}
 */
const CurveObjMixin = Base => class extends Base {
  
  /**
   * @param {Scene} scene - The scene the object belongs to.
   * @param {Object|null} jsonObj - The JSON object to be deserialized, if any.
   */
  constructor(scene, jsonObj) {
    super(scene, jsonObj);

    const points_in_curve = 4

    // Reset curLens to account for potential unfinished lenses
    // this.curLens = -1;

    // Initialize curve
    this.curve = null;

    // Extrapolate the (unoptimized) object from the (optimized) JSON object.
    if (jsonObj.points) {
      // Check to make sure there's the correct number of points
      if (jsonObj.points.length === 4) {
        this.curve = new Bezier(jsonObj.points);
        //this.p1 = jsonObj.points[0];
        //this.p2 = jsonObj.points[3];
      } else {
        console.error("Curve could not be created; jsonObj does not contain array \"points\" of length " + String(points_in_curve) + ".");
      }
    }
  }

  /**
   * Serializes the object to a JSON object.
   * @returns {Object} The serialized JSON object.
   */
  serialize() {
    let jsonObj = super.serialize();

    // Remove redundant properties of the JSON representation of the object.
    if (this.curve) {
      // For each lens in modular/composite lens
      jsonObj.points = JSON.parse(JSON.stringify(this.curve)).points.slice(0, 4);
    } else {
      jsonObj.points = [];  // Empty
    }
    delete jsonObj.curve;

    return jsonObj;
  }

  move(diffX, diffY) {
    // Move curve points
    for (let i = 0; i < this.curve.points.length; i++) {
      this.curve.points[i].x += diffX;
      this.curve.points[i].y += diffY;
    }
    this.curve = new Bezier(this.curve.points);
  }

  rotate(angle, center) {
    // Use center of object as default rotation center if none is provided
    const rotationCenter = center || this.getDefaultCenter();

    // Initialize temp variables for use applying rotations to each point
    var cur_diff_x = 0;
    var cur_diff_y = 0;
    const points_in_curve = 4;  // number of points in a curve is constant, so no need to acquire it by referencing the current curve (e.g. below)

    // Apply rotation to all path and curve points in the object
    for (let i = 0; i < points_in_curve; i++) {
      // Calculate the current difference for the current curve point
      cur_diff_x = this.curve.points[i].x - rotationCenter.x;
      cur_diff_y = this.curve.points[i].y - rotationCenter.y; 

      this.curve.points[i].x = rotationCenter.x + cur_diff_x * Math.cos(angle) - cur_diff_y * Math.sin(angle);
      this.curve.points[i].y = rotationCenter.y + cur_diff_x * Math.sin(angle) + cur_diff_y * Math.cos(angle);
    }

    // Update the current curve
    this.curve = new Bezier(this.curve.points);

    // Update anchor points
    //this.p1 = this.curve.points[0];
    //this.p2 = this.curve.points[3];
    
    return true;
  }
  
  scale(scale, center) {
    // Use center of object as default scaling center if none is provided
    const scalingCenter = center || this.getDefaultCenter();

    // Initialize temp variables for use applying scaling to each point
    var cur_diff_x = 0;
    var cur_diff_y = 0;
    const points_in_curve = 4;  // number of points in a curve is constant, so no need to acquire it by referencing the current curve (e.g. below)

    // Apply scaling to the curve's points
    for (let i = 1; i < points_in_curve; i++) {
      // Calculate the current difference for the current curve point
      cur_diff_x = this.curve.points[i].x - scalingCenter.x;
      cur_diff_y = this.curve.points[i].y - scalingCenter.y; 

      this.curve.points[i].x = scalingCenter.x + cur_diff_x * scale;
      this.curve.points[i].y = scalingCenter.y + cur_diff_y * scale;
    }

    // Update the current curve
    this.curve = new Bezier(this.curve.points);

    // Update anchor points
    //this.p1 = this.curve.points[0];
    //this.p2 = this.curve.points[3];
    
    return true;
  }
  
  getDefaultCenter() {
    return {
      x: Math.round((this.curve.points[0].x + this.curve.points[3].x) / 2),
      y: Math.round((this.curve.points[3].y + this.curve.points[0].y) / 2)
    };
  }
  
  onConstructMouseDown(mouse, ctrl, shift) {
    if (!this.constructionPoint) {
      this.constructionPoint = mouse.getPosSnappedToGrid();
      this.p1 = this.constructionPoint;
      this.p2 = this.constructionPoint;
    } 
    if (shift) {
      this.p2 = mouse.getPosSnappedToDirection(this.constructionPoint, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }]);
    } else {
      this.p2 = mouse.getPosSnappedToGrid();
    }
  }

  onConstructMouseMove(mouse, ctrl, shift) {
    if (shift) {
      this.p2 = mouse.getPosSnappedToDirection(this.constructionPoint, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }]);
    } else {
      this.p2 = mouse.getPosSnappedToGrid();
    }

    this.p1 = ctrl ? geometry.point(2 * this.constructionPoint.x - this.p2.x, 2 * this.constructionPoint.y - this.p2.y) : this.constructionPoint;
  }

  onConstructMouseUp(mouse, ctrl, shift) {
    if (!mouse.snapsOnPoint(this.p1)) {
      // Set default control points to be partially in between the anchor points
      this.curve = new Bezier(
        this.p1,
        {
          x: Math.round(0.25 * (this.p2.x - this.p1.x)) + this.p1.x,
          y: Math.round(0.25 * (this.p2.y - this.p1.y)) + this.p1.y
        },
        {
          x: Math.round(0.25 * (this.p1.x - this.p2.x)) + this.p2.x,
          y: Math.round(0.25 * (this.p1.y - this.p2.y)) + this.p2.y
        },
        this.p2
      );
      delete this.p1;
      delete this.p2;
      delete this.constructionPoint;
      return {
        isDone: true
      };
    }
  }

  checkMouseOver(mouse) {
    let dragContext = {};

    const mousePos = mouse.getPosSnappedToGrid();
    dragContext.mousePos0 = mousePos; // Mouse position when the user starts dragging
    dragContext.mousePos1 = mousePos; // Mouse position at the last moment during dragging
    dragContext.snapContext = {};
    
    // Check if on curve point
    for (let i = 0; i < this.curve.points.length; i ++) {
      if (mouse.isOnPoint(this.curve.points[i])) {
        dragContext.part = 1;
        dragContext.targetPoint = this.curve.points[i];
        return dragContext;
      }
    }
    // On the curve itself
    if (mouse.isOnCurve(geometry.curve(this.curve.points))) {
      dragContext.part = 0;
      return dragContext;
    }
  }

  onDrag(mouse, dragContext, ctrl, shift) {
    var mousePos;
    if (dragContext.part > 0 && dragContext.part < 5) {
      // Dragging one of the control points
      if (shift) {
        mousePos = mouse.getPosSnappedToDirection(dragContext.mousePos0, [{ x: 1, y: 0 }, { x: 0, y: 1 }], dragContext.snapContext);
      } else {
        mousePos = mouse.getPosSnappedToGrid();
        dragContext.snapContext = {}; // Unlock the dragging direction when the user release the shift key
      }
      this.curve.points[dragContext.part - 1].x = mousePos.x;
      this.curve.points[dragContext.part - 1].y = mousePos.y;
      //this.curves[dragContext.lens][dragContext.index].update();

      this.curve = new Bezier(this.curve.points);
    }
    if (dragContext.part === 0) {
      if (shift) {
        mousePos = mouse.getPosSnappedToDirection(dragContext.mousePos0, [{ x: 1, y: 0 }, { x: 0, y: 1 }], dragContext.snapContext);
      } else {
        mousePos = mouse.getPosSnappedToGrid();
        dragContext.snapContext = {}; // Unlock the dragging direction when the user release the shift key
      }
      this.move(mousePos.x - dragContext.mousePos1.x, mousePos.y - dragContext.mousePos1.y);
      dragContext.mousePos1 = mousePos;
    }
  }

  /**
   * Check if a ray intersects the curve.
   * In the child class, this can be called from the `checkRayIntersects` method.
   * @param {Ray} ray - The ray.
   * @returns {Point} - The (first) intersection point, or null if none
   */
  checkRayIntersectsShape(ray) {
    var rp_temps = geometry.lineCurveIntersections(geometry.line(ray.p1, ray.p2), this.curve);
    
    if (rp_temps) {
      return rp_temps[0];
    }
    return null;
  }


  // Draw curve
  drawCurve(curve, offset, canvasRenderer) {
    const ctx = canvasRenderer.ctx;
    var p = curve.points;
    ctx.strokeStyle = 'rgb(128,128,128)';
    ctx.beginPath();
    //ctx.moveTo(p[0].x + offset.x, p[0].y + offset.y);
    ctx.moveTo(p[0].x, p[0].y);
    //ctx.bezierCurveTo(p[1].x + offset.x, p[1].y + offset.y, p[2].x + offset.x, p[2].y + offset.y, p[3].x + offset.x, p[3].y + offset.y);
    ctx.bezierCurveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y);
    ctx.stroke();
    ctx.closePath();

    ctx.strokeStyle = 'rgb(255,0,0)';
    this.drawLine(p[0], p[1], offset, canvasRenderer);
    this.drawLine(p[2], p[3], offset, canvasRenderer);

    ctx.fillStyle = 'rgb(255,0,0)';
    p.forEach((cur) => this.drawPoint(cur, canvasRenderer));
  }

  // Draw line
  drawLine(p1, p2, offset, canvasRenderer) {
    const ctx = canvasRenderer.ctx;
    ctx.beginPath();
    //ctx.moveTo(p1.x + offset.x, p1.y + offset.y);
    //ctx.lineTo(p2.x + offset.x, p2.y + offset.y);
    ctx.moveTo(p1.x, p1.y);
    ctx.lineTo(p2.x, p2.y);
    ctx.stroke();
  }

  // Draw point
  drawPoint(p1, canvasRenderer) {
    const ctx = canvasRenderer.ctx;
    const ls = canvasRenderer.lengthScale;
    ctx.fillRect(p1.x - 1.5 * ls, p1.y - 1.5 * ls, 3 * ls, 3 * ls);
  }
}