fix: CC note detection with Y-aware stem matching
This commit is contained in:
+28
-54
@@ -5381,6 +5381,30 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const rightClickDragRef = React.useRef({ active: false, startX: 0, startY: 0 });
|
const rightClickDragRef = React.useRef({ active: false, startX: 0, startY: 0 });
|
||||||
const swallowContextMenuRef = React.useRef(false);
|
const swallowContextMenuRef = React.useRef(false);
|
||||||
|
|
||||||
|
const findCCNoteIndex = (b, mouseY, ccH) => {
|
||||||
|
const snapped = getSnapBeat(b, snapVal);
|
||||||
|
const hits = [];
|
||||||
|
notes.forEach((n, idx) => {
|
||||||
|
if (snapped >= n.start_beat && snapped <= n.start_beat + n.duration_beats) {
|
||||||
|
const nv = ccMode === 'pan' ? ((n.pan || 0) * 0.5 + 0.5) : (n.velocity !== undefined ? n.velocity : 0.8);
|
||||||
|
const stemTop = ccH - (nv * (ccH - 20) + 10);
|
||||||
|
hits.push({ idx, dist: Math.abs(stemTop - mouseY) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hits.length > 0) {
|
||||||
|
hits.sort((a, b) => a.dist - b.dist);
|
||||||
|
return hits[0].idx;
|
||||||
|
}
|
||||||
|
let nearest = -1;
|
||||||
|
let minDist = Infinity;
|
||||||
|
notes.forEach((n, idx) => {
|
||||||
|
const center = n.start_beat + n.duration_beats / 2;
|
||||||
|
const d = Math.abs(center - snapped);
|
||||||
|
if (d < minDist) { minDist = d; nearest = idx; }
|
||||||
|
});
|
||||||
|
return nearest;
|
||||||
|
};
|
||||||
|
|
||||||
const handleCCMouseDown = (e) => {
|
const handleCCMouseDown = (e) => {
|
||||||
const canvas = ccCanvasRef.current;
|
const canvas = ccCanvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
@@ -5390,33 +5414,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const h = rect.height;
|
const h = rect.height;
|
||||||
const beat = x / pixelsPerBeat;
|
const beat = x / pixelsPerBeat;
|
||||||
|
|
||||||
const snappedBeat = getSnapBeat(beat, snapVal);
|
const noteIdx = findCCNoteIndex(beat, y, h);
|
||||||
let noteIdx = notes.findIndex(n => snappedBeat >= n.start_beat && snappedBeat <= n.start_beat + n.duration_beats);
|
|
||||||
if (noteIdx === -1) {
|
|
||||||
let minDistance = Infinity;
|
|
||||||
notes.forEach((n, idx) => {
|
|
||||||
const center = n.start_beat + n.duration_beats / 2;
|
|
||||||
const dist = Math.abs(center - snappedBeat);
|
|
||||||
if (dist < minDistance) {
|
|
||||||
minDistance = dist;
|
|
||||||
noteIdx = idx;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const val = Math.max(0, Math.min(1, (h - y) / h));
|
const val = Math.max(0, Math.min(1, (h - y) / h));
|
||||||
|
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
if (selectedNoteIds.length > 0) {
|
if (selectedNoteIds.length > 0) {
|
||||||
let cursorNoteIdx = notes.findIndex(n => beat >= n.start_beat && beat <= n.start_beat + n.duration_beats);
|
const cursorNoteIdx = findCCNoteIndex(beat, y, h);
|
||||||
if (cursorNoteIdx === -1) {
|
|
||||||
let minDist = Infinity;
|
|
||||||
notes.forEach((n, idx) => {
|
|
||||||
const center = n.start_beat + n.duration_beats / 2;
|
|
||||||
const dist = Math.abs(center - beat);
|
|
||||||
if (dist < minDist) { minDist = dist; cursorNoteIdx = idx; }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (cursorNoteIdx !== -1 && selectedNoteIds.includes(notes[cursorNoteIdx] ? notes[cursorNoteIdx].id : -1)) {
|
if (cursorNoteIdx !== -1 && selectedNoteIds.includes(notes[cursorNoteIdx] ? notes[cursorNoteIdx].id : -1)) {
|
||||||
const currentNote = notes[cursorNoteIdx];
|
const currentNote = notes[cursorNoteIdx];
|
||||||
const currentVal = ccMode === 'pan' ? ((currentNote.pan || 0) / 2.0 + 0.5) : (currentNote.velocity !== undefined ? currentNote.velocity : 0.8);
|
const currentVal = ccMode === 'pan' ? ((currentNote.pan || 0) / 2.0 + 0.5) : (currentNote.velocity !== undefined ? currentNote.velocity : 0.8);
|
||||||
@@ -5464,15 +5467,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const painted = drag.lastPainted || [];
|
const painted = drag.lastPainted || [];
|
||||||
|
|
||||||
if (drag.selectedMode && selectedNoteIds.length > 0) {
|
if (drag.selectedMode && selectedNoteIds.length > 0) {
|
||||||
let cursorNoteIdx = notes.findIndex(n => beat >= n.start_beat && beat <= n.start_beat + n.duration_beats);
|
const cursorNoteIdx = findCCNoteIndex(beat, y, h);
|
||||||
if (cursorNoteIdx === -1) {
|
|
||||||
let minDist = Infinity;
|
|
||||||
notes.forEach((n, idx) => {
|
|
||||||
const center = n.start_beat + n.duration_beats / 2;
|
|
||||||
const dist = Math.abs(center - beat);
|
|
||||||
if (dist < minDist) { minDist = dist; cursorNoteIdx = idx; }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (cursorNoteIdx !== -1) {
|
if (cursorNoteIdx !== -1) {
|
||||||
const cursorNote = notes[cursorNoteIdx];
|
const cursorNote = notes[cursorNoteIdx];
|
||||||
if (cursorNote && selectedNoteIds.includes(cursorNote.id) && !painted.includes(cursorNoteIdx)) {
|
if (cursorNote && selectedNoteIds.includes(cursorNote.id) && !painted.includes(cursorNoteIdx)) {
|
||||||
@@ -5490,9 +5485,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const snappedBeat = getSnapBeat(beat, snapVal);
|
const candidateIdx = findCCNoteIndex(beat, y, h);
|
||||||
const candidateIdx = notes.findIndex(n => snappedBeat >= n.start_beat && snappedBeat <= n.start_beat + n.duration_beats);
|
|
||||||
|
|
||||||
if (candidateIdx !== -1 && !painted.includes(candidateIdx)) {
|
if (candidateIdx !== -1 && !painted.includes(candidateIdx)) {
|
||||||
const currentVal = ccMode === 'pan' ? ((notes[candidateIdx].pan || 0) / 2.0 + 0.5) : (notes[candidateIdx].velocity !== undefined ? notes[candidateIdx].velocity : 0.8);
|
const currentVal = ccMode === 'pan' ? ((notes[candidateIdx].pan || 0) / 2.0 + 0.5) : (notes[candidateIdx].velocity !== undefined ? notes[candidateIdx].velocity : 0.8);
|
||||||
if (Math.abs(currentVal - val) > 0.001) {
|
if (Math.abs(currentVal - val) > 0.001) {
|
||||||
@@ -5503,25 +5496,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
drag.lastPainted = [...painted, candidateIdx];
|
drag.lastPainted = [...painted, candidateIdx];
|
||||||
} else if (candidateIdx === -1) {
|
|
||||||
let nearest = -1;
|
|
||||||
let minDist = Infinity;
|
|
||||||
notes.forEach((n, idx) => {
|
|
||||||
const center = n.start_beat + n.duration_beats / 2;
|
|
||||||
const d = Math.abs(center - snappedBeat);
|
|
||||||
if (d < minDist) { minDist = d; nearest = idx; }
|
|
||||||
});
|
|
||||||
if (nearest !== -1 && !painted.includes(nearest)) {
|
|
||||||
const currentVal = ccMode === 'pan' ? ((notes[nearest].pan || 0) / 2.0 + 0.5) : (notes[nearest].velocity !== undefined ? notes[nearest].velocity : 0.8);
|
|
||||||
if (Math.abs(currentVal - val) > 0.001) {
|
|
||||||
setNotes(prev => prev.map((n, i) => {
|
|
||||||
if (i !== nearest) return n;
|
|
||||||
if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
|
|
||||||
return { ...n, velocity: val };
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
drag.lastPainted = [...painted, nearest];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -210,3 +210,8 @@
|
|||||||
- **Tóm tắt thay đổi:** Thay `!visited.includes(snappedPitch)` bằng `snappedPitch !== lastDrawnPitch`. Bug cũ: khi vẽ C→D→C, visited.includes(C) = true → không tạo note mới → note D cũ bị extend. Fix: so sánh với pitch của note cuối cùng, cho phép vẽ tiếp khi pitch thay đổi dù đã visit trước đó. Bỏ `visitedPitches` (không còn dùng).
|
- **Tóm tắt thay đổi:** Thay `!visited.includes(snappedPitch)` bằng `snappedPitch !== lastDrawnPitch`. Bug cũ: khi vẽ C→D→C, visited.includes(C) = true → không tạo note mới → note D cũ bị extend. Fix: so sánh với pitch của note cuối cùng, cho phép vẽ tiếp khi pitch thay đổi dù đã visit trước đó. Bỏ `visitedPitches` (không còn dùng).
|
||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||||||
|
|
||||||
|
### [2026-07-26 10:58] Task: Fix CC note detection with Y-aware stem matching
|
||||||
|
- **Tóm tắt thay đổi:** Thêm `findCCNoteIndex(beat, mouseY, ccH)` helper — tìm note trong CC lane dựa trên cả X (beat) và Y (vị trí stem top). Khi nhiều note cùng beat (chord), so sánh `mouseY` với stem top của từng note, chọn note có stem gần nhất. Dùng helper này thay thế tất cả `notes.findIndex` và nearest-fallback trong `handleCCMouseDown` và `handleCCMouseMove`. Fix lỗi không detect được note khi velocity gần nhau hoặc cùng beat.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||||||
|
|||||||
Reference in New Issue
Block a user