fix: đã sửa lỗi bị mất không hiển thị midi piano grid
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# Piano Roll: 4 tính năng
|
||||
|
||||
## 1. Auto-scroll brush khi drag gần cạnh
|
||||
|
||||
**File**: `app/static/js/app.jsx`
|
||||
|
||||
**Vị trí**: Trong `handleGridMouseMove`, cuối block `draggedNote.mode === 'draw'` (trước `return;` ở dòng ~5237).
|
||||
|
||||
**Code thêm** (sau visitedPitches/brushIds logic, trước `return;`):
|
||||
```js
|
||||
const container = gridScrollRef.current;
|
||||
if (container) {
|
||||
const cr = container.getBoundingClientRect();
|
||||
const edgeThreshold = 30;
|
||||
const scrollStep = 6;
|
||||
if (e.clientY < cr.top + edgeThreshold) {
|
||||
container.scrollTop = Math.max(0, container.scrollTop - scrollStep);
|
||||
} else if (e.clientY > cr.bottom - edgeThreshold) {
|
||||
container.scrollTop = Math.min(container.scrollHeight - container.clientHeight, container.scrollTop + scrollStep);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠ Edge case**: Nếu chuột dừng tại mép, `mousemove` ngưng → cuộn dừng. Để cuộn liên tục, dùng `setInterval` khi vào threshold. Tạm thời chấp nhập giới hạn này.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ctrl+drag velocity với selected notes
|
||||
|
||||
**File**: `app/static/js/app.jsx`
|
||||
|
||||
### 2a. `handleCCMouseDown` (dòng ~5360)
|
||||
Thay block `if (e.ctrlKey)` hiện tại:
|
||||
|
||||
```js
|
||||
if (e.ctrlKey) {
|
||||
if (selectedNoteIds.length > 0) {
|
||||
selectedNoteIds.forEach(id => {
|
||||
const idx = notes.findIndex(n => n.id === id);
|
||||
if (idx !== -1) paintNote(idx, val);
|
||||
});
|
||||
ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: selectedNoteIds.map(id => notes.findIndex(n => n.id === id)).filter(i => i !== -1) };
|
||||
} else {
|
||||
if (noteIdx !== -1) paintNote(noteIdx, val);
|
||||
ccDragRef.current = { active: true, lastBeat: beat, lastPainted: noteIdx !== -1 ? [noteIdx] : [] };
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 2b. `handleCCMouseMove` (sau dòng ~5381)
|
||||
Thêm block đầu `handleCCMouseMove` (SAU khi lấy `drag`, `painted`, TRƯỚC `candidateIdx`):
|
||||
|
||||
```js
|
||||
if (drag.selectedMode && selectedNoteIds.length > 0) {
|
||||
selectedNoteIds.forEach(id => {
|
||||
const idx = notes.findIndex(n => n.id === id);
|
||||
if (idx !== -1 && !painted.includes(idx)) {
|
||||
setNotes(prev => prev.map((n, i) => {
|
||||
if (i !== idx) return n;
|
||||
if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
|
||||
return { ...n, velocity: val };
|
||||
}));
|
||||
}
|
||||
});
|
||||
// Cập nhật lastPainted TRỰC TIẾP trên ref (không qua setNotes callback)
|
||||
const newPainted = selectedNoteIds
|
||||
.map(id => notes.findIndex(n => n.id === id))
|
||||
.filter(i => i !== -1 && !painted.includes(i));
|
||||
ccDragRef.current.lastPainted = [...painted, ...newPainted];
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
⚠ **Không gán `drag.lastPainted` bên trong `setNotes` callback** — `drag` là `ccDragRef.current`, gán trực tiếp vào ref ngoài callback để tránh stale closure.
|
||||
|
||||
### 2b2. Cleanup `selectedMode` khi mouseup (dòng ~5409)
|
||||
Trong `handleCCMouseUp` (và `onMouseLeave`), thêm reset:
|
||||
```js
|
||||
if (ccDragRef.current) ccDragRef.current.selectedMode = false;
|
||||
```
|
||||
|
||||
### 2c. CC canvas rendering (dòng ~4911-4932)
|
||||
Thêm `isSelected` vào loop notes; đổi màu xanh dương `#3b82f6` khi selected:
|
||||
|
||||
```js
|
||||
const isSelected = selectedNoteIds.includes(note.id);
|
||||
// ...
|
||||
ctx.strokeStyle = ccMode === 'pan' ? (isSelected ? '#60a5fa' : '#a78bfa') : (isSelected ? '#3b82f6' : '#fbbf24');
|
||||
ctx.fillStyle = ccMode === 'pan' ? (isSelected ? '#3b82f6' : '#c084fc') : (isSelected ? '#3b82f6' : '#fbbf24');
|
||||
```
|
||||
|
||||
Thêm `selectedNoteIds` vào dependency array của effect.
|
||||
|
||||
---
|
||||
|
||||
## 3. SNAP trong MIDI tab
|
||||
|
||||
**Ghi chú**: `getSnapBeat(beat, mode)` đã xử lý `mode === 'free'` bằng cách return `beat` không đổi. Không cần check `snapVal !== 'free'` riêng.
|
||||
|
||||
**File**: `app/static/js/app.jsx`
|
||||
|
||||
### 3a. Selection marquee — create (dòng ~5003-5008)
|
||||
Snap `startBeat` khi tạo marquee:
|
||||
|
||||
```js
|
||||
const snapStart = getSnapBeat(beat, snapVal);
|
||||
setSelectionMarquee({
|
||||
startBeat: snapStart, startPitch: pitch,
|
||||
currentBeat: snapStart, currentPitch: pitch
|
||||
});
|
||||
```
|
||||
|
||||
### 3a2. Selection marquee — drag update (dòng ~5140-5144)
|
||||
Snap `currentBeat` khi kéo marquee:
|
||||
|
||||
```js
|
||||
const snappedBeat = getSnapBeat(beat, snapVal);
|
||||
const marquee = {
|
||||
...selectionMarquee,
|
||||
currentBeat: snappedBeat,
|
||||
currentPitch: pitch
|
||||
};
|
||||
```
|
||||
|
||||
### 3b. Ruler drag loop range (dòng ~5742-5763)
|
||||
Snap `clickBeat` khởi tạo, snap `beat` trong onMove:
|
||||
|
||||
```js
|
||||
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
|
||||
const startData = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
|
||||
// ...
|
||||
const rawBeat = Math.max(0, bx / pixelsPerBeat);
|
||||
const beat = getSnapBeat(rawBeat, snapVal);
|
||||
```
|
||||
|
||||
### 3c. Shift+Click ruler loop (dòng ~5732-5739)
|
||||
Đổi `Math.round(clickBeat / 4) * 4` thành `getSnapBeat(clickBeat, snapVal)`:
|
||||
|
||||
```js
|
||||
const beatSnap = getSnapBeat(clickBeat, snapVal);
|
||||
```
|
||||
|
||||
### 3d. Ruler loop handles (dòng ~5791, ~5808, ~5829)
|
||||
Đổi `Math.round(bx / pixelsPerBeat / 4) * 4` thành `getSnapBeat(bx / pixelsPerBeat, snapVal)` ở cả 3 handle (left resize, right resize, grab body).
|
||||
|
||||
---
|
||||
|
||||
## 4. Synth button trong MIDI tab toolbar
|
||||
|
||||
**File**: `app/static/js/app.jsx`
|
||||
|
||||
### 4a. Prop `onInstrumentSelect` (dòng ~4546)
|
||||
Thêm `onInstrumentSelect` vào props destructuring.
|
||||
|
||||
### 4b. Button synth trong toolbar (dòng ~5662-5700)
|
||||
Chèn button sau MIDI input select, trước transport buttons:
|
||||
|
||||
```jsx
|
||||
React.createElement("button", {
|
||||
onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId),
|
||||
title: st.instrumentName || "Synth",
|
||||
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
|
||||
}, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth'))
|
||||
```
|
||||
|
||||
### 4c. Pass callback từ App (dòng ~15631-15652)
|
||||
Thêm `onInstrumentSelect: (trackId) => { openInstrumentSelector(trackId); }`.
|
||||
|
||||
### 4d. Đồng bộ subTab instrument (dòng ~6343-6353)
|
||||
Trong `setTrackInstrumentWithProgram`, thêm cập nhật `subTabs`:
|
||||
|
||||
```js
|
||||
setSubTabs(prev => prev.map(s => {
|
||||
if (s.trackId !== trackId) return s;
|
||||
return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Layout fix: thêm `h-full` (QUAN TRỌNG)
|
||||
|
||||
**File**: `app/static/js/app.jsx`, dòng ~5623
|
||||
|
||||
Đổi `className` của outer div từ:
|
||||
```
|
||||
"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col"
|
||||
```
|
||||
thành:
|
||||
```
|
||||
"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
|
||||
```
|
||||
|
||||
**Lý do**: `h-full` cung cấp height tham chiếu cho flex chain, tránh content area cao 0px.
|
||||
|
||||
---
|
||||
|
||||
## Thứ tự thực hiện
|
||||
|
||||
1. Sửa layout: thêm `h-full`
|
||||
2. Feature 3: SNAP (4 edits nhỏ — dễ verify)
|
||||
3. Feature 2: Velocity selected notes
|
||||
4. Feature 1: Auto-scroll brush
|
||||
5. Feature 4: Synth button (liên quan nhiều component nhất)
|
||||
|
||||
## Kiểm tra
|
||||
|
||||
```bash
|
||||
cd /home/locpham/SonicForgeStudio && npm run build
|
||||
```
|
||||
Build phải pass. Nếu lỗi paren, kiểm tra đóng `()` tại cuối return statement.
|
||||
Reference in New Issue
Block a user