From fd50d992501c570f2d17a47fd94f9e870397065a Mon Sep 17 00:00:00 2001 From: locphamtran Date: Wed, 26 Aug 2026 12:48:39 +0700 Subject: [PATCH] v11: fix CC11 lane sync - engine serve precompiled stale; click=add node, drag=move, right-click=remove; shared mode when no selection --- app/static/js/app.jsx | 60 ++++++++++++++++++++------------ app/static/js/app.precompiled.js | 25 +++++++------ app/templates/index.html | 2 +- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index d5c6e10..9e95413 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -9656,8 +9656,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos notes.forEach((note) => { if (previewNoteId != null) { if (note.id !== previewNoteId) return; - } else if (!selectedNoteIds.includes(note.id)) { - return; + } else if (selectedNoteIds.length > 0) { + // Co selection: chi ve curve cua note duoc chon + if (!selectedNoteIds.includes(note.id)) return; + } else { + // KHONG co selection: CC11 dung chung cho tat ca notes da ve curve + if (!(Array.isArray(note.expression_curve) && note.expression_curve.length > 0)) return; } const x0 = (renderBeatOffset + note.start_beat) * pixelsPerBeat; const x1 = (renderBeatOffset + note.start_beat + (note.duration_beats || 1)) * pixelsPerBeat; @@ -10519,6 +10523,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos } return findCC11NoteAtBeat(b); }; + const commitCC11Curve = (curve, shared, noteIdx) => { + const curNotes = notesRef.current; + const updatedNotes = shared + ? curNotes.map(n => ({ ...n, expression_curve: curve })) + : curNotes.map((n, i) => (i === noteIdx ? { ...n, expression_curve: curve } : n)); + setNotes(updatedNotes); + if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes); + }; const handleCC11MouseDown = (e) => { const canvas = ccCanvasRef.current; if (!canvas) return; @@ -10531,9 +10543,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos 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) { + // Co selection -> chi sua note do; neu click trung note khac -> chuyen + // selection sang note ay (REPLACE, khong accumulate). KHONG co selection + // -> shared mode (CC11 dung chung cho tat ca notes), khong auto-select. + const shared = selectedNoteIds.length === 0; + if (!shared && note.id && !selectedNoteIds.includes(note.id)) { setSelectedNoteIds([note.id]); } const dur = note.duration_beats || 1; @@ -10554,18 +10568,25 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos if (Math.hypot(ax - x, ay - y) <= 8) anchorIdx = i; }); if (anchorIdx === -1) { + // Click vao vi tri trong: THEM node tai do ngay lap tuc — khong keo + // node moi theo cursor (user: chi click de them node, drag node co san) 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 }; + let newCurve; + if (dup !== -1) { + newCurve = curve.slice(); + newCurve[dup] = { position_ratio: ratio, value: val }; + } else { + newCurve = curve.concat([{ position_ratio: ratio, value: val }]).sort((a, b) => a.position_ratio - b.position_ratio); + } + commitCC11Curve(newCurve, shared, noteIdx); + ccDragRef.current = null; + return; } - // 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 }; + 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: ve preview truc tiep, commit khi mouseup/mouseleave + ccDragRef.current = { mode: 'cc11', active: true, noteIdx, noteId: note.id, anchorIdx, noteStart: note.start_beat, noteDur: dur, previewCurve: curve, shared }; paintCC11Lane(note.id, curve); }; const handleCC11ContextMenu = (e) => { @@ -10597,9 +10618,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos // 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); + commitCC11Curve(curve, selectedNoteIds.length === 0, noteIdx); }; // Commit CC11 preview khi mouseup/mouseleave — truoc day drag setNotes moi // mousemove (flicker); gio chi cap nhat state 1 lan khi ket thuc drag. @@ -10607,10 +10626,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos 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); + commitCC11Curve(drag.previewCurve, drag.shared, drag.noteIdx); } ccDragRef.current = null; }; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 5a4899c..525d3a9 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -744,7 +744,9 @@ const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.u // (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;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);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();if(ccMode==='cc11'){// CC11 expression: graph thread cho note dang chon; khi dang drag ve +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.length>0){// Co selection: chi ve curve cua note duoc chon +if(!selectedNoteIds.includes(note.id))return;}else{// KHONG co selection: CC11 dung chung cho tat ca notes da ve curve +if(!(Array.isArray(note.expression_curve)&¬e.expression_curve.length>0))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;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);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();if(ccMode==='cc11'){// 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;const isSelected=selectedNoteIds.includes(note.id);let val=note.velocity!==undefined?note.velocity:0.8;if(ccMode==='pan'){val=(note.pan!==undefined?note.pan:0.0)*0.5+0.5;}const stemH=val*(h-20)+10;const y=h-stemH;ctx.strokeStyle=ccMode==='pan'?isSelected?'#60a5fa':'#a78bfa':isSelected?'#3b82f6':'#fbbf24';ctx.lineWidth=2.5;ctx.beginPath();ctx.moveTo(x,h);ctx.lineTo(x,y);ctx.stroke();ctx.fillStyle=ccMode==='pan'?isSelected?'#3b82f6':'#c084fc':isSelected?'#3b82f6':'#fbbf24';ctx.beginPath();ctx.arc(x,y,3.5,0,2*Math.PI);ctx.fill();});}},[notes,ccMode,rollZoom,viewWidth,selectedNoteIds,renderBeatOffset,ccHeight,showCC]);// Follow playhead: khi PLAY — playhead luôn ở GIỮA view, notes trôi sang // trái (scroll theo st.currentTime); khi STOP — scroll về đầu (playhead ở @@ -792,17 +794,18 @@ const prKeyStateRef=React.useRef({shift:false,ctrl:false});const prMouseInRef=Re 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{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{// 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=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=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(d8)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 +const cur=notesRef.current;for(let i=0;i=n.start_beat&&b<=n.start_beat+(n.duration_beats||1))return i;}return findCC11NoteAtBeat(b);};const commitCC11Curve=(curve,shared,noteIdx)=>{const curNotes=notesRef.current;const updatedNotes=shared?curNotes.map(n=>({...n,expression_curve:curve})):curNotes.map((n,i)=>i===noteIdx?{...n,expression_curve:curve}:n);setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);};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;// Co selection -> chi sua note do; neu click trung note khac -> chuyen +// selection sang note ay (REPLACE, khong accumulate). KHONG co selection +// -> shared mode (CC11 dung chung cho tat ca notes), khong auto-select. +const shared=selectedNoteIds.length===0;if(!shared&¬e.id&&!selectedNoteIds.includes(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){// Click vao vi tri trong: THEM node tai do ngay lap tuc — khong keo +// node moi theo cursor (user: chi click de them node, drag node co san) +const dup=curve.findIndex(p=>Math.abs(p.position_ratio-ratio)<1e-6);let newCurve;if(dup!==-1){newCurve=curve.slice();newCurve[dup]={position_ratio:ratio,value:val};}else{newCurve=curve.concat([{position_ratio:ratio,value:val}]).sort((a,b)=>a.position_ratio-b.position_ratio);}commitCC11Curve(newCurve,shared,noteIdx);ccDragRef.current=null;return;}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: ve preview truc tiep, commit khi mouseup/mouseleave +ccDragRef.current={mode:'cc11',active:true,noteIdx,noteId:note.id,anchorIdx,noteStart:note.start_beat,noteDur:dur,previewCurve:curve,shared};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(d8)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);commitCC11Curve(curve,selectedNoteIds.length===0,noteIdx);};// 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) +const commitCC11Drag=()=>{const drag=ccDragRef.current;if(!drag)return;if(drag.mode==='cc11'&&drag.previewCurve){commitCC11Curve(drag.previewCurve,drag.shared,drag.noteIdx);}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) const idxs=findCCNoteIndicesAtBeat(beat);ccDragRef.current={active:true,lastBeat:beat,lastPainted:idxs};}return;}if(noteIdx!==-1){const currentVal=ccMode==='pan'?(notes[noteIdx].pan||0)/2.0+0.5:notes[noteIdx].velocity!==undefined?notes[noteIdx].velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==noteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}}};const handleCCMouseMove=e=>{if(!ccDragRef.current||!ccDragRef.current.active)return;if(ccDragRef.current.mode==='cc11'){const drag=ccDragRef.current;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 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=(drag.previewCurve||getCC11Curve(note)).slice();const idx=Math.min(drag.anchorIdx,curve.length-1);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;iminR)minR=r;if(r>cur.position_ratio&&r0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1){const cursorNote=notes[cursorNoteIdx];if(cursorNote&&selectedNoteIds.includes(cursorNote.id)&&!painted.includes(cursorNoteIdx)){const currentVal=ccMode==='pan'?(cursorNote.pan||0)/2.0+0.5:cursorNote.velocity!==undefined?cursorNote.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);}drag.lastPainted=[...painted,cursorNoteIdx];}}return;}// Vẽ TẤT CẢ notes cùng beat (chord — user 08:25) — trước đây chỉ 1 note diff --git a/app/templates/index.html b/app/templates/index.html index fc15d26..072d51d 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +