feat: brush-pen scale snap + scale sub-menu hover + keybed swipe
- Brush (pen tool only) draws notes snapped to selected scale - Scale sub-menus open on hover instead of click - Keybed: swipe/drag across keys plays notes (mouseenter+mousedown) - Global mouseup resets keybed drag state
This commit is contained in:
+24
-4
@@ -4554,7 +4554,13 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const ccWrapperRef = React.useRef(null);
|
const ccWrapperRef = React.useRef(null);
|
||||||
const gridScrollRef = React.useRef(null);
|
const gridScrollRef = React.useRef(null);
|
||||||
const keybedRef = React.useRef(null);
|
const keybedRef = React.useRef(null);
|
||||||
|
const keybedMouseDownRef = React.useRef(false);
|
||||||
const rulerScrollRef = React.useRef(null);
|
const rulerScrollRef = React.useRef(null);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const up = () => { keybedMouseDownRef.current = false; };
|
||||||
|
window.addEventListener('mouseup', up);
|
||||||
|
return () => window.removeEventListener('mouseup', up);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const NoteHeight = 18;
|
const NoteHeight = 18;
|
||||||
const PITCH_START = 0; // C0 (render all 128 keys)
|
const PITCH_START = 0; // C0 (render all 128 keys)
|
||||||
@@ -5043,8 +5049,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
startOffsetPitch: pitch - clickedNote.pitch,
|
startOffsetPitch: pitch - clickedNote.pitch,
|
||||||
selectedNotesOffset: selectedNotesOffset
|
selectedNotesOffset: selectedNotesOffset
|
||||||
});
|
});
|
||||||
} else {
|
} else if (activeRollTool === 'pen') {
|
||||||
// Click on empty space: DRAW a new note (brush mode with visitedPitches)
|
// Click on empty space with pen tool: DRAW a new note (brush mode with visitedPitches)
|
||||||
pushToUndo(notes);
|
pushToUndo(notes);
|
||||||
const start = getSnapBeat(beat, snapVal);
|
const start = getSnapBeat(beat, snapVal);
|
||||||
const initialDur = getSnapDuration(snapVal);
|
const initialDur = getSnapDuration(snapVal);
|
||||||
@@ -5340,6 +5346,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
className: `w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${isBlack ? 'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800' : 'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,
|
className: `w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${isBlack ? 'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800' : 'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,
|
||||||
onMouseDown: (e) => {
|
onMouseDown: (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
keybedMouseDownRef.current = true;
|
||||||
try {
|
try {
|
||||||
if (window.SonicSF) {
|
if (window.SonicSF) {
|
||||||
window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null);
|
window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null);
|
||||||
@@ -5348,6 +5355,16 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
console.error('playNote error:', err);
|
console.error('playNote error:', err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onMouseEnter: (e) => {
|
||||||
|
if (keybedMouseDownRef.current) {
|
||||||
|
try {
|
||||||
|
if (window.SonicSF) {
|
||||||
|
window.SonicSF.playNote(pitch, 100, 200, undefined, st.instrumentProgram, null);
|
||||||
|
}
|
||||||
|
} catch (err) { console.error('playNote error:', err); }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMouseUp: () => { keybedMouseDownRef.current = false; },
|
||||||
onContextMenu: (e) => {
|
onContextMenu: (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -5498,11 +5515,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
const closeMenu = () => setScaleMenuPos(null);
|
const closeMenu = () => setScaleMenuPos(null);
|
||||||
const items = [];
|
const items = [];
|
||||||
const isSameScale = (a, b) => { if (!a || !b) return a === b; if (a.length !== b.length) return false; return a.every((v,i)=>v===b[i]); };
|
const isSameScale = (a, b) => { if (!a || !b) return a === b; if (a.length !== b.length) return false; return a.every((v,i)=>v===b[i]); };
|
||||||
const pushItem = (label, onClick, indent) => {
|
const pushItem = (label, onClick, indent, onHover) => {
|
||||||
const isActive = onClick._scale && isSameScale(onClick._scale, selectedScale);
|
const isActive = onClick._scale && isSameScale(onClick._scale, selectedScale);
|
||||||
items.push(React.createElement("div", {
|
items.push(React.createElement("div", {
|
||||||
key: label,
|
key: label,
|
||||||
onClick: () => { onClick(); closeMenu(); },
|
onClick: () => { onClick(); closeMenu(); },
|
||||||
|
onMouseEnter: onHover || undefined,
|
||||||
className: "px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap " + (indent ? "pl-6 " : "") + (isActive ? "bg-amber-800/40 text-amber-300" : "text-zinc-300")
|
className: "px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap " + (indent ? "pl-6 " : "") + (isActive ? "bg-amber-800/40 text-amber-300" : "text-zinc-300")
|
||||||
}, label));
|
}, label));
|
||||||
};
|
};
|
||||||
@@ -5514,7 +5532,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
} else {
|
} else {
|
||||||
const parentKey = key;
|
const parentKey = key;
|
||||||
const isOpen = scaleMenuPos && scaleMenuPos.parentKey === parentKey;
|
const isOpen = scaleMenuPos && scaleMenuPos.parentKey === parentKey;
|
||||||
pushItem(key + " ▸", () => { setScaleMenuPos({ x: scaleMenuPos.x + 120, y: scaleMenuPos.y, parentKey }); }, false);
|
pushItem(key + " ▸", () => setSelectedScale(null), false, () => {
|
||||||
|
setScaleMenuPos({ x: scaleMenuPos.x + 120, y: scaleMenuPos.y, parentKey });
|
||||||
|
});
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
Object.keys(val).forEach(subKey => {
|
Object.keys(val).forEach(subKey => {
|
||||||
pushItem(subKey, () => setSelectedScale(val[subKey]), true);
|
pushItem(subKey, () => setSelectedScale(val[subKey]), true);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user