All files / web/src/components/toys/euclid/propositions validation.ts

86.95% Statements 80/92
84.21% Branches 32/38
100% Functions 1/1
86.95% Lines 80/92

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 931x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 5x 5x 4x 5x 5x 16x 16x 16x 10x 9x 21x 8x 8x 7x 6x 6x 7x 5x 7x 2x 7x 4x 7x                 4x 4x 1x 1x 1x 8x 21x 3x 3x 3x 1x 3x 3x 5x 5x 5x 5x     21x     5x 5x 5x 5x 5x 5x  
import type {
  ConstructionElement,
  ConstructionState,
  ExpectedAction,
  IntersectionCandidate,
} from '../types'
import { isCandidateBeyondPoint } from '../engine/intersections'
import { resolveSelector } from '../engine/selectors'
 
/**
 * Validate whether the last committed element matches the expected action for a step.
 * Returns true if the step is satisfied.
 *
 * For intersection steps, optionally validates that the candidate came from the
 * expected pair of elements (ofA/ofB). These use ElementSelectors resolved at
 * runtime to decouple propositions from element creation order.
 *
 * For macro steps, validation is handled externally (the macro commit handler
 * validates input points and calls checkStep with a synthetic element).
 */
export function validateStep(
  expected: ExpectedAction,
  state: ConstructionState,
  lastElement: ConstructionElement,
  candidate?: IntersectionCandidate
): boolean {
  // Observation steps are advanced by the Continue button, not canvas interaction
  if (expected.type === 'observation') return false
 
  // Superposition steps are advanced by the interaction hook, not element validation
  if (expected.type === 'superposition') return false
 
  if (expected.type === 'compass' && lastElement.kind === 'circle') {
    return (
      lastElement.centerId === expected.centerId &&
      lastElement.radiusPointId === expected.radiusPointId
    )
  }
 
  if (
    expected.type === 'intersection' &&
    lastElement.kind === 'point' &&
    lastElement.origin === 'intersection'
  ) {
    // If ofA/ofB are specified, resolve selectors and check that the candidate matches
    if (expected.ofA != null && expected.ofB != null) {
      if (!candidate) return false
      const resolvedA = resolveSelector(expected.ofA, state)
      const resolvedB = resolveSelector(expected.ofB, state)
      if (!resolvedA || !resolvedB) return false
      const matchesElements =
        (candidate.ofA === resolvedA && candidate.ofB === resolvedB) ||
        (candidate.ofA === resolvedB && candidate.ofB === resolvedA)
      if (!matchesElements) return false
      // If beyondId is specified, candidate must be on the extension past that point
      if (expected.beyondId) {
        return isCandidateBeyondPoint(
          candidate,
          expected.beyondId,
          candidate.ofA,
          candidate.ofB,
          state
        )
      }
      return true
    }
    // Accept any intersection point when ofA/ofB are omitted
    return true
  }
 
  if (expected.type === 'straightedge' && lastElement.kind === 'segment') {
    // Flexible on order: (from, to) or (to, from)
    return (
      (lastElement.fromId === expected.fromId && lastElement.toId === expected.toId) ||
      (lastElement.fromId === expected.toId && lastElement.toId === expected.fromId)
    )
  }
 
  // Extend steps: verify new point + segment beyond throughId
  if (
    expected.type === 'extend' &&
    lastElement.kind === 'point' &&
    lastElement.origin === 'intersection'
  ) {
    return lastElement.label === expected.label
  }
 
  // Macro steps are validated and advanced directly by handleCommitMacro
  // in EuclidCanvas.tsx, not through this function.
 
  return false
}