fix: brush pitch-based scroll + CC velocity per-note paint
This commit is contained in:
+39
-35
@@ -5240,25 +5240,27 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const container = gridScrollRef.current;
|
const container = gridScrollRef.current;
|
||||||
if (container) {
|
if (container) {
|
||||||
const cr = container.getBoundingClientRect();
|
const cr = container.getBoundingClientRect();
|
||||||
const edgeThreshold = 30;
|
const visTop = container.scrollTop;
|
||||||
const scrollStep = 6;
|
const visBot = visTop + container.clientHeight;
|
||||||
if (e.clientY < cr.top + edgeThreshold) {
|
const pitchPixel = (127 - snappedPitch) * NoteHeight;
|
||||||
|
const pitchThreshold = NoteHeight * 6;
|
||||||
|
if (pitchPixel < visTop + pitchThreshold) {
|
||||||
if (!brushAutoScrollRef.current || brushAutoScrollRef.current.direction !== 'up') {
|
if (!brushAutoScrollRef.current || brushAutoScrollRef.current.direction !== 'up') {
|
||||||
if (brushAutoScrollRef.current) clearInterval(brushAutoScrollRef.current.id);
|
if (brushAutoScrollRef.current) clearInterval(brushAutoScrollRef.current.id);
|
||||||
brushAutoScrollRef.current = {
|
brushAutoScrollRef.current = {
|
||||||
direction: 'up',
|
direction: 'up',
|
||||||
id: setInterval(() => {
|
id: setInterval(() => {
|
||||||
if (gridScrollRef.current) gridScrollRef.current.scrollTop = Math.max(0, gridScrollRef.current.scrollTop - scrollStep);
|
if (gridScrollRef.current) gridScrollRef.current.scrollTop = Math.max(0, gridScrollRef.current.scrollTop - NoteHeight * 2);
|
||||||
}, 30)
|
}, 30)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (e.clientY > cr.bottom - edgeThreshold) {
|
} else if (pitchPixel + NoteHeight > visBot - pitchThreshold) {
|
||||||
if (!brushAutoScrollRef.current || brushAutoScrollRef.current.direction !== 'down') {
|
if (!brushAutoScrollRef.current || brushAutoScrollRef.current.direction !== 'down') {
|
||||||
if (brushAutoScrollRef.current) clearInterval(brushAutoScrollRef.current.id);
|
if (brushAutoScrollRef.current) clearInterval(brushAutoScrollRef.current.id);
|
||||||
brushAutoScrollRef.current = {
|
brushAutoScrollRef.current = {
|
||||||
direction: 'down',
|
direction: 'down',
|
||||||
id: setInterval(() => {
|
id: setInterval(() => {
|
||||||
if (gridScrollRef.current) gridScrollRef.current.scrollTop = Math.min(gridScrollRef.current.scrollHeight - gridScrollRef.current.clientHeight, gridScrollRef.current.scrollTop + scrollStep);
|
if (gridScrollRef.current) gridScrollRef.current.scrollTop = Math.min(gridScrollRef.current.scrollHeight - gridScrollRef.current.clientHeight, gridScrollRef.current.scrollTop + NoteHeight * 2);
|
||||||
}, 30)
|
}, 30)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -5399,22 +5401,25 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
|
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
if (selectedNoteIds.length > 0) {
|
if (selectedNoteIds.length > 0) {
|
||||||
const updates = {};
|
let cursorNoteIdx = notes.findIndex(n => beat >= n.start_beat && beat <= n.start_beat + n.duration_beats);
|
||||||
selectedNoteIds.forEach(id => {
|
if (cursorNoteIdx === -1) {
|
||||||
const idx = notes.findIndex(n => n.id === id);
|
let minDist = Infinity;
|
||||||
if (idx !== -1) updates[idx] = val;
|
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; }
|
||||||
});
|
});
|
||||||
const idxs = Object.keys(updates).map(Number);
|
}
|
||||||
if (idxs.length > 0) {
|
if (cursorNoteIdx !== -1 && selectedNoteIds.includes(notes[cursorNoteIdx] ? notes[cursorNoteIdx].id : -1)) {
|
||||||
setNotes(prev => prev.map((n, i) => {
|
setNotes(prev => prev.map((n, i) => {
|
||||||
if (i in updates) {
|
if (i !== cursorNoteIdx) return n;
|
||||||
if (ccMode === 'pan') return { ...n, pan: (updates[i] - 0.5) * 2.0 };
|
if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
|
||||||
return { ...n, velocity: updates[i] };
|
return { ...n, velocity: val };
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}));
|
}));
|
||||||
|
ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: [cursorNoteIdx] };
|
||||||
|
} else {
|
||||||
|
ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: [] };
|
||||||
}
|
}
|
||||||
ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: selectedNoteIds.map(id => notes.findIndex(n => n.id === id)).filter(i => i !== -1) };
|
|
||||||
} else {
|
} else {
|
||||||
if (noteIdx !== -1) paintNote(noteIdx, val);
|
if (noteIdx !== -1) paintNote(noteIdx, val);
|
||||||
ccDragRef.current = { active: true, lastBeat: beat, lastPainted: noteIdx !== -1 ? [noteIdx] : [] };
|
ccDragRef.current = { active: true, lastBeat: beat, lastPainted: noteIdx !== -1 ? [noteIdx] : [] };
|
||||||
@@ -5440,27 +5445,26 @@ 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) {
|
||||||
const updates = {};
|
let cursorNoteIdx = notes.findIndex(n => beat >= n.start_beat && beat <= n.start_beat + n.duration_beats);
|
||||||
selectedNoteIds.forEach(id => {
|
if (cursorNoteIdx === -1) {
|
||||||
const idx = notes.findIndex(n => n.id === id);
|
let minDist = Infinity;
|
||||||
if (idx !== -1 && !painted.includes(idx)) {
|
notes.forEach((n, idx) => {
|
||||||
updates[idx] = val;
|
const center = n.start_beat + n.duration_beats / 2;
|
||||||
}
|
const dist = Math.abs(center - beat);
|
||||||
|
if (dist < minDist) { minDist = dist; cursorNoteIdx = idx; }
|
||||||
});
|
});
|
||||||
const idxs = Object.keys(updates).map(Number);
|
}
|
||||||
if (idxs.length > 0) {
|
if (cursorNoteIdx !== -1) {
|
||||||
|
const cursorNote = notes[cursorNoteIdx];
|
||||||
|
if (cursorNote && selectedNoteIds.includes(cursorNote.id) && !painted.includes(cursorNoteIdx)) {
|
||||||
setNotes(prev => prev.map((n, i) => {
|
setNotes(prev => prev.map((n, i) => {
|
||||||
if (i in updates) {
|
if (i !== cursorNoteIdx) return n;
|
||||||
if (ccMode === 'pan') return { ...n, pan: (updates[i] - 0.5) * 2.0 };
|
if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
|
||||||
return { ...n, velocity: updates[i] };
|
return { ...n, velocity: val };
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}));
|
}));
|
||||||
|
drag.lastPainted = [...painted, cursorNoteIdx];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const newPainted = selectedNoteIds
|
|
||||||
.map(id => notes.findIndex(n => n.id === id))
|
|
||||||
.filter(i => i !== -1 && !painted.includes(i));
|
|
||||||
ccDragRef.current.lastPainted = [...painted, ...newPainted];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -22,6 +22,12 @@
|
|||||||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Thêm ref `brushAutoScrollRef` cho setInterval auto-scroll. Batch `setNotes` trong cả `handleCCMouseDown` và `handleCCMouseMove` để tránh flicker.
|
- **Ghi chú/Test (nếu có):** `npm run build` pass. Thêm ref `brushAutoScrollRef` cho setInterval auto-scroll. Batch `setNotes` trong cả `handleCCMouseDown` và `handleCCMouseMove` để tránh flicker.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### [2026-07-26 10:12] Task: Fix brush pitch-based scroll + CC velocity per-note paint
|
||||||
|
- **Tóm tắt thay đổi:** (1) Brush auto-scroll: chuyển từ scroll theo viewport edge (clientY) sang pitch-based — scroll khi note mới được vẽ ở pitch gần rìa visible area (6 note threshold). Dùng `NoteHeight * 2` scroll step, setInterval 30ms. (2) CC velocity selected mode: thay vì paint all selected notes cùng lúc, paint từng note một theo vị trí chuột — tìm note gần cursor beat nhất, chỉ paint note đó nếu đang được selected.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` pass. Thay `lastPainted` trong selectedMode từ array all indices thành array single-node index.
|
||||||
|
---
|
||||||
|
|
||||||
### [2026-07-25 07:25] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin
|
### [2026-07-25 07:25] Task: Fix auto-scroll + maxDuration tab isolation + 1-bar margin
|
||||||
- **Tóm tắt thay đổi:** (1) `maxDuration` dùng `activeTracks` + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng `updateActiveTracks`. (5) Stretched clip handler thêm auto-scroll.
|
- **Tóm tắt thay đổi:** (1) `maxDuration` dùng `activeTracks` + 4-bar buffer. (2) Cách ly MAIN vs SECTION-TAB. (3) Clip/section/MIDI drag/stretch/resize clamp 1-bar from right. (4) Clip drag + stretched clip dùng `updateActiveTracks`. (5) Stretched clip handler thêm auto-scroll.
|
||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
|
|||||||
Reference in New Issue
Block a user