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; });
return trk ? (trk.midiItems || []).find(function(m) { return m.id === st.target_id; }) : null;
}, [activeTracks, st.trackId, st.target_id]);
const sessionStartBar = sessionSyncMode && activeTargetItem
const sessionStartBar = 0;
const renderBeatOffset = sessionSyncMode && activeTargetItem
? (activeTargetItem.startTime / secondsPerBar) : 0;
const sessionLengthBars = React.useMemo(function() {
@@ -4906,7 +4907,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.fillStyle = layer.track_color || '#888';
ctx.strokeStyle = layer.track_color || '#888';
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 w = note.duration_beats * pixelsPerBeat;
var h = NoteHeight - 1;
@@ -4922,7 +4923,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Layer 3: Active notes with velocity layer representation
notes.forEach((note) => {
const x = note.start_beat * pixelsPerBeat;
const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
const y = (127 - note.pitch) * NoteHeight;
const w = note.duration_beats * pixelsPerBeat;
const isSelected = selectedNoteIds.includes(note.id);
@@ -4944,7 +4945,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Draw real-time recording notes
if (recordingState === 'RECORDING' && recTempMidiNotes && recTempMidiNotes.length > 0) {
recTempMidiNotes.forEach(note => {
const x = note.start_beat * pixelsPerBeat;
const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
const y = (127 - note.pitch) * NoteHeight;
const w = (note.duration_beats || 0.25) * pixelsPerBeat;
ctx.fillStyle = 'rgba(255, 100, 100, 0.35)';
@@ -4981,7 +4982,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Draw playhead
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;
if (phX >= 0 && phX <= viewWidth) {
ctx.strokeStyle = '#f59e0b';
@@ -4992,7 +4993,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
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(() => {
const canvas = ccCanvasRef.current;
@@ -5015,7 +5016,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.stroke();
notes.forEach((note) => {
const x = note.start_beat * pixelsPerBeat;
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') {
@@ -5037,7 +5038,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.arc(x, y, 3.5, 0, 2 * Math.PI);
ctx.fill();
});
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds]);
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset]);
React.useEffect(() => {
const scrollToC3 = () => {
@@ -5051,14 +5052,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
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 canvas = canvasRef.current;
if (!canvas) return;
@@ -6113,17 +6106,11 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */
React.createElement("div", {
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", {
ref: keybedRef,
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' }
className: "pointer-events-auto"
}, (allMidiItems.length > 0 ? function() {
var seenTracks = {};
var els = [];
@@ -6151,13 +6138,17 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
}, track ? track.name : m._trackName)));
});
return els;
}() : null)), React.createElement("div", {
className: "w-[60px] flex flex-col border-r border-zinc-900 shrink-0",
}() : null))), React.createElement("div", {
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: {
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}
}, renderKeybed())), React.createElement("div", {
}, renderKeybed()), React.createElement("div", {
ref: gridScrollRef,
onScroll: handleScroll,
className: "flex-1 overflow-auto bg-[#141414] min-w-0"
File diff suppressed because one or more lines are too long