Compare commits

...

4 Commits

Author SHA1 Message Date
semantic-release-bot
9bab0b49c3 chore(abacus-react): release v2.10.1 [skip ci]
## [2.10.1](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.10.0...abacus-react-v2.10.1) (2025-11-05)

### Bug Fixes

* **i18n:** add nav bar to 3D abacus creator page ([827a949](827a949216))
2025-11-05 16:02:52 +00:00
Thomas Hallock
827a949216 fix(i18n): add nav bar to 3D abacus creator page
- Added navTitle translation key to all 7 language files
- Wrapped abacus page with PageWithNav component
- Now matches navigation pattern of other create pages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 10:01:46 -06:00
Thomas Hallock
0c4b0c2fac style: fix formatting and add approved bash commands
- Add trailing comma in cropToActiveBeads config
- Format console.log call for better readability
- Format postMessage call parameters
- Add approved bash commands for icon testing to local settings

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 10:01:46 -06:00
Thomas Hallock
b6c3d6bda4 refactor: use package-level cropToActiveBeads in generateDayIcon script
Replace custom bounding box calculations with the new cropToActiveBeads prop:
- Remove 100+ lines of manual bbox calculation code (getAbacusBoundingBox)
- Use AbacusStatic instead of AbacusReact for simpler SSR
- Let cropToActiveBeads handle all cropping logic with padding config
- Simplify script to just parse dimensions from cropped SVG and center in canvas

This eliminates code duplication and leverages the precise cropping utilities
now available in the package.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 10:01:46 -06:00
14 changed files with 104 additions and 158 deletions

View File

@@ -8,114 +8,15 @@
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import {
AbacusReact,
numberToAbacusState,
calculateStandardDimensions,
calculateBeadPosition,
type BeadPositionConfig,
} from '@soroban/abacus-react'
import { AbacusStatic } from '@soroban/abacus-react'
// Extract just the SVG element content from rendered output
function extractSvgContent(markup: string): string {
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)
// Extract just the SVG element from rendered output
function extractSvgElement(markup: string): string {
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
if (!svgMatch) {
throw new Error('No SVG element found in rendered output')
}
return svgMatch[1]
}
// Calculate bounding box that includes active beads AND structural elements (posts, bar)
interface BoundingBox {
minX: number
minY: number
maxX: number
maxY: number
}
/**
* Calculate bounding box for icon cropping using actual bead position calculations
* This replaces fragile regex parsing with deterministic position math
*/
function getAbacusBoundingBox(
day: number,
scaleFactor: number,
columns: number
): BoundingBox {
// Get which beads are active for this day
const abacusState = numberToAbacusState(day, 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: 50 * scaleFactor, maxY: 120 * scaleFactor }
}
// Calculate bounding box from active bead positions
const beadSize = dimensions.beadSize
const beadWidth = beadSize * 2.5 // Diamond width is ~2.5x the size parameter
const beadHeight = beadSize * 1.8 // Diamond height is ~1.8x the size parameter
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 days)
// 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 }
return svgMatch[0]
}
// Get day from command line argument
@@ -128,15 +29,23 @@ if (!day || day < 1 || day > 31) {
}
// Render 2-column abacus showing day of month
// Using AbacusStatic for server-side rendering
const abacusMarkup = renderToStaticMarkup(
<AbacusReact
<AbacusStatic
value={day}
columns={2}
scaleFactor={1.8}
animated={false}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
frameVisible={true}
cropToActiveBeads={{
padding: {
top: 8,
bottom: 2,
left: 5,
right: 5,
},
}}
customStyles={{
columnPosts: {
fill: '#1c1917',
@@ -164,44 +73,43 @@ const abacusMarkup = renderToStaticMarkup(
/>
)
let svgContent = extractSvgContent(abacusMarkup)
// Extract the cropped SVG
let croppedSvg = extractSvgElement(abacusMarkup)
// Remove !important from CSS (production code policy)
svgContent = svgContent.replace(/\s*!important/g, '')
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
// Calculate bounding box using proper bead position calculations
const bbox = getAbacusBoundingBox(day, 1.8, 2)
// Parse width and height from the cropped SVG
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
// Add minimal padding around active beads (in abacus coordinates)
// Less padding below since we want to cut tight to the last bead
const paddingTop = 8
const paddingBottom = 2
const paddingSide = 5
const cropX = bbox.minX - paddingSide
const cropY = bbox.minY - paddingTop
const cropWidth = bbox.maxX - bbox.minX + paddingSide * 2
const cropHeight = bbox.maxY - bbox.minY + paddingTop + paddingBottom
if (!widthMatch || !heightMatch) {
throw new Error('Could not parse dimensions from cropped SVG')
}
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 / cropWidth, targetSize / cropHeight)
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
// Center in 100x100 canvas
const scaledWidth = cropWidth * scale
const scaledHeight = cropHeight * scale
const scaledWidth = croppedWidth * scale
const scaledHeight = croppedHeight * scale
const offsetX = (100 - scaledWidth) / 2
const offsetY = (100 - scaledHeight) / 2
// Wrap in SVG with proper viewBox for favicon sizing
// Use nested SVG with viewBox to actually CROP the content, not just scale it
// 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 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 -->
<!-- Nested SVG with viewBox does the actual cropping -->
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
viewBox="${cropX} ${cropY} ${cropWidth} ${cropHeight}">
<g class="hide-inactive-mode">
${svgContent}
</g>
viewBox="${viewBox}">
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
</svg>
</svg>
`

View File

@@ -2,6 +2,7 @@
import { useTranslations } from 'next-intl'
import { JobMonitor } from '@/components/3d-print/JobMonitor'
import { PageWithNav } from '@/components/PageWithNav'
import { STLPreview } from '@/components/3d-print/STLPreview'
import { useState } from 'react'
import { css } from '../../../../styled-system/css'
@@ -73,25 +74,26 @@ export default function ThreeDPrintPage() {
}
return (
<div
data-component="3d-print-page"
className={css({
maxWidth: '1200px',
mx: 'auto',
p: 6,
})}
>
<h1
<PageWithNav navTitle={t('navTitle')} navEmoji="🖨️">
<div
data-component="3d-print-page"
className={css({
fontSize: '3xl',
fontWeight: 'bold',
mb: 2,
maxWidth: '1200px',
mx: 'auto',
p: 6,
})}
>
{t('pageTitle')}
</h1>
<h1
className={css({
fontSize: '3xl',
fontWeight: 'bold',
mb: 2,
})}
>
{t('pageTitle')}
</h1>
<p className={css({ mb: 6, color: 'gray.600' })}>{t('pageSubtitle')}</p>
<p className={css({ mb: 6, color: 'gray.600' })}>{t('pageSubtitle')}</p>
<div
className={css({
@@ -561,5 +563,6 @@ export default function ThreeDPrintPage() {
</div>
</div>
</div>
</PageWithNav>
)
}

View File

@@ -1109,13 +1109,20 @@ function TutorialPlayerContent({
// Debug logging for custom styles
if (Object.keys(styles).length > 0) {
console.log('📋 TUTORIAL CUSTOM STYLES:', JSON.stringify({
beadLevelHighlights,
columnLevelHighlights,
finalStyles: styles,
currentStepHighlightBeads: currentStep.highlightBeads,
abacusColumns,
}, null, 2))
console.log(
'📋 TUTORIAL CUSTOM STYLES:',
JSON.stringify(
{
beadLevelHighlights,
columnLevelHighlights,
finalStyles: styles,
currentStepHighlightBeads: currentStep.highlightBeads,
abacusColumns,
},
null,
2
)
)
}
return Object.keys(styles).length > 0 ? styles : undefined

View File

@@ -29,6 +29,7 @@
}
},
"abacus": {
"navTitle": "3D-Abakus-Ersteller",
"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",

View File

@@ -29,6 +29,7 @@
}
},
"abacus": {
"navTitle": "3D Abacus Creator",
"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",

View File

@@ -29,6 +29,7 @@
}
},
"abacus": {
"navTitle": "Creador de Ábaco 3D",
"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",

View File

@@ -29,6 +29,7 @@
}
},
"abacus": {
"navTitle": "3D Abacus Giskaffari",
"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",

View File

@@ -29,6 +29,7 @@
}
},
"abacus": {
"navTitle": "3D अबेकस निर्माता",
"pageTitle": "अपने 3D प्रिंट करने योग्य अबेकस को अनुकूलित करें",
"pageSubtitle": "अपने अबेकस को अनुकूलित करने के लिए नीचे दिए गए पैरामीटर समायोजित करें, फिर 3D प्रिंटिंग के लिए फ़ाइल उत्पन्न करें और डाउनलोड करें।",
"customizationTitle": "अनुकूलन पैरामीटर",

View File

@@ -29,6 +29,7 @@
}
},
"abacus": {
"navTitle": "3Dそろばん作成",
"pageTitle": "3Dプリント可能そろばんをカスタマイズ",
"pageSubtitle": "以下のパラメータを調整してそろばんをカスタマイズし、3Dプリント用のファイルを生成してダウンロードします。",
"customizationTitle": "カスタマイズパラメータ",

View File

@@ -29,6 +29,7 @@
}
},
"abacus": {
"navTitle": "Creator Abaci 3D",
"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",

View File

@@ -119,7 +119,7 @@ export function generateCalendarComposite(options: CalendarCompositeOptions): st
compact={false}
hideInactiveBeads={true}
cropToActiveBeads={{
padding: { top: 8, bottom: 2, left: 5, right: 5 }
padding: { top: 8, bottom: 2, left: 5, right: 5 },
}}
/>
)

View File

@@ -142,10 +142,13 @@ scale([scale_factor, scale_factor, scale_factor]) {
console.log('[OpenSCAD Worker] Rendering complete:', stlBuffer.byteLength, 'bytes')
// Send the result back
self.postMessage({
type: 'result',
stl: stlBuffer,
}, [stlBuffer]) // Transfer ownership of the buffer
self.postMessage(
{
type: 'result',
stl: stlBuffer,
},
[stlBuffer]
) // Transfer ownership of the buffer
// Clean up STL file
try {

View File

@@ -28,7 +28,18 @@
"WebFetch(domain:schroer.ca)",
"WebFetch(domain:github.com)",
"Bash(npm search:*)",
"Bash(pnpm add:*)"
"Bash(pnpm add:*)",
"Bash(/tmp/icon-test.svg)",
"Bash(/tmp/icon-fixed.svg)",
"Bash(/tmp/icon-new.svg)",
"Bash(for day in 1 15 25 31)",
"Bash(do npx tsx scripts/generateDayIcon.tsx $day)",
"Bash(for day in 1 5 15 25 31)",
"Bash(do echo \"=== Day $day ===\")",
"Bash(/tmp/icon-day-$day.svg)",
"Bash(echo:*)",
"Bash(done)",
"Bash(node /tmp/test-crop.mjs:*)"
]
},
"enableAllProjectMcpServers": true,

View File

@@ -1,3 +1,10 @@
## [2.10.1](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.10.0...abacus-react-v2.10.1) (2025-11-05)
### Bug Fixes
* **i18n:** add nav bar to 3D abacus creator page ([827a949](https://github.com/antialias/soroban-abacus-flashcards/commit/827a949216709d9f6a7ea5446acb36b6d83bf861))
# [2.10.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.9.0...abacus-react-v2.10.0) (2025-11-05)