All files / web/src/components/toys/euclid/render usePropPreviews.ts

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

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                                                                                                                                                                                                                                                                 
'use client'

import { useState, useEffect } from 'react'
import { buildFinalState } from './buildFinalStates'
import { renderConstruction } from './renderConstruction'
import { renderEqualityMarks } from './renderEqualityMarks'
import { getAllPoints, getAllCircles, getPoint, getRadius } from '../engine/constructionState'
import { PROP_REGISTRY } from '../propositions/registry'
import { IMPLEMENTED_PROPS } from '../data/propositionGraph'
import type { ConstructionState, EuclidViewportState } from '../types'

const PREVIEW_W = 260
const PREVIEW_H = 200
const PADDING_FRACTION = 0.15

/**
 * Compute a viewport that fits all geometric elements (points + circle extents)
 * into the given pixel dimensions with padding.
 */
function computeFitViewport(state: ConstructionState, w: number, h: number): EuclidViewportState {
  let minX = Infinity
  let maxX = -Infinity
  let minY = Infinity
  let maxY = -Infinity

  for (const pt of getAllPoints(state)) {
    minX = Math.min(minX, pt.x)
    maxX = Math.max(maxX, pt.x)
    minY = Math.min(minY, pt.y)
    maxY = Math.max(maxY, pt.y)
  }

  for (const circle of getAllCircles(state)) {
    const center = getPoint(state, circle.centerId)
    if (!center) continue
    const r = getRadius(state, circle.id)
    minX = Math.min(minX, center.x - r)
    maxX = Math.max(maxX, center.x + r)
    minY = Math.min(minY, center.y - r)
    maxY = Math.max(maxY, center.y + r)
  }

  // Add padding
  const rangeX = maxX - minX
  const rangeY = maxY - minY
  const padX = rangeX * PADDING_FRACTION
  const padY = rangeY * PADDING_FRACTION
  minX -= padX
  maxX += padX
  minY -= padY
  maxY += padY

  const cx = (minX + maxX) / 2
  const cy = (minY + maxY) / 2
  const scaleX = w / (maxX - minX)
  const scaleY = h / (maxY - minY)
  const pixelsPerUnit = Math.min(scaleX, scaleY)

  return { center: { x: cx, y: cy }, pixelsPerUnit }
}

/**
 * Renders the final construction state for each implemented proposition
 * to an offscreen canvas, returning a Map of propId → data URL.
 * Computed once on mount and cached.
 */
export function usePropPreviews(): Map<number, string> {
  const [previews, setPreviews] = useState<Map<number, string>>(() => new Map())

  useEffect(() => {
    const map = new Map<number, string>()
    const idle = { tag: 'idle' as const }

    for (const propId of IMPLEMENTED_PROPS) {
      const result = buildFinalState(propId)
      if (!result) continue

      const prop = PROP_REGISTRY[propId]
      if (!prop) continue
      const viewport = computeFitViewport(result.state, PREVIEW_W, PREVIEW_H)

      const canvas = document.createElement('canvas')
      canvas.width = PREVIEW_W
      canvas.height = PREVIEW_H
      const ctx = canvas.getContext('2d')
      if (!ctx) continue

      renderConstruction(
        ctx,
        result.state,
        viewport,
        PREVIEW_W,
        PREVIEW_H,
        idle, // compassPhase: idle
        idle, // straightedgePhase: idle
        null, // pointerWorld
        null, // snappedPointId
        [], // candidates
        0, // nextColorIndex
        null, // candidateFilter
        true, // isComplete
        prop.resultSegments,
        undefined, // hiddenElementIds
        true // transparentBg
      )

      // Render equality tick marks on segments with proven equalities
      if (result.factStore.facts.length > 0) {
        renderEqualityMarks(
          ctx,
          result.state,
          viewport,
          PREVIEW_W,
          PREVIEW_H,
          result.factStore,
          undefined,
          prop.resultSegments
        )
      }

      map.set(propId, canvas.toDataURL('image/png'))
    }

    setPreviews(map)
  }, [])

  return previews
}