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:
+20
-29
@@ -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
Reference in New Issue
Block a user