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:
@@ -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}
|
||||
|
||||
@@ -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' }}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user