feat(i18n): add internationalization for all create pages

Implemented i18n across all three create pages with translations for 7 languages (en, de, ja, hi, es, la, goh):

- Created comprehensive translation files in src/i18n/locales/create/
- Updated /create (hub) page with all card content translations
- Updated /create/abacus page with parameter labels and help text
- Updated /create/flashcards page with UI elements and status messages
- Integrated create translations into main messages system

Translation coverage includes:
- Page titles and subtitles
- Feature descriptions and lists
- Form labels and placeholders
- Button text (normal and loading states)
- Error messages
- Help text and instructions

🤖 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-05 09:56:48 -06:00
parent 80657a6604
commit b080970d76
14 changed files with 916 additions and 72 deletions

View File

@@ -1,11 +1,13 @@
'use client'
import { useTranslations } from 'next-intl'
import { JobMonitor } from '@/components/3d-print/JobMonitor'
import { STLPreview } from '@/components/3d-print/STLPreview'
import { useState } from 'react'
import { css } from '../../../../styled-system/css'
export default function ThreeDPrintPage() {
const t = useTranslations('create.abacus')
// New unified parameter system
const [columns, setColumns] = useState(4)
const [scaleFactor, setScaleFactor] = useState(1.5)
@@ -86,13 +88,10 @@ export default function ThreeDPrintPage() {
mb: 2,
})}
>
Customize Your 3D Printable Abacus
{t('pageTitle')}
</h1>
<p className={css({ mb: 6, color: 'gray.600' })}>
Adjust the parameters below to customize your abacus, then generate and download the file
for 3D printing.
</p>
<p className={css({ mb: 6, color: 'gray.600' })}>{t('pageSubtitle')}</p>
<div
className={css({
@@ -118,7 +117,7 @@ export default function ThreeDPrintPage() {
mb: 4,
})}
>
Customization Parameters
{t('customizationTitle')}
</h2>
{/* Number of Columns */}
@@ -130,7 +129,7 @@ export default function ThreeDPrintPage() {
mb: 2,
})}
>
Number of Columns: {columns}
{t('columns.label', { count: columns })}
</label>
<input
type="range"
@@ -148,7 +147,7 @@ export default function ThreeDPrintPage() {
mt: 1,
})}
>
Total number of columns in the abacus (1-13)
{t('columns.help')}
</div>
</div>
@@ -161,7 +160,7 @@ export default function ThreeDPrintPage() {
mb: 2,
})}
>
Scale Factor: {scaleFactor.toFixed(1)}x
{t('scaleFactor.label', { factor: scaleFactor.toFixed(1) })}
</label>
<input
type="range"
@@ -179,7 +178,7 @@ export default function ThreeDPrintPage() {
mt: 1,
})}
>
Overall size multiplier (preserves aspect ratio, larger values = bigger file size)
{t('scaleFactor.help')}
</div>
</div>
@@ -192,7 +191,7 @@ export default function ThreeDPrintPage() {
mb: 2,
})}
>
Width in mm (optional)
{t('widthMm.label')}
</label>
<input
type="number"
@@ -204,7 +203,7 @@ export default function ThreeDPrintPage() {
const value = e.target.value
setWidthMm(value ? Number.parseFloat(value) : undefined)
}}
placeholder="Leave empty to use scale factor"
placeholder={t('widthMm.placeholder')}
className={css({
width: '100%',
px: 3,
@@ -225,7 +224,7 @@ export default function ThreeDPrintPage() {
mt: 1,
})}
>
Specify exact width in millimeters (overrides scale factor)
{t('widthMm.help')}
</div>
</div>
@@ -238,7 +237,7 @@ export default function ThreeDPrintPage() {
mb: 2,
})}
>
Output Format
{t('format.label')}
</label>
<div className={css({ display: 'flex', gap: 2, flexWrap: 'wrap' })}>
<button
@@ -308,7 +307,7 @@ export default function ThreeDPrintPage() {
mb: 3,
})}
>
3MF Color Customization
{t('colors.title')}
</h3>
{/* Frame Color */}
@@ -320,7 +319,7 @@ export default function ThreeDPrintPage() {
mb: 1,
})}
>
Frame Color
{t('colors.frame')}
</label>
<div className={css({ display: 'flex', gap: 2, alignItems: 'center' })}>
<input
@@ -356,7 +355,7 @@ export default function ThreeDPrintPage() {
mb: 1,
})}
>
Heaven Bead Color
{t('colors.heavenBead')}
</label>
<div className={css({ display: 'flex', gap: 2, alignItems: 'center' })}>
<input
@@ -392,7 +391,7 @@ export default function ThreeDPrintPage() {
mb: 1,
})}
>
Earth Bead Color
{t('colors.earthBead')}
</label>
<div className={css({ display: 'flex', gap: 2, alignItems: 'center' })}>
<input
@@ -428,7 +427,7 @@ export default function ThreeDPrintPage() {
mb: 1,
})}
>
Decoration Color
{t('colors.decoration')}
</label>
<div className={css({ display: 'flex', gap: 2, alignItems: 'center' })}>
<input
@@ -476,7 +475,7 @@ export default function ThreeDPrintPage() {
_hover: { bg: isGenerating ? 'blue.600' : 'blue.700' },
})}
>
{isGenerating ? 'Generating...' : 'Generate File'}
{isGenerating ? t('generate.generating') : t('generate.button')}
</button>
{/* Job Status */}
@@ -505,7 +504,7 @@ export default function ThreeDPrintPage() {
_hover: { bg: 'green.700' },
})}
>
Download {format.toUpperCase()}
{t('download', { format: format.toUpperCase() })}
</button>
)}
@@ -544,7 +543,7 @@ export default function ThreeDPrintPage() {
mb: 4,
})}
>
Preview
{t('preview.title')}
</h2>
<STLPreview columns={columns} scaleFactor={scaleFactor} />
<div
@@ -554,17 +553,9 @@ export default function ThreeDPrintPage() {
color: 'gray.600',
})}
>
<p className={css({ mb: 2 })}>
<strong>Live Preview:</strong> The preview updates automatically as you adjust
parameters (with a 1-second delay). This shows the exact mirrored book-fold design
that will be generated.
</p>
<p className={css({ mb: 2 })}>
<strong>Note:</strong> Preview generation requires OpenSCAD. If you see an error,
the preview feature only works in production (Docker). The download functionality
will still work when deployed.
</p>
<p>Use your mouse to rotate and zoom the 3D model.</p>
<p className={css({ mb: 2 })}>{t('preview.liveDescription')}</p>
<p className={css({ mb: 2 })}>{t('preview.note')}</p>
<p>{t('preview.instructions')}</p>
</div>
</div>
</div>

View File

@@ -2,6 +2,7 @@
import { useAbacusConfig } from '@soroban/abacus-react'
import { useForm } from '@tanstack/react-form'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { ConfigurationFormWithoutGenerate } from '@/components/ConfigurationFormWithoutGenerate'
import { GenerationProgress } from '@/components/GenerationProgress'
@@ -104,6 +105,7 @@ function validateAndCompleteConfig(formState: FlashcardFormState): FlashcardConf
type GenerationStatus = 'idle' | 'generating' | 'error'
export default function CreatePage() {
const t = useTranslations('create.flashcards')
const [generationStatus, setGenerationStatus] = useState<GenerationStatus>('idle')
const [error, setError] = useState<string | null>(null)
const globalConfig = useAbacusConfig()
@@ -184,7 +186,7 @@ export default function CreatePage() {
}
return (
<PageWithNav navTitle="Create Flashcards" navEmoji="✨">
<PageWithNav navTitle={t('navTitle')} navEmoji="✨">
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
{/* Main Content */}
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
@@ -197,7 +199,7 @@ export default function CreatePage() {
color: 'gray.900',
})}
>
Create Your Flashcards
{t('pageTitle')}
</h1>
<p
className={css({
@@ -205,7 +207,7 @@ export default function CreatePage() {
color: 'gray.600',
})}
>
Configure content and style, preview instantly, then generate your flashcards
{t('pageSubtitle')}
</p>
</div>
</div>
@@ -248,7 +250,7 @@ export default function CreatePage() {
color: 'gray.900',
})}
>
🎨 Visual Style
{t('stylePanel.title')}
</h3>
<p
className={css({
@@ -256,7 +258,7 @@ export default function CreatePage() {
color: 'gray.600',
})}
>
See changes instantly in the preview
{t('stylePanel.subtitle')}
</p>
</div>
@@ -337,12 +339,12 @@ export default function CreatePage() {
animation: 'spin 1s linear infinite',
})}
/>
Generating Your Flashcards...
{t('generate.generating')}
</>
) : (
<>
<div className={css({ fontSize: 'xl' })}></div>
Generate Flashcards
{t('generate.button')}
</>
)}
</span>
@@ -374,7 +376,7 @@ export default function CreatePage() {
color: 'red.800',
})}
>
Generation Failed
{t('error.title')}
</h3>
</div>
<p
@@ -399,7 +401,7 @@ export default function CreatePage() {
_hover: { bg: 'red.700' },
})}
>
Try Again
{t('error.tryAgain')}
</button>
</div>
</div>

View File

@@ -1,10 +1,13 @@
'use client'
import Link from 'next/link'
import { useTranslations } from 'next-intl'
import { PageWithNav } from '@/components/PageWithNav'
import { css } from '../../../styled-system/css'
export default function CreateHubPage() {
const t = useTranslations('create.hub')
return (
<PageWithNav navTitle="Create" navEmoji="✨">
<div
@@ -81,7 +84,7 @@ export default function CreateHubPage() {
letterSpacing: 'tight',
})}
>
Create Your Learning Tools
{t('pageTitle')}
</h1>
<p
className={css({
@@ -93,8 +96,7 @@ export default function CreateHubPage() {
textShadow: '0 1px 3px rgba(0,0,0,0.1)',
})}
>
Design custom flashcards or 3D printable abacus models to enhance your learning
experience
{t('pageSubtitle')}
</p>
</div>
@@ -162,7 +164,7 @@ export default function CreateHubPage() {
letterSpacing: 'tight',
})}
>
Flashcard Creator
{t('flashcards.title')}
</h2>
{/* Description */}
@@ -174,8 +176,7 @@ export default function CreateHubPage() {
lineHeight: '1.7',
})}
>
Design custom flashcards with abacus visualizations. Perfect for learning and
teaching number recognition and arithmetic.
{t('flashcards.description')}
</p>
{/* Features */}
@@ -212,7 +213,7 @@ export default function CreateHubPage() {
>
</span>
Customizable number ranges
{t('flashcards.feature1')}
</li>
<li
className={css({
@@ -239,7 +240,7 @@ export default function CreateHubPage() {
>
</span>
Multiple styles and layouts
{t('flashcards.feature2')}
</li>
<li
className={css({
@@ -266,7 +267,7 @@ export default function CreateHubPage() {
>
</span>
Print-ready PDF generation
{t('flashcards.feature3')}
</li>
</ul>
@@ -296,7 +297,7 @@ export default function CreateHubPage() {
},
})}
>
<span>Create Flashcards</span>
<span>{t('flashcards.button')}</span>
<span className={css({ fontSize: 'lg' })}></span>
</div>
</div>
@@ -359,7 +360,7 @@ export default function CreateHubPage() {
letterSpacing: 'tight',
})}
>
3D Abacus Creator
{t('abacus.title')}
</h2>
{/* Description */}
@@ -371,8 +372,7 @@ export default function CreateHubPage() {
lineHeight: '1.7',
})}
>
Customize and download 3D printable abacus models. Choose your size, columns, and
colors for the perfect learning tool.
{t('abacus.description')}
</p>
{/* Features */}
@@ -409,7 +409,7 @@ export default function CreateHubPage() {
>
</span>
Adjustable size and columns
{t('abacus.feature1')}
</li>
<li
className={css({
@@ -436,7 +436,7 @@ export default function CreateHubPage() {
>
</span>
3MF color customization
{t('abacus.feature2')}
</li>
<li
className={css({
@@ -463,7 +463,7 @@ export default function CreateHubPage() {
>
</span>
Live 3D preview
{t('abacus.feature3')}
</li>
</ul>
@@ -493,7 +493,7 @@ export default function CreateHubPage() {
},
})}
>
<span>Create 3D Model</span>
<span>{t('abacus.button')}</span>
<span className={css({ fontSize: 'lg' })}></span>
</div>
</div>
@@ -556,7 +556,7 @@ export default function CreateHubPage() {
letterSpacing: 'tight',
})}
>
Abacus Calendar
{t('calendar.title')}
</h2>
{/* Description */}
@@ -568,8 +568,7 @@ export default function CreateHubPage() {
lineHeight: '1.7',
})}
>
Generate printable calendars where every date is shown as an abacus. Perfect for
teaching number representation.
{t('calendar.description')}
</p>
{/* Features */}
@@ -606,7 +605,7 @@ export default function CreateHubPage() {
>
</span>
Monthly or daily formats
{t('calendar.feature1')}
</li>
<li
className={css({
@@ -633,7 +632,7 @@ export default function CreateHubPage() {
>
</span>
Multiple paper sizes
{t('calendar.feature2')}
</li>
<li
className={css({
@@ -660,7 +659,7 @@ export default function CreateHubPage() {
>
</span>
Uses your abacus styling
{t('calendar.feature3')}
</li>
</ul>
@@ -690,7 +689,7 @@ export default function CreateHubPage() {
},
})}
>
<span>Create Calendar</span>
<span>{t('calendar.button')}</span>
<span className={css({ fontSize: 'lg' })}></span>
</div>
</div>

View File

@@ -0,0 +1,88 @@
{
"create": {
"hub": {
"pageTitle": "Erstellen Sie Ihre Lernwerkzeuge",
"pageSubtitle": "Entwerfen Sie individuelle Lernkarten oder 3D-druckbare Abakus-Modelle, um Ihr Lernerlebnis zu verbessern",
"flashcards": {
"title": "Lernkarten-Creator",
"description": "Entwerfen Sie individuelle Lernkarten mit Abakus-Visualisierungen. Perfekt zum Lernen und Lehren von Zahlenerkennung und Arithmetik.",
"feature1": "Anpassbare Zahlenbereiche",
"feature2": "Mehrere Stile und Layouts",
"feature3": "Druckfertige PDF-Generierung",
"button": "Lernkarten erstellen"
},
"abacus": {
"title": "3D-Abakus-Creator",
"description": "Passen Sie 3D-druckbare Abakus-Modelle an und laden Sie sie herunter. Wählen Sie Größe, Spalten und Farben für das perfekte Lernwerkzeug.",
"feature1": "Einstellbare Größe und Spalten",
"feature2": "3MF-Farbanpassung",
"feature3": "Live-3D-Vorschau",
"button": "3D-Modell erstellen"
},
"calendar": {
"title": "Abakus-Kalender",
"description": "Erstellen Sie druckbare Kalender, bei denen jedes Datum als Abakus angezeigt wird. Perfekt zum Lehren der Zahlendarstellung.",
"feature1": "Monatliche oder tägliche Formate",
"feature2": "Mehrere Papiergrößen",
"feature3": "Verwendet Ihren Abakus-Stil",
"button": "Kalender erstellen"
}
},
"abacus": {
"pageTitle": "Passen Sie Ihren 3D-druckbaren Abakus an",
"pageSubtitle": "Passen Sie die Parameter unten an, um Ihren Abakus anzupassen, und generieren und laden Sie dann die Datei für den 3D-Druck herunter.",
"customizationTitle": "Anpassungsparameter",
"columns": {
"label": "Anzahl der Spalten: {{count}}",
"help": "Gesamtanzahl der Spalten im Abakus (1-13)"
},
"scaleFactor": {
"label": "Skalierungsfaktor: {{factor}}x",
"help": "Gesamtgrößenmultiplikator (behält Seitenverhältnis bei, größere Werte = größere Dateigröße)"
},
"widthMm": {
"label": "Breite in mm (optional)",
"placeholder": "Leer lassen, um Skalierungsfaktor zu verwenden",
"help": "Geben Sie die genaue Breite in Millimetern an (überschreibt Skalierungsfaktor)"
},
"format": {
"label": "Ausgabeformat"
},
"colors": {
"title": "3MF-Farbanpassung",
"frame": "Rahmenfarbe",
"heavenBead": "Himmelsperlenfarbe",
"earthBead": "Erdperlenfarbe",
"decoration": "Dekorationsfarbe"
},
"generate": {
"button": "Datei generieren",
"generating": "Wird generiert..."
},
"download": "{{format}} herunterladen",
"preview": {
"title": "Vorschau",
"liveDescription": "Live-Vorschau: Die Vorschau wird automatisch aktualisiert, wenn Sie Parameter anpassen (mit 1 Sekunde Verzögerung). Dies zeigt das genaue gespiegelte Buchfalz-Design, das generiert wird.",
"note": "Hinweis: Die Vorschaugenerierung erfordert OpenSCAD. Wenn Sie einen Fehler sehen, funktioniert die Vorschaufunktion nur in der Produktion (Docker). Die Download-Funktionalität funktioniert weiterhin, wenn sie bereitgestellt wird.",
"instructions": "Verwenden Sie Ihre Maus, um das 3D-Modell zu drehen und zu zoomen."
}
},
"flashcards": {
"navTitle": "Lernkarten erstellen",
"pageTitle": "Erstellen Sie Ihre Lernkarten",
"pageSubtitle": "Konfigurieren Sie Inhalt und Stil, sehen Sie die Vorschau sofort und generieren Sie dann Ihre Lernkarten",
"stylePanel": {
"title": "🎨 Visueller Stil",
"subtitle": "Sehen Sie Änderungen sofort in der Vorschau"
},
"generate": {
"button": "Lernkarten generieren",
"generating": "Ihre Lernkarten werden generiert..."
},
"error": {
"title": "Generierung fehlgeschlagen",
"tryAgain": "Erneut versuchen"
}
}
}
}

View File

@@ -0,0 +1,88 @@
{
"create": {
"hub": {
"pageTitle": "Create Your Learning Tools",
"pageSubtitle": "Design custom flashcards or 3D printable abacus models to enhance your learning experience",
"flashcards": {
"title": "Flashcard Creator",
"description": "Design custom flashcards with abacus visualizations. Perfect for learning and teaching number recognition and arithmetic.",
"feature1": "Customizable number ranges",
"feature2": "Multiple styles and layouts",
"feature3": "Print-ready PDF generation",
"button": "Create Flashcards"
},
"abacus": {
"title": "3D Abacus Creator",
"description": "Customize and download 3D printable abacus models. Choose your size, columns, and colors for the perfect learning tool.",
"feature1": "Adjustable size and columns",
"feature2": "3MF color customization",
"feature3": "Live 3D preview",
"button": "Create 3D Model"
},
"calendar": {
"title": "Abacus Calendar",
"description": "Generate printable calendars where every date is shown as an abacus. Perfect for teaching number representation.",
"feature1": "Monthly or daily formats",
"feature2": "Multiple paper sizes",
"feature3": "Uses your abacus styling",
"button": "Create Calendar"
}
},
"abacus": {
"pageTitle": "Customize Your 3D Printable Abacus",
"pageSubtitle": "Adjust the parameters below to customize your abacus, then generate and download the file for 3D printing.",
"customizationTitle": "Customization Parameters",
"columns": {
"label": "Number of Columns: {{count}}",
"help": "Total number of columns in the abacus (1-13)"
},
"scaleFactor": {
"label": "Scale Factor: {{factor}}x",
"help": "Overall size multiplier (preserves aspect ratio, larger values = bigger file size)"
},
"widthMm": {
"label": "Width in mm (optional)",
"placeholder": "Leave empty to use scale factor",
"help": "Specify exact width in millimeters (overrides scale factor)"
},
"format": {
"label": "Output Format"
},
"colors": {
"title": "3MF Color Customization",
"frame": "Frame Color",
"heavenBead": "Heaven Bead Color",
"earthBead": "Earth Bead Color",
"decoration": "Decoration Color"
},
"generate": {
"button": "Generate File",
"generating": "Generating..."
},
"download": "Download {{format}}",
"preview": {
"title": "Preview",
"liveDescription": "Live Preview: The preview updates automatically as you adjust parameters (with a 1-second delay). This shows the exact mirrored book-fold design that will be generated.",
"note": "Note: Preview generation requires OpenSCAD. If you see an error, the preview feature only works in production (Docker). The download functionality will still work when deployed.",
"instructions": "Use your mouse to rotate and zoom the 3D model."
}
},
"flashcards": {
"navTitle": "Create Flashcards",
"pageTitle": "Create Your Flashcards",
"pageSubtitle": "Configure content and style, preview instantly, then generate your flashcards",
"stylePanel": {
"title": "🎨 Visual Style",
"subtitle": "See changes instantly in the preview"
},
"generate": {
"button": "Generate Flashcards",
"generating": "Generating Your Flashcards..."
},
"error": {
"title": "Generation Failed",
"tryAgain": "Try Again"
}
}
}
}

View File

@@ -0,0 +1,88 @@
{
"create": {
"hub": {
"pageTitle": "Crea Tus Herramientas de Aprendizaje",
"pageSubtitle": "Diseña tarjetas didácticas personalizadas o modelos de ábaco imprimibles en 3D para mejorar tu experiencia de aprendizaje",
"flashcards": {
"title": "Creador de Tarjetas Didácticas",
"description": "Diseña tarjetas didácticas personalizadas con visualizaciones de ábaco. Perfecto para aprender y enseñar reconocimiento de números y aritmética.",
"feature1": "Rangos de números personalizables",
"feature2": "Múltiples estilos y diseños",
"feature3": "Generación de PDF listo para imprimir",
"button": "Crear Tarjetas Didácticas"
},
"abacus": {
"title": "Creador de Ábaco 3D",
"description": "Personaliza y descarga modelos de ábaco imprimibles en 3D. Elige tu tamaño, columnas y colores para la herramienta de aprendizaje perfecta.",
"feature1": "Tamaño y columnas ajustables",
"feature2": "Personalización de color 3MF",
"feature3": "Vista previa 3D en vivo",
"button": "Crear Modelo 3D"
},
"calendar": {
"title": "Calendario de Ábaco",
"description": "Genera calendarios imprimibles donde cada fecha se muestra como un ábaco. Perfecto para enseñar representación numérica.",
"feature1": "Formatos mensuales o diarios",
"feature2": "Múltiples tamaños de papel",
"feature3": "Usa tu estilo de ábaco",
"button": "Crear Calendario"
}
},
"abacus": {
"pageTitle": "Personaliza Tu Ábaco Imprimible en 3D",
"pageSubtitle": "Ajusta los parámetros a continuación para personalizar tu ábaco, luego genera y descarga el archivo para impresión 3D.",
"customizationTitle": "Parámetros de Personalización",
"columns": {
"label": "Número de Columnas: {{count}}",
"help": "Número total de columnas en el ábaco (1-13)"
},
"scaleFactor": {
"label": "Factor de Escala: {{factor}}x",
"help": "Multiplicador de tamaño general (preserva la relación de aspecto, valores más grandes = mayor tamaño de archivo)"
},
"widthMm": {
"label": "Ancho en mm (opcional)",
"placeholder": "Dejar vacío para usar factor de escala",
"help": "Especificar ancho exacto en milímetros (anula el factor de escala)"
},
"format": {
"label": "Formato de Salida"
},
"colors": {
"title": "Personalización de Color 3MF",
"frame": "Color del Marco",
"heavenBead": "Color de Cuenta Celestial",
"earthBead": "Color de Cuenta Terrestre",
"decoration": "Color de Decoración"
},
"generate": {
"button": "Generar Archivo",
"generating": "Generando..."
},
"download": "Descargar {{format}}",
"preview": {
"title": "Vista Previa",
"liveDescription": "Vista previa en vivo: La vista previa se actualiza automáticamente a medida que ajustas los parámetros (con un retraso de 1 segundo). Esto muestra el diseño exacto de pliegue de libro espejo que se generará.",
"note": "Nota: La generación de vista previa requiere OpenSCAD. Si ves un error, la función de vista previa solo funciona en producción (Docker). La funcionalidad de descarga seguirá funcionando cuando se implemente.",
"instructions": "Usa tu ratón para rotar y hacer zoom en el modelo 3D."
}
},
"flashcards": {
"navTitle": "Crear Tarjetas Didácticas",
"pageTitle": "Crea Tus Tarjetas Didácticas",
"pageSubtitle": "Configura contenido y estilo, previsualiza instantáneamente, luego genera tus tarjetas didácticas",
"stylePanel": {
"title": "🎨 Estilo Visual",
"subtitle": "Ver cambios instantáneamente en la vista previa"
},
"generate": {
"button": "Generar Tarjetas Didácticas",
"generating": "Generando Tus Tarjetas Didácticas..."
},
"error": {
"title": "Generación Fallida",
"tryAgain": "Intentar de Nuevo"
}
}
}
}

View File

@@ -0,0 +1,88 @@
{
"create": {
"hub": {
"pageTitle": "Thīna Lernawerkzūg Giskaffen",
"pageSubtitle": "Anamahhōn fleissachartun odo 3D drucchāri abacusmodellun ze firbezzirungu thīnēro lernunga",
"flashcards": {
"title": "Fleissachartun Giskaffari",
"description": "Anamahhōn fleissachartun mit abacussihti. Folkomano ze lernenne inti lerenne zalakennunga inti rehnunga.",
"feature1": "Anamahhōn zalafristu",
"feature2": "Managfalti stili inti legari",
"feature3": "Drucchgareiti PDF giskaffunga",
"button": "Fleissachartun Giskaffen"
},
"abacus": {
"title": "3D Abacus Giskaffari",
"description": "Anamahhōn inti hladan 3D drucchāri abacusmodellun. Giweli thīna grōzi, spaltunga inti farawa fora folkomano lernawerkzūg.",
"feature1": "Gistellbāri grōzi inti spaltunga",
"feature2": "3MF farawaanamahhōn",
"feature3": "Libenti 3D forasihti",
"button": "3D Modellun Giskaffen"
},
"calendar": {
"title": "Abacuskalendarium",
"description": "Giskaffen drucchāri kalendariun wār iegilich tag als abacus gizeignit wirdit. Folkomano ze lerenne zalaforesteldunga.",
"feature1": "Mānotlīchi odo taglīchi formen",
"feature2": "Managfalti papirgrōzi",
"feature3": "Nuzzit thīnan abacusstil",
"button": "Kalendarium Giskaffen"
}
},
"abacus": {
"pageTitle": "Gianamahho Thīnan 3D Drucchāran Abacus",
"pageSubtitle": "Gistellōn thie parameterōn untanafora ze gianamahhōnne thīnan abacus, thanne giskaffo inti hlado thia datei fora 3D drucch.",
"customizationTitle": "Anamahhōnparameterōn",
"columns": {
"label": "Zala Spaltungo: {{count}}",
"help": "Allu zala spaltungo in abaco (1-13)"
},
"scaleFactor": {
"label": "Skālefaktor: {{factor}}x",
"help": "Allugrōzimultiplikator (gihaltet aspektaferhāltnissa, grōziri wertē = grōziri dateigr ōzi)"
},
"widthMm": {
"label": "Breiti in mm (wāllīg)",
"placeholder": "Lāzzilosun fora skālefaktor ze nuzzenne",
"help": "Spezifizieri genawe breiti in millimeterun (ūbarskrībit skālefaktor)"
},
"format": {
"label": "Ūzgangaforma"
},
"colors": {
"title": "3MF Farawaanamahhōn",
"frame": "Rāmafarawa",
"heavenBead": "Himilesglobulifarawa",
"earthBead": "Erdusglobulifarawa",
"decoration": "Zieratafarawa"
},
"generate": {
"button": "Datei Giskaffen",
"generating": "Wirdit giskaffan..."
},
"download": "{{format}} Hladan",
"preview": {
"title": "Forasihti",
"liveDescription": "Libenti forasihti: Thiu forasihti wirdit automatisch girēniwit sō du parameterōn gistellōst (mit 1 secunda firziagu). Thiz zeignit thaz genawe gespegelti buohfalzentuurf thaz giskaffan wirdit.",
"note": "Nota: Forasihtigiskaffunga bidarfit OpenSCAD. Oba thu errorem gisihist, thiu forasihtikunst arbeitet nur in productione (Docker). Thiu hladukunst arbeitet noch immir wanne gistellit wirdit.",
"instructions": "Nuzzi thīna mūs ze rōtenne inti zōmenne thaz 3D modellun."
}
},
"flashcards": {
"navTitle": "Fleissachartun Giskaffen",
"pageTitle": "Giskaffen Thīna Fleissachartun",
"pageSubtitle": "Gistellōn inhalt inti stil, forasih statim, thanne giskaffo thīna fleissachartun",
"stylePanel": {
"title": "🎨 Gisihtisstil",
"subtitle": "Gisih wihsala statim in forasihti"
},
"generate": {
"button": "Fleissachartun Giskaffen",
"generating": "Thīna Fleissachartun Werdent Giskaffan..."
},
"error": {
"title": "Giskaffunga Firzōganit",
"tryAgain": "Widar Firsuachen"
}
}
}
}

View File

@@ -0,0 +1,88 @@
{
"create": {
"hub": {
"pageTitle": "अपने सीखने के उपकरण बनाएं",
"pageSubtitle": "अपने सीखने के अनुभव को बढ़ाने के लिए कस्टम फ्लैशकार्ड या 3D प्रिंट करने योग्य अबेकस मॉडल डिजाइन करें",
"flashcards": {
"title": "फ्लैशकार्ड निर्माता",
"description": "अबेकस विज़ुअलाइज़ेशन के साथ कस्टम फ्लैशकार्ड डिज़ाइन करें। संख्या पहचान और अंकगणित सीखने और पढ़ाने के लिए बिल्कुल सही।",
"feature1": "अनुकूलन योग्य संख्या श्रेणियाँ",
"feature2": "एकाधिक शैलियाँ और लेआउट",
"feature3": "प्रिंट-रेडी पीडीएफ जनरेशन",
"button": "फ्लैशकार्ड बनाएं"
},
"abacus": {
"title": "3D अबेकस निर्माता",
"description": "3D प्रिंट करने योग्य अबेकस मॉडल को अनुकूलित करें और डाउनलोड करें। सही सीखने के उपकरण के लिए अपना आकार, कॉलम और रंग चुनें।",
"feature1": "समायोज्य आकार और कॉलम",
"feature2": "3MF रंग अनुकूलन",
"feature3": "लाइव 3D पूर्वावलोकन",
"button": "3D मॉडल बनाएं"
},
"calendar": {
"title": "अबेकस कैलेंडर",
"description": "प्रिंट करने योग्य कैलेंडर उत्पन्न करें जहां प्रत्येक तिथि को अबेकस के रूप में दिखाया गया है। संख्या प्रतिनिधित्व सिखाने के लिए बिल्कुल सही।",
"feature1": "मासिक या दैनिक प्रारूप",
"feature2": "एकाधिक कागज के आकार",
"feature3": "आपकी अबेकस स्टाइलिंग का उपयोग करता है",
"button": "कैलेंडर बनाएं"
}
},
"abacus": {
"pageTitle": "अपने 3D प्रिंट करने योग्य अबेकस को अनुकूलित करें",
"pageSubtitle": "अपने अबेकस को अनुकूलित करने के लिए नीचे दिए गए पैरामीटर समायोजित करें, फिर 3D प्रिंटिंग के लिए फ़ाइल उत्पन्न करें और डाउनलोड करें।",
"customizationTitle": "अनुकूलन पैरामीटर",
"columns": {
"label": "कॉलम की संख्या: {{count}}",
"help": "अबेकस में कुल कॉलम की संख्या (1-13)"
},
"scaleFactor": {
"label": "स्केल फैक्टर: {{factor}}x",
"help": "कुल आकार गुणक (पहलू अनुपात को संरक्षित करता है, बड़े मान = बड़ी फ़ाइल आकार)"
},
"widthMm": {
"label": "चौड़ाई मिमी में (वैकल्पिक)",
"placeholder": "स्केल फैक्टर का उपयोग करने के लिए खाली छोड़ें",
"help": "मिलीमीटर में सटीक चौड़ाई निर्दिष्ट करें (स्केल फैक्टर को ओवरराइड करता है)"
},
"format": {
"label": "आउटपुट प्रारूप"
},
"colors": {
"title": "3MF रंग अनुकूलन",
"frame": "फ्रेम रंग",
"heavenBead": "स्वर्ग मनका रंग",
"earthBead": "पृथ्वी मनका रंग",
"decoration": "सजावट रंग"
},
"generate": {
"button": "फ़ाइल उत्पन्न करें",
"generating": "उत्पन्न हो रहा है..."
},
"download": "{{format}} डाउनलोड करें",
"preview": {
"title": "पूर्वावलोकन",
"liveDescription": "लाइव पूर्वावलोकन: जैसे ही आप पैरामीटर समायोजित करते हैं, पूर्वावलोकन स्वचालित रूप से अपडेट होता है (1-सेकंड की देरी के साथ)। यह उत्पन्न होने वाले सटीक मिरर बुक-फोल्ड डिज़ाइन को दिखाता है।",
"note": "नोट: पूर्वावलोकन जनरेशन के लिए OpenSCAD की आवश्यकता होती है। यदि आप एक त्रुटि देखते हैं, तो पूर्वावलोकन सुविधा केवल उत्पादन (Docker) में काम करती है। डिप्लॉय होने पर डाउनलोड कार्यक्षमता अभी भी काम करेगी।",
"instructions": "3D मॉडल को घुमाने और ज़ूम करने के लिए अपने माउस का उपयोग करें।"
}
},
"flashcards": {
"navTitle": "फ्लैशकार्ड बनाएं",
"pageTitle": "अपने फ्लैशकार्ड बनाएं",
"pageSubtitle": "सामग्री और शैली को कॉन्फ़िगर करें, तुरंत पूर्वावलोकन करें, फिर अपने फ्लैशकार्ड उत्पन्न करें",
"stylePanel": {
"title": "🎨 दृश्य शैली",
"subtitle": "पूर्वावलोकन में तुरंत परिवर्तन देखें"
},
"generate": {
"button": "फ्लैशकार्ड उत्पन्न करें",
"generating": "आपके फ्लैशकार्ड उत्पन्न हो रहे हैं..."
},
"error": {
"title": "जनरेशन विफल",
"tryAgain": "फिर से कोशिश करें"
}
}
}
}

View File

@@ -0,0 +1,88 @@
{
"create": {
"hub": {
"pageTitle": "学習ツールを作成",
"pageSubtitle": "学習体験を向上させるカスタムフラッシュカードまたは3Dプリント可能なそろばんモデルをデザイン",
"flashcards": {
"title": "フラッシュカード作成",
"description": "そろばんの視覚化を使用したカスタムフラッシュカードをデザインします。数字の認識と算術の学習と教育に最適です。",
"feature1": "カスタマイズ可能な数値範囲",
"feature2": "複数のスタイルとレイアウト",
"feature3": "印刷可能なPDF生成",
"button": "フラッシュカードを作成"
},
"abacus": {
"title": "3Dそろばん作成",
"description": "3Dプリント可能なそろばんモデルをカスタマイズしてダウンロードします。完璧な学習ツールのためにサイズ、列、色を選択してください。",
"feature1": "調整可能なサイズと列",
"feature2": "3MF色のカスタマイズ",
"feature3": "ライブ3Dプレビュー",
"button": "3Dモデルを作成"
},
"calendar": {
"title": "そろばんカレンダー",
"description": "各日付がそろばんとして表示される印刷可能なカレンダーを生成します。数字の表現を教えるのに最適です。",
"feature1": "月次または日次形式",
"feature2": "複数の用紙サイズ",
"feature3": "そろばんスタイルを使用",
"button": "カレンダーを作成"
}
},
"abacus": {
"pageTitle": "3Dプリント可能そろばんをカスタマイズ",
"pageSubtitle": "以下のパラメータを調整してそろばんをカスタマイズし、3Dプリント用のファイルを生成してダウンロードします。",
"customizationTitle": "カスタマイズパラメータ",
"columns": {
"label": "列数:{{count}}",
"help": "そろばんの総列数1-13"
},
"scaleFactor": {
"label": "スケール係数:{{factor}}x",
"help": "全体サイズの乗数(アスペクト比を保持、値が大きいほどファイルサイズが大きくなります)"
},
"widthMm": {
"label": "幅mmオプション",
"placeholder": "空白のままでスケール係数を使用",
"help": "ミリメートル単位で正確な幅を指定(スケール係数を上書き)"
},
"format": {
"label": "出力形式"
},
"colors": {
"title": "3MF色のカスタマイズ",
"frame": "フレーム色",
"heavenBead": "天珠の色",
"earthBead": "地珠の色",
"decoration": "装飾色"
},
"generate": {
"button": "ファイルを生成",
"generating": "生成中..."
},
"download": "{{format}}をダウンロード",
"preview": {
"title": "プレビュー",
"liveDescription": "ライブプレビューパラメータを調整すると自動的に更新されます1秒の遅延。これにより、生成される正確なミラー折り返しデザインが表示されます。",
"note": "注プレビュー生成にはOpenSCADが必要です。エラーが表示される場合、プレビュー機能は本番環境Dockerでのみ動作します。デプロイ時にダウンロード機能は引き続き機能します。",
"instructions": "マウスを使用して3Dモデルを回転およびズームします。"
}
},
"flashcards": {
"navTitle": "フラッシュカードを作成",
"pageTitle": "フラッシュカードを作成",
"pageSubtitle": "コンテンツとスタイルを設定し、即座にプレビューして、フラッシュカードを生成します",
"stylePanel": {
"title": "🎨 ビジュアルスタイル",
"subtitle": "プレビューで変更を即座に確認"
},
"generate": {
"button": "フラッシュカードを生成",
"generating": "フラッシュカードを生成中..."
},
"error": {
"title": "生成失敗",
"tryAgain": "再試行"
}
}
}
}

View File

@@ -0,0 +1,88 @@
{
"create": {
"hub": {
"pageTitle": "Instrumenta Discendi Tua Crea",
"pageSubtitle": "Cartas memoriativas vel modulos abaci 3D imprimibiles designa ut experientiam discendi tuam augeas",
"flashcards": {
"title": "Creator Chartarum Memorativarum",
"description": "Cartas memoriativas cum visualizationibus abaci designa. Perfectus ad discendum et docendum recognitionem numerorum et arithmeticam.",
"feature1": "Limites numerorum configurabiles",
"feature2": "Multi styli et dispositiones",
"feature3": "Generatio PDF parata ad imprimendum",
"button": "Cartas Crea"
},
"abacus": {
"title": "Creator Abaci 3D",
"description": "Modulos abaci 3D imprimibiles configura et depone. Magnitudinem, columnas, colores tuos elige pro instrumento discendi perfecto.",
"feature1": "Magnitudo et columnae adaptabiles",
"feature2": "Configuratio colorum 3MF",
"feature3": "Praevisio 3D viva",
"button": "Modulum 3D Crea"
},
"calendar": {
"title": "Calendarium Abaci",
"description": "Calendaria imprimibilia genera ubi omnis dies ut abacus monstratur. Perfectus ad docendam repraesentationem numerorum.",
"feature1": "Formae mensuales vel diurnae",
"feature2": "Magnitudines chartae multiplices",
"feature3": "Stilum abaci tui utitur",
"button": "Calendarium Crea"
}
},
"abacus": {
"pageTitle": "Abacum Tuum 3D Imprimibilem Configura",
"pageSubtitle": "Parametros infra adapta ut abacum tuum configures, deinde fasciculum genera et depone pro impressione 3D.",
"customizationTitle": "Parametri Configurationis",
"columns": {
"label": "Numerus Columnarum: {{count}}",
"help": "Numerus totalis columnarum in abaco (1-13)"
},
"scaleFactor": {
"label": "Factor Scalae: {{factor}}x",
"help": "Multiplicator magnitudinis totalis (servat proportionem aspectus, valores maiores = magnitudo fasciculi maior)"
},
"widthMm": {
"label": "Latitudo in mm (optionalis)",
"placeholder": "Vacuum relinque ut factorem scalae utaris",
"help": "Latitudinem exactam in millimetris specifica (factorem scalae superat)"
},
"format": {
"label": "Forma Exitus"
},
"colors": {
"title": "Configuratio Colorum 3MF",
"frame": "Color Cornicis",
"heavenBead": "Color Globuli Caelestis",
"earthBead": "Color Globuli Terrae",
"decoration": "Color Ornamenti"
},
"generate": {
"button": "Fasciculum Genera",
"generating": "Generatur..."
},
"download": "{{format}} Depone",
"preview": {
"title": "Praevisio",
"liveDescription": "Praevisio viva: Praevisio automatice renovatur dum parametros adaptas (cum mora 1 secundi). Hoc designum exactum libri specularis plicati quod generabitur monstrat.",
"note": "Nota: Generatio praevisionis OpenSCAD requirit. Si errorem vides, facultas praevisionis solum in productione (Docker) operatur. Facultas deponendi adhuc operabitur cum explicatur.",
"instructions": "Mure tuo utere ut modulum 3D rotas et augeas."
}
},
"flashcards": {
"navTitle": "Cartas Crea",
"pageTitle": "Cartas Tuas Crea",
"pageSubtitle": "Contentum et stylum configura, statim praevide, deinde cartas tuas genera",
"stylePanel": {
"title": "🎨 Stilus Visualis",
"subtitle": "Mutationes statim in praevisione vide"
},
"generate": {
"button": "Cartas Genera",
"generating": "Cartae Tuae Generantur..."
},
"error": {
"title": "Generatio Defecit",
"tryAgain": "Iterum Tempta"
}
}
}
}

View File

@@ -0,0 +1,17 @@
import de from './de.json'
import en from './en.json'
import es from './es.json'
import goh from './goh.json'
import hi from './hi.json'
import ja from './ja.json'
import la from './la.json'
export const createMessages = {
en: en.create,
de: de.create,
ja: ja.create,
hi: hi.create,
es: es.create,
la: la.create,
goh: goh.create,
} as const

View File

@@ -1,5 +1,6 @@
import { rithmomachiaMessages } from '@/arcade-games/rithmomachia/messages'
import { calendarMessages } from '@/i18n/locales/calendar/messages'
import { createMessages } from '@/i18n/locales/create/messages'
import { gamesMessages } from '@/i18n/locales/games/messages'
import { guideMessages } from '@/i18n/locales/guide/messages'
import { homeMessages } from '@/i18n/locales/home/messages'
@@ -42,6 +43,7 @@ export async function getMessages(locale: Locale) {
{ guide: guideMessages[locale] },
{ tutorial: tutorialMessages[locale] },
{ calendar: calendarMessages[locale] },
{ create: createMessages[locale] },
rithmomachiaMessages[locale]
)
}

View File

@@ -5,6 +5,38 @@
import type { ValidPlaceValues, BeadHighlight } from './AbacusReact'
/**
* Calculate the actual rendered dimensions of a bead based on its shape
* These values match the exact rendering in AbacusStaticBead and AbacusAnimatedBead
*
* @param size - The base bead size parameter
* @param shape - The bead shape ('circle', 'diamond', or 'square')
* @returns Object with width and height of the rendered bead
*/
export function calculateBeadDimensions(
size: number,
shape: 'circle' | 'diamond' | 'square' = 'diamond'
): { width: number; height: number } {
switch (shape) {
case 'diamond':
// Diamond polygon: points=`${size*0.7},0 ${size*1.4},${size/2} ${size*0.7},${size} 0,${size/2}`
// Spans from x=0 to x=size*1.4, y=0 to y=size
return { width: size * 1.4, height: size }
case 'circle':
// Circle with radius=size/2, so diameter=size
return { width: size, height: size }
case 'square':
// Square with width=size, height=size
return { width: size, height: size }
default:
// Default to diamond (most common/largest)
return { width: size * 1.4, height: size }
}
}
/**
* Represents the state of beads in a single column
*/
@@ -515,7 +547,7 @@ export function calculateBeadPosition(
dimensions: AbacusLayoutDimensions,
columnState?: ColumnStateForPositioning
): { x: number; y: number } {
const { beadSize, rodSpacing, heavenEarthGap, barThickness, activeGap, inactiveGap, adjacentSpacing, totalColumns } = dimensions
const { beadSize, rodSpacing, heavenEarthGap, barThickness, activeGap, inactiveGap, adjacentSpacing, totalColumns, labelHeight } = dimensions
// X position based on place value (rightmost = ones place)
const columnIndex = totalColumns - 1 - bead.placeValue
@@ -523,14 +555,15 @@ export function calculateBeadPosition(
// Y position based on bead type and active state
// These formulas match the original Typst implementation exactly
// NOTE: All Y positions are offset by labelHeight to match absolute SVG coordinates
if (bead.type === 'heaven') {
if (bead.active) {
// Active heaven bead: positioned close to reckoning bar (Typst line 175)
const y = heavenEarthGap - beadSize / 2 - activeGap
const y = labelHeight + heavenEarthGap - beadSize / 2 - activeGap
return { x, y }
} else {
// Inactive heaven bead: positioned away from reckoning bar (Typst line 178)
const y = heavenEarthGap - inactiveGap - beadSize / 2
const y = labelHeight + heavenEarthGap - inactiveGap - beadSize / 2
return { x, y }
}
} else {
@@ -539,7 +572,7 @@ export function calculateBeadPosition(
if (bead.active) {
// Active beads: positioned near reckoning bar, adjacent beads touch (Typst line 251)
const y = heavenEarthGap + barThickness + activeGap + beadSize / 2 +
const y = labelHeight + heavenEarthGap + barThickness + activeGap + beadSize / 2 +
bead.position * (beadSize + adjacentSpacing)
return { x, y }
} else {
@@ -547,16 +580,194 @@ export function calculateBeadPosition(
let y: number
if (earthActive > 0) {
// Position after the last active bead + gap, then adjacent inactive beads touch (Typst line 256)
y = heavenEarthGap + barThickness + activeGap + beadSize / 2 +
y = labelHeight + heavenEarthGap + barThickness + activeGap + beadSize / 2 +
(earthActive - 1) * (beadSize + adjacentSpacing) +
beadSize / 2 + inactiveGap + beadSize / 2 +
(bead.position - earthActive) * (beadSize + adjacentSpacing)
} else {
// No active beads: position after reckoning bar + gap, adjacent inactive beads touch (Typst line 259)
y = heavenEarthGap + barThickness + inactiveGap + beadSize / 2 +
y = labelHeight + heavenEarthGap + barThickness + inactiveGap + beadSize / 2 +
bead.position * (beadSize + adjacentSpacing)
}
return { x, y }
}
}
}
/**
* Padding configuration for cropping
*/
export interface CropPadding {
top?: number
bottom?: number
left?: number
right?: number
}
/**
* Bounding box for crop area
*/
export interface BoundingBox {
minX: number
minY: number
maxX: number
maxY: number
width: number
height: number
}
/**
* Complete crop calculation result
*/
export interface CropResult extends BoundingBox {
viewBox: string // SVG viewBox attribute value
scaledWidth: number // Width after scaling to fit target
scaledHeight: number // Height after scaling to fit target
}
/**
* Calculate bounding box around active beads for a given value
* Uses the same position calculations as the rendering engine
*
* @param value - The number to display
* @param columns - Number of columns
* @param scaleFactor - Scale factor for the abacus
* @returns Bounding box containing all active beads
*/
export function calculateActiveBeadsBounds(
value: number,
columns: number,
scaleFactor: number = 1
): BoundingBox {
// Get which beads are active for this value
const abacusState = numberToAbacusState(value, columns)
// Get layout dimensions
const dimensions = calculateStandardDimensions({
columns,
scaleFactor,
showNumbers: false,
columnLabels: [],
})
// Calculate positions of all active beads
const activeBeadPositions: Array<{ x: number; y: number }> = []
for (let placeValue = 0; placeValue < columns; placeValue++) {
const columnState = abacusState[placeValue]
if (!columnState) continue
// Heaven bead
if (columnState.heavenActive) {
const bead: BeadPositionConfig = {
type: 'heaven',
active: true,
position: 0,
placeValue,
}
const pos = calculateBeadPosition(bead, dimensions, { earthActive: columnState.earthActive })
activeBeadPositions.push(pos)
}
// Earth beads
for (let earthPos = 0; earthPos < columnState.earthActive; earthPos++) {
const bead: BeadPositionConfig = {
type: 'earth',
active: true,
position: earthPos,
placeValue,
}
const pos = calculateBeadPosition(bead, dimensions, { earthActive: columnState.earthActive })
activeBeadPositions.push(pos)
}
}
if (activeBeadPositions.length === 0) {
// Fallback if no active beads - show full abacus
return {
minX: 0,
minY: 0,
maxX: dimensions.width,
maxY: dimensions.height,
width: dimensions.width,
height: dimensions.height,
}
}
// Calculate bounding box from active bead positions
// Use diamond dimensions (largest bead shape) for consistent cropping across all shapes
const { width: beadWidth, height: beadHeight } = calculateBeadDimensions(dimensions.beadSize, 'diamond')
let minX = Infinity
let maxX = -Infinity
let minY = Infinity
let maxY = -Infinity
for (const pos of activeBeadPositions) {
// Bead center is at pos.x, pos.y
// Calculate bounding box for diamond shape
minX = Math.min(minX, pos.x - beadWidth / 2)
maxX = Math.max(maxX, pos.x + beadWidth / 2)
minY = Math.min(minY, pos.y - beadHeight / 2)
maxY = Math.max(maxY, pos.y + beadHeight / 2)
}
// HORIZONTAL BOUNDS: Always show full width of all columns (consistent across all values)
// Use rod positions for consistent horizontal bounds
const rodSpacing = dimensions.rodSpacing
minX = rodSpacing / 2 - beadWidth / 2
maxX = (columns - 0.5) * rodSpacing + beadWidth / 2
return {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
}
}
/**
* Calculate crop parameters with padding for SVG viewBox
*
* @param value - The number to display
* @param columns - Number of columns
* @param scaleFactor - Scale factor for the abacus
* @param padding - Padding to add around the crop area
* @returns Complete crop result with viewBox and dimensions
*/
export function calculateAbacusCrop(
value: number,
columns: number,
scaleFactor: number = 1,
padding: CropPadding = {}
): CropResult {
const bbox = calculateActiveBeadsBounds(value, columns, scaleFactor)
// Apply padding
const paddingTop = padding.top ?? 0
const paddingBottom = padding.bottom ?? 0
const paddingLeft = padding.left ?? 0
const paddingRight = padding.right ?? 0
const cropX = bbox.minX - paddingLeft
const cropY = bbox.minY - paddingTop
const cropWidth = bbox.width + paddingLeft + paddingRight
const cropHeight = bbox.height + paddingTop + paddingBottom
// Create viewBox string
const viewBox = `${cropX} ${cropY} ${cropWidth} ${cropHeight}`
return {
minX: cropX,
minY: cropY,
maxX: cropX + cropWidth,
maxY: cropY + cropHeight,
width: cropWidth,
height: cropHeight,
viewBox,
scaledWidth: cropWidth,
scaledHeight: cropHeight,
}
}

View File

@@ -50,6 +50,9 @@ export {
calculateAbacusDimensions,
calculateStandardDimensions, // NEW: Shared layout calculator
calculateBeadPosition, // NEW: Bead position calculator
calculateBeadDimensions, // NEW: Calculate exact bead dimensions by shape
calculateActiveBeadsBounds, // NEW: Calculate bounding box for active beads
calculateAbacusCrop, // NEW: Calculate crop parameters with padding
} from "./AbacusUtils";
export type {
BeadState,
@@ -59,6 +62,9 @@ export type {
PlaceValueBasedBead,
AbacusLayoutDimensions, // NEW: Complete layout dimensions type
BeadPositionConfig, // NEW: Bead config for position calculation
CropPadding, // NEW: Padding config for cropping
BoundingBox, // NEW: Bounding box type
CropResult, // NEW: Complete crop calculation result
} from "./AbacusUtils";
export { useAbacusDiff, useAbacusState } from "./AbacusHooks";