feat(abacus-react): add shared dimension calculator for consistent sizing
- Add calculateAbacusDimensions utility to AbacusUtils - Refactor AbacusStatic to use shared dimension calculator - Update calendar composite generator to use shared utility - Export calculateAbacusDimensions from index and static entry points - Ensures consistent sizing between preview and PDF generation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { AbacusStatic } from '@soroban/abacus-react/static'
|
import { AbacusStatic, calculateAbacusDimensions } from '@soroban/abacus-react/static'
|
||||||
|
|
||||||
interface CalendarCompositeOptions {
|
interface CalendarCompositeOptions {
|
||||||
month: number
|
month: number
|
||||||
@@ -45,9 +45,30 @@ const MARGIN = 50
|
|||||||
const CONTENT_WIDTH = WIDTH - MARGIN * 2
|
const CONTENT_WIDTH = WIDTH - MARGIN * 2
|
||||||
const CONTENT_HEIGHT = HEIGHT - MARGIN * 2
|
const CONTENT_HEIGHT = HEIGHT - MARGIN * 2
|
||||||
|
|
||||||
// Header
|
// Abacus natural size is 120x230 at scale=1
|
||||||
const HEADER_HEIGHT = 60
|
const ABACUS_NATURAL_WIDTH = 120
|
||||||
|
const ABACUS_NATURAL_HEIGHT = 230
|
||||||
|
|
||||||
|
// Calculate how many columns needed for year
|
||||||
|
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
|
||||||
|
|
||||||
|
// Year abacus dimensions (calculate first to determine header height)
|
||||||
|
// Use the shared dimension calculator so we stay in sync with AbacusStatic
|
||||||
|
const { width: yearAbacusActualWidth, height: yearAbacusActualHeight } = calculateAbacusDimensions({
|
||||||
|
columns: yearColumns,
|
||||||
|
showNumbers: false,
|
||||||
|
columnLabels: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const yearAbacusDisplayWidth = WIDTH * 0.15 // Display size on page
|
||||||
|
const yearAbacusDisplayHeight = (yearAbacusActualHeight / yearAbacusActualWidth) * yearAbacusDisplayWidth
|
||||||
|
|
||||||
|
// Header - sized to fit month name + year abacus
|
||||||
|
const MONTH_NAME_HEIGHT = 40
|
||||||
|
const HEADER_HEIGHT = MONTH_NAME_HEIGHT + yearAbacusDisplayHeight + 20 // 20px spacing
|
||||||
const TITLE_Y = MARGIN + 35
|
const TITLE_Y = MARGIN + 35
|
||||||
|
const yearAbacusX = (WIDTH - yearAbacusDisplayWidth) / 2
|
||||||
|
const yearAbacusY = TITLE_Y + 10
|
||||||
|
|
||||||
// Calendar grid
|
// Calendar grid
|
||||||
const GRID_START_Y = MARGIN + HEADER_HEIGHT
|
const GRID_START_Y = MARGIN + HEADER_HEIGHT
|
||||||
@@ -59,10 +80,7 @@ const DAY_GRID_HEIGHT = GRID_HEIGHT - WEEKDAY_ROW_HEIGHT
|
|||||||
const CELL_WIDTH = CONTENT_WIDTH / 7
|
const CELL_WIDTH = CONTENT_WIDTH / 7
|
||||||
const DAY_CELL_HEIGHT = DAY_GRID_HEIGHT / 6
|
const DAY_CELL_HEIGHT = DAY_GRID_HEIGHT / 6
|
||||||
|
|
||||||
// Abacus natural size is 120x230 at scale=1
|
// Day abacus sizing - fit in cell with padding
|
||||||
// We need to fit in cell with padding
|
|
||||||
const ABACUS_NATURAL_WIDTH = 120
|
|
||||||
const ABACUS_NATURAL_HEIGHT = 230
|
|
||||||
const CELL_PADDING = 5
|
const CELL_PADDING = 5
|
||||||
|
|
||||||
// Calculate max scale to fit in cell
|
// Calculate max scale to fit in cell
|
||||||
@@ -82,9 +100,6 @@ for (let day = 1; day <= daysInMonth; day++) {
|
|||||||
calendarCells.push(day)
|
calendarCells.push(day)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate how many columns needed for year
|
|
||||||
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
|
|
||||||
|
|
||||||
// Render individual abacus SVGs as complete SVG elements
|
// Render individual abacus SVGs as complete SVG elements
|
||||||
function renderAbacusSVG(value: number, columns: number, scale: number): string {
|
function renderAbacusSVG(value: number, columns: number, scale: number): string {
|
||||||
return renderToString(
|
return renderToString(
|
||||||
@@ -105,7 +120,7 @@ const compositeSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" h
|
|||||||
<rect width="${WIDTH}" height="${HEIGHT}" fill="white"/>
|
<rect width="${WIDTH}" height="${HEIGHT}" fill="white"/>
|
||||||
|
|
||||||
<!-- Title: Month Name -->
|
<!-- Title: Month Name -->
|
||||||
<text x="${WIDTH / 2}" y="${TITLE_Y - 20}" text-anchor="middle" font-family="Arial" font-size="32" font-weight="bold" fill="#1a1a1a">
|
<text x="${WIDTH / 2}" y="${TITLE_Y}" text-anchor="middle" font-family="Arial" font-size="32" font-weight="bold" fill="#1a1a1a">
|
||||||
${monthName}
|
${monthName}
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
@@ -113,13 +128,8 @@ const compositeSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" h
|
|||||||
${(() => {
|
${(() => {
|
||||||
const yearAbacusSVG = renderAbacusSVG(year, yearColumns, 1)
|
const yearAbacusSVG = renderAbacusSVG(year, yearColumns, 1)
|
||||||
const yearAbacusContent = yearAbacusSVG.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
|
const yearAbacusContent = yearAbacusSVG.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
|
||||||
// Scale year abacus to be smaller (about 15% of width)
|
|
||||||
const yearAbacusDisplayWidth = WIDTH * 0.15
|
|
||||||
const yearAbacusDisplayHeight = (ABACUS_NATURAL_HEIGHT / ABACUS_NATURAL_WIDTH) * yearAbacusDisplayWidth
|
|
||||||
const yearAbacusX = (WIDTH - yearAbacusDisplayWidth) / 2
|
|
||||||
const yearAbacusY = TITLE_Y - 10
|
|
||||||
return `<svg x="${yearAbacusX}" y="${yearAbacusY}" width="${yearAbacusDisplayWidth}" height="${yearAbacusDisplayHeight}"
|
return `<svg x="${yearAbacusX}" y="${yearAbacusY}" width="${yearAbacusDisplayWidth}" height="${yearAbacusDisplayHeight}"
|
||||||
viewBox="0 0 ${ABACUS_NATURAL_WIDTH} ${ABACUS_NATURAL_HEIGHT}">
|
viewBox="0 0 ${yearAbacusActualWidth} ${yearAbacusActualHeight}">
|
||||||
${yearAbacusContent}
|
${yearAbacusContent}
|
||||||
</svg>`
|
</svg>`
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Different: No hooks, no animations, no interactions, simplified rendering
|
* Different: No hooks, no animations, no interactions, simplified rendering
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { numberToAbacusState } from './AbacusUtils'
|
import { numberToAbacusState, calculateAbacusDimensions } from './AbacusUtils'
|
||||||
import { AbacusStaticBead } from './AbacusStaticBead'
|
import { AbacusStaticBead } from './AbacusStaticBead'
|
||||||
import type {
|
import type {
|
||||||
AbacusCustomStyles,
|
AbacusCustomStyles,
|
||||||
@@ -175,7 +175,14 @@ export function AbacusStatic({
|
|||||||
beadConfigs.push(beads)
|
beadConfigs.push(beads)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate dimensions (matching AbacusReact)
|
// Calculate dimensions using shared utility
|
||||||
|
const { width, height } = calculateAbacusDimensions({
|
||||||
|
columns: effectiveColumns,
|
||||||
|
showNumbers: !!showNumbers,
|
||||||
|
columnLabels,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Layout constants (must match calculateAbacusDimensions)
|
||||||
const beadSize = 20
|
const beadSize = 20
|
||||||
const rodSpacing = 40
|
const rodSpacing = 40
|
||||||
const heavenHeight = 60
|
const heavenHeight = 60
|
||||||
@@ -185,9 +192,6 @@ export function AbacusStatic({
|
|||||||
const numberHeightCalc = showNumbers ? 30 : 0
|
const numberHeightCalc = showNumbers ? 30 : 0
|
||||||
const labelHeight = columnLabels.length > 0 ? 30 : 0
|
const labelHeight = columnLabels.length > 0 ? 30 : 0
|
||||||
|
|
||||||
const width = effectiveColumns * rodSpacing + padding * 2
|
|
||||||
const height = heavenHeight + earthHeight + barHeight + padding * 2 + numberHeightCalc + labelHeight
|
|
||||||
|
|
||||||
const dimensions = {
|
const dimensions = {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|||||||
@@ -356,3 +356,37 @@ function getPlaceName(place: number): string {
|
|||||||
return `place ${place} column`
|
return `place ${place} column`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the natural dimensions of an abacus SVG
|
||||||
|
* This uses the same logic as AbacusStatic to ensure consistency
|
||||||
|
*
|
||||||
|
* @param columns - Number of columns in the abacus
|
||||||
|
* @param showNumbers - Whether numbers are shown below columns
|
||||||
|
* @param columnLabels - Array of column labels (if any)
|
||||||
|
* @returns Object with width and height in pixels (at scale=1)
|
||||||
|
*/
|
||||||
|
export function calculateAbacusDimensions({
|
||||||
|
columns,
|
||||||
|
showNumbers = true,
|
||||||
|
columnLabels = [],
|
||||||
|
}: {
|
||||||
|
columns: number
|
||||||
|
showNumbers?: boolean
|
||||||
|
columnLabels?: string[]
|
||||||
|
}): { width: number; height: number } {
|
||||||
|
// Constants matching AbacusStatic
|
||||||
|
const beadSize = 20
|
||||||
|
const rodSpacing = 40
|
||||||
|
const heavenHeight = 60
|
||||||
|
const earthHeight = 120
|
||||||
|
const barHeight = 10
|
||||||
|
const padding = 20
|
||||||
|
const numberHeightCalc = showNumbers ? 30 : 0
|
||||||
|
const labelHeight = columnLabels.length > 0 ? 30 : 0
|
||||||
|
|
||||||
|
const width = columns * rodSpacing + padding * 2
|
||||||
|
const height = heavenHeight + earthHeight + barHeight + padding * 2 + numberHeightCalc + labelHeight
|
||||||
|
|
||||||
|
return { width, height }
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export {
|
|||||||
calculateBeadDiffFromValues,
|
calculateBeadDiffFromValues,
|
||||||
validateAbacusValue,
|
validateAbacusValue,
|
||||||
areStatesEqual,
|
areStatesEqual,
|
||||||
|
calculateAbacusDimensions,
|
||||||
} from "./AbacusUtils";
|
} from "./AbacusUtils";
|
||||||
export type {
|
export type {
|
||||||
BeadState,
|
BeadState,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export { AbacusStaticBead } from './AbacusStaticBead'
|
|||||||
export type { StaticBeadProps } from './AbacusStaticBead'
|
export type { StaticBeadProps } from './AbacusStaticBead'
|
||||||
|
|
||||||
// Re-export shared utilities that are safe for server components
|
// Re-export shared utilities that are safe for server components
|
||||||
export { numberToAbacusState } from './AbacusUtils'
|
export { numberToAbacusState, calculateAbacusDimensions } from './AbacusUtils'
|
||||||
export type {
|
export type {
|
||||||
AbacusCustomStyles,
|
AbacusCustomStyles,
|
||||||
BeadConfig,
|
BeadConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user