FIX: sửa lỗi vẽ grid sai trong PIANO ROLL GRID với snap

This commit is contained in:
2026-07-30 22:14:56 +07:00
parent ca34a496eb
commit 0894f97386
2 changed files with 65 additions and 65 deletions
+47 -47
View File
@@ -3148,7 +3148,7 @@ const SubTabWaveform = ({
const yOff = (y - cTop) / cHeight;
return yOff <= 1 / 3 ? Math.max(0, Math.min(3, 3 * (1 - yOff * 3))) : Math.max(-30, Math.min(0, -30 * (yOff - 1 / 3) * (3 / 2)));
};
const snapVal = v => isPan ? Math.round(v * 20) / 20 : Math.round(v * 2) / 2;
const snapValue = v => isPan ? Math.round(v * 20) / 20 : Math.round(v * 2) / 2;
const snapTime = t => Math.round(t * 10) / 10;
// Check if clicking near existing node (any mode)
@@ -3171,7 +3171,7 @@ const SubTabWaveform = ({
const handleMouseMove = moveEvent => {
const currentX = moveEvent.clientX - rect.left + scrollLeft;
const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
const val = snapVal(valFromY(moveEvent.clientY - rect.top));
const val = snapValue(valFromY(moveEvent.clientY - rect.top));
const updated = isPan ? {
time: Math.min(snapTime(ct), wallDuration),
pan: val
@@ -3203,7 +3203,7 @@ const SubTabWaveform = ({
const currentX = moveEvent.clientX - rect.left + scrollLeft;
const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
const t = +snapTime(ct).toFixed(3);
const v = isPan ? +snapVal(valFromY(moveEvent.clientY - rect.top)).toFixed(2) : +snapVal(valFromY(moveEvent.clientY - rect.top)).toFixed(1);
const v = isPan ? +snapValue(valFromY(moveEvent.clientY - rect.top)).toFixed(2) : +snapValue(valFromY(moveEvent.clientY - rect.top)).toFixed(1);
const key = t + '|' + v;
if (key !== lastKey) {
pts.push({
@@ -3225,10 +3225,10 @@ const SubTabWaveform = ({
} else {
const newNode = isPan ? {
time: +snapTime(startTime).toFixed(3),
pan: +snapVal(valFromY(e.clientY - rect.top)).toFixed(2)
pan: +snapValue(valFromY(e.clientY - rect.top)).toFixed(2)
} : {
time: +snapTime(startTime).toFixed(3),
db: +snapVal(valFromY(e.clientY - rect.top)).toFixed(1)
db: +snapValue(valFromY(e.clientY - rect.top)).toFixed(1)
};
setSelectedNodeTime(newNode.time);
const merged = mergeNodes(curNodes, [newNode]);
@@ -3240,7 +3240,7 @@ const SubTabWaveform = ({
const handleMouseMove = moveEvent => {
const currentX = moveEvent.clientX - rect.left + scrollLeft;
const ct = Math.max(0, Math.min(wallDuration, currentX / zoom));
const val = snapVal(valFromY(moveEvent.clientY - rect.top));
const val = snapValue(valFromY(moveEvent.clientY - rect.top));
const updated = isPan ? {
time: Math.min(snapTime(ct), wallDuration),
pan: val
@@ -5792,9 +5792,8 @@ Ví dụ: Nhạc phim epic, dàn nhạc giao hưởng, tempo 130 BPM, giọng Cm
}, "Đóng"))));
};
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead }) => {
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead, snapValue, onSnapChange }) => {
const [activeRollTool, setActiveRollTool] = React.useState('select');
const [snapVal, setSnapVal] = React.useState('1');
const [renderTick, setRenderTick] = React.useState(0);
const [ccMode, setCcMode] = React.useState('velocity');
const [rollZoom, setRollZoom] = React.useState(60); // local horizontal zoom factor
@@ -5908,11 +5907,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
setSelectedNoteIds([]);
};
const totalBeats = sessionSyncMode ? Math.max(rollBeats, sessionLengthBars * 4 + 16, 64) : Math.max(rollBeats, noteMaxBeat + 16, 64);
const drawWidth = totalBeats * pixelsPerBeat;
const rawTotalBeats = Math.max(rollBeats, noteMaxBeat + 16, 64);
const drawWidth = rawTotalBeats * pixelsPerBeat;
const [rollViewWidth, setRollViewWidth] = React.useState(800);
const viewWidth = Math.max(drawWidth, rollViewWidth);
const viewBeats = Math.ceil(viewWidth / pixelsPerBeat) + 4;
const totalBeats = Math.max(rawTotalBeats, viewBeats + 32);
const [notes, setNotes] = React.useState(st.notes || []);
const notesRef = React.useRef(notes);
@@ -6147,17 +6147,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Draw snap lines
let snapBeats = 0.25;
if (snapVal === '1') snapBeats = 1.0;
else if (snapVal === '1/2') snapBeats = 0.5;
else if (snapVal === '1/4') snapBeats = 0.25;
else if (snapVal === '1/8') snapBeats = 0.125;
else if (snapVal === '1/16') snapBeats = 0.0625;
else if (snapVal === '4') snapBeats = 4.0;
else if (snapVal === '1/32') snapBeats = 0.03125;
if (snapValue === '1') snapBeats = 1.0;
else if (snapValue === '1/2') snapBeats = 0.5;
else if (snapValue === '1/4') snapBeats = 0.25;
else if (snapValue === '1/8') snapBeats = 0.125;
else if (snapValue === '1/16') snapBeats = 0.0625;
else if (snapValue === '4') snapBeats = 4.0;
else if (snapValue === '1/32') snapBeats = 0.03125;
for (let beat = 0; beat <= viewBeats; beat += snapBeats) {
const x = beat * pixelsPerBeat;
if (x > drawWidth) break;
if (x > viewWidth) break;
const isBar = beat % timeSigNum === 0;
ctx.strokeStyle = isBar ? '#444450' : '#2d2d35';
ctx.lineWidth = isBar ? 1.2 : 0.6;
@@ -6175,9 +6175,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.fillStyle = layer.track_color || '#888';
ctx.strokeStyle = layer.track_color || '#888';
layer.notes.forEach(function(note) {
var snapStart = snapVal !== 'free' ? getSnapBeat(note.relative_start_beat, snapVal) : note.relative_start_beat;
var snapStart = snapValue !== 'free' ? getSnapBeat(note.relative_start_beat, snapValue) : note.relative_start_beat;
var rawEnd = note.relative_start_beat + note.duration_beats;
var snapEnd = snapVal !== 'free' ? getSnapBeat(rawEnd, snapVal) : rawEnd;
var snapEnd = snapValue !== 'free' ? getSnapBeat(rawEnd, snapValue) : rawEnd;
var x = (renderBeatOffset + snapStart) * pixelsPerBeat;
var y = (127 - note.pitch) * NoteHeight;
var w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
@@ -6190,9 +6190,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Layer 3: Active notes with velocity layer representation
notes.forEach((note) => {
const snapStart = snapVal !== 'free' ? getSnapBeat(note.start_beat, snapVal) : note.start_beat;
const snapStart = snapValue !== 'free' ? getSnapBeat(note.start_beat, snapValue) : note.start_beat;
const rawEnd = note.start_beat + note.duration_beats;
const snapEnd = snapVal !== 'free' ? getSnapBeat(rawEnd, snapVal) : rawEnd;
const snapEnd = snapValue !== 'free' ? getSnapBeat(rawEnd, snapValue) : rawEnd;
const x = (renderBeatOffset + snapStart) * pixelsPerBeat;
const y = (127 - note.pitch) * NoteHeight;
const w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
@@ -6215,9 +6215,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Draw real-time recording notes
if (recordingState === 'RECORDING' && recTempMidiNotes && recTempMidiNotes.length > 0) {
recTempMidiNotes.forEach(note => {
const snapStart = snapVal !== 'free' ? getSnapBeat(note.start_beat, snapVal) : note.start_beat;
const snapStart = snapValue !== 'free' ? getSnapBeat(note.start_beat, snapValue) : note.start_beat;
const rawEnd = note.start_beat + (note.duration_beats || 0.25);
const snapEnd = snapVal !== 'free' ? getSnapBeat(rawEnd, snapVal) : rawEnd;
const snapEnd = snapValue !== 'free' ? getSnapBeat(rawEnd, snapValue) : rawEnd;
const x = (renderBeatOffset + snapStart) * pixelsPerBeat;
const y = (127 - note.pitch) * NoteHeight;
const w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
@@ -6266,7 +6266,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.stroke();
}
}
}, [notes, snapVal, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers, renderBeatOffset, renderTick]);
}, [notes, snapValue, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers, renderBeatOffset, renderTick]);
React.useLayoutEffect(() => {
const canvas = ccCanvasRef.current;
@@ -6406,7 +6406,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
} else {
// Ctrl+click on empty space: start selection marquee
setSelectedNoteIds([]);
const snapStart = getSnapBeat(beat, snapVal);
const snapStart = getSnapBeat(beat, snapValue);
setSelectionMarquee({
startBeat: snapStart, startPitch: pitch,
currentBeat: snapStart, currentPitch: pitch
@@ -6500,8 +6500,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
} else {
// Click on empty space with pen tool: DRAW a new note (brush mode with visitedPitches)
pushToUndo(notes);
const start = getSnapBeat(beat, snapVal);
const initialDur = getSnapDuration(snapVal);
const start = getSnapBeat(beat, snapValue);
const initialDur = getSnapDuration(snapValue);
const noteId = 'note_' + Date.now() + Math.random().toString(36).substr(2, 5);
const newNote = {
id: noteId,
@@ -6555,7 +6555,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const pitch = 127 - Math.floor(y / NoteHeight);
if (selectionMarquee) {
const snappedBeat = getSnapBeat(beat, snapVal);
const snappedBeat = getSnapBeat(beat, snapValue);
const marquee = {
...selectionMarquee,
currentBeat: snappedBeat,
@@ -6624,7 +6624,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const lastPitch = draggedNote.lastDrawnPitch !== undefined ? draggedNote.lastDrawnPitch : draggedNote.startOffsetPitch;
const pitchChanged = snappedPitch !== lastPitch;
const noteBeats = draggedNote.noteStartBeats || [];
const defaultDur = getSnapDuration(snapVal);
const defaultDur = getSnapDuration(snapValue);
function playDrawPreview(p, durMs) {
stopPreviewNote();
@@ -6730,7 +6730,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
return;
}
if (draggedNote.mode === 'scale') {
const newEnd = getSnapBeat(Math.max(draggedNote.firstStart + 0.125, beat), snapVal);
const newEnd = getSnapBeat(Math.max(draggedNote.firstStart + 0.125, beat), snapValue);
const range = draggedNote.originalEnd - draggedNote.firstStart;
if (range <= 0) return;
const scaleFactor = Math.max(0.01, (newEnd - draggedNote.firstStart) / range);
@@ -6754,7 +6754,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
return;
}
if (draggedNote.mode === 'resize') {
const newDuration = getSnapBeat(Math.max(0.125, beat - draggedNote.originalStart), snapVal);
const newDuration = getSnapBeat(Math.max(0.125, beat - draggedNote.originalStart), snapValue);
setNotes(prev => prev.map((n, idx) => {
if (idx !== draggedNote.idx) return n;
return {
@@ -6767,7 +6767,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (!firstOffset) return;
const firstNote = notes.find(n => n.id === firstOffset.id);
if (!firstNote) return;
const deltaBeat = getSnapBeat(beat - draggedNote.startOffsetBeat, snapVal) - firstOffset.originalStartBeat;
const deltaBeat = getSnapBeat(beat - draggedNote.startOffsetBeat, snapValue) - firstOffset.originalStartBeat;
// Clamp so no note goes past beat 0
const minOrigStart = Math.min(...draggedNote.selectedNotesOffset.map(o => o.originalStartBeat));
const clampedDeltaBeat = minOrigStart + deltaBeat < 0 ? -minOrigStart : deltaBeat;
@@ -6778,7 +6778,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (!offset) return n;
return {
...n,
start_beat: getSnapBeat(Math.max(0, offset.originalStartBeat + clampedDeltaBeat), snapVal),
start_beat: getSnapBeat(Math.max(0, offset.originalStartBeat + clampedDeltaBeat), snapValue),
pitch: Math.max(0, Math.min(127, offset.originalPitch + deltaPitch))
};
}));
@@ -6814,7 +6814,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const swallowContextMenuRef = React.useRef(false);
const findCCNoteIndex = (b, mouseY, ccH) => {
const snapped = getSnapBeat(b, snapVal);
const snapped = getSnapBeat(b, snapValue);
const hits = [];
notes.forEach((n, idx) => {
if (snapped >= n.start_beat && snapped <= n.start_beat + n.duration_beats) {
@@ -7199,8 +7199,8 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
}, React.createElement("span", {
className: "text-zinc-500 font-semibold"
}, "Snap:"), React.createElement("select", {
value: snapVal,
onChange: e => { setSnapVal(e.target.value); setRenderTick(t => t + 1); },
value: snapValue,
onChange: e => { onSnapChange(e.target.value); setRenderTick(t => t + 1); },
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
}, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => React.createElement("option", {
key: v,
@@ -7335,7 +7335,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
return;
}
if (e.shiftKey) {
const beatSnap = getSnapBeat(clickBeat, snapVal);
const beatSnap = getSnapBeat(clickBeat, snapValue);
if (loopStartBeat === null) {
setLoopStartBeat(Math.max(0, beatSnap - 4));
setLoopEndBeat(Math.max(4, beatSnap));
@@ -7351,7 +7351,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
}
}
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
const snappedStartBeat = getSnapBeat(clickBeat, snapValue);
rulerDragRef.current = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
const onMove = (ev) => {
const r = rulerScrollRef.current;
@@ -7359,7 +7359,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + rulerDragRef.current.scrollLeft;
const rawBeat = Math.max(0, bx / pixelsPerBeat);
const beat = getSnapBeat(rawBeat, snapVal);
const beat = getSnapBeat(rawBeat, snapValue);
if (Math.abs(ev.clientX - rulerDragRef.current.startX) > 5) {
if (clickInRange) {
const rangeWidth = loopEndBeat - loopStartBeat;
@@ -7410,7 +7410,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
if (!r) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + r.scrollLeft;
const nBeat = Math.max(0, Math.min(loopEndBeat - 1, getSnapBeat(bx / pixelsPerBeat, snapVal)));
const nBeat = Math.max(0, Math.min(loopEndBeat - 1, getSnapBeat(bx / pixelsPerBeat, snapValue)));
setLoopStartBeat(nBeat);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: nBeat * beatSec } : s));
};
@@ -7427,7 +7427,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
if (!r) return;
const rRect = r.getBoundingClientRect();
const bx = ev.clientX - rRect.left + r.scrollLeft;
const nBeat = Math.max(loopStartBeat + 1, getSnapBeat(bx / pixelsPerBeat, snapVal));
const nBeat = Math.max(loopStartBeat + 1, getSnapBeat(bx / pixelsPerBeat, snapValue));
setLoopEndBeat(nBeat);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionEnd: nBeat * beatSec } : s));
};
@@ -9059,13 +9059,13 @@ const App = () => {
}
};
const [activeTool, setActiveTool] = useState('select'); // 'select' | 'grab' | 'razor'
const [snapValue, setSnapValue] = useState('1'); // 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32'
const [snapValue, onSnapChangeue] = useState('1'); // 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32'
const snapTime = (time, snapVal, bpmVal) => {
if (snapVal === 'free') return time;
const snapTime = (time, snapValue, bpmVal) => {
if (snapValue === 'free') return time;
const beatDuration = 60 / parseFloat(bpmVal || 120);
let divisor = 1;
if (snapVal === '4') divisor = 4; else if (snapVal === '1') divisor = 1; else if (snapVal === '1/2') divisor = 0.5; else if (snapVal === '1/4') divisor = 0.25; else if (snapVal === '1/8') divisor = 0.125; else if (snapVal === '1/16') divisor = 0.0625; else if (snapVal === '1/32') divisor = 0.03125;
if (snapValue === '4') divisor = 4; else if (snapValue === '1') divisor = 1; else if (snapValue === '1/2') divisor = 0.5; else if (snapValue === '1/4') divisor = 0.25; else if (snapValue === '1/8') divisor = 0.125; else if (snapValue === '1/16') divisor = 0.0625; else if (snapValue === '1/32') divisor = 0.03125;
const gridSpacing = beatDuration * divisor;
return Math.round(time / gridSpacing) * gridSpacing;
};
@@ -18967,7 +18967,7 @@ const App = () => {
className: "text-[14px] text-zinc-500 font-bold uppercase ml-2"
}, "Snap"), /*#__PURE__*/React.createElement("select", {
value: snapValue,
onChange: e => setSnapValue(e.target.value),
onChange: e => onSnapChangeue(e.target.value),
className: "bg-black text-white text-[14px] px-1.5 py-0.5 rounded border border-zinc-700 focus:outline-none focus:border-cyan-500 font-mono cursor-pointer"
}, /*#__PURE__*/React.createElement("option", {
value: "free"
File diff suppressed because one or more lines are too long