All files / web/src/components/toys/euclid/hooks useDragTopologyTracking.ts

0% Statements 0/220
0% Branches 0/1
0% Functions 0/1
0% Lines 0/220

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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221                                                                                                                                                                                                                                                                                                                                                                                                                                                         
import { useRef, useCallback } from 'react'
import type { ConstructionState, ConstructionElement, GhostLayer, PropositionDef } from '../types'
import type { ProofFact } from '../engine/facts'
import type { ReplayResult } from '../engine/replayConstruction'
import { deriveCompletionResult } from '../engine/snapshots'
import { mergeProofFacts } from '../engine/factStore'
import type { FactStore } from '../engine/factStore'
import type { UseEuclidMusicReturn } from '../audio/useEuclidMusic'

interface UseDragTopologyTrackingOptions {
  authorProofFactsRef: React.MutableRefObject<ProofFact[]>
  proofFactsRef: React.MutableRefObject<ProofFact[]>
  ghostLayersRef: React.MutableRefObject<GhostLayer[]>
  propositionRef: React.MutableRefObject<PropositionDef>
  musicRef: React.MutableRefObject<UseEuclidMusicReturn | null>
  notifierRef: React.MutableRefObject<{
    notifyConstruction: (opts: {
      action: string
      shouldPrompt: boolean
      collapseInChat?: boolean
    }) => void
    notifyDragEnd: (label?: string) => void
  }>
  constructionRef: React.MutableRefObject<ConstructionState>
  dragLabelRef: React.MutableRefObject<string | null>
  wiggleCancelRef: React.MutableRefObject<(() => void) | null>
  factStoreRef: React.MutableRefObject<FactStore>
  handleConstructionBreakdown: () => void
  handleDragStart: (pointId: string) => void
  setTrailingEvent: (event: null) => void
  setProofFacts: (facts: ProofFact[]) => void
}

/** Build a human-readable name for a construction element. */
function describeElement(el: ConstructionElement, state: ConstructionState): string {
  if (el.kind === 'point') return `point ${el.label}`
  if (el.kind === 'circle') {
    const center = state.elements.find((e) => e.id === el.centerId)
    const radius = state.elements.find((e) => e.id === el.radiusPointId)
    const cLabel = center && 'label' in center ? center.label : '?'
    const rLabel = radius && 'label' in radius ? radius.label : '?'
    return `circle centered at ${cLabel} through ${rLabel}`
  }
  // el.kind === 'segment'
  const from = state.elements.find((e) => e.id === el.fromId)
  const to = state.elements.find((e) => e.id === el.toId)
  const fLabel = from && 'label' in from ? from.label : '?'
  const tLabel = to && 'label' in to ? to.label : '?'
  return `segment ${fLabel}${tLabel}`
}

export function useDragTopologyTracking({
  authorProofFactsRef,
  proofFactsRef,
  ghostLayersRef,
  propositionRef,
  musicRef,
  notifierRef,
  constructionRef,
  dragLabelRef,
  wiggleCancelRef,
  factStoreRef,
  handleConstructionBreakdown,
  handleDragStart,
  setTrailingEvent,
  setProofFacts,
}: UseDragTopologyTrackingOptions) {
  const constructionIntactRef = useRef(true)
  /** Baseline from drag start — never updated during drag */
  const topologyBaselineRef = useRef<{
    map: Map<string, string>
    steps: number
    factCount: number
  } | null>(null)
  /** Current frame's map — used to describe elements that may disappear next frame */
  const topologyCurrentRef = useRef<Map<string, string>>(new Map())
  const topologyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  const handleDragReplay = useCallback(
    (result: ReplayResult) => {
      // Combine replay-derived facts with author-declared facts that survive replay
      const combined =
        authorProofFactsRef.current.length > 0
          ? [...result.proofFacts, ...authorProofFactsRef.current]
          : result.proofFacts
      proofFactsRef.current = combined
      setProofFacts(combined)
      // Re-merge author facts into the (already-rebuilt) fact store so queries work
      if (authorProofFactsRef.current.length > 0) {
        mergeProofFacts(factStoreRef.current, authorProofFactsRef.current)
      }
      ghostLayersRef.current = result.ghostLayers
      musicRef.current?.notifyChange()

      // Detect construction breakdown: was intact, now incomplete
      const intact = result.stepsCompleted >= propositionRef.current.steps.length
      if (constructionIntactRef.current && !intact) {
        handleConstructionBreakdown()
      }
      constructionIntactRef.current = intact

      // Build current element map (need this every frame so we can name
      // elements that might disappear in a future frame)
      const currMap = new Map<string, string>()
      for (const el of result.state.elements) {
        currMap.set(el.id, describeElement(el, result.state))
      }

      // Set baseline on first frame (drag start)
      if (!topologyBaselineRef.current) {
        topologyBaselineRef.current = {
          map: new Map(currMap),
          steps: result.stepsCompleted,
          factCount: result.proofFacts.length,
        }
        topologyCurrentRef.current = currMap
        return
      }

      topologyCurrentRef.current = currMap

      // Compute NET diff from drag-start baseline (not frame-to-frame).
      // This resolves oscillations: if F disappears then reappears, net = nothing.
      const baseline = topologyBaselineRef.current
      const baseIds = new Set(baseline.map.keys())
      const currIds = new Set(currMap.keys())
      const appeared = [...currIds].filter((id) => !baseIds.has(id))
      const disappeared = [...baseIds].filter((id) => !currIds.has(id))
      const stepsChanged = result.stepsCompleted !== baseline.steps
      const parts: string[] = []

      if (appeared.length > 0) {
        parts.push(`${appeared.map((id) => currMap.get(id)!).join(', ')} appeared`)
      }
      if (disappeared.length > 0) {
        // Use current map first (for elements that existed recently), fall back to baseline
        parts.push(
          `${disappeared.map((id) => topologyCurrentRef.current.get(id) ?? baseline.map.get(id)!).join(', ')} disappeared`
        )
      }
      if (stepsChanged) {
        if (result.stepsCompleted < baseline.steps) {
          // Use the proof engine to describe what broke
          const cr = deriveCompletionResult(
            result.factStore,
            propositionRef.current.resultSegments,
            result.state
          )
          const failedStep = propositionRef.current.steps[result.stepsCompleted]
          let breakdown = `construction broke down at step ${result.stepsCompleted + 1}`
          if (failedStep) breakdown += `: "${failedStep.instruction}"`
          if (cr.status === 'unproven' && cr.statement) {
            breakdown += ` — cannot prove ${cr.statement}`
          }
          parts.push(breakdown)
        } else if (result.stepsCompleted > baseline.steps) {
          const cr = deriveCompletionResult(
            result.factStore,
            propositionRef.current.resultSegments,
            result.state
          )
          let restored = `construction restored through step ${result.stepsCompleted}`
          if (cr.status === 'proven' && cr.statement) {
            restored += ` — ${cr.statement} proven`
          }
          parts.push(restored)
        }
      }

      // Note proof fact count changes (tells Euclid about proof chain health)
      const factDelta = result.proofFacts.length - baseline.factCount
      if (factDelta < 0) {
        parts.push(`${Math.abs(factDelta)} proven fact${Math.abs(factDelta) > 1 ? 's' : ''} lost`)
      } else if (factDelta > 0) {
        parts.push(`${factDelta} new fact${factDelta > 1 ? 's' : ''} proven`)
      }

      // Debounce + collapse: replace the single trailing event in chat
      if (topologyTimerRef.current) clearTimeout(topologyTimerRef.current)
      topologyTimerRef.current = setTimeout(() => {
        topologyTimerRef.current = null
        if (parts.length > 0) {
          const action = `While dragging: ${parts.join('; ')}`
          notifierRef.current.notifyConstruction({
            action,
            shouldPrompt: false,
            collapseInChat: true,
          })
        } else {
          // Net change resolved to nothing — remove any trailing event
          setTrailingEvent(null)
        }
      }, 400)
    },
    [handleConstructionBreakdown, setTrailingEvent]
  )

  const onDragStart = useCallback(
    (pointId: string) => {
      wiggleCancelRef.current?.()
      wiggleCancelRef.current = null
      // Capture label for the drag-end notifier (dragPointIdRef is cleared before onDragEnd fires)
      const pt = constructionRef.current.elements.find(
        (e) => e.kind === 'point' && e.id === pointId
      )
      dragLabelRef.current = pt && 'label' in pt ? pt.label : null
      // Reset topology tracking so first replay frame sets the baseline
      topologyBaselineRef.current = null
      handleDragStart(pointId)
    },
    [handleDragStart]
  )

  const onDragEnd = useCallback(() => {
    notifierRef.current.notifyDragEnd(dragLabelRef.current ?? undefined)
    dragLabelRef.current = null
  }, [])

  return { handleDragReplay, onDragStart, onDragEnd }
}