All files / web/src/components/toys/coordinate-plane/ruler slopeGuides.ts

100% Statements 122/122
100% Branches 19/19
100% Functions 3/3
100% Lines 122/122

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 120 121 122 1231x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 22x 22x 22x 22x 22x 20x 20x 20x 20x 22x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 1x 1x 1x 1x 5x 5x 1x 1x 8x 9x 1x 1x 1x 1x 7x 7x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 7x 9x 9x 9x 59x 59x 59x 59x 41x 41x 59x 7x 7x 7x  
import type { GuideSlope, SlopeGuideState } from './types'
 
// ── Guide slope definitions ──────────────────────────────────────────
// Integers 1–11, their inverses 1/2–1/11, zero, and vertical.
 
const FRAC_LABELS: Record<number, string> = {
  2: '\u00BD', // ½
  3: '\u2153', // ⅓
  4: '\u00BC', // ¼
  5: '\u2155', // ⅕
  6: '\u2159', // ⅙
  7: '\u2150', // ⅐
  8: '\u215B', // ⅛
  9: '\u2151', // ⅑
  10: '\u2152', // ⅒
}
 
function buildGuideSlopes(): GuideSlope[] {
  const slopes: GuideSlope[] = [{ num: 0, den: 1, label: '0' }]
  for (let n = 1; n <= 11; n++) {
    // Integer slopes: n/1 and -n/1
    slopes.push({ num: n, den: 1, label: String(n) })
    slopes.push({ num: -n, den: 1, label: `\u2212${n}` })
    // Inverse slopes: 1/n and -1/n (skip 1/1, already covered)
    if (n >= 2) {
      const fracLabel = FRAC_LABELS[n] ?? `1/${n}`
      slopes.push({ num: 1, den: n, label: fracLabel })
      slopes.push({ num: -1, den: n, label: `\u2212${fracLabel}` })
    }
  }
  // Vertical
  slopes.push({ num: 1, den: 0, label: '\u221E' })
  return slopes
}
 
const GUIDE_SLOPES = buildGuideSlopes()
 
// ── Main computation ─────────────────────────────────────────────────
 
/**
 * Build slope guide state for all common slopes during a handle drag.
 *
 * @param anchorX  Stationary handle world X (integer)
 * @param anchorY  Stationary handle world Y (integer)
 * @param handleX  Grid-snapped dragging handle X
 * @param handleY  Grid-snapped dragging handle Y
 */
export function computeSlopeGuides(
  anchorX: number,
  anchorY: number,
  handleX: number,
  handleY: number
): SlopeGuideState {
  return {
    anchorX,
    anchorY,
    handleX,
    handleY,
    guides: GUIDE_SLOPES.map((slope) => ({ slope })),
  }
}
 
// ── Integer intersection computation ─────────────────────────────────
 
/**
 * Compute integer grid points along a guide line through (anchorX, anchorY)
 * with slope num/den, within the visible world-coordinate range.
 *
 * Returns array of { x, y } integer pairs.
 */
export function guideIntegerIntersections(
  anchorX: number,
  anchorY: number,
  slope: GuideSlope,
  minWorldX: number,
  maxWorldX: number,
  minWorldY: number,
  maxWorldY: number
): { x: number; y: number }[] {
  const points: { x: number; y: number }[] = []
 
  if (slope.den === 0) {
    // Vertical line at x = anchorX
    const yLo = Math.ceil(minWorldY)
    const yHi = Math.floor(maxWorldY)
    for (let y = yLo; y <= yHi; y++) {
      points.push({ x: anchorX, y })
    }
    return points
  }
 
  if (slope.num === 0) {
    // Horizontal line at y = anchorY
    const xLo = Math.ceil(minWorldX)
    const xHi = Math.floor(maxWorldX)
    for (let x = xLo; x <= xHi; x++) {
      points.push({ x, y: anchorY })
    }
    return points
  }
 
  // General case: line is y = anchorY + (num/den) * (x - anchorX)
  // Integer intersections occur where (x - anchorX) is a multiple of den
  const den = Math.abs(slope.den)
  const xLo = Math.ceil(minWorldX)
  const xHi = Math.floor(maxWorldX)
 
  // Find the first x >= xLo where (x - anchorX) % den === 0
  const remainder = (((xLo - anchorX) % den) + den) % den
  const startX = remainder === 0 ? xLo : xLo + (den - remainder)
 
  for (let x = startX; x <= xHi; x += den) {
    const y = anchorY + slope.num * ((x - anchorX) / slope.den)
    // y should be integer by construction, but round to avoid float drift
    const ry = Math.round(y)
    if (ry >= minWorldY && ry <= maxWorldY) {
      points.push({ x, y: ry })
    }
  }
 
  return points
}