feat(piano-roll): session bar0, fixed track column, renderBeatOffset

- sessionStartBar = 0 always; notes rendered at session-absolute positions
- renderBeatOffset aligns active/ghost notes to session timeline
- Track column uses absolute overlay with pointer-events for always-visible
- Removed auto-scroll (viewport starts at bar 0 in session mode)
- Grid spacer (120px) aligns grid with ruler track header
This commit is contained in:
2026-07-27 17:57:47 +07:00
parent b9113f8a06
commit 1ba9e8379f
2 changed files with 26 additions and 36 deletions
+20 -29
View File
@@ -4624,7 +4624,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var trk = activeTracks.find(function(t) { return t.id === st.trackId; }); var trk = activeTracks.find(function(t) { return t.id === st.trackId; });
return trk ? (trk.midiItems || []).find(function(m) { return m.id === st.target_id; }) : null; return trk ? (trk.midiItems || []).find(function(m) { return m.id === st.target_id; }) : null;
}, [activeTracks, st.trackId, st.target_id]); }, [activeTracks, st.trackId, st.target_id]);
const sessionStartBar = sessionSyncMode && activeTargetItem const sessionStartBar = 0;
const renderBeatOffset = sessionSyncMode && activeTargetItem
? (activeTargetItem.startTime / secondsPerBar) : 0; ? (activeTargetItem.startTime / secondsPerBar) : 0;
const sessionLengthBars = React.useMemo(function() { const sessionLengthBars = React.useMemo(function() {
@@ -4906,7 +4907,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.fillStyle = layer.track_color || '#888'; ctx.fillStyle = layer.track_color || '#888';
ctx.strokeStyle = layer.track_color || '#888'; ctx.strokeStyle = layer.track_color || '#888';
layer.notes.forEach(function(note) { layer.notes.forEach(function(note) {
var x = note.relative_start_beat * pixelsPerBeat; var x = (renderBeatOffset + note.relative_start_beat) * pixelsPerBeat;
var y = (127 - note.pitch) * NoteHeight; var y = (127 - note.pitch) * NoteHeight;
var w = note.duration_beats * pixelsPerBeat; var w = note.duration_beats * pixelsPerBeat;
var h = NoteHeight - 1; var h = NoteHeight - 1;
@@ -4922,7 +4923,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Layer 3: Active notes with velocity layer representation // Layer 3: Active notes with velocity layer representation
notes.forEach((note) => { notes.forEach((note) => {
const x = note.start_beat * pixelsPerBeat; const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
const y = (127 - note.pitch) * NoteHeight; const y = (127 - note.pitch) * NoteHeight;
const w = note.duration_beats * pixelsPerBeat; const w = note.duration_beats * pixelsPerBeat;
const isSelected = selectedNoteIds.includes(note.id); const isSelected = selectedNoteIds.includes(note.id);
@@ -4944,7 +4945,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Draw real-time recording notes // Draw real-time recording notes
if (recordingState === 'RECORDING' && recTempMidiNotes && recTempMidiNotes.length > 0) { if (recordingState === 'RECORDING' && recTempMidiNotes && recTempMidiNotes.length > 0) {
recTempMidiNotes.forEach(note => { recTempMidiNotes.forEach(note => {
const x = note.start_beat * pixelsPerBeat; const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
const y = (127 - note.pitch) * NoteHeight; const y = (127 - note.pitch) * NoteHeight;
const w = (note.duration_beats || 0.25) * pixelsPerBeat; const w = (note.duration_beats || 0.25) * pixelsPerBeat;
ctx.fillStyle = 'rgba(255, 100, 100, 0.35)'; ctx.fillStyle = 'rgba(255, 100, 100, 0.35)';
@@ -4981,7 +4982,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Draw playhead // Draw playhead
if (st.currentTime !== undefined && st.currentTime !== null && st.currentTime >= 0) { if (st.currentTime !== undefined && st.currentTime !== null && st.currentTime >= 0) {
const phBeat = st.currentTime / (60.0 / (parseInt(bpm) || 120)); const phBeat = renderBeatOffset + st.currentTime / (60.0 / (parseInt(bpm) || 120));
const phX = phBeat * pixelsPerBeat; const phX = phBeat * pixelsPerBeat;
if (phX >= 0 && phX <= viewWidth) { if (phX >= 0 && phX <= viewWidth) {
ctx.strokeStyle = '#f59e0b'; ctx.strokeStyle = '#f59e0b';
@@ -4992,7 +4993,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.stroke(); ctx.stroke();
} }
} }
}, [notes, snapVal, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers]); }, [notes, snapVal, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers, renderBeatOffset]);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
const canvas = ccCanvasRef.current; const canvas = ccCanvasRef.current;
@@ -5015,7 +5016,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.stroke(); ctx.stroke();
notes.forEach((note) => { notes.forEach((note) => {
const x = note.start_beat * pixelsPerBeat; const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
const isSelected = selectedNoteIds.includes(note.id); const isSelected = selectedNoteIds.includes(note.id);
let val = note.velocity !== undefined ? note.velocity : 0.8; let val = note.velocity !== undefined ? note.velocity : 0.8;
if (ccMode === 'pan') { if (ccMode === 'pan') {
@@ -5037,7 +5038,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.arc(x, y, 3.5, 0, 2 * Math.PI); ctx.arc(x, y, 3.5, 0, 2 * Math.PI);
ctx.fill(); ctx.fill();
}); });
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds]); }, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset]);
React.useEffect(() => { React.useEffect(() => {
const scrollToC3 = () => { const scrollToC3 = () => {
@@ -5051,14 +5052,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);
// Auto-scroll to session position when in session sync mode
React.useEffect(function() {
if (sessionSyncMode && rulerScrollRef.current && activeTargetItem) {
var scrollTargetBeats = sessionStartBar * 4;
rulerScrollRef.current.scrollLeft = scrollTargetBeats * pixelsPerBeat;
}
}, [sessionSyncMode, sessionStartBar, st.target_id, pixelsPerBeat]);
const handleGridMouseDown = (e) => { const handleGridMouseDown = (e) => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas) return;
@@ -6113,17 +6106,11 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */ /* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */
React.createElement("div", { React.createElement("div", {
className: "flex-1 flex overflow-hidden min-h-0 relative" className: "flex-1 flex overflow-hidden min-h-0 relative"
}, /* Track column overlay — always visible */ React.createElement("div", {
className: "absolute left-0 top-0 w-[120px] bg-[#16161a] border-r border-zinc-800 z-10 flex flex-col overflow-hidden pointer-events-none",
style: { height: KeybedPixelHeight + 'px' }
}, React.createElement("div", { }, React.createElement("div", {
ref: keybedRef, className: "pointer-events-auto"
onScroll: handleKeybedScroll,
className: "flex overflow-y-auto shrink-0 flex-row",
style: {
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}
}, /* Track column */ React.createElement("div", {
className: "w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col",
style: { height: KeybedPixelHeight + 'px', overflow: 'hidden' }
}, (allMidiItems.length > 0 ? function() { }, (allMidiItems.length > 0 ? function() {
var seenTracks = {}; var seenTracks = {};
var els = []; var els = [];
@@ -6151,13 +6138,17 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
}, track ? track.name : m._trackName))); }, track ? track.name : m._trackName)));
}); });
return els; return els;
}() : null)), React.createElement("div", { }() : null))), React.createElement("div", {
className: "w-[60px] flex flex-col border-r border-zinc-900 shrink-0", className: "w-[120px] shrink-0"
}), React.createElement("div", {
ref: keybedRef,
onScroll: handleKeybedScroll,
className: "w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",
style: { style: {
scrollbarWidth: 'none', scrollbarWidth: 'none',
msOverflowStyle: 'none' msOverflowStyle: 'none'
} }
}, renderKeybed())), React.createElement("div", { }, renderKeybed()), React.createElement("div", {
ref: gridScrollRef, ref: gridScrollRef,
onScroll: handleScroll, onScroll: handleScroll,
className: "flex-1 overflow-auto bg-[#141414] min-w-0" className: "flex-1 overflow-auto bg-[#141414] min-w-0"
File diff suppressed because one or more lines are too long