Source: core/primitive/regionMembership.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 { BVH_OWNER_KIND_MASKS } from './bvh.js';
import { intersectRayBounds } from './bvhTraversal.js';
import { countCurveRayCrossings } from './curveRayCrossings.js';
import { getIntersectionTolerancePolicy } from './numeric.js';

/**
 * Create reusable output storage for one region-membership ray cast.
 *
 * @param {number} regionCount
 * @returns {Object}
 */
export function createRegionMembershipResult(regionCount) {
  return {
    regionMask: new Uint8Array(regionCount),
    ambiguousRegionMask: new Uint8Array(regionCount),
    ambiguousCurveId: -1,
    nearestForwardS: Infinity,
    discardRay: false
  };
}

/**
 * Determine region membership by counting every forward region-boundary
 * crossing reached through the BVH. Nodes without region boundaries are
 * ignored using their owner-kind mask.
 *
 * @param {Object} description
 * @param {Object} ray
 * @param {Object} result
 * @param {number} numericEpsilon
 * @returns {Object}
 */
export function traverseBvhForRegionMembership(
  description,
  ray,
  result,
  numericEpsilon
) {
  result.regionMask.fill(0);
  result.ambiguousRegionMask.fill(0);
  result.ambiguousCurveId = -1;
  result.nearestForwardS = Infinity;
  result.discardRay = false;

  const { root, nodes, curveIds } = description.bvh;
  if (
    root < 0 ||
    !(nodes[root].ownerKindMask & BVH_OWNER_KIND_MASKS.region)
  ) {
    return result;
  }

  const originTolerance =
    description.numericalTolerances?.forwardDistance ?? 0;
  const tolerancePolicy = getIntersectionTolerancePolicy(numericEpsilon);
  if (!Number.isFinite(intersectRayBounds(
    ray,
    nodes[root].bounds,
    0
  ))) {
    return result;
  }

  const crossing = {};
  const stack = [root];
  while (stack.length > 0) {
    const nodeIndex = stack.pop();
    const node = nodes[nodeIndex];

    if (node.count > 0) {
      for (let offset = 0; offset < node.count; offset++) {
        const curveId = curveIds[node.start + offset];
        const curve = description.curves[curveId];
        if (curve.ownerKind !== 'region') continue;

        countCurveRayCrossings(
          curve.geometry,
          ray,
          {
            numericEpsilon,
            tolerancePolicy,
            originTolerance: Math.max(
              curve.geometry.positionTolerance,
              originTolerance
            )
          },
          crossing
        );
        result.nearestForwardS = Math.min(
          result.nearestForwardS,
          crossing.nearestForwardS
        );
        if (crossing.count & 1) {
          result.regionMask[curve.ownerId] ^= 1;
        }
        if (crossing.ambiguous) {
          result.ambiguousRegionMask[curve.ownerId] = 1;
          if (result.ambiguousCurveId < 0) {
            result.ambiguousCurveId = curveId;
          }
        }
      }
      continue;
    }

    for (const childIndex of node.children) {
      testAndPushRegionChild(
        description,
        ray,
        childIndex,
        stack
      );
    }
  }
  if (!Number.isFinite(result.nearestForwardS)) {
    result.regionMask.fill(0);
    result.ambiguousRegionMask.fill(0);
    result.ambiguousCurveId = -1;
  }
  return result;
}

function testAndPushRegionChild(
  description,
  ray,
  nodeIndex,
  stack
) {
  const node = description.bvh.nodes[nodeIndex];
  if (!(node.ownerKindMask & BVH_OWNER_KIND_MASKS.region)) return;
  if (Number.isFinite(intersectRayBounds(
    ray,
    node.bounds,
    0
  ))) {
    stack.push(nodeIndex);
  }
}