All files / tools/src/utilities/spatial clipWorldLineToViewportCanvas.ts

3.12% Statements 1/32
0% Branches 0/16
0% Functions 0/2
3.12% Lines 1/32

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 113 114 115 116 117 118 119              128x                                                                                                                                                                                                                              
import { vec3 } from 'gl-matrix';
import type { Types } from '@cornerstonejs/core';
import liangBarksyClip from '../math/vec2/liangBarksyClip';
import getDisplayedCanvasSize from './getDisplayedCanvasSize';
import type { WorldLine } from './types';
 
/** Canvas segments shorter than this are treated as degenerate. */
const MIN_SEGMENT_LENGTH = 1e-3;
 
/**
 * Clips an infinite world-space line to the visible canvas of a viewport.
 *
 * A sufficiently long segment of the line (centered on the point of the line
 * closest to the viewport center, to stay numerically well-behaved) is
 * converted to canvas space and clipped against the canvas bounds.
 *
 * Returns the two clipped canvas endpoints, or null when the line does not
 * intersect the visible canvas (including the degenerate case where the line
 * projects to a single canvas point, e.g. a line perpendicular to the view
 * plane).
 */
export default function clipWorldLineToViewportCanvas(
  line: WorldLine,
  viewport: Types.IVolumeViewport | Types.IViewport
): [Types.Point2, Types.Point2] | null {
  if (!line || !viewport) {
    return null;
  }
 
  const { clientWidth, clientHeight } = getDisplayedCanvasSize(viewport);
  if (!clientWidth || !clientHeight) {
    return null;
  }
 
  const direction = vec3.normalize(vec3.create(), line.direction);
  if (vec3.length(direction) < 1e-10) {
    return null;
  }
 
  // Re-anchor the line on the point closest to the viewport center so the
  // projected segment endpoints stay near the canvas even when line.point is
  // far away in world space.
  const centerWorld = viewport.canvasToWorld([
    clientWidth / 2,
    clientHeight / 2,
  ]);
  const toCenter = vec3.subtract(vec3.create(), centerWorld, line.point);
  const projectedLength = vec3.dot(toCenter, direction);
  const anchor = vec3.scaleAndAdd(
    vec3.create(),
    line.point as Types.Point3,
    direction,
    projectedLength
  );
 
  // Half-length that comfortably covers the canvas: twice the world-space
  // distance between the canvas center and a canvas corner.
  const cornerWorld = viewport.canvasToWorld([0, 0]);
  const halfDiagonalWorld = vec3.distance(centerWorld, cornerWorld);
  const halfLength = Math.max(halfDiagonalWorld * 2, 1);
 
  const startWorld = vec3.scaleAndAdd(
    vec3.create(),
    anchor,
    direction,
    -halfLength
  );
  const endWorld = vec3.scaleAndAdd(
    vec3.create(),
    anchor,
    direction,
    halfLength
  );
 
  const startCanvas = viewport.worldToCanvas([
    startWorld[0],
    startWorld[1],
    startWorld[2],
  ]);
  const endCanvas = viewport.worldToCanvas([
    endWorld[0],
    endWorld[1],
    endWorld[2],
  ]);
 
  if (
    ![...startCanvas, ...endCanvas].every((component) =>
      Number.isFinite(component)
    )
  ) {
    return null;
  }
 
  const clippedStart: Types.Point2 = [startCanvas[0], startCanvas[1]];
  const clippedEnd: Types.Point2 = [endCanvas[0], endCanvas[1]];
 
  const inside = liangBarksyClip(clippedStart, clippedEnd, [
    0,
    0,
    clientWidth,
    clientHeight,
  ]);
 
  if (inside !== 1) {
    return null;
  }
 
  const segmentLength = Math.hypot(
    clippedEnd[0] - clippedStart[0],
    clippedEnd[1] - clippedStart[1]
  );
 
  if (segmentLength < MIN_SEGMENT_LENGTH) {
    return null;
  }
 
  return [clippedStart, clippedEnd];
}