fix(cc11): replace auto-select (khong accumulate) + prefer selected note khi click lane (chord) + bump cache-buster v-param

This commit is contained in:
2026-08-26 12:01:21 +07:00
parent 74dccaf54a
commit b63e5b21d6
3 changed files with 23 additions and 11 deletions
+16 -6
View File
@@ -10509,6 +10509,16 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
});
return best;
};
const findCC11TargetNote = (b) => {
// Chord: prefer note dang duoc chon duoi click de khong ve/anchor
// nham cho note khac cung beat
const cur = notesRef.current;
for (let i = 0; i < cur.length; i++) {
const n = cur[i];
if (selectedNoteIds.includes(n.id) && b >= n.start_beat && b <= n.start_beat + (n.duration_beats || 1)) return i;
}
return findCC11NoteAtBeat(b);
};
const handleCC11MouseDown = (e) => {
const canvas = ccCanvasRef.current;
if (!canvas) return;
@@ -10517,14 +10527,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const y = e.clientY - rect.top;
const h = rect.height;
const beat = x / pixelsPerBeat - renderBeatOffset;
const noteIdx = findCC11NoteAtBeat(beat);
const noteIdx = findCC11TargetNote(beat);
if (noteIdx === -1) { ccDragRef.current = null; return; }
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.id && !selectedNoteIds.includes(note.id)) {
setSelectedNoteIds(prev => [...prev, note.id]);
// Auto-select note duoc click: REPLACE selection (khong accumulate —
// truoc day [...prev, id] cong don nhieu note -> lane ve graph cua note khac)
if (note.id) {
setSelectedNoteIds([note.id]);
}
const dur = note.duration_beats || 1;
const ratio = Math.max(0, Math.min(1, (beat - note.start_beat) / dur));
@@ -10568,7 +10578,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const y = e.clientY - rect.top;
const h = rect.height;
const beat = x / pixelsPerBeat - renderBeatOffset;
const noteIdx = findCC11NoteAtBeat(beat);
const noteIdx = findCC11TargetNote(beat);
if (noteIdx === -1) return;
const note = notes[noteIdx];
const dur = note.duration_beats || 1;
+6 -4
View File
@@ -790,14 +790,16 @@ const prKeyStateRef=React.useRef({shift:false,ctrl:false});const prMouseInRef=Re
// user 08:25: ctrl+draw qua vị trí → vẽ lại TẤT CẢ velocity cùng vị trí
// (trước đây findCCNoteIndex chỉ trả 1 note → chord khó draw).
const findCCNoteIndicesAtBeat=b=>{const snapped=getSnapBeat(b,snapValue);const out=[];notes.forEach((n,idx)=>{if(Math.abs(n.start_beat-snapped)<0.01)out.push(idx);});return out;};// ── CC11 EXPRESSION CURVE (piano roll lane) ──
const getCC11Curve=n=>Array.isArray(n&&n.expression_curve)?n.expression_curve:[];const cc11ValueAt=(curve,ratio)=>{if(!curve||!curve.length)return 127;if(curve.length===1)return curve[0].value;const pts=curve.slice().sort((a,b)=>a.position_ratio-b.position_ratio);if(ratio<=pts[0].position_ratio)return pts[0].value;if(ratio>=pts[pts.length-1].position_ratio)return pts[pts.length-1].value;for(let i=1;i<pts.length;i++){if(ratio<=pts[i].position_ratio){const a=pts[i-1],b=pts[i];const t=(ratio-a.position_ratio)/(b.position_ratio-a.position_ratio||1);return Math.round(a.value+(b.value-a.value)*t);}}return pts[pts.length-1].value;};const findCC11NoteAtBeat=b=>{const snapped=getSnapBeat(b,snapValue);let best=-1,bestDist=Infinity;notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+(n.duration_beats||1)){const d=Math.min(Math.abs(snapped-n.start_beat),Math.abs(snapped-(n.start_beat+(n.duration_beats||1))));if(d<bestDist){bestDist=d;best=idx;}}});return best;};const handleCC11MouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCC11NoteAtBeat(beat);if(noteIdx===-1){ccDragRef.current=null;return;}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.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)
const getCC11Curve=n=>Array.isArray(n&&n.expression_curve)?n.expression_curve:[];const cc11ValueAt=(curve,ratio)=>{if(!curve||!curve.length)return 127;if(curve.length===1)return curve[0].value;const pts=curve.slice().sort((a,b)=>a.position_ratio-b.position_ratio);if(ratio<=pts[0].position_ratio)return pts[0].value;if(ratio>=pts[pts.length-1].position_ratio)return pts[pts.length-1].value;for(let i=1;i<pts.length;i++){if(ratio<=pts[i].position_ratio){const a=pts[i-1],b=pts[i];const t=(ratio-a.position_ratio)/(b.position_ratio-a.position_ratio||1);return Math.round(a.value+(b.value-a.value)*t);}}return pts[pts.length-1].value;};const findCC11NoteAtBeat=b=>{const snapped=getSnapBeat(b,snapValue);let best=-1,bestDist=Infinity;notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+(n.duration_beats||1)){const d=Math.min(Math.abs(snapped-n.start_beat),Math.abs(snapped-(n.start_beat+(n.duration_beats||1))));if(d<bestDist){bestDist=d;best=idx;}}});return best;};const findCC11TargetNote=b=>{// Chord: prefer note dang duoc chon duoi click de khong ve/anchor
// nham cho note khac cung beat
const cur=notesRef.current;for(let i=0;i<cur.length;i++){const n=cur[i];if(selectedNoteIds.includes(n.id)&&b>=n.start_beat&&b<=n.start_beat+(n.duration_beats||1))return i;}return findCC11NoteAtBeat(b);};const handleCC11MouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCC11TargetNote(beat);if(noteIdx===-1){ccDragRef.current=null;return;}const note=notesRef.current[noteIdx];if(!note)return;// Auto-select note duoc click: REPLACE selection (khong accumulate —
// truoc day [...prev, id] cong don nhieu note -> lane ve graph cua note khac)
if(note.id){setSelectedNoteIds([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);let anchorIdx=-1;curve.forEach((p,i)=>{const ax=x0+p.position_ratio*(x1-x0);const ay=yOf(p.value);if(Math.hypot(ax-x,ay-y)<=8)anchorIdx=i;});if(anchorIdx===-1){const dup=curve.findIndex(p=>Math.abs(p.position_ratio-ratio)<1e-6);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{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};}// 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;const canvas=ccCanvasRef.current;if(!canvas)return;e.preventDefault();const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCC11NoteAtBeat(beat);if(noteIdx===-1)return;const note=notes[noteIdx];const dur=note.duration_beats||1;const x0=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const x1=x0+dur*pixelsPerBeat;const yOf=v=>h-(v/127*(h-20)+10);const curve=getCC11Curve(note).slice();let delIdx=-1,delDist=Infinity;curve.forEach((p,i)=>{const ax=x0+p.position_ratio*(x1-x0);const ay=yOf(p.value);const d=Math.hypot(ax-x,ay-y);if(d<delDist){delDist=d;delIdx=i;}});if(delIdx===-1||delDist>8)return;// Node dau/cuoi co dinh (graph thread) — khong cho xoa
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;const canvas=ccCanvasRef.current;if(!canvas)return;e.preventDefault();const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCC11TargetNote(beat);if(noteIdx===-1)return;const note=notes[noteIdx];const dur=note.duration_beats||1;const x0=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const x1=x0+dur*pixelsPerBeat;const yOf=v=>h-(v/127*(h-20)+10);const curve=getCC11Curve(note).slice();let delIdx=-1,delDist=Infinity;curve.forEach((p,i)=>{const ax=x0+p.position_ratio*(x1-x0);const ay=yOf(p.value);const d=Math.hypot(ax-x,ay-y);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;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCCNoteIndex(beat,y,h);const val=Math.max(0,Math.min(1,(h-y)/h));if(e.ctrlKey){if(selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1&&selectedNoteIds.includes(notes[cursorNoteIdx]?notes[cursorNoteIdx].id:-1)){const currentNote=notes[cursorNoteIdx];const currentVal=ccMode==='pan'?(currentNote.pan||0)/2.0+0.5:currentNote.velocity!==undefined?currentNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[cursorNoteIdx]};}else{ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[]};}}else{// Không chọn notes: vẽ TẤT CẢ notes cùng beat (chord — user 08:25)
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
<script src="/static/js/app.precompiled.js?v=202608250700" defer></script>
<script src="/static/js/app.precompiled.js?v=202608270700" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {