Source: core/sceneObjs/glass/CurveGrinGlass.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 BaseGrinGlass from '../BaseGrinGlass.js';
import i18next from 'i18next';
import Simulator from '../../Simulator.js';
import geometry from '../../geometry.js';
import { Bezier, PolyBezier } from 'bezier-js';

/**
 * Glass of the shape consists of Bezier curves.
 * 
 * Tools -> Glass -> GRIN Cubic Bezier Curves
 * @class
 * @extends BaseGrinGlass
 * @memberof sceneObjs
 * @property {Array<object>} path - The path of the glass, connecting each curve. Each element is an object with `x` and `y` properties for coordinates.
 * @property {Array<object>} curves - The curves connected in series along the path. Each element is an object represented by a `Bezier` object whose points (a_1, c_1, c_2, a_2) may be acquired via `object_name.points`. Any modification to these points requires creation of a new `Bezier` object defined by those points (e.g. `new Bezier(a_1, c_1, c_2, a_2)`).
 * @property {Array<object>} bboxes - Bounding boxes of the curves. Calculated and stored whenever a curve is created. Used to prevent unnecesary recalculation of bounding boxes of curves.
 * @property {Array<object>} curIntersections - Most recent intersections calculated in countIntersections for each curve. Used to prevent unnecesary recalculation of bounding boxes of curves.
 * @property {boolean} notDone - Whether the user is still drawing the path of the glass.
 * @property {string} refIndexFn - The refractive index function in x and y in LaTeX format.
 * @property {Point} origin - The origin of the (x,y) coordinates used in the refractive index function.
 * @property {number} stepSize - The step size for the ray trajectory equation.
 * @property {number} intersectTol - Tolerance for intersection calculations (unit: pixels).
 */
class CurveGrinGlass extends BaseGrinGlass {
  static type = 'CurveGrinGlass';
  static isOptical = true;
  static mergesWithGlass = true;
  static serializableDefaults = {
    points: [],
    notDone: false,
    refIndexFn: '1.1+0.1\\cdot\\cos\\left(0.1\\cdot y\\right)',
    absorptionFn: '0',
    origin: { x: 0, y: 0 },
    stepSize: 1,
    intersectTol: 5e-2
  }
  
  /**
   * @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);

    // Initialize curves and bboxes
    this.curves = [];
    this.bboxes = [];

    // Extrapolate the (unoptimized) object from the (optimized) JSON object.
    // NOTE: The "curves" and "path" properties should eventually be combined and optimized to be consistent with the JSON representation.
    if (jsonObj.points) {
      // Go through each of the curves in the current lens
      for (let curCurve = 0; curCurve < jsonObj.points.length; curCurve++) {
        // The first point is the first anchor point, the second two control points, and the first of the next curve the last anchor point
        this.newCurve(
          [ 
            jsonObj.points[curCurve].a1, 
            jsonObj.points[curCurve].c1, 
            jsonObj.points[curCurve].c2, 
            jsonObj.points[(curCurve + 1) % jsonObj.points.length].a1
          ], 
          -1
        );
      }
    } 
  }

  serialize() {
    let jsonObj = super.serialize();

    // Remove redundant properties of the JSON representation of the object.
    if (this.curves) {
      jsonObj.points = this.curves.map(curve => {
        // Get the first three points of the curve
        const pts = JSON.parse(JSON.stringify(curve)).points.slice(0, 3).map(pts => {
          // Get only the x and y, not the t and d
          return geometry.point(pts.x, pts.y);
        });
        return {
          a1: pts[0],
          c1: pts[1],
          c2: pts[2]
        };
      });
    } else {
      // Empty if no curves or in middle of construction
      jsonObj.points = [];
    }
    delete jsonObj.curves;
    if (jsonObj.path) delete jsonObj.path;

    return jsonObj;
  }

  populateObjBar(objBar) {
    objBar.setTitle(i18next.t('main:tools.CurveGrinGlass.title'));
    super.populateObjBar(objBar);
  }

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

    // Draw lens as it's being drawn
    if (this.notDone) {
      // Ensure the path exists before attempting to draw it
      if (this.path) {
        if (this.path.length === 2 && this.path[0].x === this.path[1].x && this.path[0].y === this.path[1].y) {
          ctx.fillStyle = 'rgb(255,0,0)';
          ctx.fillRect(this.path[0].x - 1.5 * ls, this.path[0].y - 1.5 * ls, 3 * ls, 3 * ls);
          return;
        }
        // The user has not finished drawing the object yet
        ctx.beginPath();
        ctx.moveTo(this.path[0].x, this.path[0].y);

        for (let i = 0; i < this.path.length - 1; i++) {
          ctx.lineTo(this.path[(i + 1)].x, this.path[(i + 1)].y);
          ctx.fillStyle = 'rgb(255,0,0)';
          this.drawPoint(this.path[i], canvasRenderer);
        }
        ctx.globalAlpha = 1;
        ctx.strokeStyle = 'rgb(128,128,128)';
        ctx.lineWidth = 1 * ls;
        ctx.setLineDash([5*ls, 5*ls]);
        ctx.stroke();
        ctx.setLineDash([]);
      }
    } else {
      // The user has completed drawing the object

      // Draw the curves
      ctx.beginPath();
      let tmp = this.curves[0].points[0];
      ctx.moveTo(tmp.x, tmp.y);
      // Fill each curve bound individually
      for (let i = 0; i < this.curves.length; i++) {
        //tmpPath.push(this.curves[i].points[0]);
        this.drawCurve(this.curves[i], canvasRenderer, isHovered);
        //this.fillGlass(canvasRenderer, isAboveLight, isHovered);
        //ctx.closePath();
      }
      //ctx.stroke();
      //ctx.fill();
      this.fillGlass(canvasRenderer, isAboveLight, isHovered);
      
      // Draw the in-between
      /*ctx.beginPath();
      ctx.moveTo(tmpPath[0].x, tmpPath[0].y);
      for (let i = 1; i < this.curves.length; i++) {
        ctx.lineTo(tmpPath[i].x, tmpPath[i].y);
      }
      this.fillGlass(canvasRenderer, isAboveLight, isHovered);*/


      // Draw their points if hovered
      if (isHovered) {
        ctx.beginPath();
        ctx.moveTo(this.curves[0].points[0].x, this.curves[0].points[0].y);

        for (let i = 0; i < this.curves.length; i++) {
          let p = this.curves[i].points;

          // Lines connecting anchors and control points
          ctx.strokeStyle = 'rgb(255,0,0)';
          //this.drawLine(p[0], p[1], offset, canvasRenderer);
          //this.drawLine(p[2], p[3], offset, canvasRenderer);
          this.drawLine(p[0], p[1], canvasRenderer, ctx.strokeStyle);
          this.drawLine(p[2], p[3], canvasRenderer, ctx.strokeStyle);

          // Draw points at each point in curve
          p.forEach((cur) => this.drawPoint(cur, canvasRenderer));
        }
      }
    }
    ctx.lineWidth = 1;
  }

  move(diffX, diffY) {
    // Each curve in lens
    for (let i = 0; i < this.curves.length; i++) {
      // Move curve points
      for (let j = 0; j < this.curves[i].points.length; j++) {
        this.curves[i].points[j].x += diffX;
        this.curves[i].points[j].y += diffY;
      }
      this.newCurve(this.curves[i].points, i);
    }

    return false;
  }

  rotate(angle, center) {
    // Use center of object as default rotation center if none is provided
    const rotationCenter = center || this.getDefaultCenter();
    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)

    // Initialize temp variables for use applying rotations to each point
    var cur_diff_x = 0;
    var cur_diff_y = 0;

    // Apply rotation to all curve points in the object
    for (let i = 0; i < this.curves.length; i++) {    // path length should equal curve length, hence they are effectively interchangeable here
      // Apply rotation to the rest of the current curve's points
      for (let j = 0; j < points_in_curve; j++) {
        // Calculate the current difference for the current curve point
        cur_diff_x = this.curves[i].points[j].x - rotationCenter.x;
        cur_diff_y = this.curves[i].points[j].y - rotationCenter.y; 

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

      // Update the current curve
      this.newCurve(this.curves[i].points, i);
    }
    
    return false;
  }
  
  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 all curve points in the object
    for (let i = 0; i < this.curves.length; i++) {    // path length should equal curve length, hence they are effectively interchangeable here
      // Apply scaling to the rest of the current curve's points
      for (let j = 0; j < points_in_curve; j++) {
        // Calculate the current difference for the current curve point
        cur_diff_x = this.curves[i].points[j].x - scalingCenter.x;
        cur_diff_y = this.curves[i].points[j].y - scalingCenter.y; 

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

      // Update the current curve
      this.newCurve(this.curves[i].points, i);
    }
    
    return false;
  }
  
  getDefaultCenter() {
    var curPath = []

    // Get the current path from the current curves
    for (let i = 0; i < this.curves.length; i++) {
      curPath.push(this.curves[i].points[0]);
    }

    return {
      x: Math.round(curPath.reduce((partialPointsSum, curPoint) => partialPointsSum + curPoint.x, 0) / this.curves.length),
      y: Math.round(curPath.reduce((partialPointsSum, curPoint) => partialPointsSum + curPoint.y, 0) / this.curves.length)
    };
  }
  
  onConstructMouseDown(mouse, ctrl, shift) {
    const mousePos = mouse.getPosSnappedToGrid();
    
    // Initialize new curve object
    if (!this.notDone) {
      // Initialize the construction stage
      this.notDone = true;
      
      // Initialize path and curves and bboxes
      this.curves = [];
      this.bboxes = [];
      this.path = [{ x: mousePos.x, y: mousePos.y }];
    } 

    if (this.path.length > 0) {
      // Check if clicked on first point in path of current lens
      if (this.path.length > 3 && mouse.snapsOnPoint(this.path[0])) {
        // Clicked the first point
        this.path.length--;   // Remove the ast one (removes the duplicate point at the end)
        this.notDone = false;

        this.generatePolyBezier();

        return {
          isDone: true
        };
      } else {
        // Move the last point
        this.path.push({ x: mousePos.x, y: mousePos.y });
      }
    }
  }

  onConstructMouseMove(mouse, ctrl, shift) {
    if (!this.notDone) return { isDone: true };
    const mousePos = mouse.getPosSnappedToGrid();
    this.path[this.path.length - 1] = { x: mousePos.x, y: mousePos.y }; // Move the last point
  }

  onConstructMouseUp(mouse, ctrl, shift) {
    if (!this.notDone) return { isDone: true };
  }

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

  checkMouseOver(mouse) {
    var dragContext = {};
    var curCurvePts = [];

    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 = {};
    
    // Go thru each curve
    for (let c = 0; c < this.curves.length; c++) {
      curCurvePts = this.curves[c].points;
      // Handle path point separate from control points due to overlap with previous curve's final path point
      if (mouse.isOnPoint(this.curves[c].points[0])) {// || mouse.isOnPoint(this.curves[(c - 1 + this.curves.length) % this.curves.length].points[3])) {
        dragContext.part = 1;
        dragContext.targetPoint = this.curves[c].points[0];
        dragContext.index = c;
        return dragContext;
      }

      // Check if on one of the control points
      for (let i = 1; i < curCurvePts.length - 1; i++) {
        if (mouse.isOnPoint(curCurvePts[i])) {
          dragContext.part = i + 1;
          dragContext.targetPoint = curCurvePts[i];
          dragContext.index = c;
          return dragContext;
        }
      }
    }
    // On the curve itself. Done outside of previous loop due to conflicts with path points
    for (let c = 0; c < this.curves.length; c++) {
      if (mouse.isOnCurve(this.curves[c])) {
        dragContext.part = 0;
        return dragContext;
      }
    }
  }
  
  onDrag(mouse, dragContext, ctrl, shift) {
    var mousePos;
    var mod = 0;
    var closest = { x: 0, y: 0 };
    const curPrev = (dragContext.index - 1 + this.curves.length) % this.curves.length;

    if (shift) {
      mod++;
    }
    if (ctrl) {
      mod += 2;
    }

    if (dragContext.part === 1) {
      switch (mod) {
        // Default behavior (no movement restrictions)
        default:
          mousePos = mouse.getPosSnappedToGrid();
          dragContext.snapContext = {}; // Unlock the dragging direction when the user release the shift key
          break;

        // Snapping to a direction
        case 1://, 3:
          mousePos = mouse.getPosSnappedToDirection(dragContext.mousePos0, [{ x: 1, y: 0 }, { x: 0, y: 1 }], dragContext.snapContext);
          break;

        // Essentially default movement, but also taking the relevant control points along with the moving path point
        case 2:

          mousePos = mouse.getPosSnappedToGrid();
          dragContext.snapContext = {}; // Unlock the dragging direction when the user release the shift key

          // Get the difference in position between mouse and path point being moved to figure out how we want to translate the control points
          var diffX = mousePos.x - this.curves[dragContext.index].points[0].x;
          var diffY = mousePos.y - this.curves[dragContext.index].points[0].y;

          // First control point of the latter curve
          this.curves[dragContext.index].points[1].x += diffX; 
          this.curves[dragContext.index].points[1].y += diffY; 
          // Second control point of the former curve
          this.curves[curPrev].points[2].x += diffX;
          this.curves[curPrev].points[2].y += diffY;

          this.newCurve(this.curves[dragContext.index].points, dragContext.index);
          this.newCurve(this.curves[curPrev].points, curPrev);

         break;
      }
      // Move the associated point on the current curve and the previous curve
      this.curves[dragContext.index].points[0].x = mousePos.x;
      this.curves[dragContext.index].points[0].y = mousePos.y;
      this.curves[curPrev].points[3].x = mousePos.x;
      this.curves[curPrev].points[3].y = mousePos.y;
      
      this.newCurve(this.curves[dragContext.index].points, dragContext.index);
      this.newCurve(this.curves[curPrev].points, curPrev);

    }

    else if (dragContext.part === 2 || dragContext.part === 3) {
      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.curves[dragContext.index].points[dragContext.part - 1].x = mousePos.x;
      this.curves[dragContext.index].points[dragContext.part - 1].y = mousePos.y;
      //this.curves[dragContext.index].update();

      this.newCurve(this.curves[dragContext.index].points, dragContext.index);
    }

    else 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;
    }
  }
  
  checkRayIntersects(ray) {
    if (!this.fn_p) {
      this.initFns();
    }
    if (this.notDone) { return; }
    if (this.isInsideGlass(ray.p1) || this.isOnBoundary(ray.p1))//this.isInsideGlass(ray.p1) || this.isOnBoundary(ray.p1)) // if the first point of the ray is inside the circle, or on its boundary
    {
      this.rayLen = geometry.distance(ray.p1, ray.p2);
      let x = ray.p1.x + (this.stepSize / this.rayLen) * (ray.p2.x - ray.p1.x);
      let y = ray.p1.y + (this.stepSize / this.rayLen) * (ray.p2.y - ray.p1.y);
      const intersection_point = geometry.point(x, y);
      if (this.isInsideGlass(intersection_point)) // if intersection_point is inside the circle
        return intersection_point;
    }

    // Calculate current intersections
    this.countIntersections(ray.p1, ray.p2);
    if (this.curIntersections.shortest.i > -1 && this.curIntersections.shortest.j > -1) {
      const s_point = this.curves[this.curIntersections.shortest.i].get(this.curIntersections.curves[this.curIntersections.shortest.i][this.curIntersections.shortest.j]);
      return geometry.point(s_point.x, s_point.y);
    }
  }

  onRayIncident(ray, rayIndex, incidentPoint, surfaceMergingObjs) {
    if (!this.fn_p) {
      this.initFns();
      // This means that some error has been occuring eariler in parsing the equation.
      return {
        isAbsorbed: true
      };
    }
    this.error = null;
    try {
      var incidentData = this.getIncidentData(ray);
      //console.log("Checking incident...");
      if ((this.isInsideGlass(ray.p1) || this.isOutsideGlass(ray.p1)) && this.isOnBoundary(incidentPoint)) {// && this.isOnBoundary(incidentData.s_point)) {
        //const count = this.countIntersections(ray.p1);
        //if (!this.isOnBoundary(ray.p1) && this.isOnBoundary(incidentPoint)) // || this.isInsideGlass(geometry.point(incidentPoint.x, incidentPoint.y))))) // if the ray is hitting the circle from the outside, or from the inside (meaning that the point incidentPoint is on the boundary of the circle, and the point ray.p1 is inside/outside the circle)
      
        let r_bodyMerging_obj = ray.bodyMergingObj; // save the current bodyMergingObj of the ray, to pass it later to the reflected ray in the 'refract' function

        var incidentType = incidentData.incidentType;

        if (incidentType === 1) {
          // From inside to outside
          var n1 = this.getRefIndexAt(incidentData.s_point, ray);
          this.onRayExit(ray);
        } else if (incidentType === -1) {
          // From outside to inside
          var n1 = 1 / this.getRefIndexAt(incidentData.s_point, ray);
          this.onRayEnter(ray);
        } else if (incidentType === 0) {
          // Equivalent to not intersecting with the object (e.g. two interfaces overlap)
          var n1 = 1;
        } else {
          // The situation that may cause bugs (e.g. incident on an edge point)
          // To prevent shooting the ray to a wrong direction, absorb the ray
          return {
            isAbsorbed: true,
            isUndefinedBehavior: true
          };
        }
        return this.refract(ray, rayIndex, incidentData.s_point, incidentData.normal, n1, surfaceMergingObjs, r_bodyMerging_obj);
      } else {//if (this.countIntersections(ray.p1) % 2 === 1) {
        console.log("test");
        if (ray.bodyMergingObj === undefined)
          ray.bodyMergingObj = this.initRefIndex(ray); // Initialize the bodyMerging object of the ray
        const next_point = this.step(ray.p1, incidentPoint, ray);
        ray.p1 = incidentPoint;
        ray.p2 = next_point;
      }
    } catch (e) {
      this.error = e.toString();
      // Incident point is null
      console.log("Incident point is null!");
      console.log(this.error);
      return {
        isAbsorbed: true,
      };
    }
  }

  getIncidentType(ray) {
    return this.getIncidentData(ray).incidentType;
  }

  isOutsideGlass(point, point2) {
    return (!(!point2 && this.isOnBoundary(point)) && this.countIntersections(point, point2) % 2 === 0)
  }

  isInsideGlass(point, point2) {
    return (!(!point2 && this.isOnBoundary(point)) && this.countIntersections(point, point2) % 2 === 1)
  }
  
  isOnBoundary(p3) {
    // New (curve-oriented)
    for (let i = 0; i < this.curves.length; i++) {
      // First, check if the point is within the bounding box of the curve. This prevents unnecessary calculations of the projection
      if (p3.x <= this.bboxes[i].x.max && p3.x >= this.bboxes[i].x.min || p3.y <= this.bboxes[i].y.max && p3.y >= this.bboxes[i].y.min) {
        // Check how far away the nearest point on the curve to p3 is from p3
        const proj = this.curves[i].project({ x: p3.x, y: p3.y }).d;
        if (Math.pow(proj, 2) <= this.intersectTol) {
          return true;
        }
      }
    }
    return false;
  }

  /* Utility methods */

  getIncidentData(ray) {
    var incidentType;
    if (this.isInsideGlass(ray.p1, ray.p2)) {
      incidentType = 1; // Inside to outside
    } else {
      incidentType = -1; // Outside to inside
    }

    // No intersection
    if (this.curIntersections.shortest.i === -1) {
      return {
        s_point: null,
        normal: { x: NaN, y: NaN },
        incidentType: 0
      };
    }

    const i = this.curIntersections.shortest.i;
    const j = this.curIntersections.shortest.j;
    var s_point = this.curves[i].get(this.curIntersections.curves[i][j]);
    s_point.t = this.curIntersections.curves[i][j];

    // Handle if entire ray within bounds of the lens
    let len = geometry.distance(ray.p1, ray.p2);
    if (incidentType === 1 && Math.pow(len, 2) < this.curIntersections.shortest.val && this.isInsideGlass(ray.p2)) {//this.curIntersections.shortest.val > Simulator.MIN_RAY_SEGMENT_LENGTH_SQUARED * this.scene.lengthScale * this.scene.lengthScale) {

      return {
        s_point: geometry.point(ray.p1.x + (this.stepSize / len) * (ray.p2.x - ray.p1.x), ray.p1.y + (this.stepSize / len) * (ray.p2.y - ray.p1.y)),
        normal: { x: NaN, y: NaN },
        incidentType: 2
      }
    }

    // Get the normalized normal vector of the curve at the intersection point
    var normal = this.curves[i].normal(s_point.t);
    normal = geometry.point(normal.x, normal.y);

    // Reorient tangent if necessary, to ensure it's on the same side of the curve as p1. 
    //  This is necessary due to how BezierJS calculates normals to always be on the same side relative to the path from its first anchor point to its second anchor point. 
    //  All this does is flip the normal vector about the point on the curve which acts as its point of origin if the normal is pointing in the same direction of the ray as per the sign of the dot product.
    if (normal.x * (ray.p2.x - ray.p1.x) + normal.y * (ray.p2.y - ray.p1.y) > 0) {
      normal.x = -normal.x;
      normal.y = -normal.y;
    }

    return {
      s_point: geometry.point(s_point.x, s_point.y),
      normal: geometry.point(normal.x, normal.y),
      incidentType: incidentType
    };
  }
  
  countIntersections(p3, p4) {//, lens) {
    var cnt = 0;

    // Replace p4 with a point outside the glass if not passed
    if (typeof p4 === "undefined") {
      // Calculate a point that's guaranteed to be outside the glass by finding the minimum x coordinate
      // from all bounding boxes and subtracting a large offset
      let minX = Infinity;
      for (let i = 0; i < this.bboxes.length; i++) {
        if (this.bboxes[i].x.min < minX) {
          minX = this.bboxes[i].x.min;
        }
      }
      const outsidePoint = { x: minX - 1.23456789, y: p3.y + 1.23456789 };
      
      // Go thru each curve
      for (let i = 0; i < this.curves.length; i++) {
        // Get the current intersections, add the number of intersections found on the current curve from p3 to the outside point
        cnt += this.curves[i].lineIntersects(geometry.line(geometry.point(p3.x, p3.y), outsidePoint)).length;//, this.intersectTol);
      }
    } else {
      this.curIntersections = {
        curves: [],
        shortest: {
          val: Infinity,
          i: -1,
          j: -1
        }
      };

      // Scale the line from p3 to p4 in order to ensure the ray is always longer than the lens
      let mod_len = 0;
      for (let i = 0; i < this.bboxes.length; i++) {
        mod_len += Math.abs(this.bboxes[i].x.max) > Math.abs(this.bboxes[i].y.max) ? Math.abs(this.bboxes[i].x.max) : Math.abs(this.bboxes[i].y.max);
      }

      // Go thru each curve
      for (let i = 0; i < this.curves.length; i++) {
        // Get the current intersections
        this.curIntersections.curves.push(this.curves[i].lineIntersects(geometry.line(p3, geometry.point(p4.x + ((p4.x - p3.x) * mod_len), p4.y + ((p4.y - p3.y) * mod_len)))));
        
        // If the intersection is too close to the first point, ignore it
        for (let j = 0; j < this.curIntersections.curves[i].length; j++) {
          if (geometry.distanceSquared(p3, this.curves[i].get(this.curIntersections.curves[i][j])) < Simulator.MIN_RAY_SEGMENT_LENGTH_SQUARED * this.scene.lengthScale * this.scene.lengthScale) {
            this.curIntersections.curves[i].splice(j, 1);
          }
        }

        // Add the number of intersections found on the current curve from p3 to (0, 0)
        cnt += this.curIntersections.curves[i].length;
        
        // Keep track of the nearest intersection to p3
        for (let j = 0; j < this.curIntersections.curves[i].length; j++) {
          let tmp = geometry.distanceSquared(p3, this.curves[i].get(this.curIntersections.curves[i][j]));
          if (tmp < this.curIntersections.shortest.val) {
            this.curIntersections.shortest = {
              val: tmp,
              i: i,
              j: j
            };
          }
        }
      }
    }
    return cnt; // Returns the number of intersections between a horizontal ray (that originates from the point - p3) and the Free-shape glass object - this.
  }

  // Generate default control points from path (helper method)
  generateDefaultControlPoints(pts) {
    const cpVec1 = geometry.normalizeVec(geometry.point(pts[2].x - pts[0].x, pts[2].y - pts[0].y));
    const cpVec2 = geometry.normalizeVec(geometry.point(pts[3].x - pts[1].x, pts[3].y - pts[1].y));

    return [ geometry.point(pts[1].x + Math.floor(cpVec1.x * 50 + 0.5), pts[1].y + Math.floor(cpVec1.y * 50 + 0.5)), geometry.point(pts[2].x - Math.floor(cpVec2.x * 50 + 0.5), pts[2].y - Math.floor(cpVec2.y * 50 + 0.5)) ];
  }

  // Generate Poly Bezier (i.e. set of Bezier curves which will form the boundaries of the lens) from path
  generatePolyBezier() {
    var curCtrlPts = null;
    // Create one curve for each line
    for (var i = 0; i < this.path.length; i++) {
      curCtrlPts = this.generateDefaultControlPoints([ this.path[(i - 1 + this.path.length) % this.path.length], this.path[i], this.path[(i + 1) % this.path.length], this.path[(i + 2) % this.path.length] ]);
      this.newCurve([{ x: this.path[i].x, y: this.path[i].y }, { x: curCtrlPts[0].x, y: curCtrlPts[0].y }, { x: curCtrlPts[1].x, y: curCtrlPts[1].y }, { x: this.path[(i + 1) % this.path.length].x, y: this.path[(i + 1) % this.path.length].y }]);
    }
    //this.polyBezier = new PolyBezier(this.curves);
  }
  
  // Draw curve
  drawCurve(curve, 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();
  }

  // Draw line
  drawLine(p1, p2, canvasRenderer, strokeStyle) {
    const ctx = canvasRenderer.ctx;
    ctx.strokeStyle = strokeStyle || 'rgb(128,128,128)';
    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();
    ctx.closePath();

    // Draw points
    ctx.fillStyle = 'rgb(255,0,0)';
    [p1, p2].forEach((cur) => this.drawPoint(cur, canvasRenderer));
  }

  // 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);
  }

  // Create new curve, set new bounding box
  newCurve(pts, i) {
    // Making complete new poly-Bezier curve (default behavior)
    if (typeof i === "undefined" || i === -1) {
      this.curves.push(new Bezier(pts));
      this.bboxes.push(this.curves[this.curves.length - 1].bbox());

      return this.curves[this.curves.length - 1];
    } else {
      // Otherwise, handle the given index
      this.curves[i] = new Bezier(pts);
      this.bboxes[i] = this.curves[i].bbox();

      return this.curves[i];
    }
  }
};

export default CurveGrinGlass;