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; });
|
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
Reference in New Issue
Block a user