import React, { useMemo, useState, useRef, useEffect } from 'react'; import { DEBUG_MODE } from '../../constants'; import { KGMidiRegion } from '../../core/region/KGMidiRegion'; import { KGMidiNote } from '../../core/midi/KGMidiNote'; import { KGTrack } from '../../core/track/KGTrack'; import { KGCore } from '../../core/KGCore'; import PianoNote from './PianoNote'; import PianoKeys from './PianoKeys'; import PianoGridHeader from './PianoGridHeader'; import PianoGrid from './PianoGrid'; import { useNoteOperations } from '../../hooks/useNoteOperations'; import { useNoteSelection } from '../../hooks/useNoteSelection'; import type { KeySignature } from '../../core/KGProject'; interface PianoRollContentProps { contentRef: React.MutableRefObject; pianoGridRef: React.MutableRefObject; maxBars: number; timeSignature: { numerator: number; denominator: number }; activeRegion: KGMidiRegion | null; updateTrack: (track: KGTrack) => void; tracks: KGTrack[]; onSetNoteUpdateTrigger?: (setNoteFn: React.Dispatch>) => void; onSetDeleteNotesTrigger?: (deleteFn: () => boolean) => void; selectedMode: string; keySignature: KeySignature; } const PianoRollContent: React.FC = ({ contentRef, pianoGridRef, maxBars, timeSignature, activeRegion, updateTrack, tracks, onSetNoteUpdateTrigger, onSetDeleteNotesTrigger, selectedMode, keySignature }) => { // Get KGCore instance const core = KGCore.instance(); // Use the note operations hook for resize and drag functionality const { resizingNoteId, draggingNoteId, tempNoteStyles, noteUpdateCounter, setNoteUpdateCounter, handleGridDoubleClick, handleGridClick, handleNoteResizeStart, handleNoteResize, handleNoteResizeEnd, handleNoteDragStart, handleNoteDrag, handleNoteDragEnd, deleteSelectedNotes } = useNoteOperations({ activeRegion, timeSignature, updateTrack, tracks, pianoGridRef }); // Expose setNoteUpdateCounter to parent component useEffect(() => { if (onSetNoteUpdateTrigger) { onSetNoteUpdateTrigger(setNoteUpdateCounter); } }, [onSetNoteUpdateTrigger, setNoteUpdateCounter]); // Expose deleteSelectedNotes to parent component useEffect(() => { if (onSetDeleteNotesTrigger) { onSetDeleteNotesTrigger(deleteSelectedNotes); } }, [onSetDeleteNotesTrigger, deleteSelectedNotes]); // Use the note selection hook for selection functionality const { selectedNoteIds, isBoxSelectingRef, selectionBoxRef, selectionBoxRender, handleNoteClick, handleBackgroundClick, handleBackgroundMouseDown, cleanupSelectionListeners } = useNoteSelection({ activeRegion, updateTrack, tracks }); // Combined click handler for both pointer and pencil modes const handleCombinedClick = (e: React.MouseEvent) => { // Handle selection click (pointer mode) handleBackgroundClick(e); // Handle pencil mode note creation handleGridClick(e); }; // Sync selected notes with KGCore on mount and when selection changes useEffect(() => { if (!activeRegion) return; // Get currently selected items from KGCore const selectedItems = core.getSelectedItems(); // Filter for KGMidiNote items that belong to this region const selectedNotes = selectedItems.filter(item => item instanceof KGMidiNote && activeRegion.getNotes().some(note => note.getId() === item.getId()) ) as KGMidiNote[]; // Make sure the note objects have the correct selection state activeRegion.getNotes().forEach(note => { const isSelected = selectedNotes.some(selectedNote => selectedNote.getId() === note.getId()); if (isSelected && !note.isSelected()) { note.select(); } else if (!isSelected && note.isSelected()) { note.deselect(); } }); }, [activeRegion]); // Clean up event listeners on unmount useEffect(() => { return () => { cleanupSelectionListeners(); }; }, []); // Memoize the notes rendering to prevent unnecessary recalculations const memoizedNotes = useMemo(() => { if (!activeRegion) return null; if (DEBUG_MODE.PIANO_ROLL) { console.log(`Rendering notes for region: ${activeRegion.getId()}`); console.log(`Number of notes: ${activeRegion.getNotes().length}`); console.log(`Note update counter: ${noteUpdateCounter}`); console.log(`Selected notes: ${Array.from(selectedNoteIds).join(', ')}`); } const notes = activeRegion.getNotes(); const beatWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40; const noteHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20; const regionStartBeat = activeRegion.getStartFromBeat(); return notes.map((note, index) => { // Calculate position and size const startBeat = note.getStartBeat() + regionStartBeat; // Absolute beat position const endBeat = note.getEndBeat() + regionStartBeat; // Absolute beat position const pitch = note.getPitch(); // Convert pitch to y position (higher notes have lower y values) // We need to find the index of the pitch in our piano roll const pitchIndex = 107 - pitch; // Reverse the pitch to get the index (B7 is 107) // Calculate position and dimensions const left = startBeat * beatWidth; const top = pitchIndex * noteHeight; const width = (endBeat - startBeat) * beatWidth; const noteId = note.getId(); // Check if this note is being resized or dragged and has a temporary style if ((resizingNoteId === noteId || draggingNoteId === noteId) && tempNoteStyles[noteId]) { // Use the temporary style for position and size const tempStyle = tempNoteStyles[noteId]; return ( ); } return ( ); }); }, [activeRegion, noteUpdateCounter, resizingNoteId, draggingNoteId, tempNoteStyles, selectedNoteIds, selectionBoxRender, tracks]); return (
{memoizedNotes}
); }; export default PianoRollContent;