fix(worksheets): dynamically size grid based on actual problem digits

Problem: All problems rendered with max-digits columns (e.g., 5 columns
for 2-digit problems), showing scaffolding for unused place values.

Root cause: Grid column count was fixed at page-level max-digits, and
all rendering loops used max-digits instead of per-problem actual-digits.

Solution:
- Calculate actual-digits per problem (max of addends + sum, accounting
  for overflow like 99+1=100)
- Extract max-digits+1 positions to capture sum overflow
- Generate column-list dynamically in Typst based on actual-digits
- Update all loops to use actual-digits instead of max-digits
- Hide leading zeros by checking i <= highest position

Now a 2-digit problem gets a 3-column grid (allowing sum overflow),
and a 5-digit problem gets a 6-column grid. Each problem renders
exactly the scaffolding it needs.

Includes:
- Leading zero detection (a-highest, b-highest, sum-highest)
- Dynamic column list generation in Typst
- Ten-frames support for all place values (removed digit restrictions)
- Proper overflow handling for sums

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-07 22:09:02 -06:00
parent c0298cf65d
commit 130bbd49dd

View File

@@ -16,6 +16,24 @@ function chunkProblems(problems: AdditionProblem[], pageSize: number): AdditionP
return pages
}
/**
* Calculate maximum number of digits in any problem on this page
* Returns max digits across all addends (handles 1-6 digit sums)
*/
function calculateMaxDigits(problems: AdditionProblem[]): number {
let maxDigits = 1
for (const problem of problems) {
const digitsA = problem.a.toString().length
const digitsB = problem.b.toString().length
const maxProblemDigits = Math.max(digitsA, digitsB)
maxDigits = Math.max(maxDigits, maxProblemDigits)
}
// Sum might have one more digit than the largest addend
// e.g., 99999 + 99999 = 199998 (5 digits + 5 digits = 6 digits)
// But we render based on addend size, not sum size
return maxDigits
}
/**
* Generate Typst source code for a single page
*/
@@ -31,6 +49,10 @@ function generatePageTypst(
showTenFrames: config.mode === 'manual' ? config.showTenFrames : 'N/A (smart mode)',
})
// Calculate maximum digits for proper column layout
const maxDigits = calculateMaxDigits(pageProblems)
console.log('[typstGenerator] Max digits on this page:', maxDigits)
// Enrich problems with display options based on mode
const enrichedProblems = pageProblems.map((p, index) => {
if (config.mode === 'smart') {
@@ -103,10 +125,19 @@ function generatePageTypst(
const anyProblemMayShowTenFrames = enrichedProblems.some((p) => p.showTenFrames)
// Calculate cell size to fill the entire problem box
// Without ten-frames: 5 rows (carry, first number, second number, line, answer)
// With ten-frames: 5 rows + ten-frames row (0.8 * cellSize for square cells)
// Total with ten-frames: 5.8 rows, reduced breathing room to maximize size
const cellSize = anyProblemMayShowTenFrames ? problemBoxHeight / 6.0 : problemBoxHeight / 4.5
// Base vertical stack: carry row + addend1 + addend2 + line + answer = 5 rows
// With ten-frames: add 0.8 * cellSize row
// Total with ten-frames: ~5.8 rows
//
// Horizontal constraint: maxDigits columns + 1 for + sign
// Cell size must fit: (maxDigits + 1) * cellSize <= problemBoxWidth
const maxCellSizeForWidth = problemBoxWidth / (maxDigits + 1)
const maxCellSizeForHeight = anyProblemMayShowTenFrames
? problemBoxHeight / 6.0
: problemBoxHeight / 4.5
// Use the smaller of width/height constraints
const cellSize = Math.min(maxCellSizeForWidth, maxCellSizeForHeight)
return String.raw`
// addition-worksheet-page.typ (auto-generated)
@@ -135,15 +166,11 @@ function generatePageTypst(
${generateTypstHelpers(cellSize)}
${generateProblemStackFunction(cellSize)}
${generateProblemStackFunction(cellSize, maxDigits)}
#let problem-box(problem, index) = {
let a = problem.a
let b = problem.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)
// Extract per-problem display flags
let grid-stroke = if problem.showCellBorder { (thickness: 1pt, dash: "dashed", paint: gray.darken(20%)) } else { none }
@@ -156,7 +183,7 @@ ${generateProblemStackFunction(cellSize)}
)[
#align(center + horizon)[
#problem-stack(
a, b, aT, aO, bT, bO, index,
a, b, index,
problem.showCarryBoxes,
problem.showAnswerBoxes,
problem.showPlaceValueColors,