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>
This commit is contained in:
@@ -5,34 +5,29 @@
|
||||
* This script captures git commit, branch, timestamp, and other metadata
|
||||
*/
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require('child_process')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
function exec(command) {
|
||||
try {
|
||||
return execSync(command, { encoding: "utf-8" }).trim();
|
||||
return execSync(command, { encoding: 'utf-8' }).trim()
|
||||
} catch (_error) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getBuildInfo() {
|
||||
// Try to get git info from environment variables first (for Docker builds)
|
||||
// Fall back to git commands (for local development)
|
||||
const gitCommit = process.env.GIT_COMMIT || exec("git rev-parse HEAD");
|
||||
const gitCommitShort =
|
||||
process.env.GIT_COMMIT_SHORT || exec("git rev-parse --short HEAD");
|
||||
const gitBranch =
|
||||
process.env.GIT_BRANCH || exec("git rev-parse --abbrev-ref HEAD");
|
||||
const gitTag =
|
||||
process.env.GIT_TAG ||
|
||||
exec("git describe --tags --exact-match 2>/dev/null");
|
||||
const gitCommit = process.env.GIT_COMMIT || exec('git rev-parse HEAD')
|
||||
const gitCommitShort = process.env.GIT_COMMIT_SHORT || exec('git rev-parse --short HEAD')
|
||||
const gitBranch = process.env.GIT_BRANCH || exec('git rev-parse --abbrev-ref HEAD')
|
||||
const gitTag = process.env.GIT_TAG || exec('git describe --tags --exact-match 2>/dev/null')
|
||||
const gitDirty =
|
||||
process.env.GIT_DIRTY === "true" ||
|
||||
exec('git diff --quiet || echo "dirty"') === "dirty";
|
||||
process.env.GIT_DIRTY === 'true' || exec('git diff --quiet || echo "dirty"') === 'dirty'
|
||||
|
||||
const packageJson = require("../package.json");
|
||||
const packageJson = require('../package.json')
|
||||
|
||||
return {
|
||||
version: packageJson.version,
|
||||
@@ -45,28 +40,22 @@ function getBuildInfo() {
|
||||
tag: gitTag,
|
||||
isDirty: gitDirty,
|
||||
},
|
||||
environment: process.env.NODE_ENV || "development",
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
buildNumber: process.env.BUILD_NUMBER || null,
|
||||
nodeVersion: process.version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const buildInfo = getBuildInfo();
|
||||
const outputPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"generated",
|
||||
"build-info.json",
|
||||
);
|
||||
const buildInfo = getBuildInfo()
|
||||
const outputPath = path.join(__dirname, '..', 'src', 'generated', 'build-info.json')
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(outputPath);
|
||||
const dir = path.dirname(outputPath)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(buildInfo, null, 2));
|
||||
fs.writeFileSync(outputPath, JSON.stringify(buildInfo, null, 2))
|
||||
|
||||
console.log("✅ Build info generated:", outputPath);
|
||||
console.log(JSON.stringify(buildInfo, null, 2));
|
||||
console.log('✅ Build info generated:', outputPath)
|
||||
console.log(JSON.stringify(buildInfo, null, 2))
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
* SVG output as the interactive client-side version (without animations).
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { AbacusReact } from "@soroban/abacus-react";
|
||||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
|
||||
// Extract just the SVG element content from rendered output
|
||||
function extractSvgContent(markup: string): string {
|
||||
// Find the opening <svg and closing </svg> tags
|
||||
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/);
|
||||
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)
|
||||
if (!svgMatch) {
|
||||
throw new Error("No SVG element found in rendered output");
|
||||
throw new Error('No SVG element found in rendered output')
|
||||
}
|
||||
return svgMatch[1]; // Return just the inner content
|
||||
return svgMatch[1] // Return just the inner content
|
||||
}
|
||||
|
||||
// Generate the favicon (icon.svg) - single column showing value 5
|
||||
@@ -34,27 +34,27 @@ function generateFavicon(): string {
|
||||
interactive={false}
|
||||
showNumbers={false}
|
||||
customStyles={{
|
||||
heavenBeads: { fill: "#7c2d12", stroke: "#451a03", strokeWidth: 1 },
|
||||
earthBeads: { fill: "#7c2d12", stroke: "#451a03", strokeWidth: 1 },
|
||||
heavenBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
|
||||
columnPosts: {
|
||||
fill: "#451a03",
|
||||
stroke: "#292524",
|
||||
fill: '#451a03',
|
||||
stroke: '#292524',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: "#292524",
|
||||
stroke: "#292524",
|
||||
fill: '#292524',
|
||||
stroke: '#292524',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
/>
|
||||
)
|
||||
|
||||
// Extract just the SVG content (without div wrapper)
|
||||
let svgContent = extractSvgContent(abacusMarkup);
|
||||
let svgContent = extractSvgContent(abacusMarkup)
|
||||
|
||||
// Remove !important from CSS (production code policy)
|
||||
svgContent = svgContent.replace(/\s*!important/g, "");
|
||||
svgContent = svgContent.replace(/\s*!important/g, '')
|
||||
|
||||
// Wrap in SVG with proper viewBox for favicon sizing
|
||||
// AbacusReact with 1 column + scaleFactor 1.0 = ~25×120px
|
||||
@@ -68,7 +68,7 @@ function generateFavicon(): string {
|
||||
${svgContent}
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
// Generate the Open Graph image (og-image.svg)
|
||||
@@ -83,46 +83,46 @@ function generateOGImage(): string {
|
||||
showNumbers={false}
|
||||
customStyles={{
|
||||
columnPosts: {
|
||||
fill: "rgb(255, 255, 255)",
|
||||
stroke: "rgb(200, 200, 200)",
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: "rgb(255, 255, 255)",
|
||||
stroke: "rgb(200, 200, 200)",
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
columns: {
|
||||
0: {
|
||||
// Ones place (rightmost) - Blue
|
||||
heavenBeads: { fill: "#60a5fa", stroke: "#3b82f6", strokeWidth: 1 },
|
||||
earthBeads: { fill: "#60a5fa", stroke: "#3b82f6", strokeWidth: 1 },
|
||||
heavenBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
|
||||
},
|
||||
1: {
|
||||
// Tens place - Green
|
||||
heavenBeads: { fill: "#4ade80", stroke: "#22c55e", strokeWidth: 1 },
|
||||
earthBeads: { fill: "#4ade80", stroke: "#22c55e", strokeWidth: 1 },
|
||||
heavenBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
|
||||
},
|
||||
2: {
|
||||
// Hundreds place - Yellow/Gold
|
||||
heavenBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 1 },
|
||||
earthBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 1 },
|
||||
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
|
||||
},
|
||||
3: {
|
||||
// Thousands place (leftmost) - Purple
|
||||
heavenBeads: { fill: "#c084fc", stroke: "#a855f7", strokeWidth: 1 },
|
||||
earthBeads: { fill: "#c084fc", stroke: "#a855f7", strokeWidth: 1 },
|
||||
heavenBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
|
||||
earthBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
/>
|
||||
)
|
||||
|
||||
// Extract just the SVG content (without div wrapper)
|
||||
let svgContent = extractSvgContent(abacusMarkup);
|
||||
let svgContent = extractSvgContent(abacusMarkup)
|
||||
|
||||
// Remove !important from CSS (production code policy)
|
||||
svgContent = svgContent.replace(/\s*!important/g, "");
|
||||
svgContent = svgContent.replace(/\s*!important/g, '')
|
||||
|
||||
return `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Dark background like homepage -->
|
||||
@@ -199,24 +199,22 @@ function generateOGImage(): string {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const appDir = __dirname.replace("/scripts", "");
|
||||
const appDir = __dirname.replace('/scripts', '')
|
||||
|
||||
try {
|
||||
console.log("Generating Open Graph image from AbacusReact...");
|
||||
const ogImageSvg = generateOGImage();
|
||||
writeFileSync(join(appDir, "public", "og-image.svg"), ogImageSvg);
|
||||
console.log("✓ Generated public/og-image.svg");
|
||||
console.log('Generating Open Graph image from AbacusReact...')
|
||||
const ogImageSvg = generateOGImage()
|
||||
writeFileSync(join(appDir, 'public', 'og-image.svg'), ogImageSvg)
|
||||
console.log('✓ Generated public/og-image.svg')
|
||||
|
||||
console.log("\n✅ Icon generated successfully!");
|
||||
console.log(
|
||||
"\nNote: Day-of-month favicons are generated on-demand by src/app/icon/route.tsx",
|
||||
);
|
||||
console.log("which calls scripts/generateDayIcon.tsx as a subprocess.");
|
||||
console.log('\n✅ Icon generated successfully!')
|
||||
console.log('\nNote: Day-of-month favicons are generated on-demand by src/app/icon/route.tsx')
|
||||
console.log('which calls scripts/generateDayIcon.tsx as a subprocess.')
|
||||
} catch (error) {
|
||||
console.error("❌ Error generating icons:", error);
|
||||
process.exit(1);
|
||||
console.error('❌ Error generating icons:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
*/
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedImports: React is required for JSX transform
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { writeFileSync, mkdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { AbacusStatic } from "@soroban/abacus-react";
|
||||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { writeFileSync, mkdirSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { AbacusStatic } from '@soroban/abacus-react'
|
||||
|
||||
// Extract just the SVG element from rendered output
|
||||
function extractSvgElement(markup: string): string {
|
||||
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/);
|
||||
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
|
||||
if (!svgMatch) {
|
||||
throw new Error("No SVG element found in rendered output");
|
||||
throw new Error('No SVG element found in rendered output')
|
||||
}
|
||||
return svgMatch[0];
|
||||
return svgMatch[0]
|
||||
}
|
||||
|
||||
// Generate a single day icon
|
||||
@@ -44,97 +44,95 @@ function generateDayIcon(day: number): string {
|
||||
}}
|
||||
customStyles={{
|
||||
columnPosts: {
|
||||
fill: "#1c1917",
|
||||
stroke: "#0c0a09",
|
||||
fill: '#1c1917',
|
||||
stroke: '#0c0a09',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: "#1c1917",
|
||||
stroke: "#0c0a09",
|
||||
fill: '#1c1917',
|
||||
stroke: '#0c0a09',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
columns: {
|
||||
0: {
|
||||
// Ones place - Gold (royal theme)
|
||||
heavenBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 2 },
|
||||
earthBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 2 },
|
||||
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
|
||||
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
|
||||
},
|
||||
1: {
|
||||
// Tens place - Purple (royal theme)
|
||||
heavenBeads: { fill: "#a855f7", stroke: "#7e22ce", strokeWidth: 2 },
|
||||
earthBeads: { fill: "#a855f7", stroke: "#7e22ce", strokeWidth: 2 },
|
||||
heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
|
||||
earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
/>
|
||||
)
|
||||
|
||||
// Extract the cropped SVG
|
||||
let croppedSvg = extractSvgElement(abacusMarkup);
|
||||
let croppedSvg = extractSvgElement(abacusMarkup)
|
||||
|
||||
// Remove !important from CSS (production code policy)
|
||||
croppedSvg = croppedSvg.replace(/\s*!important/g, "");
|
||||
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
|
||||
|
||||
// Parse width and height from the cropped SVG
|
||||
const widthMatch = croppedSvg.match(/width="([^"]+)"/);
|
||||
const heightMatch = croppedSvg.match(/height="([^"]+)"/);
|
||||
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
|
||||
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
|
||||
|
||||
if (!widthMatch || !heightMatch) {
|
||||
throw new Error("Could not parse dimensions from cropped SVG");
|
||||
throw new Error('Could not parse dimensions from cropped SVG')
|
||||
}
|
||||
|
||||
const croppedWidth = parseFloat(widthMatch[1]);
|
||||
const croppedHeight = parseFloat(heightMatch[1]);
|
||||
const croppedWidth = parseFloat(widthMatch[1])
|
||||
const croppedHeight = parseFloat(heightMatch[1])
|
||||
|
||||
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
|
||||
const targetSize = 96;
|
||||
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight);
|
||||
const targetSize = 96
|
||||
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
|
||||
|
||||
// Center in 100x100 canvas
|
||||
const scaledWidth = croppedWidth * scale;
|
||||
const scaledHeight = croppedHeight * scale;
|
||||
const offsetX = (100 - scaledWidth) / 2;
|
||||
const offsetY = (100 - scaledHeight) / 2;
|
||||
const scaledWidth = croppedWidth * scale
|
||||
const scaledHeight = croppedHeight * scale
|
||||
const offsetX = (100 - scaledWidth) / 2
|
||||
const offsetY = (100 - scaledHeight) / 2
|
||||
|
||||
// Wrap in 100x100 SVG canvas for favicon
|
||||
// Extract viewBox from cropped SVG to preserve it
|
||||
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/);
|
||||
const viewBox = viewBoxMatch
|
||||
? viewBoxMatch[1]
|
||||
: `0 0 ${croppedWidth} ${croppedHeight}`;
|
||||
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
|
||||
const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`
|
||||
|
||||
return `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Abacus showing day ${day.toString().padStart(2, "0")} (US Central Time) - cropped to active beads -->
|
||||
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
|
||||
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
|
||||
viewBox="${viewBox}">
|
||||
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ""}
|
||||
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
|
||||
</svg>
|
||||
</svg>
|
||||
`;
|
||||
`
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const publicDir = join(__dirname, "..", "public");
|
||||
const iconsDir = join(publicDir, "icons");
|
||||
const publicDir = join(__dirname, '..', 'public')
|
||||
const iconsDir = join(publicDir, 'icons')
|
||||
|
||||
try {
|
||||
// Ensure icons directory exists
|
||||
mkdirSync(iconsDir, { recursive: true });
|
||||
mkdirSync(iconsDir, { recursive: true })
|
||||
|
||||
console.log("Generating all 31 day-of-month favicons...\n");
|
||||
console.log('Generating all 31 day-of-month favicons...\n')
|
||||
|
||||
// Generate all 31 days
|
||||
for (let day = 1; day <= 31; day++) {
|
||||
const svg = generateDayIcon(day);
|
||||
const filename = `icon-day-${day.toString().padStart(2, "0")}.svg`;
|
||||
const filepath = join(iconsDir, filename);
|
||||
writeFileSync(filepath, svg);
|
||||
console.log(`✓ Generated ${filename}`);
|
||||
const svg = generateDayIcon(day)
|
||||
const filename = `icon-day-${day.toString().padStart(2, '0')}.svg`
|
||||
const filepath = join(iconsDir, filename)
|
||||
writeFileSync(filepath, svg)
|
||||
console.log(`✓ Generated ${filename}`)
|
||||
}
|
||||
|
||||
console.log("\n✅ All day icons generated successfully!");
|
||||
console.log(` Location: public/icons/icon-day-*.svg`);
|
||||
console.log('\n✅ All day icons generated successfully!')
|
||||
console.log(` Location: public/icons/icon-day-*.svg`)
|
||||
} catch (error) {
|
||||
console.error("❌ Error generating day icons:", error);
|
||||
process.exit(1);
|
||||
console.error('❌ Error generating day icons:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -1,102 +1,97 @@
|
||||
// Script to generate example worksheet images for the blog post
|
||||
// Shows different scaffolding levels for the 2D difficulty blog post
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { generateWorksheetPreview } from "../src/app/create/worksheets/addition/generatePreview";
|
||||
import { DIFFICULTY_PROFILES } from "../src/app/create/worksheets/addition/difficultyProfiles";
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { generateWorksheetPreview } from '../src/app/create/worksheets/addition/generatePreview'
|
||||
import { DIFFICULTY_PROFILES } from '../src/app/create/worksheets/addition/difficultyProfiles'
|
||||
|
||||
// Output directory
|
||||
const outputDir = path.join(
|
||||
process.cwd(),
|
||||
"public",
|
||||
"blog",
|
||||
"difficulty-examples",
|
||||
);
|
||||
const outputDir = path.join(process.cwd(), 'public', 'blog', 'difficulty-examples')
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Generate examples with SAME regrouping level but different scaffolding
|
||||
// This clearly shows how scaffolding changes while keeping problem complexity constant
|
||||
const examples = [
|
||||
{
|
||||
name: "full-scaffolding",
|
||||
filename: "full-scaffolding.svg",
|
||||
description: "Full Scaffolding: Maximum visual support",
|
||||
name: 'full-scaffolding',
|
||||
filename: 'full-scaffolding.svg',
|
||||
description: 'Full Scaffolding: Maximum visual support',
|
||||
// Use medium regrouping with full scaffolding
|
||||
config: {
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.7,
|
||||
displayRules: {
|
||||
carryBoxes: "always" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "always" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'always' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'always' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "medium-scaffolding",
|
||||
filename: "medium-scaffolding.svg",
|
||||
description: "Medium Scaffolding: Strategic support",
|
||||
name: 'medium-scaffolding',
|
||||
filename: 'medium-scaffolding.svg',
|
||||
description: 'Medium Scaffolding: Strategic support',
|
||||
config: {
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.7,
|
||||
displayRules: {
|
||||
carryBoxes: "whenRegrouping" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "when3PlusDigits" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'whenRegrouping' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'when3PlusDigits' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minimal-scaffolding",
|
||||
filename: "minimal-scaffolding.svg",
|
||||
description: "Minimal Scaffolding: Carry boxes only",
|
||||
name: 'minimal-scaffolding',
|
||||
filename: 'minimal-scaffolding.svg',
|
||||
description: 'Minimal Scaffolding: Carry boxes only',
|
||||
config: {
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.7,
|
||||
displayRules: {
|
||||
carryBoxes: "whenMultipleRegroups" as const,
|
||||
answerBoxes: "never" as const,
|
||||
placeValueColors: "never" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'whenMultipleRegroups' as const,
|
||||
answerBoxes: 'never' as const,
|
||||
placeValueColors: 'never' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-scaffolding",
|
||||
filename: "no-scaffolding.svg",
|
||||
description: "No Scaffolding: Students work independently",
|
||||
name: 'no-scaffolding',
|
||||
filename: 'no-scaffolding.svg',
|
||||
description: 'No Scaffolding: Students work independently',
|
||||
config: {
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.7,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "never" as const,
|
||||
placeValueColors: "never" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'never' as const,
|
||||
placeValueColors: 'never' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
] as const
|
||||
|
||||
console.log("Generating blog example worksheets...\n");
|
||||
console.log('Generating blog example worksheets...\n')
|
||||
|
||||
for (const example of examples) {
|
||||
console.log(`Generating ${example.description}...`);
|
||||
console.log(`Generating ${example.description}...`)
|
||||
|
||||
const config = {
|
||||
pAllStart: example.config.pAllStart,
|
||||
@@ -105,28 +100,28 @@ for (const example of examples) {
|
||||
problemsPerPage: 4,
|
||||
pages: 1,
|
||||
cols: 2,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = generateWorksheetPreview(config);
|
||||
const result = generateWorksheetPreview(config)
|
||||
|
||||
if (!result.success || !result.pages || result.pages.length === 0) {
|
||||
console.error(`Failed to generate ${example.name}:`, result.error);
|
||||
continue;
|
||||
console.error(`Failed to generate ${example.name}:`, result.error)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the first page's SVG
|
||||
const svg = result.pages[0];
|
||||
const svg = result.pages[0]
|
||||
|
||||
// Save to file
|
||||
const outputPath = path.join(outputDir, example.filename);
|
||||
fs.writeFileSync(outputPath, svg, "utf-8");
|
||||
const outputPath = path.join(outputDir, example.filename)
|
||||
fs.writeFileSync(outputPath, svg, 'utf-8')
|
||||
|
||||
console.log(` ✓ Saved to ${outputPath}`);
|
||||
console.log(` ✓ Saved to ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error generating ${example.name}:`, error);
|
||||
console.error(` ✗ Error generating ${example.name}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nDone! Example worksheets generated.");
|
||||
console.log(`\nFiles saved to: ${outputDir}`);
|
||||
console.log('\nDone! Example worksheets generated.')
|
||||
console.log(`\nFiles saved to: ${outputDir}`)
|
||||
|
||||
@@ -6,26 +6,26 @@
|
||||
* Example: npx tsx scripts/generateDayIcon.tsx 15
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { AbacusStatic } from "@soroban/abacus-react";
|
||||
import React from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { AbacusStatic } from '@soroban/abacus-react'
|
||||
|
||||
// Extract just the SVG element from rendered output
|
||||
function extractSvgElement(markup: string): string {
|
||||
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/);
|
||||
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
|
||||
if (!svgMatch) {
|
||||
throw new Error("No SVG element found in rendered output");
|
||||
throw new Error('No SVG element found in rendered output')
|
||||
}
|
||||
return svgMatch[0];
|
||||
return svgMatch[0]
|
||||
}
|
||||
|
||||
// Get day from command line argument
|
||||
const day = parseInt(process.argv[2], 10);
|
||||
const day = parseInt(process.argv[2], 10)
|
||||
|
||||
if (!day || day < 1 || day > 31) {
|
||||
console.error("Usage: npx tsx scripts/generateDayIcon.tsx <day>");
|
||||
console.error("Example: npx tsx scripts/generateDayIcon.tsx 15");
|
||||
process.exit(1);
|
||||
console.error('Usage: npx tsx scripts/generateDayIcon.tsx <day>')
|
||||
console.error('Example: npx tsx scripts/generateDayIcon.tsx 15')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Render 2-column abacus showing day of month
|
||||
@@ -48,73 +48,71 @@ const abacusMarkup = renderToStaticMarkup(
|
||||
}}
|
||||
customStyles={{
|
||||
columnPosts: {
|
||||
fill: "#1c1917",
|
||||
stroke: "#0c0a09",
|
||||
fill: '#1c1917',
|
||||
stroke: '#0c0a09',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: "#1c1917",
|
||||
stroke: "#0c0a09",
|
||||
fill: '#1c1917',
|
||||
stroke: '#0c0a09',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
columns: {
|
||||
0: {
|
||||
// Ones place - Gold (royal theme)
|
||||
heavenBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 2 },
|
||||
earthBeads: { fill: "#fbbf24", stroke: "#f59e0b", strokeWidth: 2 },
|
||||
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
|
||||
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
|
||||
},
|
||||
1: {
|
||||
// Tens place - Purple (royal theme)
|
||||
heavenBeads: { fill: "#a855f7", stroke: "#7e22ce", strokeWidth: 2 },
|
||||
earthBeads: { fill: "#a855f7", stroke: "#7e22ce", strokeWidth: 2 },
|
||||
heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
|
||||
earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
/>
|
||||
)
|
||||
|
||||
// Extract the cropped SVG
|
||||
let croppedSvg = extractSvgElement(abacusMarkup);
|
||||
let croppedSvg = extractSvgElement(abacusMarkup)
|
||||
|
||||
// Remove !important from CSS (production code policy)
|
||||
croppedSvg = croppedSvg.replace(/\s*!important/g, "");
|
||||
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
|
||||
|
||||
// Parse width and height from the cropped SVG
|
||||
const widthMatch = croppedSvg.match(/width="([^"]+)"/);
|
||||
const heightMatch = croppedSvg.match(/height="([^"]+)"/);
|
||||
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
|
||||
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
|
||||
|
||||
if (!widthMatch || !heightMatch) {
|
||||
throw new Error("Could not parse dimensions from cropped SVG");
|
||||
throw new Error('Could not parse dimensions from cropped SVG')
|
||||
}
|
||||
|
||||
const croppedWidth = parseFloat(widthMatch[1]);
|
||||
const croppedHeight = parseFloat(heightMatch[1]);
|
||||
const croppedWidth = parseFloat(widthMatch[1])
|
||||
const croppedHeight = parseFloat(heightMatch[1])
|
||||
|
||||
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
|
||||
const targetSize = 96;
|
||||
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight);
|
||||
const targetSize = 96
|
||||
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
|
||||
|
||||
// Center in 100x100 canvas
|
||||
const scaledWidth = croppedWidth * scale;
|
||||
const scaledHeight = croppedHeight * scale;
|
||||
const offsetX = (100 - scaledWidth) / 2;
|
||||
const offsetY = (100 - scaledHeight) / 2;
|
||||
const scaledWidth = croppedWidth * scale
|
||||
const scaledHeight = croppedHeight * scale
|
||||
const offsetX = (100 - scaledWidth) / 2
|
||||
const offsetY = (100 - scaledHeight) / 2
|
||||
|
||||
// Wrap in 100x100 SVG canvas for favicon
|
||||
// Extract viewBox from cropped SVG to preserve it
|
||||
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/);
|
||||
const viewBox = viewBoxMatch
|
||||
? viewBoxMatch[1]
|
||||
: `0 0 ${croppedWidth} ${croppedHeight}`;
|
||||
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
|
||||
const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`
|
||||
|
||||
const svg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Abacus showing day ${day.toString().padStart(2, "0")} (US Central Time) - cropped to active beads -->
|
||||
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
|
||||
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
|
||||
viewBox="${viewBox}">
|
||||
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ""}
|
||||
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
|
||||
</svg>
|
||||
</svg>
|
||||
`;
|
||||
`
|
||||
|
||||
// Output to stdout so parent process can capture it
|
||||
process.stdout.write(svg);
|
||||
process.stdout.write(svg)
|
||||
|
||||
@@ -1,42 +1,37 @@
|
||||
// Script to generate multi-digit (>2 digits) worksheet examples for the blog post
|
||||
// Shows how scaffolding adapts to different digit ranges
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { generateWorksheetPreview } from "../src/app/create/worksheets/addition/generatePreview";
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { generateWorksheetPreview } from '../src/app/create/worksheets/addition/generatePreview'
|
||||
|
||||
// Output directory
|
||||
const outputDir = path.join(
|
||||
process.cwd(),
|
||||
"public",
|
||||
"blog",
|
||||
"multi-digit-examples",
|
||||
);
|
||||
const outputDir = path.join(process.cwd(), 'public', 'blog', 'multi-digit-examples')
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Generate examples showing different digit ranges and adaptive scaffolding
|
||||
const examples = [
|
||||
{
|
||||
name: "two-digit-addition",
|
||||
filename: "two-digit.svg",
|
||||
description: "2-digit addition (baseline)",
|
||||
name: 'two-digit-addition',
|
||||
filename: 'two-digit.svg',
|
||||
description: '2-digit addition (baseline)',
|
||||
config: {
|
||||
operator: "addition" as const,
|
||||
operator: 'addition' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 0.5,
|
||||
digitRange: { min: 2, max: 2 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "always" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "never" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'always' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'never' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: false,
|
||||
showBorrowingHints: false,
|
||||
@@ -44,22 +39,22 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three-digit-with-colors",
|
||||
filename: "three-digit-colors.svg",
|
||||
description: "3-digit addition with place value colors",
|
||||
name: 'three-digit-with-colors',
|
||||
filename: 'three-digit-colors.svg',
|
||||
description: '3-digit addition with place value colors',
|
||||
config: {
|
||||
operator: "addition" as const,
|
||||
operator: 'addition' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 0.5,
|
||||
digitRange: { min: 3, max: 3 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "always" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'always' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: false,
|
||||
showBorrowingHints: false,
|
||||
@@ -68,22 +63,22 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "four-digit-addition",
|
||||
filename: "four-digit.svg",
|
||||
description: "4-digit addition with adaptive scaffolding",
|
||||
name: 'four-digit-addition',
|
||||
filename: 'four-digit.svg',
|
||||
description: '4-digit addition with adaptive scaffolding',
|
||||
config: {
|
||||
operator: "addition" as const,
|
||||
operator: 'addition' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 0.6,
|
||||
digitRange: { min: 4, max: 4 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "always" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'always' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: false,
|
||||
showBorrowingHints: false,
|
||||
@@ -92,22 +87,22 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "five-digit-addition",
|
||||
filename: "five-digit.svg",
|
||||
description: "5-digit addition (maximum complexity)",
|
||||
name: 'five-digit-addition',
|
||||
filename: 'five-digit.svg',
|
||||
description: '5-digit addition (maximum complexity)',
|
||||
config: {
|
||||
operator: "addition" as const,
|
||||
operator: 'addition' as const,
|
||||
pAllStart: 0.3,
|
||||
pAnyStart: 0.8,
|
||||
digitRange: { min: 5, max: 5 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "always" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'always' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: false,
|
||||
showBorrowingHints: false,
|
||||
@@ -116,22 +111,22 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed-digit-range",
|
||||
filename: "mixed-range.svg",
|
||||
description: "Mixed problem sizes (2-4 digits)",
|
||||
name: 'mixed-digit-range',
|
||||
filename: 'mixed-range.svg',
|
||||
description: 'Mixed problem sizes (2-4 digits)',
|
||||
config: {
|
||||
operator: "addition" as const,
|
||||
operator: 'addition' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 0.5,
|
||||
digitRange: { min: 2, max: 4 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "always" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'always' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: false,
|
||||
showBorrowingHints: false,
|
||||
@@ -140,33 +135,33 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three-digit-subtraction",
|
||||
filename: "three-digit-subtraction.svg",
|
||||
description: "3-digit subtraction with borrowing",
|
||||
name: 'three-digit-subtraction',
|
||||
filename: 'three-digit-subtraction.svg',
|
||||
description: '3-digit subtraction with borrowing',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 0.8,
|
||||
digitRange: { min: 3, max: 3 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: true,
|
||||
showBorrowingHints: false,
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
] as const
|
||||
|
||||
console.log("Generating multi-digit example worksheets...\n");
|
||||
console.log('Generating multi-digit example worksheets...\n')
|
||||
|
||||
for (const example of examples) {
|
||||
console.log(`Generating ${example.description}...`);
|
||||
console.log(`Generating ${example.description}...`)
|
||||
|
||||
const config = {
|
||||
...example.config,
|
||||
@@ -174,29 +169,29 @@ for (const example of examples) {
|
||||
pages: 1,
|
||||
cols: 2,
|
||||
seed: 54321, // Fixed seed for consistent examples
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = generateWorksheetPreview(config);
|
||||
const result = generateWorksheetPreview(config)
|
||||
|
||||
if (!result.success || !result.pages || result.pages.length === 0) {
|
||||
console.error(`Failed to generate ${example.name}:`, result.error);
|
||||
console.error(`Details:`, result.details);
|
||||
continue;
|
||||
console.error(`Failed to generate ${example.name}:`, result.error)
|
||||
console.error(`Details:`, result.details)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the first page's SVG
|
||||
const svg = result.pages[0];
|
||||
const svg = result.pages[0]
|
||||
|
||||
// Save to file
|
||||
const outputPath = path.join(outputDir, example.filename);
|
||||
fs.writeFileSync(outputPath, svg, "utf-8");
|
||||
const outputPath = path.join(outputDir, example.filename)
|
||||
fs.writeFileSync(outputPath, svg, 'utf-8')
|
||||
|
||||
console.log(` ✓ Saved to ${outputPath}`);
|
||||
console.log(` ✓ Saved to ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error generating ${example.name}:`, error);
|
||||
console.error(` ✗ Error generating ${example.name}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nDone! Multi-digit example worksheets generated.");
|
||||
console.log(`\nFiles saved to: ${outputDir}`);
|
||||
console.log('\nDone! Multi-digit example worksheets generated.')
|
||||
console.log(`\nFiles saved to: ${outputDir}`)
|
||||
|
||||
@@ -1,174 +1,169 @@
|
||||
// Script to generate subtraction worksheet examples for the blog post
|
||||
// Shows different scaffolding levels for subtraction problems
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { generateWorksheetPreview } from "../src/app/create/worksheets/addition/generatePreview";
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { generateWorksheetPreview } from '../src/app/create/worksheets/addition/generatePreview'
|
||||
|
||||
// Output directory
|
||||
const outputDir = path.join(
|
||||
process.cwd(),
|
||||
"public",
|
||||
"blog",
|
||||
"subtraction-examples",
|
||||
);
|
||||
const outputDir = path.join(process.cwd(), 'public', 'blog', 'subtraction-examples')
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Generate examples showing different subtraction scaffolding options
|
||||
const examples = [
|
||||
{
|
||||
name: "subtraction-no-borrowing",
|
||||
filename: "no-borrowing.svg",
|
||||
description: "Simple subtraction (no borrowing needed)",
|
||||
name: 'subtraction-no-borrowing',
|
||||
filename: 'no-borrowing.svg',
|
||||
description: 'Simple subtraction (no borrowing needed)',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 0.0, // No borrowing problems
|
||||
pAnyStart: 0.0,
|
||||
digitRange: { min: 2, max: 2 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "never" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'never' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: false,
|
||||
showBorrowingHints: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtraction-with-borrow-notation",
|
||||
filename: "with-borrow-notation.svg",
|
||||
description: "Subtraction with borrow notation boxes",
|
||||
name: 'subtraction-with-borrow-notation',
|
||||
filename: 'with-borrow-notation.svg',
|
||||
description: 'Subtraction with borrow notation boxes',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 1.0, // Some borrowing
|
||||
digitRange: { min: 2, max: 2 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: true,
|
||||
showBorrowingHints: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtraction-with-hints",
|
||||
filename: "with-borrowing-hints.svg",
|
||||
description: "Subtraction with borrow notation and hints",
|
||||
name: 'subtraction-with-hints',
|
||||
filename: 'with-borrowing-hints.svg',
|
||||
description: 'Subtraction with borrow notation and hints',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 1.0, // Some borrowing
|
||||
digitRange: { min: 2, max: 2 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: true,
|
||||
showBorrowingHints: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtraction-multiple-borrows",
|
||||
filename: "multiple-borrows.svg",
|
||||
description: "Complex subtraction with multiple borrows",
|
||||
name: 'subtraction-multiple-borrows',
|
||||
filename: 'multiple-borrows.svg',
|
||||
description: 'Complex subtraction with multiple borrows',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 1.0, // All problems require borrowing
|
||||
pAnyStart: 1.0,
|
||||
digitRange: { min: 3, max: 3 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: true,
|
||||
showBorrowingHints: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtraction-single-borrow",
|
||||
filename: "single-borrow-ones.svg",
|
||||
description: "Single borrow in ones place only",
|
||||
name: 'subtraction-single-borrow',
|
||||
filename: 'single-borrow-ones.svg',
|
||||
description: 'Single borrow in ones place only',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 1.0, // Some borrowing
|
||||
digitRange: { min: 2, max: 2 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: true,
|
||||
showBorrowingHints: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtraction-comparison-no-notation",
|
||||
filename: "comparison-no-notation.svg",
|
||||
description: "Borrowing problems WITHOUT notation boxes",
|
||||
name: 'subtraction-comparison-no-notation',
|
||||
filename: 'comparison-no-notation.svg',
|
||||
description: 'Borrowing problems WITHOUT notation boxes',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 1.0,
|
||||
digitRange: { min: 2, max: 2 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "never" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'never' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: false,
|
||||
showBorrowingHints: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtraction-comparison-with-notation",
|
||||
filename: "comparison-with-notation.svg",
|
||||
description: "Same problems WITH notation boxes",
|
||||
name: 'subtraction-comparison-with-notation',
|
||||
filename: 'comparison-with-notation.svg',
|
||||
description: 'Same problems WITH notation boxes',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 1.0,
|
||||
digitRange: { min: 2, max: 2 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "never" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'never' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: true,
|
||||
showBorrowingHints: false,
|
||||
@@ -176,44 +171,44 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtraction-cascading-borrows",
|
||||
filename: "cascading-borrows.svg",
|
||||
description: "Cascading borrows across multiple places",
|
||||
name: 'subtraction-cascading-borrows',
|
||||
filename: 'cascading-borrows.svg',
|
||||
description: 'Cascading borrows across multiple places',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 1.0,
|
||||
pAnyStart: 1.0,
|
||||
digitRange: { min: 4, max: 4 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: true,
|
||||
showBorrowingHints: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subtraction-hints-detail",
|
||||
filename: "hints-detail.svg",
|
||||
description: "Detailed view of borrowing hints",
|
||||
name: 'subtraction-hints-detail',
|
||||
filename: 'hints-detail.svg',
|
||||
description: 'Detailed view of borrowing hints',
|
||||
config: {
|
||||
operator: "subtraction" as const,
|
||||
operator: 'subtraction' as const,
|
||||
pAllStart: 0.0,
|
||||
pAnyStart: 1.0,
|
||||
digitRange: { min: 3, max: 3 },
|
||||
mode: "manual" as const,
|
||||
mode: 'manual' as const,
|
||||
displayRules: {
|
||||
carryBoxes: "never" as const,
|
||||
answerBoxes: "always" as const,
|
||||
placeValueColors: "always" as const,
|
||||
tenFrames: "never" as const,
|
||||
problemNumbers: "always" as const,
|
||||
cellBorders: "always" as const,
|
||||
carryBoxes: 'never' as const,
|
||||
answerBoxes: 'always' as const,
|
||||
placeValueColors: 'always' as const,
|
||||
tenFrames: 'never' as const,
|
||||
problemNumbers: 'always' as const,
|
||||
cellBorders: 'always' as const,
|
||||
},
|
||||
showBorrowNotation: true,
|
||||
showBorrowingHints: true,
|
||||
@@ -221,12 +216,12 @@ const examples = [
|
||||
cols: 1,
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
] as const
|
||||
|
||||
console.log("Generating subtraction example worksheets...\n");
|
||||
console.log('Generating subtraction example worksheets...\n')
|
||||
|
||||
for (const example of examples) {
|
||||
console.log(`Generating ${example.description}...`);
|
||||
console.log(`Generating ${example.description}...`)
|
||||
|
||||
const config = {
|
||||
problemsPerPage: 4,
|
||||
@@ -234,29 +229,29 @@ for (const example of examples) {
|
||||
cols: 2,
|
||||
seed: 12345, // Fixed seed for consistent examples
|
||||
...example.config, // Spread example config last so it can override defaults
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = generateWorksheetPreview(config);
|
||||
const result = generateWorksheetPreview(config)
|
||||
|
||||
if (!result.success || !result.pages || result.pages.length === 0) {
|
||||
console.error(`Failed to generate ${example.name}:`, result.error);
|
||||
console.error(`Details:`, result.details);
|
||||
continue;
|
||||
console.error(`Failed to generate ${example.name}:`, result.error)
|
||||
console.error(`Details:`, result.details)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the first page's SVG
|
||||
const svg = result.pages[0];
|
||||
const svg = result.pages[0]
|
||||
|
||||
// Save to file
|
||||
const outputPath = path.join(outputDir, example.filename);
|
||||
fs.writeFileSync(outputPath, svg, "utf-8");
|
||||
const outputPath = path.join(outputDir, example.filename)
|
||||
fs.writeFileSync(outputPath, svg, 'utf-8')
|
||||
|
||||
console.log(` ✓ Saved to ${outputPath}`);
|
||||
console.log(` ✓ Saved to ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error generating ${example.name}:`, error);
|
||||
console.error(` ✗ Error generating ${example.name}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nDone! Subtraction example worksheets generated.");
|
||||
console.log(`\nFiles saved to: ${outputDir}`);
|
||||
console.log('\nDone! Subtraction example worksheets generated.')
|
||||
console.log(`\nFiles saved to: ${outputDir}`)
|
||||
|
||||
@@ -13,37 +13,32 @@
|
||||
// 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 fs from 'fs'
|
||||
import path from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
import {
|
||||
generateTypstHelpers,
|
||||
generateProblemStackFunction,
|
||||
} from "../src/app/create/worksheets/addition/typstHelpers";
|
||||
} from '../src/app/create/worksheets/addition/typstHelpers'
|
||||
|
||||
// Output directory
|
||||
const outputDir = path.join(
|
||||
process.cwd(),
|
||||
"public",
|
||||
"blog",
|
||||
"ten-frame-examples",
|
||||
);
|
||||
const outputDir = path.join(process.cwd(), 'public', 'blog', 'ten-frame-examples')
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
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;
|
||||
showCarryBoxes?: boolean
|
||||
showAnswerBoxes?: boolean
|
||||
showPlaceValueColors?: boolean
|
||||
showTenFrames?: boolean
|
||||
showProblemNumbers?: boolean
|
||||
transparentBackground?: boolean
|
||||
fontSize?: number
|
||||
addend1: number
|
||||
addend2: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,26 +47,26 @@ interface ExampleOptions {
|
||||
* 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)
|
||||
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;
|
||||
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 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"}
|
||||
#let show-ten-frames-for-all = ${showTenFramesForAll ? 'true' : 'false'}
|
||||
|
||||
${generateTypstHelpers(cellSize)}
|
||||
|
||||
@@ -87,7 +82,7 @@ ${generateProblemStackFunction(cellSize)}
|
||||
#align(center + horizon)[
|
||||
#problem-stack(
|
||||
a, b, aT, aO, bT, bO,
|
||||
${showNumbers ? "0" : "none"},
|
||||
${showNumbers ? '0' : 'none'},
|
||||
${showCarries},
|
||||
${showAnswers},
|
||||
${showColors},
|
||||
@@ -95,16 +90,16 @@ ${generateProblemStackFunction(cellSize)}
|
||||
${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",
|
||||
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
|
||||
@@ -117,9 +112,9 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "without-ten-frames",
|
||||
filename: "without-ten-frames.svg",
|
||||
description: "Without Ten-Frames: Abstract representation",
|
||||
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
|
||||
@@ -132,9 +127,9 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "beginner-with-ten-frames",
|
||||
filename: "beginner-ten-frames.svg",
|
||||
description: "Beginner: Learning regrouping with ten-frames",
|
||||
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
|
||||
@@ -147,9 +142,9 @@ const examples = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ten-frames-both-columns",
|
||||
filename: "ten-frames-both-columns.svg",
|
||||
description: "Ten-frames in both columns: Double regrouping",
|
||||
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
|
||||
@@ -161,22 +156,22 @@ const examples = [
|
||||
transparentBackground: true,
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
] as const
|
||||
|
||||
console.log("Generating ten-frame example images (single problems)...\n");
|
||||
console.log('Generating ten-frame example images (single problems)...\n')
|
||||
|
||||
for (const example of examples) {
|
||||
console.log(`Generating ${example.description}...`);
|
||||
console.log(`Generating ${example.description}...`)
|
||||
|
||||
try {
|
||||
const typstSource = generateExampleTypst(example.options);
|
||||
const typstSource = generateExampleTypst(example.options)
|
||||
|
||||
// Compile to SVG
|
||||
let svg = execSync("typst compile --format svg - -", {
|
||||
let svg = execSync('typst compile --format svg - -', {
|
||||
input: typstSource,
|
||||
encoding: "utf8",
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
})
|
||||
|
||||
// Post-process: Make SVG visible on dark background
|
||||
// - Digits on white cells should stay BLACK
|
||||
@@ -184,23 +179,23 @@ for (const example of examples) {
|
||||
// - 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(/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)",
|
||||
);
|
||||
'$1rgba(255, 255, 255, 0.9)'
|
||||
)
|
||||
|
||||
// Save to file
|
||||
const outputPath = path.join(outputDir, example.filename);
|
||||
fs.writeFileSync(outputPath, svg, "utf-8");
|
||||
const outputPath = path.join(outputDir, example.filename)
|
||||
fs.writeFileSync(outputPath, svg, 'utf-8')
|
||||
|
||||
console.log(` ✓ Saved to ${outputPath}`);
|
||||
console.log(` ✓ Saved to ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error generating ${example.name}:`, error);
|
||||
console.error(` ✗ Error generating ${example.name}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nDone! Ten-frame example images generated.");
|
||||
console.log(`\nFiles saved to: ${outputDir}`);
|
||||
console.log('\nDone! Ten-frame example images generated.')
|
||||
console.log(`\nFiles saved to: ${outputDir}`)
|
||||
|
||||
@@ -4,83 +4,83 @@
|
||||
* Test script to parse the Rithmomachia board CSV and verify the layout.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const csvPath = path.join(
|
||||
process.env.HOME,
|
||||
"Downloads",
|
||||
"rithmomachia board setup - Sheet1 (1).csv",
|
||||
);
|
||||
'Downloads',
|
||||
'rithmomachia board setup - Sheet1 (1).csv'
|
||||
)
|
||||
|
||||
function parseCSV(csvContent) {
|
||||
const lines = csvContent.trim().split("\n");
|
||||
const pieces = [];
|
||||
const lines = csvContent.trim().split('\n')
|
||||
const pieces = []
|
||||
|
||||
// Process in triplets (color, shape, number)
|
||||
for (let rankIndex = 0; rankIndex < 16; rankIndex++) {
|
||||
const colorRowIndex = rankIndex * 3;
|
||||
const shapeRowIndex = rankIndex * 3 + 1;
|
||||
const numberRowIndex = rankIndex * 3 + 2;
|
||||
const colorRowIndex = rankIndex * 3
|
||||
const shapeRowIndex = rankIndex * 3 + 1
|
||||
const numberRowIndex = rankIndex * 3 + 2
|
||||
|
||||
if (numberRowIndex >= lines.length) break;
|
||||
if (numberRowIndex >= lines.length) break
|
||||
|
||||
const colorRow = lines[colorRowIndex].split(",");
|
||||
const shapeRow = lines[shapeRowIndex].split(",");
|
||||
const numberRow = lines[numberRowIndex].split(",");
|
||||
const colorRow = lines[colorRowIndex].split(',')
|
||||
const shapeRow = lines[shapeRowIndex].split(',')
|
||||
const numberRow = lines[numberRowIndex].split(',')
|
||||
|
||||
// Process each column (8 total)
|
||||
for (let colIndex = 0; colIndex < 8; colIndex++) {
|
||||
const color = colorRow[colIndex]?.trim();
|
||||
const shape = shapeRow[colIndex]?.trim();
|
||||
const numberStr = numberRow[colIndex]?.trim();
|
||||
const color = colorRow[colIndex]?.trim()
|
||||
const shape = shapeRow[colIndex]?.trim()
|
||||
const numberStr = numberRow[colIndex]?.trim()
|
||||
|
||||
// Skip empty cells (but allow empty number for Pyramids)
|
||||
if (!color || !shape) continue;
|
||||
if (!color || !shape) continue
|
||||
|
||||
// Map CSV position to game square
|
||||
// CSV column → game row (1-8)
|
||||
// CSV rank → game column (A-P)
|
||||
const gameRow = colIndex + 1; // CSV col 0 → row 1, col 7 → row 8
|
||||
const gameCol = String.fromCharCode(65 + rankIndex); // rank 0 → A, rank 15 → P
|
||||
const square = `${gameCol}${gameRow}`;
|
||||
const gameRow = colIndex + 1 // CSV col 0 → row 1, col 7 → row 8
|
||||
const gameCol = String.fromCharCode(65 + rankIndex) // rank 0 → A, rank 15 → P
|
||||
const square = `${gameCol}${gameRow}`
|
||||
|
||||
// Parse color
|
||||
const pieceColor = color.toLowerCase() === "black" ? "B" : "W";
|
||||
const pieceColor = color.toLowerCase() === 'black' ? 'B' : 'W'
|
||||
|
||||
// Parse type
|
||||
let pieceType;
|
||||
const shapeLower = shape.toLowerCase();
|
||||
if (shapeLower === "circle") pieceType = "C";
|
||||
else if (shapeLower === "triangle" || shapeLower === "traingle")
|
||||
pieceType = "T"; // Handle typo
|
||||
else if (shapeLower === "square") pieceType = "S";
|
||||
else if (shapeLower === "pyramid") pieceType = "P";
|
||||
let pieceType
|
||||
const shapeLower = shape.toLowerCase()
|
||||
if (shapeLower === 'circle') pieceType = 'C'
|
||||
else if (shapeLower === 'triangle' || shapeLower === 'traingle')
|
||||
pieceType = 'T' // Handle typo
|
||||
else if (shapeLower === 'square') pieceType = 'S'
|
||||
else if (shapeLower === 'pyramid') pieceType = 'P'
|
||||
else {
|
||||
console.warn(`Unknown shape "${shape}" at ${square}`);
|
||||
continue;
|
||||
console.warn(`Unknown shape "${shape}" at ${square}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse value/pyramid faces
|
||||
if (pieceType === "P") {
|
||||
if (pieceType === 'P') {
|
||||
// Pyramid - number cell should be empty, use default faces
|
||||
pieces.push({
|
||||
color: pieceColor,
|
||||
type: pieceType,
|
||||
pyramidFaces: pieceColor === "B" ? [36, 25, 16, 4] : [64, 49, 36, 25],
|
||||
pyramidFaces: pieceColor === 'B' ? [36, 25, 16, 4] : [64, 49, 36, 25],
|
||||
square,
|
||||
});
|
||||
})
|
||||
} else {
|
||||
// Regular piece needs a number
|
||||
if (!numberStr) {
|
||||
console.warn(`Missing number for non-Pyramid ${shape} at ${square}`);
|
||||
continue;
|
||||
console.warn(`Missing number for non-Pyramid ${shape} at ${square}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const value = parseInt(numberStr, 10);
|
||||
const value = parseInt(numberStr, 10)
|
||||
if (isNaN(value)) {
|
||||
console.warn(`Invalid number "${numberStr}" at ${square}`);
|
||||
continue;
|
||||
console.warn(`Invalid number "${numberStr}" at ${square}`)
|
||||
continue
|
||||
}
|
||||
|
||||
pieces.push({
|
||||
@@ -88,120 +88,119 @@ function parseCSV(csvContent) {
|
||||
type: pieceType,
|
||||
value,
|
||||
square,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pieces;
|
||||
return pieces
|
||||
}
|
||||
|
||||
function generateBoardDisplay(pieces) {
|
||||
const lines = [];
|
||||
const lines = []
|
||||
|
||||
lines.push("\n=== Board Layout (Game Orientation) ===");
|
||||
lines.push("BLACK (top)\n");
|
||||
lines.push('\n=== Board Layout (Game Orientation) ===')
|
||||
lines.push('BLACK (top)\n')
|
||||
lines.push(
|
||||
" A B C D E F G H I J K L M N O P",
|
||||
);
|
||||
' A B C D E F G H I J K L M N O P'
|
||||
)
|
||||
|
||||
for (let row = 8; row >= 1; row--) {
|
||||
let line = `${row} `;
|
||||
let line = `${row} `
|
||||
for (let colCode = 65; colCode <= 80; colCode++) {
|
||||
const col = String.fromCharCode(colCode);
|
||||
const square = `${col}${row}`;
|
||||
const piece = pieces.find((p) => p.square === square);
|
||||
const col = String.fromCharCode(colCode)
|
||||
const square = `${col}${row}`
|
||||
const piece = pieces.find((p) => p.square === square)
|
||||
|
||||
if (piece) {
|
||||
const val =
|
||||
piece.type === "P" ? " P" : piece.value.toString().padStart(3, " ");
|
||||
line += ` ${piece.color}${piece.type}${val} `;
|
||||
const val = piece.type === 'P' ? ' P' : piece.value.toString().padStart(3, ' ')
|
||||
line += ` ${piece.color}${piece.type}${val} `
|
||||
} else {
|
||||
line += " ---- ";
|
||||
line += ' ---- '
|
||||
}
|
||||
}
|
||||
lines.push(line);
|
||||
lines.push(line)
|
||||
}
|
||||
|
||||
lines.push("\nWHITE (bottom)\n");
|
||||
lines.push('\nWHITE (bottom)\n')
|
||||
|
||||
return lines.join("\n");
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function generateColumnSummaries(pieces) {
|
||||
const lines = [];
|
||||
const lines = []
|
||||
|
||||
lines.push("\n=== Column-by-Column Summary ===\n");
|
||||
lines.push('\n=== Column-by-Column Summary ===\n')
|
||||
|
||||
for (let colCode = 65; colCode <= 80; colCode++) {
|
||||
const col = String.fromCharCode(colCode);
|
||||
const col = String.fromCharCode(colCode)
|
||||
const columnPieces = pieces
|
||||
.filter((p) => p.square[0] === col)
|
||||
.sort((a, b) => {
|
||||
const rowA = parseInt(a.square.substring(1));
|
||||
const rowB = parseInt(b.square.substring(1));
|
||||
return rowA - rowB;
|
||||
});
|
||||
const rowA = parseInt(a.square.substring(1))
|
||||
const rowB = parseInt(b.square.substring(1))
|
||||
return rowA - rowB
|
||||
})
|
||||
|
||||
if (columnPieces.length === 0) continue;
|
||||
if (columnPieces.length === 0) continue
|
||||
|
||||
const color = columnPieces[0].color === "B" ? "BLACK" : "WHITE";
|
||||
lines.push(`Column ${col} (${color}):`);
|
||||
const color = columnPieces[0].color === 'B' ? 'BLACK' : 'WHITE'
|
||||
lines.push(`Column ${col} (${color}):`)
|
||||
for (const piece of columnPieces) {
|
||||
const val = piece.type === "P" ? "P[36,25,16,4]" : piece.value;
|
||||
lines.push(` ${piece.square}: ${piece.type}(${val})`);
|
||||
const val = piece.type === 'P' ? 'P[36,25,16,4]' : piece.value
|
||||
lines.push(` ${piece.square}: ${piece.type}(${val})`)
|
||||
}
|
||||
lines.push("");
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function countPieces(pieces) {
|
||||
const blackPieces = pieces.filter((p) => p.color === "B");
|
||||
const whitePieces = pieces.filter((p) => p.color === "W");
|
||||
const blackPieces = pieces.filter((p) => p.color === 'B')
|
||||
const whitePieces = pieces.filter((p) => p.color === 'W')
|
||||
|
||||
const countByType = (pieces) => {
|
||||
const counts = { C: 0, T: 0, S: 0, P: 0 };
|
||||
for (const p of pieces) counts[p.type]++;
|
||||
return counts;
|
||||
};
|
||||
const counts = { C: 0, T: 0, S: 0, P: 0 }
|
||||
for (const p of pieces) counts[p.type]++
|
||||
return counts
|
||||
}
|
||||
|
||||
const blackCounts = countByType(blackPieces);
|
||||
const whiteCounts = countByType(whitePieces);
|
||||
const blackCounts = countByType(blackPieces)
|
||||
const whiteCounts = countByType(whitePieces)
|
||||
|
||||
console.log("\n=== Piece Counts ===");
|
||||
console.log('\n=== Piece Counts ===')
|
||||
console.log(
|
||||
`Black: ${blackPieces.length} total (C:${blackCounts.C}, T:${blackCounts.T}, S:${blackCounts.S}, P:${blackCounts.P})`,
|
||||
);
|
||||
`Black: ${blackPieces.length} total (C:${blackCounts.C}, T:${blackCounts.T}, S:${blackCounts.S}, P:${blackCounts.P})`
|
||||
)
|
||||
console.log(
|
||||
`White: ${whitePieces.length} total (C:${whiteCounts.C}, T:${whiteCounts.T}, S:${whiteCounts.S}, P:${whiteCounts.P})`,
|
||||
);
|
||||
`White: ${whitePieces.length} total (C:${whiteCounts.C}, T:${whiteCounts.T}, S:${whiteCounts.S}, P:${whiteCounts.P})`
|
||||
)
|
||||
}
|
||||
|
||||
// Main
|
||||
try {
|
||||
const csvContent = fs.readFileSync(csvPath, "utf-8");
|
||||
const pieces = parseCSV(csvContent);
|
||||
const csvContent = fs.readFileSync(csvPath, 'utf-8')
|
||||
const pieces = parseCSV(csvContent)
|
||||
|
||||
console.log(`\nParsed ${pieces.length} pieces from CSV`);
|
||||
console.log(generateBoardDisplay(pieces));
|
||||
console.log(generateColumnSummaries(pieces));
|
||||
countPieces(pieces);
|
||||
console.log(`\nParsed ${pieces.length} pieces from CSV`)
|
||||
console.log(generateBoardDisplay(pieces))
|
||||
console.log(generateColumnSummaries(pieces))
|
||||
countPieces(pieces)
|
||||
|
||||
// Save parsed data
|
||||
const outputPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"arcade-games",
|
||||
"rithmomachia",
|
||||
"utils",
|
||||
"parsedBoard.json",
|
||||
);
|
||||
fs.writeFileSync(outputPath, JSON.stringify(pieces, null, 2));
|
||||
console.log(`\n✅ Saved parsed board to: ${outputPath}`);
|
||||
'..',
|
||||
'src',
|
||||
'arcade-games',
|
||||
'rithmomachia',
|
||||
'utils',
|
||||
'parsedBoard.json'
|
||||
)
|
||||
fs.writeFileSync(outputPath, JSON.stringify(pieces, null, 2))
|
||||
console.log(`\n✅ Saved parsed board to: ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error("Error:", error.message);
|
||||
process.exit(1);
|
||||
console.error('Error:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -6,104 +6,102 @@ import {
|
||||
findScaffoldingIndex,
|
||||
REGROUPING_PROGRESSION,
|
||||
SCAFFOLDING_PROGRESSION,
|
||||
} from "../src/app/create/worksheets/addition/difficultyProfiles";
|
||||
} from '../src/app/create/worksheets/addition/difficultyProfiles'
|
||||
|
||||
// Start from beginner
|
||||
let state = {
|
||||
pAnyStart: DIFFICULTY_PROFILES.beginner.regrouping.pAnyStart,
|
||||
pAllStart: DIFFICULTY_PROFILES.beginner.regrouping.pAllStart,
|
||||
displayRules: DIFFICULTY_PROFILES.beginner.displayRules,
|
||||
};
|
||||
}
|
||||
|
||||
console.log("=== MAKE HARDER PATH ===\n");
|
||||
console.log("Format: (regroupingIdx, scaffoldingIdx) - description\n");
|
||||
console.log('=== MAKE HARDER PATH ===\n')
|
||||
console.log('Format: (regroupingIdx, scaffoldingIdx) - description\n')
|
||||
|
||||
const harderPath: Array<{ r: number; s: number; desc: string }> = [];
|
||||
const harderPath: Array<{ r: number; s: number; desc: string }> = []
|
||||
|
||||
// Record starting point
|
||||
let rIdx = findRegroupingIndex(state.pAnyStart, state.pAllStart);
|
||||
let sIdx = findScaffoldingIndex(state.displayRules);
|
||||
harderPath.push({ r: rIdx, s: sIdx, desc: "START (beginner)" });
|
||||
console.log(`(${rIdx}, ${sIdx}) - START (beginner)`);
|
||||
let rIdx = findRegroupingIndex(state.pAnyStart, state.pAllStart)
|
||||
let sIdx = findScaffoldingIndex(state.displayRules)
|
||||
harderPath.push({ r: rIdx, s: sIdx, desc: 'START (beginner)' })
|
||||
console.log(`(${rIdx}, ${sIdx}) - START (beginner)`)
|
||||
|
||||
// Click "Make Harder" 30 times or until max
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const result = makeHarder(state);
|
||||
const result = makeHarder(state)
|
||||
|
||||
const newR = findRegroupingIndex(result.pAnyStart, result.pAllStart);
|
||||
const newS = findScaffoldingIndex(result.displayRules);
|
||||
const newR = findRegroupingIndex(result.pAnyStart, result.pAllStart)
|
||||
const newS = findScaffoldingIndex(result.displayRules)
|
||||
|
||||
if (newR === rIdx && newS === sIdx) {
|
||||
console.log(`\n(${newR}, ${newS}) - ${result.changeDescription} (STOPPED)`);
|
||||
break;
|
||||
console.log(`\n(${newR}, ${newS}) - ${result.changeDescription} (STOPPED)`)
|
||||
break
|
||||
}
|
||||
|
||||
rIdx = newR;
|
||||
sIdx = newS;
|
||||
state = result;
|
||||
rIdx = newR
|
||||
sIdx = newS
|
||||
state = result
|
||||
|
||||
harderPath.push({ r: rIdx, s: sIdx, desc: result.changeDescription });
|
||||
console.log(`(${rIdx}, ${sIdx}) - ${result.changeDescription}`);
|
||||
harderPath.push({ r: rIdx, s: sIdx, desc: result.changeDescription })
|
||||
console.log(`(${rIdx}, ${sIdx}) - ${result.changeDescription}`)
|
||||
}
|
||||
|
||||
console.log("\n\n=== PATH VISUALIZATION ===\n");
|
||||
console.log("Regrouping Index →");
|
||||
console.log("Scaffolding ↓\n");
|
||||
console.log('\n\n=== PATH VISUALIZATION ===\n')
|
||||
console.log('Regrouping Index →')
|
||||
console.log('Scaffolding ↓\n')
|
||||
|
||||
// Create 2D grid visualization
|
||||
const grid: string[][] = [];
|
||||
const grid: string[][] = []
|
||||
for (let s = 0; s <= 12; s++) {
|
||||
grid[s] = [];
|
||||
grid[s] = []
|
||||
for (let r = 0; r <= 18; r++) {
|
||||
grid[s][r] = " ·";
|
||||
grid[s][r] = ' ·'
|
||||
}
|
||||
}
|
||||
|
||||
// Mark path
|
||||
harderPath.forEach((point, idx) => {
|
||||
if (idx === 0) {
|
||||
grid[point.s][point.r] = " S"; // Start
|
||||
grid[point.s][point.r] = ' S' // Start
|
||||
} else if (idx === harderPath.length - 1) {
|
||||
grid[point.s][point.r] = " E"; // End
|
||||
grid[point.s][point.r] = ' E' // End
|
||||
} else {
|
||||
grid[point.s][point.r] = `${idx.toString().padStart(3)}`;
|
||||
grid[point.s][point.r] = `${idx.toString().padStart(3)}`
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Mark presets
|
||||
const presets = [
|
||||
{ label: "BEG", profile: DIFFICULTY_PROFILES.beginner },
|
||||
{ label: "EAR", profile: DIFFICULTY_PROFILES.earlyLearner },
|
||||
{ label: "INT", profile: DIFFICULTY_PROFILES.intermediate },
|
||||
{ label: "ADV", profile: DIFFICULTY_PROFILES.advanced },
|
||||
{ label: "EXP", profile: DIFFICULTY_PROFILES.expert },
|
||||
];
|
||||
{ label: 'BEG', profile: DIFFICULTY_PROFILES.beginner },
|
||||
{ label: 'EAR', profile: DIFFICULTY_PROFILES.earlyLearner },
|
||||
{ label: 'INT', profile: DIFFICULTY_PROFILES.intermediate },
|
||||
{ label: 'ADV', profile: DIFFICULTY_PROFILES.advanced },
|
||||
{ label: 'EXP', profile: DIFFICULTY_PROFILES.expert },
|
||||
]
|
||||
|
||||
presets.forEach((preset) => {
|
||||
const r = findRegroupingIndex(
|
||||
preset.profile.regrouping.pAnyStart,
|
||||
preset.profile.regrouping.pAllStart,
|
||||
);
|
||||
const s = findScaffoldingIndex(preset.profile.displayRules);
|
||||
preset.profile.regrouping.pAllStart
|
||||
)
|
||||
const s = findScaffoldingIndex(preset.profile.displayRules)
|
||||
|
||||
// Only mark if not already part of path
|
||||
const onPath = harderPath.some((p) => p.r === r && p.s === s);
|
||||
const onPath = harderPath.some((p) => p.r === r && p.s === s)
|
||||
if (!onPath) {
|
||||
grid[s][r] = preset.label;
|
||||
grid[s][r] = preset.label
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Print grid (inverted so scaffolding increases upward)
|
||||
console.log(
|
||||
" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18",
|
||||
);
|
||||
console.log(' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18')
|
||||
for (let s = 12; s >= 0; s--) {
|
||||
console.log(`${s.toString().padStart(2)} ${grid[s].join("")}`);
|
||||
console.log(`${s.toString().padStart(2)} ${grid[s].join('')}`)
|
||||
}
|
||||
|
||||
console.log("\nLegend:");
|
||||
console.log(" S = Start (beginner)");
|
||||
console.log(" E = End (maximum)");
|
||||
console.log(" 1-29 = Step number");
|
||||
console.log(" BEG/EAR/INT/ADV/EXP = Preset profiles");
|
||||
console.log(" · = Not visited");
|
||||
console.log('\nLegend:')
|
||||
console.log(' S = Start (beginner)')
|
||||
console.log(' E = End (maximum)')
|
||||
console.log(' 1-29 = Step number')
|
||||
console.log(' BEG/EAR/INT/ADV/EXP = Preset profiles')
|
||||
console.log(' · = Not visited')
|
||||
|
||||
@@ -6,98 +6,93 @@
|
||||
* produces identical Typst output to ensure no regressions.
|
||||
*/
|
||||
|
||||
import { generateSubtractionProblemStackFunction } from "../src/app/create/worksheets/addition/typstHelpers";
|
||||
import { generateTypstHelpers } from "../src/app/create/worksheets/addition/typstHelpers";
|
||||
import { generatePlaceValueColors } from "../src/app/create/worksheets/addition/typstHelpers";
|
||||
import { generateSubtractionProblemStackFunction } from '../src/app/create/worksheets/addition/typstHelpers'
|
||||
import { generateTypstHelpers } from '../src/app/create/worksheets/addition/typstHelpers'
|
||||
import { generatePlaceValueColors } from '../src/app/create/worksheets/addition/typstHelpers'
|
||||
|
||||
console.log("🔍 Validating typstHelpers refactoring...\n");
|
||||
console.log('🔍 Validating typstHelpers refactoring...\n')
|
||||
|
||||
// Test 1: Check that functions are exported and callable
|
||||
console.log("✓ Test 1: Functions are exported");
|
||||
console.log('✓ Test 1: Functions are exported')
|
||||
console.log(
|
||||
` - generateSubtractionProblemStackFunction: ${typeof generateSubtractionProblemStackFunction}`,
|
||||
);
|
||||
console.log(` - generateTypstHelpers: ${typeof generateTypstHelpers}`);
|
||||
console.log(` - generatePlaceValueColors: ${typeof generatePlaceValueColors}`);
|
||||
` - generateSubtractionProblemStackFunction: ${typeof generateSubtractionProblemStackFunction}`
|
||||
)
|
||||
console.log(` - generateTypstHelpers: ${typeof generateTypstHelpers}`)
|
||||
console.log(` - generatePlaceValueColors: ${typeof generatePlaceValueColors}`)
|
||||
|
||||
if (typeof generateSubtractionProblemStackFunction !== "function") {
|
||||
console.error(
|
||||
"❌ generateSubtractionProblemStackFunction is not a function!",
|
||||
);
|
||||
process.exit(1);
|
||||
if (typeof generateSubtractionProblemStackFunction !== 'function') {
|
||||
console.error('❌ generateSubtractionProblemStackFunction is not a function!')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Test 2: Generate sample Typst code
|
||||
console.log("\n✓ Test 2: Generate sample Typst code");
|
||||
const cellSize = 0.55;
|
||||
const maxDigits = 3;
|
||||
console.log('\n✓ Test 2: Generate sample Typst code')
|
||||
const cellSize = 0.55
|
||||
const maxDigits = 3
|
||||
|
||||
const helpers = generateTypstHelpers(cellSize);
|
||||
console.log(` - Helper functions: ${helpers.length} characters`);
|
||||
const helpers = generateTypstHelpers(cellSize)
|
||||
console.log(` - Helper functions: ${helpers.length} characters`)
|
||||
|
||||
const colors = generatePlaceValueColors();
|
||||
console.log(` - Color definitions: ${colors.length} characters`);
|
||||
const colors = generatePlaceValueColors()
|
||||
console.log(` - Color definitions: ${colors.length} characters`)
|
||||
|
||||
const problemStack = generateSubtractionProblemStackFunction(
|
||||
cellSize,
|
||||
maxDigits,
|
||||
);
|
||||
console.log(` - Problem stack function: ${problemStack.length} characters`);
|
||||
const problemStack = generateSubtractionProblemStackFunction(cellSize, maxDigits)
|
||||
console.log(` - Problem stack function: ${problemStack.length} characters`)
|
||||
|
||||
// Test 3: Verify key features are present
|
||||
console.log("\n✓ Test 3: Verify key features in generated Typst");
|
||||
console.log('\n✓ Test 3: Verify key features in generated Typst')
|
||||
|
||||
const checks = [
|
||||
{ name: "Borrow boxes row", pattern: /Borrow boxes row/ },
|
||||
{ name: "Minuend row", pattern: /Minuend row/ },
|
||||
{ name: "Subtrahend row", pattern: /Subtrahend row/ },
|
||||
{ name: "Answer boxes", pattern: /Answer boxes/ },
|
||||
{ name: "Ten-frames", pattern: /Ten-frames row/ },
|
||||
{ name: "Borrowing hints", pattern: /show-borrowing-hints/ },
|
||||
{ name: "Arrow rendering", pattern: /path\(/ },
|
||||
{ name: "Place value colors", pattern: /place-colors/ },
|
||||
{ name: "Scratch work boxes", pattern: /dotted.*paint: gray/ },
|
||||
];
|
||||
{ name: 'Borrow boxes row', pattern: /Borrow boxes row/ },
|
||||
{ name: 'Minuend row', pattern: /Minuend row/ },
|
||||
{ name: 'Subtrahend row', pattern: /Subtrahend row/ },
|
||||
{ name: 'Answer boxes', pattern: /Answer boxes/ },
|
||||
{ name: 'Ten-frames', pattern: /Ten-frames row/ },
|
||||
{ name: 'Borrowing hints', pattern: /show-borrowing-hints/ },
|
||||
{ name: 'Arrow rendering', pattern: /path\(/ },
|
||||
{ name: 'Place value colors', pattern: /place-colors/ },
|
||||
{ name: 'Scratch work boxes', pattern: /dotted.*paint: gray/ },
|
||||
]
|
||||
|
||||
let allPassed = true;
|
||||
let allPassed = true
|
||||
for (const check of checks) {
|
||||
const found = check.pattern.test(problemStack);
|
||||
const found = check.pattern.test(problemStack)
|
||||
if (found) {
|
||||
console.log(` ✓ ${check.name}`);
|
||||
console.log(` ✓ ${check.name}`)
|
||||
} else {
|
||||
console.log(` ❌ ${check.name} - NOT FOUND`);
|
||||
allPassed = false;
|
||||
console.log(` ❌ ${check.name} - NOT FOUND`)
|
||||
allPassed = false
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4: Verify structure
|
||||
console.log("\n✓ Test 4: Verify Typst structure");
|
||||
console.log('\n✓ Test 4: Verify Typst structure')
|
||||
const structureChecks = [
|
||||
{ name: "Function definition", pattern: /#let subtraction-problem-stack\(/ },
|
||||
{ name: "Grid structure", pattern: /grid\(/ },
|
||||
{ name: "Stack structure", pattern: /stack\(/ },
|
||||
{ name: "Problem number display", pattern: /problem-number-display/ },
|
||||
];
|
||||
{ name: 'Function definition', pattern: /#let subtraction-problem-stack\(/ },
|
||||
{ name: 'Grid structure', pattern: /grid\(/ },
|
||||
{ name: 'Stack structure', pattern: /stack\(/ },
|
||||
{ name: 'Problem number display', pattern: /problem-number-display/ },
|
||||
]
|
||||
|
||||
for (const check of structureChecks) {
|
||||
const found = check.pattern.test(problemStack);
|
||||
const found = check.pattern.test(problemStack)
|
||||
if (found) {
|
||||
console.log(` ✓ ${check.name}`);
|
||||
console.log(` ✓ ${check.name}`)
|
||||
} else {
|
||||
console.log(` ❌ ${check.name} - NOT FOUND`);
|
||||
allPassed = false;
|
||||
console.log(` ❌ ${check.name} - NOT FOUND`)
|
||||
allPassed = false
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log("\n" + "=".repeat(60));
|
||||
console.log('\n' + '='.repeat(60))
|
||||
if (allPassed) {
|
||||
console.log("✅ All validation checks passed!");
|
||||
console.log("\nThe refactored code generates valid Typst output with all");
|
||||
console.log("expected features present.");
|
||||
process.exit(0);
|
||||
console.log('✅ All validation checks passed!')
|
||||
console.log('\nThe refactored code generates valid Typst output with all')
|
||||
console.log('expected features present.')
|
||||
process.exit(0)
|
||||
} else {
|
||||
console.log("❌ Some validation checks failed!");
|
||||
console.log("\nPlease review the output above for details.");
|
||||
process.exit(1);
|
||||
console.log('❌ Some validation checks failed!')
|
||||
console.log('\nPlease review the output above for details.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user