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>
This commit is contained in:
Thomas Hallock 2025-11-05 10:00:31 -06:00
parent ac6c3c1376
commit b6c3d6bda4
1 changed files with 38 additions and 130 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>
`