181 lines
6.1 KiB
TypeScript
181 lines
6.1 KiB
TypeScript
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<PianoGridHeaderProps> = ({
|
|
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<HTMLDivElement | null>(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<HTMLDivElement>) => {
|
|
// 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<HTMLDivElement>) => {
|
|
// 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 (
|
|
<div
|
|
className="piano-grid-header"
|
|
ref={headerElementRef}
|
|
onMouseDown={handleMouseDown}
|
|
onClick={handlePianoGridHeaderClick}
|
|
>
|
|
{Array.from({ length: maxBars }, (_, i) => (
|
|
<div key={i} className="piano-bar-number">{i + 1}</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PianoGridHeader;
|