Source: core/primitive/drawPreparedCurve.js

/*
 * Copyright 2026 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.
 */

/**
 * Draw a prepared primitive curve for host-side diagnostics or highlighting.
 *
 * @param {Object} renderer
 * @param {Object} geometry
 * @param {number[]|Object} color
 * @param {number} [lineWidth=1]
 */
export function drawPreparedCurve(
  renderer,
  geometry,
  color,
  lineWidth = 1
) {
  const ctx = renderer.ctx;
  ctx.save();
  ctx.setLineDash([]);
  ctx.strokeStyle = renderer.rgbaToCssColor(color);
  ctx.lineWidth = lineWidth * renderer.lengthScale;
  ctx.beginPath();
  const start = getPreparedCurveEndpoint(geometry, false);
  ctx.moveTo(start.x, start.y);
  appendPreparedCurvePath(ctx, geometry);
  ctx.stroke();
  ctx.restore();
}

/**
 * Fill and outline an ordered set of prepared region-boundary curves.
 *
 * @param {Object} renderer
 * @param {Object[]} geometries
 * @param {number[]|Object} fillColor
 * @param {number[]|Object} outlineColor
 * @param {number} [lineWidth=1]
 */
export function drawPreparedRegion(
  renderer,
  geometries,
  fillColor,
  outlineColor,
  lineWidth = 1
) {
  const ctx = renderer.ctx;
  ctx.save();
  ctx.setLineDash([]);
  ctx.fillStyle = renderer.rgbaToCssColor(fillColor);
  ctx.strokeStyle = renderer.rgbaToCssColor(outlineColor);
  ctx.lineWidth = lineWidth * renderer.lengthScale;
  ctx.beginPath();

  let previousEnd = null;
  for (const geometry of geometries) {
    const start = getPreparedCurveEndpoint(geometry, false);
    const connectionTolerance = Math.max(
      geometry.positionTolerance,
      geometry.endpointTolerance ?? 0
    );
    if (
      !previousEnd ||
      Math.hypot(
        previousEnd.x - start.x,
        previousEnd.y - start.y
      ) > connectionTolerance
    ) {
      if (previousEnd) ctx.closePath();
      ctx.moveTo(start.x, start.y);
    } else {
      ctx.lineTo(start.x, start.y);
    }
    appendPreparedCurvePath(ctx, geometry);
    if (geometry.kind === 'circle') {
      ctx.closePath();
      previousEnd = null;
    } else {
      previousEnd = getPreparedCurveEndpoint(geometry, true);
    }
  }
  if (previousEnd) ctx.closePath();
  ctx.fill('evenodd');
  ctx.stroke();
  ctx.restore();
}

function appendPreparedCurvePath(ctx, geometry) {
  switch (geometry.kind) {
    case 'lineSegment':
    case 'smoothLineSegment': {
      const length = 1 / geometry.invLength;
      ctx.lineTo(
        geometry.originX + geometry.tangentX * length,
        geometry.originY + geometry.tangentY * length
      );
      break;
    }
    case 'circularArc': {
      const chordLength = 1 / geometry.invChordLength;
      const halfChordX = geometry.tangentX * chordLength * 0.5;
      const halfChordY = geometry.tangentY * chordLength * 0.5;
      const centerOffset =
        chordLength * (1 - geometry.bulge * geometry.bulge) /
        (4 * geometry.bulge);
      const centerX = geometry.originX - geometry.tangentY * centerOffset;
      const centerY = geometry.originY + geometry.tangentX * centerOffset;
      const startX = geometry.originX - halfChordX;
      const startY = geometry.originY - halfChordY;
      const startAngle = Math.atan2(startY - centerY, startX - centerX);
      const sweep = 4 * Math.atan(geometry.bulge);
      ctx.arc(
        centerX,
        centerY,
        chordLength * (1 + geometry.bulge * geometry.bulge) /
          (4 * Math.abs(geometry.bulge)),
        startAngle,
        startAngle + sweep,
        sweep < 0
      );
      break;
    }
    case 'cubicBezier': {
      const scale = 1 / geometry.invScale;
      const worldX = x => geometry.originX + x * scale;
      const worldY = y => geometry.originY + y * scale;
      ctx.bezierCurveTo(
        worldX(geometry.control1X),
        worldY(geometry.control1Y),
        worldX(geometry.control2X),
        worldY(geometry.control2Y),
        worldX(geometry.endX),
        worldY(geometry.endY)
      );
      break;
    }
    case 'circle':
      ctx.arc(
        geometry.centerX,
        geometry.centerY,
        1 / Math.abs(geometry.signedInvRadius),
        0,
        2 * Math.PI
      );
      break;
    default:
      throw new TypeError(
        `Unsupported prepared curve kind: ${JSON.stringify(geometry.kind)}`
      );
  }
}

function getPreparedCurveEndpoint(geometry, end) {
  switch (geometry.kind) {
    case 'lineSegment':
    case 'smoothLineSegment': {
      if (!end) {
        return { x: geometry.originX, y: geometry.originY };
      }
      const length = 1 / geometry.invLength;
      return {
        x: geometry.originX + geometry.tangentX * length,
        y: geometry.originY + geometry.tangentY * length
      };
    }
    case 'circularArc': {
      const direction = end ? 0.5 : -0.5;
      const chordLength = 1 / geometry.invChordLength;
      return {
        x: geometry.originX +
          direction * geometry.tangentX * chordLength,
        y: geometry.originY +
          direction * geometry.tangentY * chordLength
      };
    }
    case 'cubicBezier': {
      const scale = 1 / geometry.invScale;
      return {
        x: geometry.originX +
          (end ? geometry.endX : geometry.startX) * scale,
        y: geometry.originY +
          (end ? geometry.endY : geometry.startY) * scale
      };
    }
    case 'circle': {
      const radius = 1 / Math.abs(geometry.signedInvRadius);
      return {
        x: geometry.centerX + radius,
        y: geometry.centerY
      };
    }
    default:
      throw new TypeError(
        `Unsupported prepared curve kind: ${JSON.stringify(geometry.kind)}`
      );
  }
}