import React, { useRef, useEffect, useCallback } from 'react'; import { KGCore } from '../../core/KGCore'; import { KGPianoRollState } from '../../core/state/KGPianoRollState'; import { useProjectStore } from '../../stores/projectStore'; import { DEBUG_MODE } from '../../constants'; interface PianoGridHeaderProps { maxBars: number; timeSignature?: { numerator: number; denominator: number }; } const PianoGridHeader: React.FC = ({ maxBars, timeSignature = { numerator: 4, denominator: 4 } // Default to 4/4 if not provided }) => { // Get store access for playhead position updates const { setPlayheadPosition, requestMainContentScroll } = useProjectStore(); // Refs for drag functionality const isDraggingRef = useRef(false); const headerElementRef = useRef(null); // Utility function to calculate snapped beat position (based on useNoteOperations.ts) const getSnappedBeatPosition = (beatPosition: number): number => { const currentSnap = KGPianoRollState.instance().getCurrentSnap(); // If no snapping is enabled, return the original position if (currentSnap === 'NO SNAP') { return beatPosition; } // Parse the snap value (e.g., "1/4", "1/8", "1/16", "1/32") const denominator = parseInt(currentSnap.split('/')[1]); if (isNaN(denominator)) { return beatPosition; // Fallback to no snapping if invalid } // Calculate the snap step in beats // snapStep should ALWAYS be 4 / denominator regardless of time signature const snapStep = 4 / denominator; // Use round snapping for playhead positioning const snappedPosition = Math.round(beatPosition / snapStep) * snapStep; if (DEBUG_MODE.PIANO_ROLL) { console.log(`Piano Grid Header Snapping: ${beatPosition} -> ${snappedPosition} (snap: ${currentSnap}, step: ${snapStep})`); } return snappedPosition; }; // Utility function to calculate playhead position from mouse coordinates const calculatePlayheadFromMouse = useCallback((clientX: number): number | null => { if (!headerElementRef.current) return null; const rect = headerElementRef.current.getBoundingClientRect(); const relativeX = clientX - rect.left; // Account for the piano keys width offset const pianoKeysWidth = parseInt( getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width') ) || 60; const adjustedX = relativeX - pianoKeysWidth; // If the click is in the piano keys area (left side), ignore it if (adjustedX < 0) { return null; } // Calculate the width of each beat const beatWidth = parseInt( getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width') ) || 40; // Calculate the raw beat position using the adjusted X position const rawBeatPosition = adjustedX / beatWidth; // Apply quantization if enabled return getSnappedBeatPosition(rawBeatPosition); }, []); // Handle mouse down to start dragging const handleMouseDown = (e: React.MouseEvent) => { // Only handle left mouse button if (e.button !== 0) return; isDraggingRef.current = true; // Calculate and set initial playhead position const newPosition = calculatePlayheadFromMouse(e.clientX); if (newPosition !== null) { setPlayheadPosition(newPosition); if (DEBUG_MODE.PIANO_ROLL) { console.log(`Piano Grid Header drag started - Initial position: ${newPosition}`); } } // Prevent text selection during drag e.preventDefault(); }; // Handle click (when not dragging) - this will be the fallback for simple clicks const handlePianoGridHeaderClick = (e: React.MouseEvent) => { // If we were dragging, don't process as a click if (isDraggingRef.current) { return; } const newPosition = calculatePlayheadFromMouse(e.clientX); if (newPosition !== null) { const core = KGCore.instance(); const currentPlayheadPosition = core.getPlayheadPosition(); const beatsPerBar = timeSignature.numerator; const currentBarNumber = Math.floor(currentPlayheadPosition / beatsPerBar) + 1; // 1-indexed const destinationBarNumber = Math.floor(newPosition / beatsPerBar) + 1; // 1-indexed // Debug logging if (DEBUG_MODE.PIANO_ROLL) { console.log(`Piano Grid Header click - Position: ${newPosition}`); console.log(`Current bar: ${currentBarNumber} (beat ${currentPlayheadPosition})`); console.log(`Destination bar: ${destinationBarNumber} (beat ${newPosition})`); } setPlayheadPosition(newPosition); requestMainContentScroll(newPosition); } }; // Global mouse move and mouse up handlers useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isDraggingRef.current) return; const newPosition = calculatePlayheadFromMouse(e.clientX); if (newPosition !== null) { setPlayheadPosition(newPosition); if (DEBUG_MODE.PIANO_ROLL) { console.log(`Piano Grid Header drag - Position: ${newPosition}`); } } }; const handleMouseUp = () => { if (isDraggingRef.current) { isDraggingRef.current = false; if (DEBUG_MODE.PIANO_ROLL) { console.log('Piano Grid Header drag ended'); } } }; // Add global event listeners for drag functionality document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); // Cleanup event listeners on unmount return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [calculatePlayheadFromMouse, setPlayheadPosition]); return (
{Array.from({ length: maxBars }, (_, i) => (
{i + 1}
))}
); }; export default PianoGridHeader;