feat: add cropToActiveBeads prop to AbacusStatic and AbacusReact

- Add cropToActiveBeads prop to AbacusSVGRenderer that accepts boolean or {padding} object
- Pass actual scaleFactor to calculateAbacusCrop for correct crop calculations at any scale
- Remove double scaling (was multiplying width/height by scaleFactor after crop already included it)
- Add cropToActiveBeads prop to AbacusStatic config and pass through to renderer
- Add cropToActiveBeads prop to AbacusReact config and pass through to renderer

This enables both components to crop the SVG viewBox to show only active beads with optional padding, working correctly at all scale factors (0.8, 1.0, 1.5, 2.0, etc.).

🤖 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:57:58 -06:00
parent b080970d76
commit 35b0824fc4
3 changed files with 33 additions and 6 deletions

View File

@@ -7,7 +7,7 @@ import NumberFlow from "@number-flow/react";
import { useAbacusConfig, getDefaultAbacusConfig } from "./AbacusContext";
import { playBeadSound } from "./soundManager";
import * as Abacus3DUtils from "./Abacus3DUtils";
import { calculateStandardDimensions, calculateBeadPosition } from "./AbacusUtils";
import { calculateStandardDimensions, calculateBeadPosition, type CropPadding } from "./AbacusUtils";
import { AbacusSVGRenderer } from "./AbacusSVGRenderer";
import { AbacusAnimatedBead } from "./AbacusAnimatedBead";
import "./Abacus3D.css";
@@ -296,6 +296,9 @@ export interface AbacusConfig {
disabledColumns?: number[]; // Disable interaction on specific columns (legacy - array indices)
disabledBeads?: BeadHighlight[]; // Support both place-value and column-index based disabling
// Cropping
cropToActiveBeads?: boolean | { padding?: CropPadding }; // Crop viewBox to show only active beads
// Legacy callbacks for backward compatibility
onClick?: (bead: BeadConfig) => void;
onValueChange?: (newValue: number | bigint) => void;
@@ -1595,6 +1598,8 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
showDirectionIndicators = false,
disabledColumns = [],
disabledBeads = [],
// Cropping
cropToActiveBeads,
// Legacy callbacks
onClick,
onValueChange,
@@ -2215,6 +2220,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
interactive={finalConfig.interactive}
highlightColumns={highlightColumns}
columnLabels={columnLabels}
cropToActiveBeads={cropToActiveBeads}
defsContent={defsContent}
BeadComponent={AbacusAnimatedBead}
getBeadColor={getBeadColor}

View File

@@ -34,7 +34,7 @@
import React from 'react'
import type { AbacusLayoutDimensions } from './AbacusUtils'
import type { BeadConfig, AbacusCustomStyles, ValidPlaceValues } from './AbacusReact'
import { numberToAbacusState, calculateBeadPosition, type AbacusState } from './AbacusUtils'
import { numberToAbacusState, calculateBeadPosition, calculateAbacusCrop, type AbacusState, type CropPadding } from './AbacusUtils'
/**
* Props that bead components must accept
@@ -83,6 +83,9 @@ export interface AbacusSVGRendererProps {
customStyles?: AbacusCustomStyles
interactive?: boolean // Enable interactive CSS styles
// Cropping
cropToActiveBeads?: boolean | { padding?: CropPadding }
// Tutorial features
highlightColumns?: number[]
columnLabels?: string[]
@@ -127,6 +130,7 @@ export function AbacusSVGRenderer({
showNumbers,
customStyles,
interactive = false,
cropToActiveBeads,
highlightColumns = [],
columnLabels = [],
defsContent,
@@ -141,12 +145,26 @@ export function AbacusSVGRenderer({
}: AbacusSVGRendererProps) {
const { width, height, rodSpacing, barY, beadSize, barThickness, labelHeight, numbersHeight } = dimensions
// Calculate crop viewBox if enabled
let viewBox = `0 0 ${width} ${height}`
let svgWidth = width
let svgHeight = height
if (cropToActiveBeads) {
const padding = typeof cropToActiveBeads === 'object' ? cropToActiveBeads.padding : undefined
// Use the actual scaleFactor so crop calculations match the rendered abacus size
const crop = calculateAbacusCrop(Number(value), columns, scaleFactor, padding)
viewBox = crop.viewBox
svgWidth = crop.width
svgHeight = crop.height
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width * scaleFactor}
height={height * scaleFactor}
viewBox={`0 0 ${width} ${height}`}
width={svgWidth}
height={svgHeight}
viewBox={viewBox}
className={`abacus-svg ${hideInactiveBeads ? 'hide-inactive-mode' : ''} ${interactive ? 'interactive' : ''}`}
style={{ overflow: 'visible', display: 'block' }}
>

View File

@@ -7,7 +7,7 @@
* Different: No hooks, no animations, no interactions, simplified bead rendering
*/
import { numberToAbacusState, calculateStandardDimensions } from './AbacusUtils'
import { numberToAbacusState, calculateStandardDimensions, type CropPadding } from './AbacusUtils'
import { AbacusSVGRenderer } from './AbacusSVGRenderer'
import { AbacusStaticBead } from './AbacusStaticBead'
import type {
@@ -30,6 +30,7 @@ export interface AbacusStaticConfig {
customStyles?: AbacusCustomStyles
highlightColumns?: number[]
columnLabels?: string[]
cropToActiveBeads?: boolean | { padding?: CropPadding }
}
// Shared color logic (matches AbacusReact)
@@ -106,6 +107,7 @@ export function AbacusStatic({
customStyles,
highlightColumns = [],
columnLabels = [],
cropToActiveBeads,
}: AbacusStaticConfig) {
// Calculate columns
const valueStr = value.toString().replace('-', '')
@@ -175,6 +177,7 @@ export function AbacusStatic({
customStyles={customStyles}
highlightColumns={highlightColumns}
columnLabels={columnLabels}
cropToActiveBeads={cropToActiveBeads}
BeadComponent={AbacusStaticBead}
getBeadColor={getBeadColor}
/>