Source: core/sceneObjs/other/CustomArcSurface.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 BaseCustomSurface from '../BaseCustomSurface.js';
import i18next from 'i18next';
import Simulator from '../../Simulator.js';
import geometry from '../../geometry.js';

/**
 * A custom surface with shape of a circular arc.
 * 
 * Tools -> Other -> Custom Arc Surface
 * @class
 * @extends BaseCustomSurface
 * @memberof sceneObjs
 * @property {Point} p1 - The first endpoint.
 * @property {Point} p2 - The second endpoint.
 * @property {Point} p3 - The control point on the arc.
 * @property {Array<OutRay>} outRays - The expressions of the outgoing rays.
 * @property {boolean} twoSided - Whether the surface is two-sided.
 */
class CustomArcSurface extends BaseCustomSurface {
  static type = 'CustomArcSurface';
  static isOptical = true;
  static mergesWithGlass = true;
  static serializableDefaults = {
    p1: null,
    p2: null,
    p3: null,
    outRays: [
      {
        eqnTheta: "\\theta_0",
        eqnP: "0.7\\cdot P_0"
      },
      {
        eqnTheta: "\\pi-\\theta_0",
        eqnP: "P_0-P_1"
      }
    ],
    twoSided: false,
  };

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

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

    // Use theme colors and width
    const theme = this.scene.theme.customSurface;
    const color = isHovered ? this.scene.highlightColorCss : canvasRenderer.rgbaToCssColor(theme.color);
    const baseWidth = theme.width * ls;

    ctx.fillStyle = 'rgb(255,0,255)';
    if (this.p3 && this.p2) {
      var center = geometry.linesIntersection(geometry.perpendicularBisector(geometry.line(this.p1, this.p3)), geometry.perpendicularBisector(geometry.line(this.p2, this.p3)));
      if (isFinite(center.x) && isFinite(center.y)) {
        var r = geometry.distance(center, this.p3);
        var a1 = Math.atan2(this.p1.y - center.y, this.p1.x - center.x);
        var a2 = Math.atan2(this.p2.y - center.y, this.p2.x - center.x);
        var a3 = Math.atan2(this.p3.y - center.y, this.p3.x - center.x);
        var anticlockwise = (a2 < a3 && a3 < a1) || (a1 < a2 && a2 < a3) || (a3 < a1 && a1 < a2);
        
        if (this.twoSided) {
          // For two-sided surfaces, draw just one arc with the full width
          ctx.strokeStyle = color;
          ctx.lineWidth = baseWidth;
          ctx.setLineDash([]);
          ctx.beginPath();
          ctx.arc(center.x, center.y, r, a1, a2, anticlockwise);
          ctx.stroke();
        } else {
          // For one-sided surfaces, draw two arcs each with half the width
          const halfWidth = baseWidth / 2;
          
          // Draw the main solid arc
          ctx.strokeStyle = color;
          ctx.lineWidth = halfWidth;
          ctx.setLineDash([]);
          ctx.beginPath();
          ctx.arc(center.x, center.y, r, a1, a2, anticlockwise);
          ctx.stroke();

          // Determine which side should be dashed in a rotation-invariant way
          // Calculate the cross product to determine which side p3 is on relative to p2->p1 (exchanged roles)
          var crossProduct = (this.p1.x - this.p2.x) * (this.p3.y - this.p2.y) - (this.p1.y - this.p2.y) * (this.p3.x - this.p2.x);
          
          // Rule: left side is solid, right side is dashed (relative to p1->p2 direction)
          // By using p2->p1 cross product (exchanged roles), we get the correct left=solid, right=dashed behavior
          var drawDashedInner = crossProduct < 0;
          
          // Calculate the offset for the dashed arc
          var radiusOffset = drawDashedInner ? -halfWidth : halfWidth;
          var dashedRadius = r + radiusOffset;
          
          if (dashedRadius > 0) {
            // Draw the dashed arc
            ctx.lineWidth = halfWidth;
            ctx.setLineDash(theme.dash.map(d => d * ls));
            ctx.beginPath();
            ctx.arc(center.x, center.y, dashedRadius, a1, a2, anticlockwise);
            ctx.stroke();
          }
        }
        
        if (isHovered) {
          ctx.fillRect(this.p3.x - 1.5 * ls, this.p3.y - 1.5 * ls, 3 * ls, 3 * ls);
          ctx.fillStyle = 'rgb(255,0,0)';
          ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls);
          ctx.fillRect(this.p2.x - 1.5 * ls, this.p2.y - 1.5 * ls, 3 * ls, 3 * ls);
        }
      } else {
        // The three points on the arc is colinear. Treat as a line segment.
        if (this.twoSided) {
          // For two-sided surfaces, draw just one line with the full width
          ctx.strokeStyle = color;
          ctx.lineWidth = baseWidth;
          ctx.setLineDash([]);
          ctx.beginPath();
          ctx.moveTo(this.p1.x, this.p1.y);
          ctx.lineTo(this.p2.x, this.p2.y);
          ctx.stroke();
        } else {
          // For one-sided surfaces, draw two lines each with half the width
          const halfWidth = baseWidth / 2;
          
          // Draw the main solid line
          ctx.strokeStyle = color;
          ctx.lineWidth = halfWidth;
          ctx.setLineDash([]);
          ctx.beginPath();
          ctx.moveTo(this.p1.x, this.p1.y);
          ctx.lineTo(this.p2.x, this.p2.y);
          ctx.stroke();

          // Calculate the perpendicular vector (normal to the line)
          const dx = this.p2.x - this.p1.x;
          const dy = this.p2.y - this.p1.y;
          const length = Math.sqrt(dx * dx + dy * dy);
          
          if (length > 0) {
            // Normalize the perpendicular vector
            const perpX = -dy / length;
            const perpY = dx / length;
            
            // Shift distance is the half width
            const shiftDistance = halfWidth;
            
            // Calculate shifted points (shift toward the side where rays are ignored)
            // Since getIncidentType returns -1 for "outside to inside", we want to shift
            // toward the "outside" (negative cross product side)
            const shiftedP1x = this.p1.x - perpX * shiftDistance;
            const shiftedP1y = this.p1.y - perpY * shiftDistance;
            const shiftedP2x = this.p2.x - perpX * shiftDistance;
            const shiftedP2y = this.p2.y - perpY * shiftDistance;

            // Draw the dashed line with half width
            ctx.lineWidth = halfWidth;
            ctx.setLineDash(theme.dash.map(d => d * ls));
            ctx.beginPath();
            ctx.moveTo(shiftedP1x, shiftedP1y);
            ctx.lineTo(shiftedP2x, shiftedP2y);
            ctx.stroke();
          }
        }

        if (isHovered) {
          ctx.fillRect(this.p3.x - 1.5 * ls, this.p3.y - 1.5 * ls, 3 * ls, 3 * ls);
          ctx.fillStyle = 'rgb(255,0,0)';
          ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls);
          ctx.fillRect(this.p2.x - 1.5 * ls, this.p2.y - 1.5 * ls, 3 * ls, 3 * ls);
        }
      }
    } else if (this.p2) {
      ctx.fillStyle = 'rgb(255,0,0)';
      ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls);
      ctx.fillRect(this.p2.x - 1.5 * ls, this.p2.y - 1.5 * ls, 3 * ls, 3 * ls);
    } else {
      ctx.fillStyle = 'rgb(255,0,0)';
      ctx.fillRect(this.p1.x - 1.5 * ls, this.p1.y - 1.5 * ls, 3 * ls, 3 * ls);
    }

    // Reset line dash
    ctx.setLineDash([]);
  }


  move(diffX, diffY) {
    this.p1.x = this.p1.x + diffX;
    this.p1.y = this.p1.y + diffY;
    this.p2.x = this.p2.x + diffX;
    this.p2.y = this.p2.y + diffY;
    this.p3.x = this.p3.x + diffX;
    this.p3.y = this.p3.y + diffY;

    return true;
  }

  rotate(angle, center) {
    // Use p3 as default rotation center if none is provided
    const rotationCenter = center || this.p3;
    
    // Calculate differences from rotation center for all points
    const diff_p1_x = this.p1.x - rotationCenter.x;
    const diff_p1_y = this.p1.y - rotationCenter.y;
    const diff_p2_x = this.p2.x - rotationCenter.x;
    const diff_p2_y = this.p2.y - rotationCenter.y;
    const diff_p3_x = this.p3.x - rotationCenter.x;
    const diff_p3_y = this.p3.y - rotationCenter.y;

    // Apply rotation matrix to p1
    this.p1.x = rotationCenter.x + diff_p1_x * Math.cos(angle) - diff_p1_y * Math.sin(angle);
    this.p1.y = rotationCenter.y + diff_p1_x * Math.sin(angle) + diff_p1_y * Math.cos(angle);

    // Apply rotation matrix to p2
    this.p2.x = rotationCenter.x + diff_p2_x * Math.cos(angle) - diff_p2_y * Math.sin(angle);
    this.p2.y = rotationCenter.y + diff_p2_x * Math.sin(angle) + diff_p2_y * Math.cos(angle);
    
    // Apply rotation matrix to p3
    this.p3.x = rotationCenter.x + diff_p3_x * Math.cos(angle) - diff_p3_y * Math.sin(angle);
    this.p3.y = rotationCenter.y + diff_p3_x * Math.sin(angle) + diff_p3_y * Math.cos(angle);
    
    return true;
  }
  
  scale(scale, center) {
    // Use p3 as default scaling center if none is provided
    const scalingCenter = center || this.p3;
    
    // Calculate differences from scaling center for all points
    const diff_p1_x = this.p1.x - scalingCenter.x;
    const diff_p1_y = this.p1.y - scalingCenter.y;
    const diff_p2_x = this.p2.x - scalingCenter.x;
    const diff_p2_y = this.p2.y - scalingCenter.y;
    const diff_p3_x = this.p3.x - scalingCenter.x;
    const diff_p3_y = this.p3.y - scalingCenter.y;
    
    // Apply scaling to p1
    this.p1.x = scalingCenter.x + diff_p1_x * scale;
    this.p1.y = scalingCenter.y + diff_p1_y * scale;
    
    // Apply scaling to p2
    this.p2.x = scalingCenter.x + diff_p2_x * scale;
    this.p2.y = scalingCenter.y + diff_p2_y * scale;
    
    // Apply scaling to p3
    this.p3.x = scalingCenter.x + diff_p3_x * scale;
    this.p3.y = scalingCenter.y + diff_p3_y * scale;
    
    return true;
  }
  
  getDefaultCenter() {
    return this.p3;
  }

  onConstructMouseDown(mouse, ctrl, shift) {
    if (!this.constructionPoint) {
      // Initialize the construction stage.
      this.constructionPoint = mouse.getPosSnappedToGrid();
      this.p1 = this.constructionPoint;
      this.p2 = null;
      this.p3 = null;
    }

    if (!this.p2 && !this.p3) {
      this.p2 = mouse.getPosSnappedToGrid();
      return;
    }

    if (this.p2 && !this.p3 && !mouse.snapsOnPoint(this.p1)) {
      if (shift) {
        this.p2 = mouse.getPosSnappedToDirection(this.p1, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }]);
      } else {
        this.p2 = mouse.getPosSnappedToGrid();
      }
      this.p3 = mouse.getPosSnappedToGrid();
      return;
    }
  }

  onConstructMouseMove(mouse, ctrl, shift) {
    if (!this.p3 && !mouse.isOnPoint(this.p1)) {
      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;

      return;
    }
    if (this.p3) {
      this.p3 = mouse.getPosSnappedToGrid();
      return;
    }
  }

  onConstructMouseUp(mouse, ctrl, shift) {
    if (this.p2 && !this.p3 && !mouse.isOnPoint(this.p1)) {
      this.p3 = mouse.getPosSnappedToGrid();
      return;
    }
    if (this.p3 && !mouse.isOnPoint(this.p2)) {
      this.p3 = mouse.getPosSnappedToGrid();
      delete this.constructionPoint;
      return {
        isDone: true
      };
    }
  }

  checkMouseOver(mouse) {
    let dragContext = {};
    if (mouse.isOnPoint(this.p1) && geometry.distanceSquared(mouse.pos, this.p1) <= geometry.distanceSquared(mouse.pos, this.p2) && geometry.distanceSquared(mouse.pos, this.p1) <= geometry.distanceSquared(mouse.pos, this.p3)) {
      dragContext.part = 1;
      dragContext.targetPoint = geometry.point(this.p1.x, this.p1.y);
      return dragContext;
    }
    if (mouse.isOnPoint(this.p2) && geometry.distanceSquared(mouse.pos, this.p2) <= geometry.distanceSquared(mouse.pos, this.p3)) {
      dragContext.part = 2;
      dragContext.targetPoint = geometry.point(this.p2.x, this.p2.y);
      return dragContext;
    }
    if (mouse.isOnPoint(this.p3)) {
      dragContext.part = 3;
      dragContext.targetPoint = geometry.point(this.p3.x, this.p3.y);
      return dragContext;
    }

    var center = geometry.linesIntersection(geometry.perpendicularBisector(geometry.line(this.p1, this.p3)), geometry.perpendicularBisector(geometry.line(this.p2, this.p3)));
    const mousePos = mouse.getPosSnappedToGrid();
    if (isFinite(center.x) && isFinite(center.y)) {
      var r = geometry.distance(center, this.p3);
      var a1 = Math.atan2(this.p1.y - center.y, this.p1.x - center.x);
      var a2 = Math.atan2(this.p2.y - center.y, this.p2.x - center.x);
      var a3 = Math.atan2(this.p3.y - center.y, this.p3.x - center.x);
      var a_m = Math.atan2(mouse.pos.y - center.y, mouse.pos.x - center.x);
      if (Math.abs(geometry.distance(center, mouse.pos) - r) < mouse.getClickExtent() && (((a2 < a3 && a3 < a1) || (a1 < a2 && a2 < a3) || (a3 < a1 && a1 < a2)) == ((a2 < a_m && a_m < a1) || (a1 < a2 && a2 < a_m) || (a_m < a1 && a1 < a2)))) {
        // Dragging the entire obj
        dragContext.part = 0;
        dragContext.mousePos0 = mousePos; // Mouse position when the user starts dragging
        dragContext.mousePos1 = mousePos; // Mouse position at the last moment during dragging
        dragContext.snapContext = {};
        return dragContext;
      }
    } else {
      // The three points on the arc is colinear. Treat as a line segment.
      if (mouse.isOnSegment(this)) {
        dragContext.part = 0;
        dragContext.mousePos0 = mousePos; // Mouse position when the user starts dragging
        dragContext.mousePos1 = mousePos; // Mouse position at the last moment during dragging
        dragContext.snapContext = {};
        return dragContext;
      }
    }
  }

  onDrag(mouse, dragContext, ctrl, shift) {
    var basePoint;
    if (dragContext.part == 1) {
      // Dragging the first endpoint
      basePoint = ctrl ? geometry.segmentMidpoint(dragContext.originalObj) : dragContext.originalObj.p2;

      this.p1 = shift ? mouse.getPosSnappedToDirection(basePoint, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }, { x: (dragContext.originalObj.p2.x - dragContext.originalObj.p1.x), y: (dragContext.originalObj.p2.y - dragContext.originalObj.p1.y) }]) : mouse.getPosSnappedToGrid();
      this.p2 = ctrl ? geometry.point(2 * basePoint.x - this.p1.x, 2 * basePoint.y - this.p1.y) : basePoint;
    }
    if (dragContext.part == 2) {
      // Dragging the second endpoint
      basePoint = ctrl ? geometry.segmentMidpoint(dragContext.originalObj) : dragContext.originalObj.p1;

      this.p2 = shift ? mouse.getPosSnappedToDirection(basePoint, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 1, y: -1 }, { x: (dragContext.originalObj.p2.x - dragContext.originalObj.p1.x), y: (dragContext.originalObj.p2.y - dragContext.originalObj.p1.y) }]) : mouse.getPosSnappedToGrid();
      this.p1 = ctrl ? geometry.point(2 * basePoint.x - this.p2.x, 2 * basePoint.y - this.p2.y) : basePoint;
    }
    if (dragContext.part == 3) {
      // Dragging the third endpoint
      this.p3 = mouse.getPosSnappedToGrid();
    }
    if (dragContext.part == 0) {
      // Dragging the entire obj
      if (shift) {
        var mousePos = mouse.getPosSnappedToDirection(dragContext.mousePos0, [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: (dragContext.originalObj.p2.x - dragContext.originalObj.p1.x), y: (dragContext.originalObj.p2.y - dragContext.originalObj.p1.y) }, { x: (dragContext.originalObj.p2.y - dragContext.originalObj.p1.y), y: -(dragContext.originalObj.p2.x - dragContext.originalObj.p1.x) }], dragContext.snapContext);
      } else {
        var mousePos = mouse.getPosSnappedToGrid();;
        dragContext.snapContext = {}; // Unlock the dragging direction when the user release the shift key
      }

      var mouseDiffX = dragContext.mousePos1.x - mousePos.x; // The X difference between the mouse position now and at the previous moment
      var mouseDiffY = dragContext.mousePos1.y - mousePos.y; // The Y difference between the mouse position now and at the previous moment
      // Move the first point
      this.p1.x = this.p1.x - mouseDiffX;
      this.p1.y = this.p1.y - mouseDiffY;
      // Move the second point
      this.p2.x = this.p2.x - mouseDiffX;
      this.p2.y = this.p2.y - mouseDiffY;

      this.p3.x = this.p3.x - mouseDiffX;
      this.p3.y = this.p3.y - mouseDiffY;

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

  checkRayIntersects(ray) {
    if (!this.p3) { return null; }
    var incidentData = this.getIncidentData(ray);
    if (!this.twoSided && incidentData.incidentType == -1) {
      return null;
    }
    return incidentData.s_point;
  }

  onRayIncident(ray, rayIndex, incidentPoint, surfaceMergingObjs) {
    var incidentData = this.getIncidentData(ray);
    var center = geometry.linesIntersection(geometry.perpendicularBisector(geometry.line(this.p1, this.p3)), geometry.perpendicularBisector(geometry.line(this.p2, this.p3)));
    
    var incidentPos;
    
    if (isFinite(center.x) && isFinite(center.y)) {
      // Arc case: calculate incidentPos using angle relative to center
      var a1 = Math.atan2(this.p1.y - center.y, this.p1.x - center.x);
      var a2 = Math.atan2(this.p2.y - center.y, this.p2.x - center.x);
      var a_incident = Math.atan2(incidentPoint.y - center.y, incidentPoint.x - center.x);
      
      // Shift angles so p1 is 0
      var a1_shifted = 0;
      var a2_shifted = a2 - a1;
      var a_incident_shifted = a_incident - a1;
      
      // Normalize to 0-2π range
      if (a2_shifted < 0) a2_shifted += 2 * Math.PI;
      if (a_incident_shifted < 0) a_incident_shifted += 2 * Math.PI;
      
      // Calculate incidentPos: simple proportion along the arc
      if (Math.abs(a2_shifted) > 1e-10) {
        incidentPos = -1 + 2 * a_incident_shifted / a2_shifted;
      } else {
        incidentPos = 0; // Degenerate case
      }
    } else {
      // Colinear case: fall back to line segment behavior (same as CustomSurface)
      const dist1 = geometry.distance(incidentPoint, this.p1);
      const dist2 = geometry.distance(incidentPoint, this.p2);
      incidentPos = -1 + 2 * dist1 / (dist1 + dist2);
    }
    
    return this.handleOutRays(ray, rayIndex, incidentPoint, incidentData.normal, incidentPos, surfaceMergingObjs, ray.bodyMergingObj);
  }

  getIncidentData(ray) {
    var center = geometry.linesIntersection(geometry.perpendicularBisector(geometry.line(this.p1, this.p3)), geometry.perpendicularBisector(geometry.line(this.p2, this.p3)));
    
    if (isFinite(center.x) && isFinite(center.y)) {
      // Arc case: check all valid intersections and their incident types
      var rp_temp = geometry.lineCircleIntersections(geometry.line(ray.p1, ray.p2), geometry.circle(center, this.p2));
      var validIntersections = [];
      
      for (var i = 1; i <= 2; i++) {
        var isValid = !geometry.intersectionIsOnSegment(geometry.linesIntersection(geometry.line(this.p1, this.p2), geometry.line(this.p3, rp_temp[i])), geometry.line(this.p3, rp_temp[i])) && 
                      geometry.intersectionIsOnRay(rp_temp[i], ray) && 
                      geometry.distanceSquared(rp_temp[i], ray.p1) > Simulator.MIN_RAY_SEGMENT_LENGTH_SQUARED * this.scene.lengthScale * this.scene.lengthScale;
        
        if (isValid) {
          // Calculate local tangent direction at this intersection point (perpendicular to radius)
          var radialX = rp_temp[i].x - center.x;
          var radialY = rp_temp[i].y - center.y;
          var tangentX = -radialY;  // 90° rotation of radial vector
          var tangentY = radialX;
          
          // Determine arc direction using the same logic as drawing (considering p3 position)
          var a1 = Math.atan2(this.p1.y - center.y, this.p1.x - center.x);
          var a2 = Math.atan2(this.p2.y - center.y, this.p2.x - center.x);
          var a3 = Math.atan2(this.p3.y - center.y, this.p3.x - center.x);
          var anticlockwise = (a2 < a3 && a3 < a1) || (a1 < a2 && a2 < a3) || (a3 < a1 && a1 < a2);
          
          // If arc goes anticlockwise, flip tangent to match p1->p2 direction
          if (anticlockwise) {
            tangentX = -tangentX;
            tangentY = -tangentY;
          }
          
          // Cross product of ray direction with local tangent
          var rcrosst = (ray.p2.x - ray.p1.x) * tangentY - (ray.p2.y - ray.p1.y) * tangentX;
          
          var incidentType = NaN;
          if (rcrosst > 0) {
            incidentType = 1; // From inside to outside
          } else if (rcrosst < 0) {
            incidentType = -1; // From outside to inside
          }
          
          // Calculate normal vector (radial direction from center)
          var normal = { 
            x: center.x - rp_temp[i].x, 
            y: center.y - rp_temp[i].y 
          };
          
          validIntersections.push({
            point: rp_temp[i],
            distance: geometry.distanceSquared(ray.p1, rp_temp[i]),
            incidentType: incidentType,
            normal: normal
          });
        }
      }
      
      if (validIntersections.length === 0) {
        return { s_point: null, normal: null, incidentType: NaN };
      }
      
      var selectedIntersection;
      
      // For two-sided surfaces, use the nearest intersection
      if (this.twoSided) {
        validIntersections.sort((a, b) => a.distance - b.distance);
        selectedIntersection = validIntersections[0];
      } else {
        // For one-sided surfaces, prefer forward-facing intersections (incidentType = 1)
        var forwardIntersections = validIntersections.filter(intersection => intersection.incidentType === 1);
        if (forwardIntersections.length > 0) {
          // Use the nearest forward-facing intersection
          forwardIntersections.sort((a, b) => a.distance - b.distance);
          selectedIntersection = forwardIntersections[0];
        } else {
          // If no forward-facing intersections, use the nearest intersection
          validIntersections.sort((a, b) => a.distance - b.distance);
          selectedIntersection = validIntersections[0];
        }
      }
      
      return { 
        s_point: selectedIntersection.point, 
        normal: selectedIntersection.normal, 
        incidentType: selectedIntersection.incidentType 
      };
    } else {
      // Colinear case: treat as a line segment
      var rp_temp = geometry.linesIntersection(geometry.line(ray.p1, ray.p2), geometry.line(this.p1, this.p2));

      if (geometry.intersectionIsOnSegment(rp_temp, this) && geometry.intersectionIsOnRay(rp_temp, ray)) {
        // Calculate incident type using chord direction (same as CustomSurface)
        var rcrosss = (ray.p2.x - ray.p1.x) * (this.p2.y - this.p1.y) - (ray.p2.y - ray.p1.y) * (this.p2.x - this.p1.x);
        var incidentType;
        if (rcrosss > 0) {
          incidentType = 1; // From inside to outside
        } else if (rcrosss < 0) {
          incidentType = -1; // From outside to inside
        } else {
          incidentType = NaN;
        }
        
        // Calculate normal vector for line segment
        const rdots = (ray.p2.x - ray.p1.x) * (this.p2.x - this.p1.x) + (ray.p2.y - ray.p1.y) * (this.p2.y - this.p1.y);
        const ssq = (this.p2.x - this.p1.x) * (this.p2.x - this.p1.x) + (this.p2.y - this.p1.y) * (this.p2.y - this.p1.y);
        const normal = { 
          x: rdots * (this.p2.x - this.p1.x) - ssq * (ray.p2.x - ray.p1.x), 
          y: rdots * (this.p2.y - this.p1.y) - ssq * (ray.p2.y - ray.p1.y) 
        };

        return { s_point: rp_temp, normal: normal, incidentType: incidentType };
      } else {
        return { s_point: null, normal: null, incidentType: NaN };
      }
    }
  }


};

export default CustomArcSurface;