feat: added edit MIDI notes' attribute feature
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { KGMidiNote } from '../../core/midi/KGMidiNote';
|
||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||
import { KGCore } from '../../core/KGCore';
|
||||
import { UpdateNotePropertiesCommand } from '../../core/commands/note/UpdateNotePropertiesCommand';
|
||||
import { useProjectStore } from '../../stores/projectStore';
|
||||
import { showAlert } from '../../util/dialogUtil';
|
||||
|
||||
interface NoteAttributeBarProps {
|
||||
selectedNotes: KGMidiNote[];
|
||||
isSpectrogram: boolean;
|
||||
activeRegion: KGMidiRegion | null;
|
||||
}
|
||||
|
||||
type TextField = 'pitch' | 'length';
|
||||
|
||||
function clamp(v: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, v));
|
||||
}
|
||||
|
||||
function parsePitchInput(raw: string, notes: KGMidiNote[]): number[] | null {
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed.startsWith('+') || trimmed.startsWith('-')) {
|
||||
const delta = parseInt(trimmed, 10);
|
||||
if (isNaN(delta)) return null;
|
||||
return notes.map(n => clamp(n.getPitch() + delta, 0, 127));
|
||||
}
|
||||
const abs = parseInt(trimmed, 10);
|
||||
if (isNaN(abs)) return null;
|
||||
return notes.map(() => clamp(abs, 0, 127));
|
||||
}
|
||||
|
||||
function parseLengthInput(raw: string): number | null {
|
||||
const v = parseFloat(raw.trim());
|
||||
if (isNaN(v) || v <= 0) return null;
|
||||
return v;
|
||||
}
|
||||
|
||||
interface VelocitySession {
|
||||
originalVelocities: number[];
|
||||
notes: KGMidiNote[];
|
||||
}
|
||||
|
||||
const NoteAttributeBar: React.FC<NoteAttributeBarProps> = ({ selectedNotes, isSpectrogram, activeRegion }) => {
|
||||
const { tracks, updateTrack } = useProjectStore();
|
||||
|
||||
// Text field popup state (pitch / length)
|
||||
const [openTextField, setOpenTextField] = useState<TextField | null>(null);
|
||||
const [textInputValue, setTextInputValue] = useState('');
|
||||
|
||||
// Velocity slider popup state
|
||||
const [velocityOpen, setVelocityOpen] = useState(false);
|
||||
const [sliderValue, setSliderValue] = useState(64);
|
||||
const velocitySessionRef = useRef<VelocitySession | null>(null);
|
||||
|
||||
const barRef = useRef<HTMLDivElement>(null);
|
||||
const commitVelocityRef = useRef<(() => void) | null>(null);
|
||||
|
||||
// ── Text field helpers ──────────────────────────────────────────────────────
|
||||
|
||||
const commitTextField = () => {
|
||||
if (!openTextField || !activeRegion) { setOpenTextField(null); return; }
|
||||
|
||||
const snapshots = selectedNotes.map(n => ({
|
||||
noteId: n.getId(),
|
||||
pitch: n.getPitch(),
|
||||
velocity: n.getVelocity(),
|
||||
startBeat: n.getStartBeat(),
|
||||
endBeat: n.getEndBeat(),
|
||||
}));
|
||||
|
||||
const updates: { noteId: string; pitch?: number; endBeat?: number }[] = [];
|
||||
|
||||
if (openTextField === 'pitch') {
|
||||
const newPitches = parsePitchInput(textInputValue, selectedNotes);
|
||||
if (!newPitches) { setOpenTextField(null); return; }
|
||||
selectedNotes.forEach((note, i) => updates.push({ noteId: note.getId(), pitch: newPitches[i] }));
|
||||
} else {
|
||||
const newLength = parseLengthInput(textInputValue);
|
||||
if (!newLength) { setOpenTextField(null); return; }
|
||||
selectedNotes.forEach(note => updates.push({ noteId: note.getId(), endBeat: note.getStartBeat() + newLength }));
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
const command = new UpdateNotePropertiesCommand(activeRegion.getId(), snapshots, updates);
|
||||
KGCore.instance().executeCommand(command);
|
||||
const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId());
|
||||
if (track) updateTrack(track);
|
||||
}
|
||||
setOpenTextField(null);
|
||||
};
|
||||
|
||||
const handleTextKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') commitTextField();
|
||||
if (e.key === 'Escape') setOpenTextField(null);
|
||||
};
|
||||
|
||||
// ── Velocity slider helpers ─────────────────────────────────────────────────
|
||||
|
||||
const commitVelocity = () => {
|
||||
const session = velocitySessionRef.current;
|
||||
if (!session || !activeRegion) { setVelocityOpen(false); return; }
|
||||
|
||||
// Build snapshots from original velocities captured at open time
|
||||
const snapshots = session.notes.map((n, i) => ({
|
||||
noteId: n.getId(),
|
||||
pitch: n.getPitch(),
|
||||
velocity: session.originalVelocities[i], // original, before live edits
|
||||
startBeat: n.getStartBeat(),
|
||||
endBeat: n.getEndBeat(),
|
||||
}));
|
||||
const updates = session.notes.map(n => ({ noteId: n.getId(), velocity: sliderValue }));
|
||||
|
||||
// Restore originals so the command's execute() applies cleanly
|
||||
session.notes.forEach((n, i) => n.setVelocity(session.originalVelocities[i]));
|
||||
|
||||
const command = new UpdateNotePropertiesCommand(activeRegion.getId(), snapshots, updates);
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId());
|
||||
if (track) updateTrack(track);
|
||||
|
||||
velocitySessionRef.current = null;
|
||||
setVelocityOpen(false);
|
||||
};
|
||||
|
||||
const cancelVelocity = () => {
|
||||
const session = velocitySessionRef.current;
|
||||
if (session) {
|
||||
session.notes.forEach((n, i) => n.setVelocity(session.originalVelocities[i]));
|
||||
const track = tracks.find(t => t.getId().toString() === activeRegion?.getTrackId());
|
||||
if (track) updateTrack(track);
|
||||
}
|
||||
velocitySessionRef.current = null;
|
||||
setVelocityOpen(false);
|
||||
};
|
||||
|
||||
// Keep ref current so the outside-click handler always calls the latest commit
|
||||
commitVelocityRef.current = commitVelocity;
|
||||
|
||||
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
setSliderValue(val);
|
||||
// Live-apply to notes so colors update immediately
|
||||
velocitySessionRef.current?.notes.forEach(n => n.setVelocity(val));
|
||||
const track = tracks.find(t => t.getId().toString() === activeRegion?.getTrackId());
|
||||
if (track) updateTrack(track);
|
||||
};
|
||||
|
||||
const handleSliderKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') cancelVelocity();
|
||||
};
|
||||
|
||||
// ── Outside-click handler ───────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
const isAnyOpen = openTextField !== null || velocityOpen;
|
||||
if (!isAnyOpen) return;
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (barRef.current && !barRef.current.contains(e.target as Node)) {
|
||||
if (velocityOpen) {
|
||||
commitVelocityRef.current?.();
|
||||
} else {
|
||||
setOpenTextField(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleMouseDown);
|
||||
return () => document.removeEventListener('mousedown', handleMouseDown);
|
||||
}, [openTextField, velocityOpen]);
|
||||
|
||||
if (isSpectrogram || selectedNotes.length === 0) {
|
||||
return <div className="note-attribute-bar" ref={barRef} />;
|
||||
}
|
||||
|
||||
// ── Compute display values ─────────────────────────────────────────────────
|
||||
|
||||
let pitch: string;
|
||||
let length: string;
|
||||
let velocity: string;
|
||||
let pitchDefault: string;
|
||||
let lengthDefault: string;
|
||||
let velocityDefault: number;
|
||||
|
||||
if (selectedNotes.length === 0) {
|
||||
pitch = 'N/A'; length = 'N/A'; velocity = 'N/A';
|
||||
pitchDefault = ''; lengthDefault = ''; velocityDefault = 64;
|
||||
} else if (selectedNotes.length === 1) {
|
||||
const note = selectedNotes[0];
|
||||
pitch = String(note.getPitch());
|
||||
length = (note.getEndBeat() - note.getStartBeat()).toFixed(2);
|
||||
velocity = String(note.getVelocity());
|
||||
pitchDefault = pitch; lengthDefault = length; velocityDefault = note.getVelocity();
|
||||
} else {
|
||||
const pitches = selectedNotes.map(n => n.getPitch());
|
||||
const lengths = selectedNotes.map(n => (n.getEndBeat() - n.getStartBeat()).toFixed(2));
|
||||
const velocities = selectedNotes.map(n => n.getVelocity());
|
||||
pitch = pitches.every(p => p === pitches[0]) ? String(pitches[0]) : '--';
|
||||
length = lengths.every(l => l === lengths[0]) ? lengths[0] : '--';
|
||||
velocity = velocities.every(v => v === velocities[0]) ? String(velocities[0]) : '--';
|
||||
pitchDefault = pitch === '--' ? '' : pitch;
|
||||
lengthDefault = length === '--' ? '' : length;
|
||||
velocityDefault = velocity === '--' ? 64 : velocities[0];
|
||||
}
|
||||
|
||||
// ── Field click handlers ───────────────────────────────────────────────────
|
||||
|
||||
const handleTextFieldClick = (field: TextField, defaultVal: string) => {
|
||||
if (selectedNotes.length === 0) {
|
||||
showAlert('Please select one or more notes to edit their properties.');
|
||||
return;
|
||||
}
|
||||
setTextInputValue(defaultVal);
|
||||
setOpenTextField(field);
|
||||
};
|
||||
|
||||
const handleVelocityClick = () => {
|
||||
if (selectedNotes.length === 0) {
|
||||
showAlert('Please select one or more notes to edit their properties.');
|
||||
return;
|
||||
}
|
||||
velocitySessionRef.current = {
|
||||
originalVelocities: selectedNotes.map(n => n.getVelocity()),
|
||||
notes: [...selectedNotes],
|
||||
};
|
||||
setSliderValue(velocityDefault);
|
||||
setVelocityOpen(true);
|
||||
};
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
<div className="note-attribute-bar" ref={barRef}>
|
||||
{/* Pitch */}
|
||||
<span className="attr-item">
|
||||
<span className="attr-label">Pitch:</span>
|
||||
<div className="quant-dropdown-container">
|
||||
<button className="quant-button" onClick={() => handleTextFieldClick('pitch', pitchDefault)}>
|
||||
{pitch}
|
||||
</button>
|
||||
{openTextField === 'pitch' && (
|
||||
<div className="note-attr-popup">
|
||||
<input
|
||||
type="text"
|
||||
className="note-attr-input"
|
||||
value={textInputValue}
|
||||
onChange={e => setTextInputValue(e.target.value)}
|
||||
onKeyDown={handleTextKeyDown}
|
||||
placeholder="0–127 or ±n"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
|
||||
{/* Length */}
|
||||
<span className="attr-item">
|
||||
<span className="attr-label">Length:</span>
|
||||
<div className="quant-dropdown-container">
|
||||
<button className="quant-button" onClick={() => handleTextFieldClick('length', lengthDefault)}>
|
||||
{length}
|
||||
</button>
|
||||
{openTextField === 'length' && (
|
||||
<div className="note-attr-popup">
|
||||
<input
|
||||
type="text"
|
||||
className="note-attr-input"
|
||||
value={textInputValue}
|
||||
onChange={e => setTextInputValue(e.target.value)}
|
||||
onKeyDown={handleTextKeyDown}
|
||||
placeholder="beats e.g. 1.00"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
|
||||
{/* Velocity */}
|
||||
<span className="attr-item">
|
||||
<span className="attr-label">Velocity:</span>
|
||||
<div className="quant-dropdown-container">
|
||||
<button className="quant-button" onClick={handleVelocityClick}>
|
||||
{velocity}
|
||||
</button>
|
||||
{velocityOpen && (
|
||||
<div className="piano-roll-zoom-popup">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="127"
|
||||
step="1"
|
||||
value={sliderValue}
|
||||
onChange={handleSliderChange}
|
||||
onKeyDown={handleSliderKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
<span className="piano-roll-zoom-value">{sliderValue}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoteAttributeBar;
|
||||
@@ -2,6 +2,20 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { PIANO_ROLL_CONSTANTS, DEBUG_MODE } from '../../constants';
|
||||
import { useProjectStore } from '../../stores/projectStore';
|
||||
|
||||
// Purple (vel=0) → green (vel=64) → red (vel=127), matching Logic Pro
|
||||
function velocityToColor(v: number, alpha = 1): string {
|
||||
const lerp = (a: number, b: number, t: number) => Math.round(a + (b - a) * t);
|
||||
let r: number, g: number, b: number;
|
||||
if (v <= 64) {
|
||||
const t = v / 64;
|
||||
r = lerp(123, 90, t); g = lerp(95, 176, t); b = lerp(160, 106, t);
|
||||
} else {
|
||||
const t = (v - 64) / 63;
|
||||
r = lerp(90, 255, t); g = lerp(176, 85, t); b = lerp(106, 85, t);
|
||||
}
|
||||
return alpha === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
interface PianoNoteProps {
|
||||
id: string;
|
||||
index: number;
|
||||
@@ -9,6 +23,7 @@ interface PianoNoteProps {
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
velocity: number;
|
||||
onResizeStart?: (noteId: string, resizeEdge: 'start' | 'end', initialX: number) => void;
|
||||
onResize?: (noteId: string, resizeEdge: 'start' | 'end', deltaX: number) => void;
|
||||
onResizeEnd?: (noteId: string, resizeEdge: 'start' | 'end') => void;
|
||||
@@ -25,6 +40,7 @@ const PianoNote: React.FC<PianoNoteProps> = ({
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
velocity,
|
||||
onResizeStart,
|
||||
onResize,
|
||||
onResizeEnd,
|
||||
@@ -41,6 +57,7 @@ const PianoNote: React.FC<PianoNoteProps> = ({
|
||||
const [resizeEdge, setResizeEdge] = useState<'none' | 'start' | 'end'>('none');
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// Use refs to track states for immediate access
|
||||
const isResizingRef = useRef<boolean>(false);
|
||||
@@ -80,6 +97,7 @@ const PianoNote: React.FC<PianoNoteProps> = ({
|
||||
|
||||
// Reset cursor when mouse leaves
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
if (!isResizingRef.current && !isDraggingRef.current) {
|
||||
setCursor('default');
|
||||
setResizeEdge('none');
|
||||
@@ -250,9 +268,16 @@ const PianoNote: React.FC<PianoNoteProps> = ({
|
||||
top: `${top}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
cursor: cursor
|
||||
cursor: cursor,
|
||||
backgroundColor: velocityToColor(velocity),
|
||||
boxShadow: isResizing
|
||||
? `0 0 10px ${velocityToColor(velocity, 0.7)}`
|
||||
: isHovered
|
||||
? `0 0 5px ${velocityToColor(velocity, 0.5)}`
|
||||
: undefined,
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseDown={handleMouseDown}
|
||||
id={id}
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
/* Piano notes */
|
||||
.piano-note {
|
||||
position: absolute;
|
||||
background-color: #ff5555;
|
||||
/* Red color */
|
||||
border: 1px solid #999;
|
||||
border-radius: 2px;
|
||||
z-index: 10;
|
||||
@@ -30,7 +28,6 @@
|
||||
|
||||
.piano-note:hover {
|
||||
filter: brightness(1.1);
|
||||
box-shadow: 0 0 5px rgba(255, 85, 85, 0.5);
|
||||
}
|
||||
|
||||
.piano-note.dragging {
|
||||
@@ -42,7 +39,6 @@
|
||||
|
||||
.piano-note.resizing {
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 10px rgba(255, 85, 85, 0.7);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@@ -77,6 +73,70 @@
|
||||
/* Ensure toolbar and its dropdowns appear above piano roll content */
|
||||
}
|
||||
|
||||
/* Note attribute bar */
|
||||
.note-attribute-bar {
|
||||
height: 30px;
|
||||
background-color: #252525;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
gap: 24px;
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.note-attribute-bar .attr-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-attribute-bar .attr-label {
|
||||
color: #777;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.note-attribute-bar .attr-value {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.note-attribute-bar .quant-button {
|
||||
font-size: 11px;
|
||||
margin-left: 0;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.note-attr-popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #444;
|
||||
border-radius: 3px;
|
||||
padding: 6px 8px;
|
||||
z-index: 1500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
margin-top: 2px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.note-attr-input {
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
color: #e0e0e0;
|
||||
font-size: 11px;
|
||||
padding: 3px 6px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.note-attr-input:focus {
|
||||
border-color: #4a6b8a;
|
||||
}
|
||||
|
||||
.piano-roll-toolbar .tool-button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect, useState, useCallback, useLayoutEffect } from 'react';
|
||||
import React, { useRef, useEffect, useState, useCallback, useLayoutEffect, useMemo } from 'react';
|
||||
import './PianoRoll.css';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useProjectStore } from '../../stores/projectStore';
|
||||
@@ -8,6 +8,7 @@ import type { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
||||
import { DEBUG_MODE, PIANO_ROLL_CONSTANTS, TOOLBAR_CONSTANTS } from '../../constants';
|
||||
import PianoRollHeader from './PianoRollHeader';
|
||||
import PianoRollToolbar from './PianoRollToolbar';
|
||||
import NoteAttributeBar from './NoteAttributeBar';
|
||||
import PianoRollContent from './PianoRollContent';
|
||||
import { KGCore } from '../../core/KGCore';
|
||||
import { KGMidiNote } from '../../core/midi/KGMidiNote';
|
||||
@@ -41,7 +42,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
}) => {
|
||||
const isSpectrogram = mode === 'spectrogram';
|
||||
const isHybrid = mode === 'hybrid';
|
||||
const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest } = useProjectStore();
|
||||
const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds } = useProjectStore();
|
||||
|
||||
// Tool state for piano roll
|
||||
const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer');
|
||||
@@ -73,6 +74,12 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const [activeRegion, setActiveRegion] = useState<KGMidiRegion | null>(null);
|
||||
|
||||
const selectedNotes = useMemo(
|
||||
() => activeRegion?.getNotes().filter(n => selectedNoteIds.includes(n.getId())) ?? [],
|
||||
[activeRegion, selectedNoteIds]
|
||||
);
|
||||
|
||||
const pianoRollRef = useRef<HTMLDivElement>(null);
|
||||
const pianoRollContentRef = useRef<HTMLDivElement>(null);
|
||||
const pianoGridRef = useRef<HTMLDivElement>(null);
|
||||
@@ -1019,6 +1026,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
onZoomChange={handleZoomChange}
|
||||
/>
|
||||
|
||||
<NoteAttributeBar selectedNotes={selectedNotes} isSpectrogram={isSpectrogram} activeRegion={activeRegion} />
|
||||
|
||||
<PianoRollContent
|
||||
contentRef={pianoRollContentRef}
|
||||
pianoGridRef={pianoGridRef}
|
||||
|
||||
@@ -211,6 +211,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
top={parseFloat(tempStyle.top as string)}
|
||||
width={parseFloat(tempStyle.width as string)}
|
||||
height={noteHeight}
|
||||
velocity={note.getVelocity()}
|
||||
onResizeStart={handleNoteResizeStart}
|
||||
onResize={handleNoteResize}
|
||||
onResizeEnd={handleNoteResizeEnd}
|
||||
@@ -221,7 +222,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<PianoNote
|
||||
key={`note-${noteId}`}
|
||||
@@ -231,6 +232,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
top={top}
|
||||
width={width}
|
||||
height={noteHeight}
|
||||
velocity={note.getVelocity()}
|
||||
onResizeStart={handleNoteResizeStart}
|
||||
onResize={handleNoteResize}
|
||||
onResizeEnd={handleNoteResizeEnd}
|
||||
|
||||
Reference in New Issue
Block a user