Compare commits
4 Commits
abacus-rea
...
abacus-rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bab0b49c3 | ||
|
|
827a949216 | ||
|
|
0c4b0c2fac | ||
|
|
b6c3d6bda4 |
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
}
|
||||
},
|
||||
"abacus": {
|
||||
"navTitle": "3D अबेकस निर्माता",
|
||||
"pageTitle": "अपने 3D प्रिंट करने योग्य अबेकस को अनुकूलित करें",
|
||||
"pageSubtitle": "अपने अबेकस को अनुकूलित करने के लिए नीचे दिए गए पैरामीटर समायोजित करें, फिर 3D प्रिंटिंग के लिए फ़ाइल उत्पन्न करें और डाउनलोड करें।",
|
||||
"customizationTitle": "अनुकूलन पैरामीटर",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
}
|
||||
},
|
||||
"abacus": {
|
||||
"navTitle": "3Dそろばん作成",
|
||||
"pageTitle": "3Dプリント可能そろばんをカスタマイズ",
|
||||
"pageSubtitle": "以下のパラメータを調整してそろばんをカスタマイズし、3Dプリント用のファイルを生成してダウンロードします。",
|
||||
"customizationTitle": "カスタマイズパラメータ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user