Source: core/simulationEngines/cpu/cpuRayRenderer.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.
 */

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

export function createCpuRayRenderState() {
  return {
    lastRay: null,
    lastIntersection: null
  };
}

export function beginCpuRayRendering(ctxMain, rendering) {
  if (!ctxMain) return;
  ctxMain.globalAlpha = 1;
  ctxMain.globalCompositeOperation =
    rendering.simulateColors ? 'screen' : 'source-over';
}

/**
 * Render one ray from an immutable ray-buffer entry and its completed hit.
 * Inactive rays break nearby-ray adjacency, matching inactive WebGPU source
 * slots. Prefix-sampled outgoing queues are compact and contain no such gaps.
 */
export function renderCpuRay({
  ray,
  hit,
  renderer,
  ctxMain,
  rendering,
  lengthScale,
  state,
  firstPass = false
}) {
  if (!isRayActive(ray)) {
    state.lastRay = null;
    state.lastIntersection = null;
    return false;
  }
  if (!(hit.s > 0)) {
    return false;
  }
  if (!renderer) return false;

  const rayLine = {
    p1: {
      x: ray.originX,
      y: ray.originY
    },
    p2: {
      x: ray.originX + ray.directionX,
      y: ray.originY + ray.directionY
    },
    powerS: ray.powerS,
    powerP: ray.powerP,
    wavelength: ray.wavelength
  };
  const hasFiniteEnd = Number.isFinite(hit.s);
  const endPoint = hasFiniteEnd
    ? {
        x: ray.originX + hit.s * ray.directionX,
        y: ray.originY + hit.s * ray.directionY
      }
    : null;
  const segmentLengthSquared = hasFiniteEnd
    ? geometry.distanceSquared(rayLine.p1, endPoint)
    : Infinity;
  const power = ray.powerS + ray.powerP;
  const appearance = getRayAppearance(
    ray,
    power,
    renderer,
    rendering
  );
  let drew = false;

  if (rendering.mode === 'rays' || rendering.mode === 'extended') {
    if (hasFiniteEnd) {
      renderer.drawSegment(
        geometry.line(rayLine.p1, endPoint),
        appearance.rayColor,
        rendering.showRayArrows,
        appearance.rayDash
      );
    } else {
      renderer.drawRay(
        rayLine,
        appearance.rayColor,
        rendering.showRayArrows,
        appearance.rayDash
      );
    }
    drew = true;

    if (rendering.mode === 'extended' && !firstPass) {
      const backwardRay = geometry.line(
        rayLine.p1,
        geometry.point(
          rayLine.p1.x - ray.directionX,
          rayLine.p1.y - ray.directionY
        )
      );
      renderer.drawRay(
        backwardRay,
        appearance.extendedRayColor,
        undefined,
        appearance.extendedRayDash
      );
      if (hasFiniteEnd) {
        renderer.drawRay(
          geometry.line(
            endPoint,
            geometry.point(
              endPoint.x + ray.directionX,
              endPoint.y + ray.directionY
            )
          ),
          appearance.forwardExtendedRayColor,
          undefined,
          appearance.forwardExtendedRayDash
        );
      }
    }
  }

  let observedPoint = null;
  let observed = false;
  if (rendering.mode === 'observer' && rendering.observer) {
    observedPoint =
      geometry.lineCircleIntersections(rayLine, rendering.observer)[2];
    if (observedPoint) {
      observed = hasFiniteEnd
        ? geometry.intersectionIsOnSegment(
            observedPoint,
            geometry.line(rayLine.p1, endPoint)
          )
        : geometry.intersectionIsOnRay(observedPoint, rayLine);
    }
  }

  if (
    rendering.mode === 'observer' &&
    state.lastRay &&
    rendering.observer
  ) {
    const intersection =
      geometry.linesIntersection(rayLine, state.lastRay);
    if (observed) {
      drew = renderObservedRay({
        ray,
        rayLine,
        endPoint,
        segmentLengthSquared,
        observedPoint,
        intersection,
        renderer,
        ctxMain,
        rendering,
        lengthScale,
        state
      }) || drew;
    }
    state.lastIntersection = intersection;
  }

  if (rendering.mode === 'images' && state.lastRay) {
    const intersection =
      geometry.linesIntersection(rayLine, state.lastRay);
    if (
      state.lastIntersection &&
      geometry.distanceSquared(
        state.lastIntersection,
        intersection
      ) < 25 * lengthScale * lengthScale
    ) {
      drawImagePoint({
        ray,
        rayLine,
        endPoint,
        segmentLengthSquared,
        intersection,
        renderer,
        ctxMain,
        rendering,
        previousRay: state.lastRay,
        includeVirtualObject: true
      });
      drew = true;
    }
    state.lastIntersection = intersection;
  }

  state.lastRay = rayLine;
  return drew;
}

export function finishCpuRayRendering({
  renderer,
  ctxMain,
  rendering,
  colorMode
}) {
  if (!renderer) return;
  if (rendering.simulateColors && !renderer.isSVG) {
    renderer.applyColorTransformation();
  }
  if (colorMode !== 'default') renderer.flush();
  if (ctxMain) ctxMain.globalAlpha = 1;
}

function renderObservedRay({
  ray,
  rayLine,
  endPoint,
  segmentLengthSquared,
  observedPoint,
  intersection,
  renderer,
  ctxMain,
  rendering,
  lengthScale,
  state
}) {
  const intersectionsAreNear =
    state.lastIntersection &&
    geometry.distanceSquared(
      state.lastIntersection,
      intersection
    ) < 25 * lengthScale * lengthScale;
  if (!intersectionsAreNear) {
    if (state.lastIntersection) {
      drawObservedExtension(
        ray,
        observedPoint,
        rayLine.p1,
        renderer,
        rendering
      );
      return true;
    }
    return false;
  }

  const pointsTowardImage = geometry.intersectionIsOnRay(
    intersection,
    geometry.line(observedPoint, rayLine.p1)
  );
  const observerIsAwayFromOrigin =
    geometry.distanceSquared(observedPoint, rayLine.p1) >
    1e-5 * lengthScale * lengthScale;
  if (!pointsTowardImage || !observerIsAwayFromOrigin) {
    drawObservedExtension(
      ray,
      observedPoint,
      rayLine.p1,
      renderer,
      rendering
    );
    return true;
  }

  drawImagePoint({
    ray,
    rayLine,
    endPoint,
    segmentLengthSquared,
    intersection,
    renderer,
    ctxMain,
    rendering,
    previousRay: state.lastRay,
    includeVirtualObject: false
  });
  const appearance = getNearbyRayAppearance(
    ray,
    state.lastRay,
    renderer,
    ctxMain,
    rendering
  );
  renderer.drawSegment(
    geometry.line(observedPoint, intersection),
    rendering.simulateColors
      ? appearance.color
      : rendering.getThemeRayColor(
          'observedRay',
          appearance.alpha
        ),
    undefined,
    rendering.getThemeRayDash(
      rendering.simulateColors ? 'colorObservedRay' : 'observedRay'
    )
  );
  return true;
}

function drawObservedExtension(
  ray,
  observedPoint,
  origin,
  renderer,
  rendering
) {
  const power = ray.powerS + ray.powerP;
  const color = rendering.simulateColors
    ? rendering.wavelengthToColor(
        ray.wavelength,
        power,
        shouldTransformColor(renderer, rendering)
      )
    : rendering.getThemeRayColor('observedRay', power);
  renderer.drawRay(
    geometry.line(observedPoint, origin),
    color,
    undefined,
    rendering.getThemeRayDash(
      rendering.simulateColors ? 'colorObservedRay' : 'observedRay'
    )
  );
}

function drawImagePoint({
  ray,
  rayLine,
  endPoint,
  segmentLengthSquared,
  intersection,
  renderer,
  ctxMain,
  rendering,
  previousRay,
  includeVirtualObject
}) {
  const appearance = getNearbyRayAppearance(
    ray,
    previousRay,
    renderer,
    ctxMain,
    rendering
  );
  const endVector = endPoint
    ? {
        x: endPoint.x - rayLine.p1.x,
        y: endPoint.y - rayLine.p1.y
      }
    : {
        x: ray.directionX,
        y: ray.directionY
      };
  const imageVector = {
    x: intersection.x - rayLine.p1.x,
    y: intersection.y - rayLine.p1.y
  };
  const rayPositionDot =
    imageVector.x * endVector.x + imageVector.y * endVector.y;

  let imageType;
  if (rayPositionDot < 0) {
    imageType = 'virtualImage';
  } else if (rayPositionDot < segmentLengthSquared) {
    imageType = 'realImage';
  } else if (includeVirtualObject) {
    imageType = 'virtualObject';
  } else {
    return;
  }
  const themedType = rendering.simulateColors
    ? `color${imageType[0].toUpperCase()}${imageType.slice(1)}`
    : imageType;
  renderer.drawPoint(
    intersection,
    rendering.simulateColors
      ? appearance.color
      : rendering.getThemeImageColor(
          imageType,
          appearance.alpha
        ),
    rendering.getThemeImageSize(themedType)
  );
}

function getNearbyRayAppearance(
  ray,
  previousRay,
  renderer,
  ctxMain,
  rendering
) {
  if (rendering.simulateColors) {
    return {
      color: rendering.wavelengthToColor(
        ray.wavelength,
        0.5 * (ray.powerS + ray.powerP),
        shouldTransformColor(renderer, rendering)
      )
    };
  }
  const nearbyAlpha = 0.5 * (
    ray.powerS +
    ray.powerP +
    previousRay.powerS +
    previousRay.powerP
  );
  const usesLegacyCanvas = rendering.colorMode === 'default';
  if (ctxMain && usesLegacyCanvas) {
    ctxMain.globalAlpha = nearbyAlpha;
  }
  return {
    alpha: usesLegacyCanvas ? 1 : nearbyAlpha
  };
}

function getRayAppearance(ray, power, renderer, rendering) {
  if (rendering.simulateColors) {
    const color = rendering.wavelengthToColor(
      ray.wavelength,
      power,
      shouldTransformColor(renderer, rendering)
    );
    return {
      rayColor: color,
      rayDash: rendering.getThemeRayDash('colorRay'),
      extendedRayColor: color,
      extendedRayDash:
        rendering.getThemeRayDash('colorExtendedRay'),
      forwardExtendedRayColor: color,
      forwardExtendedRayDash:
        rendering.getThemeRayDash('colorForwardExtendedRay')
    };
  }
  return {
    rayColor: rendering.getThemeRayColor('ray', power),
    rayDash: rendering.getThemeRayDash('ray'),
    extendedRayColor:
      rendering.getThemeRayColor('extendedRay', power),
    extendedRayDash:
      rendering.getThemeRayDash('extendedRay'),
    forwardExtendedRayColor:
      rendering.getThemeRayColor('forwardExtendedRay', power),
    forwardExtendedRayDash:
      rendering.getThemeRayDash('forwardExtendedRay')
  };
}

function shouldTransformColor(renderer, rendering) {
  return !renderer.isSVG && rendering.colorMode === 'default';
}

function isRayActive(ray) {
  return ray.powerS !== 0 || ray.powerP !== 0;
}