feat(cc11): lane header CC dropdown + graph thread (2 fixed end nodes, preview-draw drag fix flicker)
This commit is contained in:
+126
-53
@@ -9631,6 +9631,66 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
const [showCC, setShowCC] = React.useState(true);
|
||||
const [ccHeight, setCcHeight] = React.useState(80);
|
||||
|
||||
// Ve CC11 lane (graph thread). Tach rieng de drag ve preview truc tiep
|
||||
// (truoc day setNotes tung mousemove + doc notes tu closure stale -> node
|
||||
// cu/moi xen ke -> flicker). previewNoteId != null: chi ve note do voi
|
||||
// previewCurve thay cho expression_curve da commit.
|
||||
const paintCC11Lane = (previewNoteId, previewCurve) => {
|
||||
const canvas = ccCanvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const h = ccHeight;
|
||||
canvas.width = viewWidth * dpr;
|
||||
canvas.height = h * dpr;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.fillStyle = '#161616';
|
||||
ctx.fillRect(0, 0, viewWidth, h);
|
||||
ctx.strokeStyle = '#252525';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h / 2);
|
||||
ctx.lineTo(viewWidth, h / 2);
|
||||
ctx.stroke();
|
||||
const yOf = v => h - ((v / 127) * (h - 20) + 10);
|
||||
notes.forEach((note) => {
|
||||
if (previewNoteId != null) {
|
||||
if (note.id !== previewNoteId) return;
|
||||
} else if (!selectedNoteIds.includes(note.id)) {
|
||||
return;
|
||||
}
|
||||
const x0 = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
|
||||
const x1 = (renderBeatOffset + note.start_beat + (note.duration_beats || 1)) * pixelsPerBeat;
|
||||
const curve = (previewNoteId != null && previewCurve ? previewCurve : (Array.isArray(note.expression_curve) ? note.expression_curve : [])).slice().sort((a, b) => a.position_ratio - b.position_ratio);
|
||||
ctx.strokeStyle = '#34d399';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
if (!curve.length) {
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.moveTo(x0, yOf(127));
|
||||
ctx.lineTo(x1, yOf(127));
|
||||
ctx.setLineDash([]);
|
||||
} else if (curve.length === 1) {
|
||||
ctx.moveTo(x0, yOf(curve[0].value));
|
||||
ctx.lineTo(x1, yOf(curve[0].value));
|
||||
} else {
|
||||
curve.forEach((p, i) => {
|
||||
const x = x0 + p.position_ratio * (x1 - x0);
|
||||
const y = yOf(p.value);
|
||||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
});
|
||||
}
|
||||
ctx.stroke();
|
||||
curve.forEach((p) => {
|
||||
const x = x0 + p.position_ratio * (x1 - x0);
|
||||
ctx.fillStyle = '#a7f3d0';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, yOf(p.value), 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const canvas = ccCanvasRef.current;
|
||||
if (!canvas) return;
|
||||
@@ -9652,41 +9712,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
ctx.stroke();
|
||||
|
||||
if (ccMode === 'cc11') {
|
||||
// CC11 expression: curve anchors {position_ratio 0..1, value 0..127}
|
||||
// ve cho note dang chon. Rong -> flat 127 (khong automation).
|
||||
const yOf = v => h - ((v / 127) * (h - 20) + 10);
|
||||
notes.forEach((note) => {
|
||||
if (!selectedNoteIds.includes(note.id)) return;
|
||||
const x0 = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
|
||||
const x1 = (renderBeatOffset + note.start_beat + (note.duration_beats || 1)) * pixelsPerBeat;
|
||||
const curve = (Array.isArray(note.expression_curve) ? note.expression_curve : []).slice().sort((a, b) => a.position_ratio - b.position_ratio);
|
||||
ctx.strokeStyle = '#34d399';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
if (!curve.length) {
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.moveTo(x0, yOf(127));
|
||||
ctx.lineTo(x1, yOf(127));
|
||||
ctx.setLineDash([]);
|
||||
} else if (curve.length === 1) {
|
||||
ctx.moveTo(x0, yOf(curve[0].value));
|
||||
ctx.lineTo(x1, yOf(curve[0].value));
|
||||
} else {
|
||||
curve.forEach((p, i) => {
|
||||
const x = x0 + p.position_ratio * (x1 - x0);
|
||||
const y = yOf(p.value);
|
||||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
});
|
||||
}
|
||||
ctx.stroke();
|
||||
curve.forEach((p) => {
|
||||
const x = x0 + p.position_ratio * (x1 - x0);
|
||||
ctx.fillStyle = '#a7f3d0';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, yOf(p.value), 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
});
|
||||
// CC11 expression: graph thread cho note dang chon; khi dang drag ve
|
||||
// preview tu ccDragRef (khong phu thuoc state da commit).
|
||||
const drag = ccDragRef.current;
|
||||
if (drag && drag.mode === 'cc11' && drag.previewCurve) {
|
||||
paintCC11Lane(drag.noteId, drag.previewCurve);
|
||||
} else {
|
||||
paintCC11Lane();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
notes.forEach((note) => {
|
||||
const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
|
||||
@@ -10485,16 +10519,21 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
const beat = x / pixelsPerBeat - renderBeatOffset;
|
||||
const noteIdx = findCC11NoteAtBeat(beat);
|
||||
if (noteIdx === -1) { ccDragRef.current = null; return; }
|
||||
const note = notes[noteIdx];
|
||||
const note = notesRef.current[noteIdx];
|
||||
if (!note) return;
|
||||
// Auto-select note duoc click: draw branch chi ve curve cho note duoc chon
|
||||
// (selectedNoteIds) -> khong select -> anchor them nhung vo hinh (bug: khong ve duoc CC11)
|
||||
if (note && note.id && !selectedNoteIds.includes(note.id)) {
|
||||
if (note.id && !selectedNoteIds.includes(note.id)) {
|
||||
setSelectedNoteIds(prev => [...prev, note.id]);
|
||||
}
|
||||
const dur = note.duration_beats || 1;
|
||||
const ratio = Math.max(0, Math.min(1, (beat - note.start_beat) / dur));
|
||||
const val = Math.max(0, Math.min(127, Math.round(((h - y) / h) * 127)));
|
||||
const curve = getCC11Curve(note).slice();
|
||||
// Curve rong -> khoi tao 2 node co dinh dau/cuoi note (graph thread)
|
||||
if (!curve.length) {
|
||||
curve.push({ position_ratio: 0, value: 127 }, { position_ratio: 1, value: 127 });
|
||||
}
|
||||
const x0 = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
|
||||
const x1 = x0 + dur * pixelsPerBeat;
|
||||
const yOf = v => h - ((v / 127) * (h - 20) + 10);
|
||||
@@ -10509,12 +10548,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
if (dup !== -1) { curve[dup] = { position_ratio: ratio, value: val }; anchorIdx = dup; }
|
||||
else { curve.push({ position_ratio: ratio, value: val }); anchorIdx = curve.length - 1; }
|
||||
} else {
|
||||
curve[anchorIdx] = { position_ratio: ratio, value: val };
|
||||
const cur = curve[anchorIdx];
|
||||
// Node dau/cuoi co dinh: giu ratio 0/1, chi doi value
|
||||
curve[anchorIdx] = (cur.position_ratio <= 0.001 || cur.position_ratio >= 0.999) ? { position_ratio: cur.position_ratio, value: val } : { position_ratio: ratio, value: val };
|
||||
}
|
||||
const updatedNotes = notes.map((n, i) => (i === noteIdx ? { ...n, expression_curve: curve } : n));
|
||||
setNotes(updatedNotes);
|
||||
if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
|
||||
ccDragRef.current = { mode: 'cc11', active: true, noteIdx, anchorIdx, noteStart: note.start_beat, noteDur: dur };
|
||||
// Khong setNotes ngay (truoc day setNotes moi mousemove + doc notes tu
|
||||
// closure stale -> node cu/moi xen ke -> flicker): ve preview truc tiep,
|
||||
// commit vao notes chi khi mouseup/mouseleave.
|
||||
ccDragRef.current = { mode: 'cc11', active: true, noteIdx, noteId: note.id, anchorIdx, noteStart: note.start_beat, noteDur: dur, previewCurve: curve };
|
||||
paintCC11Lane(note.id, curve);
|
||||
};
|
||||
const handleCC11ContextMenu = (e) => {
|
||||
if (ccMode !== 'cc11') return;
|
||||
@@ -10542,11 +10584,26 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
if (d < delDist) { delDist = d; delIdx = i; }
|
||||
});
|
||||
if (delIdx === -1 || delDist > 8) return;
|
||||
// Node dau/cuoi co dinh (graph thread) — khong cho xoa
|
||||
if (curve[delIdx].position_ratio <= 0.001 || curve[delIdx].position_ratio >= 0.999) return;
|
||||
curve.splice(delIdx, 1);
|
||||
const updatedNotes = notes.map((n, i) => (i === noteIdx ? { ...n, expression_curve: curve } : n));
|
||||
setNotes(updatedNotes);
|
||||
if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
|
||||
};
|
||||
// Commit CC11 preview khi mouseup/mouseleave — truoc day drag setNotes moi
|
||||
// mousemove (flicker); gio chi cap nhat state 1 lan khi ket thuc drag.
|
||||
const commitCC11Drag = () => {
|
||||
const drag = ccDragRef.current;
|
||||
if (!drag) return;
|
||||
if (drag.mode === 'cc11' && drag.previewCurve) {
|
||||
const curNotes = notesRef.current;
|
||||
const updatedNotes = curNotes.map((n, i) => (i === drag.noteIdx ? { ...n, expression_curve: drag.previewCurve } : n));
|
||||
setNotes(updatedNotes);
|
||||
if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
|
||||
}
|
||||
ccDragRef.current = null;
|
||||
};
|
||||
const handleCCMouseDown = (e) => {
|
||||
if (ccMode === 'cc11') { handleCC11MouseDown(e); return; }
|
||||
const canvas = ccCanvasRef.current;
|
||||
@@ -10612,17 +10669,30 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
const y = e.clientY - rect.top;
|
||||
const h = rect.height;
|
||||
const beat = x / pixelsPerBeat - renderBeatOffset;
|
||||
const note = notes[drag.noteIdx];
|
||||
const note = notesRef.current[drag.noteIdx];
|
||||
if (!note) return;
|
||||
const dur = drag.noteDur || note.duration_beats || 1;
|
||||
const ratio = Math.max(0, Math.min(1, (beat - drag.noteStart) / dur));
|
||||
const val = Math.max(0, Math.min(127, Math.round(((h - y) / h) * 127)));
|
||||
const curve = getCC11Curve(note).slice();
|
||||
const curve = (drag.previewCurve || getCC11Curve(note)).slice();
|
||||
const idx = Math.min(drag.anchorIdx, curve.length - 1);
|
||||
curve[idx] = { position_ratio: ratio, value: val };
|
||||
const updatedNotes = notes.map((n, i) => (i === drag.noteIdx ? { ...n, expression_curve: curve } : n));
|
||||
setNotes(updatedNotes);
|
||||
if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
|
||||
const cur = curve[idx] || { position_ratio: ratio, value: val };
|
||||
if (cur.position_ratio <= 0.001 || cur.position_ratio >= 0.999) {
|
||||
// Node dau/cuoi co dinh: chi doi value, giu ratio 0/1
|
||||
curve[idx] = { position_ratio: cur.position_ratio, value: val };
|
||||
} else {
|
||||
// Node giua: doi ca ratio+value, clamp giua 2 node lan can
|
||||
let minR = 0, maxR = 1;
|
||||
for (let i = 0; i < curve.length; i++) {
|
||||
if (i === idx) continue;
|
||||
const r = curve[i].position_ratio;
|
||||
if (r < cur.position_ratio && r > minR) minR = r;
|
||||
if (r > cur.position_ratio && r < maxR) maxR = r;
|
||||
}
|
||||
curve[idx] = { position_ratio: Math.min(maxR, Math.max(minR, ratio)), value: val };
|
||||
}
|
||||
drag.previewCurve = curve;
|
||||
paintCC11Lane(drag.noteId, curve);
|
||||
return;
|
||||
}
|
||||
const canvas = ccCanvasRef.current;
|
||||
@@ -11657,9 +11727,12 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
||||
className: "absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"
|
||||
}), React.createElement("div", {
|
||||
className: "w-[120px] shrink-0"
|
||||
}), 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()), React.createElement("div", {
|
||||
}), React.createElement("select", {
|
||||
value: ccMode,
|
||||
onChange: e => setCcMode(e.target.value),
|
||||
title: "CC control",
|
||||
className: "w-[60px] bg-[#222222] border-r border-zinc-900 text-[10px] text-zinc-200 font-mono shrink-0 font-bold text-center cursor-pointer outline-none"
|
||||
}, React.createElement("option", { value: "velocity" }, "Vel"), React.createElement("option", { value: "sustain" }, "Sus"), React.createElement("option", { value: "modulation" }, "Mod"), React.createElement("option", { value: "pitch_bend" }, "Bend"), React.createElement("option", { value: "pan" }, "Pan"), React.createElement("option", { value: "cc11" }, "Expr")), React.createElement("div", {
|
||||
ref: ccWrapperRef,
|
||||
className: "flex-1 overflow-x-hidden min-w-0"
|
||||
}, React.createElement("div", {
|
||||
@@ -11673,8 +11746,8 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
||||
onMouseDown: handleCCMouseDown,
|
||||
onMouseMove: handleCCMouseMove,
|
||||
onContextMenu: handleCC11ContextMenu,
|
||||
onMouseUp: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
|
||||
onMouseLeave: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
|
||||
onMouseUp: commitCC11Drag,
|
||||
onMouseLeave: commitCC11Drag,
|
||||
className: "absolute inset-0"
|
||||
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
|
||||
style: {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user