All files / qr-code/src/__testing__ path.ts

91.52% Statements 54/59
80% Branches 12/15
100% Functions 11/11
90.9% Lines 50/55

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                                            45x 45x 45x 45x 45x 45x   3765x 45x 635x     45x 3045x 3045x   590x 590x 590x 590x 590x 590x             1125x 1125x 1125x   620x 620x 620x   120x 120x 120x 120x 120x 120x 120x 120x 120x     590x           45x 45x         580x                       10x 10x 10x 52x 52x 52x   10x         39x 39x 2403x 2403x 2403x 2403x      
/**
 * A minimal SVG path reader for tests.
 *
 * The renderers emit compact paths that lean on `H`/`V` shorthands and arcs,
 * so a naive "grab every pair of numbers" regex reads arc radii and flags as
 * if they were coordinates. This walks the commands properly and returns only
 * real endpoints — which is what bounds and winding assertions need.
 */
 
export interface Point {
  x: number;
  y: number;
}
 
export interface Subpath {
  points: Point[];
  /** Net arc direction: +1 per clockwise sweep, −1 per counter-clockwise. */
  arcSweep: number;
}
 
/** Every subpath's endpoints, in order. */
export function subpaths(d: string): Subpath[] {
  const tokens = d.match(/[MLHVAZ]|-?\d+(?:\.\d+)?/gi) ?? [];
  const result: Subpath[] = [];
  let current: Subpath = { points: [], arcSweep: 0 };
  let x = 0;
  let y = 0;
  let i = 0;
 
  const number = (): number => Number(tokens[i++]);
  const flush = (): void => {
    if (current.points.length > 0) result.push(current);
  };
 
  while (i < tokens.length) {
    const command = tokens[i++];
    switch (command) {
      case "M":
        flush();
        current = { points: [], arcSweep: 0 };
        x = number();
        y = number();
        current.points.push({ x, y });
        break;
      case "L":
        x = number();
        y = number();
        current.points.push({ x, y });
        break;
      case "H":
        x = number();
        current.points.push({ x, y });
        break;
      case "V":
        y = number();
        current.points.push({ x, y });
        break;
      case "A": {
        number(); // rx
        number(); // ry
        number(); // x-axis rotation
        number(); // large-arc flag
        current.arcSweep += number() === 1 ? 1 : -1;
        x = number();
        y = number();
        current.points.push({ x, y });
        break;
      }
      case "Z":
        break;
      default:
        throw new Error(`Unsupported path command: ${String(command)}`);
    }
  }
 
  flush();
  return result;
}
 
/** Every endpoint in a path, across all subpaths. */
export function points(d: string): Point[] {
  return subpaths(d).flatMap((subpath) => subpath.points);
}
 
/**
 * Direction of a subpath: +1 clockwise in screen coordinates (y down), −1
 * counter-clockwise.
 *
 * The shoelace formula over endpoints answers this for anything with area. A
 * circle drawn as two half-arcs has *collinear* endpoints and zero shoelace
 * area, so its direction is read from the arc sweep flags instead.
 */
export function windingSign(subpath: Subpath): number {
  let area = 0;
  const { points: list } = subpath;
  for (let i = 0; i < list.length; i++) {
    const a = list[i]!;
    const b = list[(i + 1) % list.length]!;
    area += a.x * b.y - b.x * a.y;
  }
  return area !== 0 ? Math.sign(area) : Math.sign(subpath.arcSweep);
}
 
/** Axis-aligned bounding box of a path's endpoints. */
export function bounds(d: string): { minX: number; minY: number; maxX: number; maxY: number } {
  const all = points(d);
  return {
    minX: Math.min(...all.map((p) => p.x)),
    minY: Math.min(...all.map((p) => p.y)),
    maxX: Math.max(...all.map((p) => p.x)),
    maxY: Math.max(...all.map((p) => p.y)),
  };
}