feat: MIDI note duration inheritance & Ctrl+Shift+click split
This commit is contained in:
+49
-17
@@ -6007,6 +6007,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
notesRef.current = notes;
|
notesRef.current = notes;
|
||||||
React.useEffect(() => { if (!draggedNoteRef.current) setNotes(st.notes || []); }, [st.notes]);
|
React.useEffect(() => { if (!draggedNoteRef.current) setNotes(st.notes || []); }, [st.notes]);
|
||||||
const brushVelocityRef = React.useRef(0.8);
|
const brushVelocityRef = React.useRef(0.8);
|
||||||
|
const lastNoteDurationRef = React.useRef(null);
|
||||||
const previewPitchRef = React.useRef(null);
|
const previewPitchRef = React.useRef(null);
|
||||||
const previewNodesRef = React.useRef(null);
|
const previewNodesRef = React.useRef(null);
|
||||||
var stopPreviewNote = function() {
|
var stopPreviewNote = function() {
|
||||||
@@ -6503,28 +6504,50 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl+Shift+click: duplicate selected + clicked notes
|
// Ctrl+Shift+click on note → split at click position
|
||||||
if (e.ctrlKey && e.shiftKey) {
|
if (e.ctrlKey && e.shiftKey) {
|
||||||
if (clickedNoteIdx !== -1) {
|
if (clickedNoteIdx !== -1) {
|
||||||
|
const target = notes[clickedNoteIdx];
|
||||||
|
const splitBeat = getSnapBeat(beat, snapValue);
|
||||||
|
if (splitBeat > target.start_beat + 0.03125 && splitBeat < target.start_beat + target.duration_beats - 0.03125) {
|
||||||
|
pushToUndo(notes);
|
||||||
|
const noteA = {
|
||||||
|
...JSON.parse(JSON.stringify(target)),
|
||||||
|
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8),
|
||||||
|
duration_beats: splitBeat - target.start_beat
|
||||||
|
};
|
||||||
|
const noteB = {
|
||||||
|
...JSON.parse(JSON.stringify(target)),
|
||||||
|
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8),
|
||||||
|
start_beat: splitBeat,
|
||||||
|
duration_beats: target.start_beat + target.duration_beats - splitBeat
|
||||||
|
};
|
||||||
|
setNotes(prev => prev.map(n => n.id === target.id ? noteA : n).concat([noteB]));
|
||||||
|
setSelectedNoteIds([noteA.id, noteB.id]);
|
||||||
|
showToast('Đã tách nốt!', 'info');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ctrl+Shift+click on empty space → duplicate selected + clicked notes
|
||||||
pushToUndo(notes);
|
pushToUndo(notes);
|
||||||
const clickedNote = notes[clickedNoteIdx];
|
const clones = notes.filter(n => selectedNoteIds.includes(n.id)).map(n => ({
|
||||||
const idsToClone = [...new Set([...selectedNoteIds, clickedNote.id])];
|
|
||||||
const clones = notes.filter(n => idsToClone.includes(n.id)).map(n => ({
|
|
||||||
...JSON.parse(JSON.stringify(n)),
|
...JSON.parse(JSON.stringify(n)),
|
||||||
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8)
|
id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8)
|
||||||
}));
|
}));
|
||||||
setNotes(prev => [...prev, ...clones]);
|
if (clones.length > 0) {
|
||||||
const cloneIds = clones.map(c => c.id);
|
setNotes(prev => [...prev, ...clones]);
|
||||||
setSelectedNoteIds(cloneIds);
|
const cloneIds = clones.map(c => c.id);
|
||||||
notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
|
setSelectedNoteIds(cloneIds);
|
||||||
const cloneOffsets = clones.map(n => ({ id: n.id, originalStartBeat: n.start_beat, originalPitch: n.pitch }));
|
notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
|
||||||
setDraggedNote({
|
const cloneOffsets = clones.map(n => ({ id: n.id, originalStartBeat: n.start_beat, originalPitch: n.pitch }));
|
||||||
mode: 'move',
|
setDraggedNote({
|
||||||
idx: -1,
|
mode: 'move',
|
||||||
startOffsetBeat: beat,
|
idx: -1,
|
||||||
startOffsetPitch: pitch,
|
startOffsetBeat: beat,
|
||||||
selectedNotesOffset: cloneOffsets
|
startOffsetPitch: pitch,
|
||||||
});
|
selectedNotesOffset: cloneOffsets
|
||||||
|
});
|
||||||
|
showToast('Đã nhân bản ' + clones.length + ' nốt!', 'info');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -6589,7 +6612,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
// Click on empty space with pen tool: DRAW a new note (brush mode with visitedPitches)
|
// Click on empty space with pen tool: DRAW a new note (brush mode with visitedPitches)
|
||||||
pushToUndo(notes);
|
pushToUndo(notes);
|
||||||
const start = getSnapBeat(beat, snapValue);
|
const start = getSnapBeat(beat, snapValue);
|
||||||
const initialDur = getSnapDuration(snapValue);
|
const initialDur = lastNoteDurationRef.current || getSnapDuration(snapValue);
|
||||||
const noteId = 'note_' + Date.now() + Math.random().toString(36).substr(2, 5);
|
const noteId = 'note_' + Date.now() + Math.random().toString(36).substr(2, 5);
|
||||||
const newNote = {
|
const newNote = {
|
||||||
id: noteId,
|
id: noteId,
|
||||||
@@ -6874,6 +6897,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGridMouseUp = () => {
|
const handleGridMouseUp = () => {
|
||||||
|
if (draggedNote && draggedNote.mode === 'draw') {
|
||||||
|
const dn = draggedNote;
|
||||||
|
const brushIds = dn.brushIds || [];
|
||||||
|
const lastBrushId = brushIds.length > 0 ? brushIds[brushIds.length - 1] : dn.drawNoteId;
|
||||||
|
if (lastBrushId) {
|
||||||
|
const lastNote = notes.find(n => n.id === lastBrushId);
|
||||||
|
if (lastNote) lastNoteDurationRef.current = lastNote.duration_beats;
|
||||||
|
}
|
||||||
|
}
|
||||||
setDraggedNote(null);
|
setDraggedNote(null);
|
||||||
setSelectionMarquee(null);
|
setSelectionMarquee(null);
|
||||||
rightClickDragRef.current = { active: false, startX: 0, startY: 0 };
|
rightClickDragRef.current = { active: false, startX: 0, startY: 0 };
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user