All files / webview-app/src/lib geometry.ts

100% Statements 25/25
100% Branches 20/20
100% Functions 4/4
100% Lines 25/25

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                                                      4x 1x     3x                     49x 28x     21x                                 6x   6x 6x 6x 6x   6x 3x     6x 3x     6x 2x 2x 2x     6x 2x 2x 2x     6x                                 5x    
export type ResizeHandle = "n" | "ne" | "e" | "se" | "s" | "sw" | "w" | "nw";
 
export interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}
 
export interface ResizeOptions {
  handle: ResizeHandle;
  deltaX: number;
  deltaY: number;
  minWidth?: number;
  minHeight?: number;
  gridSize?: number;
}
 
/**
 * Restricts a numeric value to a bounded range.
 *
 * @param value - The input value to constrain.
 * @param min - The inclusive lower bound.
 * @param max - The inclusive upper bound.
 * @returns The value clamped to the `[min, max]` interval.
 */
export function clamp(value: number, min: number, max: number): number {
  if (min > max) {
    throw new Error("clamp: min cannot be greater than max");
  }
 
  return Math.min(max, Math.max(min, value));
}
 
/**
 * Snaps a numeric value to the nearest grid step.
 *
 * @param value - The raw coordinate/size value.
 * @param gridSize - The grid step in pixels. Values `<= 1` behave like integer rounding.
 * @returns The value snapped to the closest grid increment.
 */
export function snapToGrid(value: number, gridSize = 1): number {
  if (gridSize <= 1) {
    return Math.round(value);
  }
 
  return Math.round(value / gridSize) * gridSize;
}
 
/**
 * Calculates the next rectangle while resizing from a given handle.
 *
 * Behavior mirrors the current canvas resize interaction:
 * - East/South handles grow from the fixed north-west origin.
 * - West/North handles move the origin while keeping opposite edges anchored.
 * - Width and height are clamped to minimums.
 * - Returned values are snapped to grid (default integer pixels).
 *
 * @param startRect - The rectangle at drag start.
 * @param options - Resize metadata such as handle, deltas, and constraints.
 * @returns A new rectangle representing the resized bounds.
 */
export function calculateResize(startRect: Rect, options: ResizeOptions): Rect {
  const { handle, deltaX, deltaY, minWidth = 1, minHeight = 1, gridSize = 1 } = options;
 
  let nextX = startRect.x;
  let nextY = startRect.y;
  let nextWidth = startRect.width;
  let nextHeight = startRect.height;
 
  if (handle.includes("e")) {
    nextWidth = Math.max(minWidth, snapToGrid(startRect.width + deltaX, gridSize));
  }
 
  if (handle.includes("s")) {
    nextHeight = Math.max(minHeight, snapToGrid(startRect.height + deltaY, gridSize));
  }
 
  if (handle.includes("w")) {
    const candidateWidth = startRect.width - deltaX;
    nextWidth = Math.max(minWidth, snapToGrid(candidateWidth, gridSize));
    nextX = snapToGrid(startRect.x + (startRect.width - nextWidth), gridSize);
  }
 
  if (handle.includes("n")) {
    const candidateHeight = startRect.height - deltaY;
    nextHeight = Math.max(minHeight, snapToGrid(candidateHeight, gridSize));
    nextY = snapToGrid(startRect.y + (startRect.height - nextHeight), gridSize);
  }
 
  return {
    x: snapToGrid(nextX, gridSize),
    y: snapToGrid(nextY, gridSize),
    width: nextWidth,
    height: nextHeight,
  };
}
 
/**
 * Checks whether a point is inside (or on the edge of) a rectangle.
 *
 * @param x - Point x coordinate.
 * @param y - Point y coordinate.
 * @param rect - Rectangle bounds to test.
 * @returns `true` when the point lies within inclusive rectangle bounds.
 */
export function isPointInRect(x: number, y: number, rect: Rect): boolean {
  return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
}