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>
202 lines
6.3 KiB
TypeScript
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}`)
|