Files
KGStudio/src/components/piano-roll/PianoRollContent.tsx
T
2025-12-09 23:50:03 -08:00

245 lines
8.0 KiB
TypeScript

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<HTMLDivElement | null>;
pianoGridRef: React.MutableRefObject<HTMLDivElement | null>;
maxBars: number;
timeSignature: { numerator: number; denominator: number };
activeRegion: KGMidiRegion | null;
updateTrack: (track: KGTrack) => void;
tracks: KGTrack[];
onSetNoteUpdateTrigger?: (setNoteFn: React.Dispatch<React.SetStateAction<number>>) => void;
onSetDeleteNotesTrigger?: (deleteFn: () => boolean) => void;
selectedMode: string;
keySignature: KeySignature;
}
const PianoRollContent: React.FC<PianoRollContentProps> = ({
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 (
<PianoNote
key={`note-${noteId}`}
id={noteId}
index={index}
left={parseFloat(tempStyle.left as string)}
top={parseFloat(tempStyle.top as string)}
width={parseFloat(tempStyle.width as string)}
height={noteHeight}
onResizeStart={handleNoteResizeStart}
onResize={handleNoteResize}
onResizeEnd={handleNoteResizeEnd}
onDragStart={handleNoteDragStart}
onDrag={handleNoteDrag}
onDragEnd={handleNoteDragEnd}
onClick={handleNoteClick}
/>
);
}
return (
<PianoNote
key={`note-${noteId}`}
id={noteId}
index={index}
left={left}
top={top}
width={width}
height={noteHeight}
onResizeStart={handleNoteResizeStart}
onResize={handleNoteResize}
onResizeEnd={handleNoteResizeEnd}
onDragStart={handleNoteDragStart}
onDrag={handleNoteDrag}
onDragEnd={handleNoteDragEnd}
onClick={handleNoteClick}
/>
);
});
}, [activeRegion, noteUpdateCounter, resizingNoteId, draggingNoteId, tempNoteStyles, selectedNoteIds, selectionBoxRender, tracks]);
return (
<div
className="piano-roll-content"
ref={contentRef}
>
<PianoGridHeader maxBars={maxBars} timeSignature={timeSignature} />
<div className="piano-roll-body">
<PianoKeys activeRegion={activeRegion} />
<PianoGrid
gridRef={pianoGridRef}
onDoubleClick={handleGridDoubleClick}
onClick={handleCombinedClick}
onMouseDown={handleBackgroundMouseDown}
isBoxSelecting={isBoxSelectingRef.current}
selectionBox={selectionBoxRef.current}
regionStartBeat={activeRegion?.getStartFromBeat() || 0}
selectedMode={selectedMode}
keySignature={keySignature}
>
{memoizedNotes}
</PianoGrid>
</div>
</div>
);
};
export default PianoRollContent;