All files / core/src/utilities/voxelSlab/shapes createUnionShape.ts

92.59% Statements 25/27
100% Branches 8/8
71.42% Functions 5/7
95.83% Lines 23/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95                                                              609x 216x                         37226x   37226x 74452x       58266x       37226x 6315x     30911x   30911x   30911x 27355x       27355x 27345x 27345x     10x 10x 10x     30911x     393x           393x 786x 786x        
import type { Point2 } from '../../../types';
import type { Point3 } from '../../../types';
import type { IndexSpaceSlab } from '../indexSpaceSlab';
import type { VoxelSlabShape } from './shapeGeometry';
 
/**
 * The union of several plane-anchored shapes, as one shape.
 *
 * A brush that the user drags paints the union of one disc per sample along the
 * stroke, and a single shape cannot describe that. Every member must share the
 * annotation plane and the normal that the iterator is built with, because the
 * runs of all members are emitted along one column axis.
 *
 * The runs stay exact. Each member's runs are merged into a disjoint, ascending
 * sequence, so `iterateVoxelsInShape` visits a voxel that two members share
 * exactly once - which matters, because a fill that writes a voxel twice
 * records two undo entries for it.
 *
 * Cost is proportional to the number of members per row, not per voxel. A
 * per-voxel `containsPoint` over the same members costs the member count for
 * every voxel of the bounding box instead.
 *
 * See `docs/docs/concepts/cornerstone-tools/annotation/voxel-statistics.md`.
 *
 * @param shapes - The members. An empty list gives a shape that contains
 *   nothing, which lets a caller build a union without a special case for a
 *   stroke that recorded no points.
 */
export function createUnionShape(shapes: VoxelSlabShape[]): VoxelSlabShape {
  // One member is its own union, and the merge below would only add a layer of
  // generators to every row.
  if (shapes.length === 1) {
    return shapes[0];
  }
 
  function containsPoint(point: Point3): boolean {
    return shapes.some((shape) => shape.containsPoint(point));
  }
 
  function* getRuns(
    outerIndex: number,
    rowIndex: number,
    depthRun: Point2,
    slab: IndexSpaceSlab
  ): Generator<Point2, void, undefined> {
    const runs: Point2[] = [];
 
    for (const shape of shapes) {
      for (const run of shape.getRuns(outerIndex, rowIndex, depthRun, slab)) {
        // The member reuses no run array in any shape here, but copy anyway:
        // the merge below holds every run until the row is complete, and a
        // future member that reuses one would corrupt the whole row.
        runs.push([run[0], run[1]]);
      }
    }
 
    if (!runs.length) {
      return;
    }
 
    runs.sort((a, b) => a[0] - b[0]);
 
    let [low, high] = runs[0];
 
    for (let index = 1; index < runs.length; index++) {
      const [nextLow, nextHigh] = runs[index];
 
      // `high + 1` merges runs that only touch, so two discs that meet at a
      // voxel boundary yield one run rather than two.
      if (nextLow <= high + 1) {
        high = Math.max(high, nextHigh);
        continue;
      }
 
      yield [low, high];
      low = nextLow;
      high = nextHigh;
    }
 
    yield [low, high];
  }
 
  return {
    containsPoint,
    getRuns,
    // The slab must hold every member, so the deepest one sets the thickness.
    // A union of planar shapes reports 0, and the caller's own thickness stands.
    getRequiredThickness: () =>
      shapes.reduce((deepest, shape) => {
        const required = shape.getRequiredThickness();
        return required > deepest ? required : deepest;
      }, 0),
  };
}