Files
soroban-abacus-flashcards/apps/web/scripts/generateTenFrameExamples.ts
Thomas Hallock b36df3a40c fix(worksheets): ten-frames not rendering in mastery mode
Fixed two critical bugs preventing ten-frames from rendering:

1. **Mastery mode not handled** (typstGenerator.ts:61)
   - Code only checked for 'smart' | 'manual' modes
   - Mastery mode fell into manual path, tried to use boolean flags that don't exist
   - Resulted in all display options being `undefined`
   - Fix: Check for both 'smart' OR 'mastery' modes (both use displayRules)

2. **Typst array membership syntax** (already fixed in previous commit)
   - Used `(i in array)` which doesn't work in Typst
   - Changed to `array.contains(i)`

Added comprehensive unit tests (tenFrames.test.ts):
- Problem analysis tests (regrouping detection)
- Display rule evaluation tests
- Full Typst template generation tests
- Mastery mode specific tests
- All 14 tests now passing

Added debug logging to trace display rules resolution:
- displayRules.ts: Shows rule evaluation per problem
- typstGenerator.ts: Shows enriched problems and Typst data
- Helps diagnose future issues

The issue was that mastery mode (which uses displayRules like smart mode)
was being treated as manual mode (which uses boolean flags), resulting in
undefined display options.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 10:06:27 -06:00

202 lines
6.3 KiB
TypeScript

// Script to generate ten-frame example images for blog post
// Shows single problems with different ten-frame scaffolding levels
//
// REUSABLE PATTERN: This script demonstrates how to generate single-problem
// examples for blog posts using the SAME code that powers the display options
// preview in the worksheet generator UI. The generateExampleTypst function
// in src/app/api/create/worksheets/addition/example/route.ts is the single
// source of truth for rendering individual problems with display options.
//
// To generate examples for other blog posts:
// 1. Import generateTypstHelpers and generateProblemStackFunction from typstHelpers.ts
// 2. Use the generateExampleTypst pattern below with your desired options
// 3. Compile to SVG using typst
// 4. Save to public/blog/[your-post-name]/
import fs from 'fs'
import path from 'path'
import { execSync } from 'child_process'
import {
generateTypstHelpers,
generateProblemStackFunction,
} from '../src/app/create/worksheets/addition/typstHelpers'
// Output directory
const outputDir = path.join(process.cwd(), 'public', 'blog', 'ten-frame-examples')
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
interface ExampleOptions {
showCarryBoxes?: boolean
showAnswerBoxes?: boolean
showPlaceValueColors?: boolean
showTenFrames?: boolean
showProblemNumbers?: boolean
transparentBackground?: boolean
fontSize?: number
addend1: number
addend2: number
}
/**
* Generate a single compact problem example
* This is the SAME logic used by the API route for display option previews
* Extracted here so we can generate static examples for blog posts
*/
function generateExampleTypst(config: ExampleOptions): string {
const a = config.addend1
const b = config.addend2
const fontSize = config.fontSize || 16
const cellSize = 0.45 // Slightly larger for blog examples vs UI previews (0.35)
// Boolean flags matching worksheet generator
const showCarries = config.showCarryBoxes ?? false
const showAnswers = config.showAnswerBoxes ?? false
const showColors = config.showPlaceValueColors ?? false
const showNumbers = config.showProblemNumbers ?? false
const showTenFrames = config.showTenFrames ?? false
const showTenFramesForAll = false // Not used for blog examples
const transparentBg = config.transparentBackground ?? false
return String.raw`
#set page(width: auto, height: auto, margin: 12pt, fill: ${transparentBg ? 'none' : 'white'})
#set text(size: ${fontSize}pt, font: "New Computer Modern Math")
#let heavy-stroke = 0.8pt
#let show-ten-frames-for-all = ${showTenFramesForAll ? 'true' : 'false'}
${generateTypstHelpers(cellSize)}
${generateProblemStackFunction(cellSize)}
#let a = ${a}
#let b = ${b}
#let aT = calc.floor(calc.rem(a, 100) / 10)
#let aO = calc.rem(a, 10)
#let bT = calc.floor(calc.rem(b, 100) / 10)
#let bO = calc.rem(b, 10)
#align(center + horizon)[
#problem-stack(
a, b, aT, aO, bT, bO,
${showNumbers ? '0' : 'none'},
${showCarries},
${showAnswers},
${showColors},
${showTenFrames},
${showNumbers}
)
]
`
}
// Generate examples showing ten-frames in action
// Use problems that WILL have regrouping to show ten-frames
const examples = [
{
name: 'with-ten-frames',
filename: 'with-ten-frames.svg',
description: 'With Ten-Frames: Visual scaffolding for regrouping',
options: {
addend1: 47,
addend2: 38, // 7+8=15 requires regrouping, will show ten-frames
showCarryBoxes: false,
showAnswerBoxes: false,
showPlaceValueColors: false,
showTenFrames: true,
showProblemNumbers: false,
transparentBackground: true,
},
},
{
name: 'without-ten-frames',
filename: 'without-ten-frames.svg',
description: 'Without Ten-Frames: Abstract representation',
options: {
addend1: 47,
addend2: 38, // Same problem, no ten-frames
showCarryBoxes: false,
showAnswerBoxes: false,
showPlaceValueColors: false,
showTenFrames: false, // No ten-frames
showProblemNumbers: false,
transparentBackground: true,
},
},
{
name: 'beginner-with-ten-frames',
filename: 'beginner-ten-frames.svg',
description: 'Beginner: Learning regrouping with ten-frames',
options: {
addend1: 28,
addend2: 15, // 8+5=13 requires regrouping
showCarryBoxes: false,
showAnswerBoxes: false,
showPlaceValueColors: false,
showTenFrames: true,
showProblemNumbers: false,
transparentBackground: true,
},
},
{
name: 'ten-frames-both-columns',
filename: 'ten-frames-both-columns.svg',
description: 'Ten-frames in both columns: Double regrouping',
options: {
addend1: 57,
addend2: 68, // Both ones (7+8=15) and tens (5+6+1=12) regroup
showCarryBoxes: false,
showAnswerBoxes: false,
showPlaceValueColors: false,
showTenFrames: true,
showProblemNumbers: false,
transparentBackground: true,
},
},
] as const
console.log('Generating ten-frame example images (single problems)...\n')
for (const example of examples) {
console.log(`Generating ${example.description}...`)
try {
const typstSource = generateExampleTypst(example.options)
// Compile to SVG
let svg = execSync('typst compile --format svg - -', {
input: typstSource,
encoding: 'utf8',
maxBuffer: 2 * 1024 * 1024,
})
// Post-process: Make SVG visible on dark background
// - Digits on white cells should stay BLACK
// - Operator symbols (+) should be WHITE
// - Structural elements (borders, bars) should be WHITE
svg = svg
.replace(/stroke="#000000"/g, 'stroke="rgba(255, 255, 255, 0.8)"')
.replace(/stroke="#0000004d"/g, 'stroke="rgba(255, 255, 255, 0.4)"')
// Replace operator (+) fill specifically to white
svg = svg.replace(
/(<use xlink:href="#gCFEF70472F9D2AA9AC128F96529819DA"[^>]*fill=")#000000/g,
'$1rgba(255, 255, 255, 0.9)'
)
// Save to file
const outputPath = path.join(outputDir, example.filename)
fs.writeFileSync(outputPath, svg, 'utf-8')
console.log(` ✓ Saved to ${outputPath}`)
} catch (error) {
console.error(` ✗ Error generating ${example.name}:`, error)
}
}
console.log('\nDone! Ten-frame example images generated.')
console.log(`\nFiles saved to: ${outputDir}`)