All files / tools/src/utilities/contourSegmentation polylineSetOps.ts

0% Statements 0/32
0% Branches 0/18
0% Functions 0/7
0% Lines 0/31

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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112                                                                                                                                                                                                                               
/**
 * Bridges PolylineInfoCanvas (canvas-space polylines + viewReference + holes)
 * to the pure-geometry clipperBooleanOps. Groups polylines by viewReference,
 * runs Clipper once per group, then flattens back to PolylineInfoCanvas[].
 *
 * View references must match exactly — operations between polylines on
 * different slices/views never combine.
 */
 
import type { PolylineInfoCanvas } from './polylineInfoTypes';
import { areViewReferencesEqual } from './areViewReferencesEqual';
import {
  applyBoolean,
  BooleanOp,
  type PolygonWithHoles,
} from './clipperBooleanOps';
import type { Types } from '@cornerstonejs/core';
 
type Group = {
  viewReference: PolylineInfoCanvas['viewReference'];
  polygons: PolygonWithHoles[];
};
 
function toPolygon(info: PolylineInfoCanvas): PolygonWithHoles {
  return {
    outer: info.polyline,
    holes: info.holePolylines?.length ? info.holePolylines : undefined,
  };
}
 
function groupByViewReference(set: PolylineInfoCanvas[]): Group[] {
  const groups: Group[] = [];
  for (const info of set) {
    if (info.polyline.length < 3) {
      continue;
    }
    const existing = groups.find((g) =>
      areViewReferencesEqual(g.viewReference, info.viewReference)
    );
    if (existing) {
      existing.polygons.push(toPolygon(info));
    } else {
      groups.push({
        viewReference: info.viewReference,
        polygons: [toPolygon(info)],
      });
    }
  }
  return groups;
}
 
function flatten(
  result: PolygonWithHoles[],
  viewReference: PolylineInfoCanvas['viewReference']
): PolylineInfoCanvas[] {
  return result.map((p) => ({
    polyline: p.outer,
    viewReference,
    ...(p.holes?.length ? { holePolylines: p.holes } : {}),
  }));
}
 
/**
 * Run `op` between two PolylineInfoCanvas sets, grouping by view reference so
 * polygons on different views never interact.
 *
 * - For commutative ops (Union, Xor): subject-only and clip-only view groups
 *   are passed through unchanged.
 * - For Difference: subject groups without a matching clip group pass through;
 *   clip-only groups are dropped (nothing to subtract from).
 * - For Intersection: only groups present on both sides contribute.
 */
export function runBooleanOpByView(
  setA: PolylineInfoCanvas[],
  setB: PolylineInfoCanvas[],
  op: BooleanOp
): PolylineInfoCanvas[] {
  const aGroups = groupByViewReference(setA);
  const bGroups = groupByViewReference(setB);
 
  const out: PolylineInfoCanvas[] = [];
  const matchedB = new Set<Group>();
 
  for (const aGroup of aGroups) {
    const bGroup = bGroups.find((g) =>
      areViewReferencesEqual(g.viewReference, aGroup.viewReference)
    );
    if (bGroup) {
      matchedB.add(bGroup);
      const result = applyBoolean(aGroup.polygons, bGroup.polygons, op);
      out.push(...flatten(result, aGroup.viewReference));
    } else if (op === BooleanOp.Intersection) {
      // No matching clip → empty.
    } else {
      // Union / Xor / Difference with empty clip: subjects unchanged.
      out.push(...flatten(aGroup.polygons, aGroup.viewReference));
    }
  }
 
  // B-only groups: only meaningful for Union and Xor.
  if (op === BooleanOp.Union || op === BooleanOp.Xor) {
    for (const bGroup of bGroups) {
      if (matchedB.has(bGroup)) {
        continue;
      }
      out.push(...flatten(bGroup.polygons, bGroup.viewReference));
    }
  }
 
  return out;
}