fix: đã sửa lỗi bị mất không hiển thị midi piano grid

This commit is contained in:
2026-07-26 10:05:23 +07:00
parent a765998455
commit a7417b9bfa
4 changed files with 533 additions and 305 deletions
@@ -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``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.
+70 -55
View File
@@ -5657,112 +5657,117 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
return labels;
};
const beatSec = 60.0 / (parseInt(bpm) || 120);
const beatSec = 60.0 / (parseInt(bpm) || 120);
const playHeadX = (st.currentTime || 0) / beatSec * pixelsPerBeat;
return React.createElement("div", {
className: "flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
}, React.createElement("div", {
},
/* 1. TOOLBAR HEADER */
React.createElement("div", {
className: "h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
className: "flex items-center gap-4"
}, /*#__PURE__*/React.createElement("span", {
}, React.createElement("span", {
className: "text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"
}, /*#__PURE__*/React.createElement("i", {
}, React.createElement("i", {
"data-lucide": "music",
className: "w-3.5 h-3.5"
}), st.label), /*#__PURE__*/React.createElement("div", {
}), st.label), React.createElement("div", {
className: "flex items-center gap-1 text-xs"
}, /*#__PURE__*/React.createElement("span", {
}, React.createElement("span", {
className: "text-zinc-500 font-semibold"
}, "Snap to Scale"), /*#__PURE__*/React.createElement("button", {
}, "Snap to Scale"), React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)),
className: `w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'bg-yellow-600' : 'bg-zinc-700'}`,
style: { padding: 0 }
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}`
}))), /*#__PURE__*/React.createElement("div", {
}))), React.createElement("div", {
className: "flex items-center gap-1 text-xs"
}, /*#__PURE__*/React.createElement("span", {
}, React.createElement("span", {
className: "text-zinc-500 font-semibold"
}, "Snap:"), /*#__PURE__*/React.createElement("select", {
}, "Snap:"), React.createElement("select", {
value: snapVal,
onChange: e => setSnapVal(e.target.value),
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 => /*#__PURE__*/React.createElement("option", {
}, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => React.createElement("option", {
key: v,
value: v
}, v)))), /*#__PURE__*/React.createElement("button", {
}, v)))), React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isArmed: !s.isArmed } : s)),
className: `px-2 py-1 rounded text-xs font-bold ${st.isArmed ? 'bg-red-600 text-white' : 'bg-zinc-800 text-zinc-400'}`
}, "ARM"), /*#__PURE__*/React.createElement("select", {
}, "ARM"), React.createElement("select", {
value: selectedMidiInputId || '',
onChange: e => onMidiInputSelect(e.target.value),
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"
}, /*#__PURE__*/React.createElement("option", { value: "" }, "Input"), /*#__PURE__*/React.createElement("option", { value: "ALL" }, "Omni"), (midiDevices || []).map(d => /*#__PURE__*/React.createElement("option", { key: d.id, value: d.id }, d.name || d.id))), /*#__PURE__*/React.createElement("button", {
}, React.createElement("option", { value: "" }, "Input"), React.createElement("option", { value: "ALL" }, "Omni"), (midiDevices || []).map(d => React.createElement("option", { key: d.id, value: d.id }, d.name || d.id))), 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'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), /*#__PURE__*/React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth')), /*#__PURE__*/React.createElement("div", {
}, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth')), React.createElement("div", {
className: "flex items-center gap-0.5 ml-1"
}, /*#__PURE__*/React.createElement("button", {
}, React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.max(0, (st.currentTime || 0) - beatSec * 4) } : s)),
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
title: "Back 1 bar"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "step-back", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
}, React.createElement("i", { "data-lucide": "step-back", className: "w-3 h-3" })), React.createElement("button", {
onClick: onPlayPause,
className: `w-6 h-6 flex items-center justify-center rounded border ${isPlaying ? 'bg-emerald-600 text-black' : 'bg-cyan-600 text-white'} border-cyan-500`,
title: isPlaying ? "Pause" : "Play"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": isPlaying ? "pause" : "play", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
}, React.createElement("i", { "data-lucide": isPlaying ? "pause" : "play", className: "w-3 h-3 fill-current" })), React.createElement("button", {
onClick: onStop,
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
title: "Stop"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "square", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
}, React.createElement("i", { "data-lucide": "square", className: "w-3 h-3 fill-current" })), React.createElement("button", {
onClick: onRecord,
className: `w-6 h-6 flex items-center justify-center rounded border ${recordingState === 'RECORDING' ? 'bg-red-600 text-white border-red-500 animate-pulse' : 'bg-zinc-800 text-red-500 border-zinc-700'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "circle", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
}, React.createElement("i", { "data-lucide": "circle", className: "w-3 h-3 fill-current" })), React.createElement("button", {
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: (st.currentTime || 0) + beatSec * 4 } : s)),
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
title: "Forward 1 bar"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "step-forward", className: "w-3 h-3" }))), /*#__PURE__*/React.createElement("div", {
}, React.createElement("i", { "data-lucide": "step-forward", className: "w-3 h-3" }))), React.createElement("div", {
className: "flex items-center gap-1 ml-1 text-xs"
}, /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "AI:"), /*#__PURE__*/React.createElement("input", {
}, React.createElement("span", { className: "text-zinc-500" }, "AI:"), React.createElement("input", {
type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0),
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
}), /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "-"), /*#__PURE__*/React.createElement("input", {
}), React.createElement("span", { className: "text-zinc-500" }, "-"), React.createElement("input", {
type: "number", value: aiBarEnd, onChange: e => setAiBarEnd(parseInt(e.target.value) || 1),
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
}), /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "bar"), /*#__PURE__*/React.createElement("button", {
}), React.createElement("span", { className: "text-zinc-500" }, "bar"), React.createElement("button", {
onClick: () => {
setIsLooping(!isLooping);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isLooping: !(s.isLooping || false) } : s));
},
className: `px-2 py-0.5 rounded text-xs ${isLooping ? 'bg-emerald-700 text-white border border-emerald-500' : 'bg-zinc-800 text-zinc-400'}`
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "repeat", className: "w-3 h-3" }))), /*#__PURE__*/React.createElement("div", {
}, React.createElement("i", { "data-lucide": "repeat", className: "w-3 h-3" }))), React.createElement("div", {
className: "flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"
}, ['velocity', 'pan'].map(mode => /*#__PURE__*/React.createElement("button", {
}, ['velocity', 'pan'].map(mode => React.createElement("button", {
key: mode,
onClick: () => setCcMode(mode),
className: `px-2.5 py-1 rounded capitalize ${ccMode === mode ? 'bg-purple-900/60 text-purple-300 font-bold border border-purple-700' : 'text-zinc-400 hover:text-zinc-200'}`
}, mode)))), /*#__PURE__*/React.createElement("button", {
}, mode)))), React.createElement("button", {
onClick: () => setShowCC(!showCC),
className: `px-2 py-1 rounded text-xs ${showCC ? 'bg-purple-900/60 text-purple-300 border border-purple-700' : 'text-zinc-500 hover:text-zinc-300'}`
}, ccMode === 'pan' ? 'Pan' : 'Vel'), /*#__PURE__*/React.createElement("div", {
}, ccMode === 'pan' ? 'Pan' : 'Vel'), React.createElement("div", {
className: "flex items-center gap-1"
}, /*#__PURE__*/React.createElement("button", {
}, React.createElement("button", {
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), /*#__PURE__*/React.createElement("button", {
}, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), React.createElement("button", {
onClick: onClose,
className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"
}, /*#__PURE__*/React.createElement("i", {
}, React.createElement("i", {
"data-lucide": "x",
className: "w-3 h-3"
}), "Đóng"))), /*#__PURE__*/React.createElement("div", {
}), "Đóng"))),
/* 2. BAR RULER (ĐÃ SỬA LẠI ĐÓNG NGOẶC ĐÚNG TẠI ĐÂY) */
React.createElement("div", {
className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
}), /*#__PURE__*/React.createElement("div", {
}), React.createElement("div", {
ref: rulerScrollRef,
className: "flex-1 overflow-hidden",
onMouseDown: (e) => {
@@ -5811,20 +5816,20 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
style: {
width: `${viewWidth}px`,
height: '100%'
},
className: "relative h-full font-mono text-[9px] text-zinc-500 font-bold"
}, renderBarLabels(), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
}, renderBarLabels(), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/15 border-l border-r border-emerald-400"
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
onMouseDown: (e) => {
e.stopPropagation();
@@ -5842,7 +5847,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}), /*#__PURE__*/React.createElement("div", {
}), React.createElement("div", {
style: { position: 'absolute', right: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
onMouseDown: (e) => {
e.stopPropagation();
@@ -5859,7 +5864,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
}), /*#__PURE__*/React.createElement("div", {
}), React.createElement("div", {
style: { position: 'absolute', left: '4px', right: '4px', top: 0, bottom: 0, cursor: 'grab' },
onMouseDown: (e) => {
e.stopPropagation();
@@ -5882,9 +5887,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
})))), /*#__PURE__*/React.createElement("div", {
}))))),
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + CANVAS) */
React.createElement("div", {
className: "flex-1 flex overflow-hidden min-h-0 relative"
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
ref: keybedRef,
onScroll: handleKeybedScroll,
className: "w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",
@@ -5892,17 +5900,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}
}, renderKeybed()), /*#__PURE__*/React.createElement("div", {
}, renderKeybed()), React.createElement("div", {
ref: gridScrollRef,
onScroll: handleScroll,
className: "flex-1 overflow-auto bg-[#141414] min-w-0"
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
style: {
width: `${viewWidth}px`,
height: `${(128 - PITCH_START) * NoteHeight}px`
},
className: "relative"
}, /*#__PURE__*/React.createElement("canvas", {
}, React.createElement("canvas", {
ref: canvasRef,
onMouseDown: handleGridMouseDown,
onMouseMove: handleGridMouseMove,
@@ -5910,17 +5918,20 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
onMouseLeave: handleGridMouseUp,
onContextMenu: handleContextMenu,
className: "absolute inset-0 cursor-crosshair"
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
})))), showCC && /*#__PURE__*/React.createElement("div", {
})))),
/* 4. CC / VELOCITY LANE (ĐẶT Ở ĐÁY) */
showCC && React.createElement("div", {
style: { height: `${ccHeight}px` },
className: "bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
onMouseDown: e => {
e.preventDefault();
const startY = e.clientY;
@@ -5931,32 +5942,36 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
document.addEventListener('mouseup', onUp);
},
className: "absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"
}), /*#__PURE__*/React.createElement("div", {
}), React.createElement("div", {
className: "w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"
}, ccMode.toUpperCase()), /*#__PURE__*/React.createElement("div", {
}, ccMode.toUpperCase()), React.createElement("div", {
ref: ccWrapperRef,
className: "flex-1 overflow-x-hidden min-w-0"
}, /*#__PURE__*/React.createElement("div", {
}, React.createElement("div", {
style: {
width: `${viewWidth}px`,
height: '100%'
},
className: "relative"
}, /*#__PURE__*/React.createElement("canvas", {
}, React.createElement("canvas", {
ref: ccCanvasRef,
onMouseDown: handleCCMouseDown,
onMouseMove: handleCCMouseMove,
onMouseUp: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
onMouseLeave: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
className: "absolute inset-0"
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
style: {
left: `${loopStartBeat * pixelsPerBeat}px`,
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
top: 0, bottom: 0
},
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
})))), scaleMenuPos && renderScaleContextMenu());
})))),
/* 5. OVERLAY / CONTEXT MENU */
scaleMenuPos && renderScaleContextMenu()
);
};
const serializeTracksList = (tracksList, secondsPerBar) => {
File diff suppressed because one or more lines are too long
Binary file not shown.