initial public release.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
|
||||
/**
|
||||
* Command to create a new MIDI note in a region
|
||||
* Handles both the core model update and provides undo functionality
|
||||
*/
|
||||
export class CreateNoteCommand extends KGCommand {
|
||||
private regionId: string;
|
||||
private noteId: string;
|
||||
private startBeat: number;
|
||||
private endBeat: number;
|
||||
private pitch: number;
|
||||
private velocity: number;
|
||||
private createdNote: KGMidiNote | null = null;
|
||||
|
||||
constructor(
|
||||
regionId: string,
|
||||
startBeat: number,
|
||||
endBeat: number,
|
||||
pitch: number,
|
||||
velocity: number = 127,
|
||||
noteId?: string
|
||||
) {
|
||||
super();
|
||||
this.regionId = regionId;
|
||||
this.startBeat = startBeat;
|
||||
this.endBeat = endBeat;
|
||||
this.pitch = pitch;
|
||||
this.velocity = velocity;
|
||||
this.noteId = noteId || generateUniqueId('KGMidiNote');
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Find the region that contains this note
|
||||
let targetRegion: KGMidiRegion | null = null;
|
||||
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === this.regionId);
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
targetRegion = region;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRegion) {
|
||||
throw new Error(`MIDI region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
// Create the new MIDI note
|
||||
this.createdNote = new KGMidiNote(
|
||||
this.noteId,
|
||||
this.startBeat,
|
||||
this.endBeat,
|
||||
this.pitch,
|
||||
this.velocity
|
||||
);
|
||||
|
||||
// Add the note to the region
|
||||
targetRegion.addNote(this.createdNote);
|
||||
|
||||
console.log(`Created note in region ${this.regionId}: pitch=${this.pitch}, start=${this.startBeat}, end=${this.endBeat}`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.createdNote) {
|
||||
throw new Error('Cannot undo: no note was created');
|
||||
}
|
||||
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Find the region that contains this note
|
||||
let targetRegion: KGMidiRegion | null = null;
|
||||
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === this.regionId);
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
targetRegion = region;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRegion) {
|
||||
throw new Error(`MIDI region with ID ${this.regionId} not found during undo`);
|
||||
}
|
||||
|
||||
// Remove the note from the region
|
||||
targetRegion.removeNote(this.noteId);
|
||||
|
||||
console.log(`Removed note from region ${this.regionId}: pitch=${this.pitch}`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
// Convert MIDI pitch to note name for user-friendly description
|
||||
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
const octave = Math.floor(this.pitch / 12) - 1;
|
||||
const noteName = noteNames[this.pitch % 12];
|
||||
return `Create note ${noteName}${octave}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the note that was/will be created
|
||||
*/
|
||||
public getNoteId(): string {
|
||||
return this.noteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the created note instance (only available after execute)
|
||||
*/
|
||||
public getCreatedNote(): KGMidiNote | null {
|
||||
return this.createdNote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the region ID where the note was/will be created
|
||||
*/
|
||||
public getRegionId(): string {
|
||||
return this.regionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a note command from UI coordinates
|
||||
*/
|
||||
public static fromUICoordinates(
|
||||
regionId: string,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
pianoGridElement: HTMLElement,
|
||||
beatWidth: number,
|
||||
noteHeight: number,
|
||||
regionStartBeat: number,
|
||||
noteLength: number,
|
||||
velocity: number = 127
|
||||
): CreateNoteCommand {
|
||||
// Calculate relative position within the grid
|
||||
const rect = pianoGridElement.getBoundingClientRect();
|
||||
const x = mouseX - rect.left;
|
||||
const y = mouseY - rect.top;
|
||||
|
||||
// Calculate the beat number (relative to region start)
|
||||
const rawBeatNumber = x / beatWidth;
|
||||
const beatNumber = Math.floor(rawBeatNumber); // Snap to beat grid
|
||||
|
||||
// Calculate the pitch (MIDI note number)
|
||||
// The piano roll is drawn from bottom to top, with higher notes at the top
|
||||
const pitchIndex = Math.floor(y / noteHeight);
|
||||
const pitch = 107 - pitchIndex; // Convert index to pitch (B7 is 107)
|
||||
|
||||
// Calculate note start and end beats
|
||||
const noteStartBeat = beatNumber;
|
||||
const noteEndBeat = noteStartBeat + noteLength;
|
||||
|
||||
return new CreateNoteCommand(
|
||||
regionId,
|
||||
noteStartBeat,
|
||||
noteEndBeat,
|
||||
pitch,
|
||||
velocity
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
|
||||
/**
|
||||
* Data structure for a note to be created
|
||||
*/
|
||||
export interface NoteCreationData {
|
||||
regionId: string;
|
||||
startBeat: number;
|
||||
endBeat: number;
|
||||
pitch: number;
|
||||
velocity: number;
|
||||
noteId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to create multiple MIDI notes in regions
|
||||
* Handles bulk creation as a single undoable operation
|
||||
*/
|
||||
export class CreateNotesCommand extends KGCommand {
|
||||
private noteCreationData: NoteCreationData[];
|
||||
private createdNotes: Array<{
|
||||
note: KGMidiNote;
|
||||
regionId: string;
|
||||
}> = [];
|
||||
|
||||
constructor(noteCreationData: NoteCreationData[]) {
|
||||
super();
|
||||
this.noteCreationData = noteCreationData.map(data => ({
|
||||
...data,
|
||||
noteId: data.noteId || generateUniqueId('KGMidiNote')
|
||||
}));
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Clear any existing created note data to prevent duplicates on re-execution
|
||||
this.createdNotes = [];
|
||||
|
||||
// Create all notes
|
||||
for (const noteData of this.noteCreationData) {
|
||||
// Find the target region
|
||||
let targetRegion: KGMidiRegion | null = null;
|
||||
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === noteData.regionId);
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
targetRegion = region;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRegion) {
|
||||
throw new Error(`MIDI region with ID ${noteData.regionId} not found`);
|
||||
}
|
||||
|
||||
// Create the new MIDI note
|
||||
const newNote = new KGMidiNote(
|
||||
noteData.noteId!,
|
||||
noteData.startBeat,
|
||||
noteData.endBeat,
|
||||
noteData.pitch,
|
||||
noteData.velocity
|
||||
);
|
||||
|
||||
// Add the note to the region
|
||||
targetRegion.addNote(newNote);
|
||||
|
||||
// Store for undo
|
||||
this.createdNotes.push({
|
||||
note: newNote,
|
||||
regionId: noteData.regionId
|
||||
});
|
||||
}
|
||||
|
||||
const noteCount = this.createdNotes.length;
|
||||
const regionCount = new Set(this.createdNotes.map(data => data.regionId)).size;
|
||||
console.log(`Created ${noteCount} notes in ${regionCount} region${regionCount > 1 ? 's' : ''}`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.createdNotes.length === 0) {
|
||||
throw new Error('Cannot undo: no notes were created');
|
||||
}
|
||||
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Remove all created notes from their regions
|
||||
for (const data of this.createdNotes) {
|
||||
// Find the region
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === data.regionId);
|
||||
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
region.removeNote(data.note.getId());
|
||||
|
||||
// Clear selection if this note was selected
|
||||
const selectedItems = core.getSelectedItems();
|
||||
const selectedNote = selectedItems.find(item =>
|
||||
item instanceof KGMidiNote && item.getId() === data.note.getId()
|
||||
);
|
||||
if (selectedNote) {
|
||||
core.removeSelectedItem(selectedNote);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Removed ${this.createdNotes.length} created notes from ${new Set(this.createdNotes.map(d => d.regionId)).size} regions`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if (this.noteCreationData.length === 1) {
|
||||
const noteData = this.noteCreationData[0];
|
||||
// Convert MIDI pitch to note name for user-friendly description
|
||||
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
const octave = Math.floor(noteData.pitch / 12) - 1;
|
||||
const noteName = noteNames[noteData.pitch % 12];
|
||||
return `Create note ${noteName}${octave}`;
|
||||
}
|
||||
return `Create ${this.noteCreationData.length} notes`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note creation data that was/will be processed
|
||||
*/
|
||||
public getNoteCreationData(): NoteCreationData[] {
|
||||
return this.noteCreationData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the created note instances (only available after execute)
|
||||
*/
|
||||
public getCreatedNotes(): Array<{note: KGMidiNote; regionId: string}> {
|
||||
return this.createdNotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the regions that were affected by this creation
|
||||
*/
|
||||
public getAffectedRegionIds(): string[] {
|
||||
return Array.from(new Set(this.noteCreationData.map(data => data.regionId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IDs of notes that were/will be created
|
||||
*/
|
||||
public getCreatedNoteIds(): string[] {
|
||||
return this.noteCreationData.map(data => data.noteId!);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to create a single MIDI note (convenience wrapper)
|
||||
*/
|
||||
export class CreateNoteCommand extends CreateNotesCommand {
|
||||
constructor(
|
||||
regionId: string,
|
||||
startBeat: number,
|
||||
endBeat: number,
|
||||
pitch: number,
|
||||
velocity: number = 127,
|
||||
noteId?: string
|
||||
) {
|
||||
super([{
|
||||
regionId,
|
||||
startBeat,
|
||||
endBeat,
|
||||
pitch,
|
||||
velocity,
|
||||
noteId
|
||||
}]);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const noteData = this.getNoteCreationData()[0];
|
||||
// Convert MIDI pitch to note name for user-friendly description
|
||||
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
const octave = Math.floor(noteData.pitch / 12) - 1;
|
||||
const noteName = noteNames[noteData.pitch % 12];
|
||||
return `Create note ${noteName}${octave}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the note that was/will be created
|
||||
*/
|
||||
public getNoteId(): string {
|
||||
return this.getCreatedNoteIds()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the created note instance (only available after execute)
|
||||
*/
|
||||
public getCreatedNote(): KGMidiNote | null {
|
||||
const createdNotes = this.getCreatedNotes();
|
||||
return createdNotes.length > 0 ? createdNotes[0].note : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the region ID where the note was/will be created
|
||||
*/
|
||||
public getRegionId(): string {
|
||||
return this.getNoteCreationData()[0].regionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a note command from UI coordinates
|
||||
*/
|
||||
public static fromUICoordinates(
|
||||
regionId: string,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
pianoGridElement: HTMLElement,
|
||||
beatWidth: number,
|
||||
noteHeight: number,
|
||||
regionStartBeat: number,
|
||||
noteLength: number,
|
||||
velocity: number = 127
|
||||
): CreateNoteCommand {
|
||||
// Calculate relative position within the grid
|
||||
const rect = pianoGridElement.getBoundingClientRect();
|
||||
const x = mouseX - rect.left;
|
||||
const y = mouseY - rect.top;
|
||||
|
||||
// Calculate the beat number (relative to region start)
|
||||
const rawBeatNumber = x / beatWidth;
|
||||
const beatNumber = Math.floor(rawBeatNumber); // Snap to beat grid
|
||||
|
||||
// Calculate the pitch (MIDI note number)
|
||||
// The piano roll is drawn from bottom to top, with higher notes at the top
|
||||
const pitchIndex = Math.floor(y / noteHeight);
|
||||
const pitch = 107 - pitchIndex; // Convert index to pitch (B7 is 107)
|
||||
|
||||
// Calculate note start and end beats
|
||||
const noteStartBeat = beatNumber;
|
||||
const noteEndBeat = noteStartBeat + noteLength;
|
||||
|
||||
return new CreateNoteCommand(
|
||||
regionId,
|
||||
noteStartBeat,
|
||||
noteEndBeat,
|
||||
pitch,
|
||||
velocity
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
|
||||
/**
|
||||
* Command to delete multiple MIDI notes from regions
|
||||
* Handles bulk deletion as a single undoable operation
|
||||
*/
|
||||
export class DeleteNotesCommand extends KGCommand {
|
||||
private noteIds: string[];
|
||||
private deletedNoteData: Array<{
|
||||
note: KGMidiNote;
|
||||
regionId: string;
|
||||
originalIndex: number;
|
||||
}> = [];
|
||||
|
||||
constructor(noteIds: string[]) {
|
||||
super();
|
||||
this.noteIds = noteIds;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Clear any existing deleted note data to prevent duplicates on re-execution
|
||||
this.deletedNoteData = [];
|
||||
|
||||
// Find and store all notes to delete
|
||||
for (const noteId of this.noteIds) {
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
|
||||
for (const region of regions) {
|
||||
if (region instanceof KGMidiRegion) {
|
||||
const notes = region.getNotes();
|
||||
const noteIndex = notes.findIndex(note => note.getId() === noteId);
|
||||
|
||||
if (noteIndex !== -1) {
|
||||
const noteToDelete = notes[noteIndex];
|
||||
|
||||
// Store for undo
|
||||
this.deletedNoteData.push({
|
||||
note: noteToDelete,
|
||||
regionId: region.getId(),
|
||||
originalIndex: noteIndex
|
||||
});
|
||||
|
||||
break; // Found the note, move to next noteId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.deletedNoteData.length === 0) {
|
||||
throw new Error('No notes found to delete');
|
||||
}
|
||||
|
||||
// Sort by original index in descending order to maintain correct indices during deletion
|
||||
this.deletedNoteData.sort((a, b) => b.originalIndex - a.originalIndex);
|
||||
|
||||
// Delete all notes from their regions
|
||||
for (const data of this.deletedNoteData) {
|
||||
// Find the region again
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === data.regionId);
|
||||
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
region.removeNote(data.note.getId());
|
||||
|
||||
// Clear selection if this note was selected
|
||||
const selectedItems = core.getSelectedItems();
|
||||
const selectedNote = selectedItems.find(item =>
|
||||
item instanceof KGMidiNote && item.getId() === data.note.getId()
|
||||
);
|
||||
if (selectedNote) {
|
||||
core.removeSelectedItem(selectedNote);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Deleted ${this.deletedNoteData.length} notes from ${new Set(this.deletedNoteData.map(d => d.regionId)).size} regions`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.deletedNoteData.length === 0) {
|
||||
throw new Error('Cannot undo: no note data stored');
|
||||
}
|
||||
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Restore all notes in reverse order (original index ascending)
|
||||
const sortedData = [...this.deletedNoteData].sort((a, b) => a.originalIndex - b.originalIndex);
|
||||
|
||||
for (const data of sortedData) {
|
||||
// Find the target region
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === data.regionId);
|
||||
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
const notes = region.getNotes();
|
||||
|
||||
// Insert note at original position
|
||||
if (data.originalIndex >= 0 && data.originalIndex <= notes.length) {
|
||||
notes.splice(data.originalIndex, 0, data.note);
|
||||
region.setNotes(notes);
|
||||
} else {
|
||||
// Fallback: add to the end
|
||||
region.addNote(data.note);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Restored ${this.deletedNoteData.length} notes to their original positions`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if (this.deletedNoteData.length === 1) {
|
||||
const note = this.deletedNoteData[0].note;
|
||||
// Convert MIDI pitch to note name for user-friendly description
|
||||
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
const octave = Math.floor(note.getPitch() / 12) - 1;
|
||||
const noteName = noteNames[note.getPitch() % 12];
|
||||
return `Delete note ${noteName}${octave}`;
|
||||
}
|
||||
return `Delete ${this.noteIds.length} notes`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IDs of notes that were/will be deleted
|
||||
*/
|
||||
public getNoteIds(): string[] {
|
||||
return this.noteIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the deleted note data (only available after execute)
|
||||
*/
|
||||
public getDeletedNoteData(): Array<{note: KGMidiNote; regionId: string; originalIndex: number}> {
|
||||
return this.deletedNoteData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the regions that were affected by this deletion
|
||||
*/
|
||||
public getAffectedRegionIds(): string[] {
|
||||
return Array.from(new Set(this.deletedNoteData.map(data => data.regionId)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to delete a single MIDI note (convenience wrapper)
|
||||
*/
|
||||
export class DeleteNoteCommand extends DeleteNotesCommand {
|
||||
constructor(noteId: string) {
|
||||
super([noteId]);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if (this.getDeletedNoteData().length === 1) {
|
||||
const note = this.getDeletedNoteData()[0].note;
|
||||
// Convert MIDI pitch to note name for user-friendly description
|
||||
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
const octave = Math.floor(note.getPitch() / 12) - 1;
|
||||
const noteName = noteNames[note.getPitch() % 12];
|
||||
return `Delete note ${noteName}${octave}`;
|
||||
}
|
||||
return 'Delete note';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
|
||||
/**
|
||||
* Interface for storing move data for each note
|
||||
*/
|
||||
interface NoteMoveData {
|
||||
noteId: string;
|
||||
originalStartBeat: number;
|
||||
originalEndBeat: number;
|
||||
originalPitch: number;
|
||||
newStartBeat: number;
|
||||
newEndBeat: number;
|
||||
newPitch: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to move multiple notes simultaneously
|
||||
* Handles moving notes in both time (beat position) and pitch with delta application
|
||||
*/
|
||||
export class MoveNotesCommand extends KGCommand {
|
||||
private primaryNoteId: string;
|
||||
private startBeatDelta: number;
|
||||
private pitchDelta: number;
|
||||
private regionId: string;
|
||||
private noteIdsToMove: string[]; // Store the specific note IDs to move
|
||||
private noteMoveData: NoteMoveData[] = [];
|
||||
private targetRegion: KGMidiRegion | null = null;
|
||||
private parentTrack: KGTrack | null = null;
|
||||
|
||||
constructor(
|
||||
primaryNoteId: string,
|
||||
startBeatDelta: number,
|
||||
pitchDelta: number,
|
||||
regionId: string,
|
||||
noteIdsToMove: string[]
|
||||
) {
|
||||
super();
|
||||
this.primaryNoteId = primaryNoteId;
|
||||
this.startBeatDelta = startBeatDelta;
|
||||
this.pitchDelta = pitchDelta;
|
||||
this.regionId = regionId;
|
||||
this.noteIdsToMove = [...noteIdsToMove]; // Create a copy to avoid reference issues
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Find the region containing the notes
|
||||
let targetRegion: KGMidiRegion | null = null;
|
||||
let parentTrack: KGTrack | null = null;
|
||||
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === this.regionId) as KGMidiRegion | undefined;
|
||||
if (region) {
|
||||
targetRegion = region;
|
||||
parentTrack = track;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRegion || !parentTrack) {
|
||||
throw new Error(`Region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
this.targetRegion = targetRegion;
|
||||
this.parentTrack = parentTrack;
|
||||
|
||||
// Clear any existing move data (for re-execution)
|
||||
this.noteMoveData = [];
|
||||
|
||||
// Find all notes to move based on the stored note IDs
|
||||
const notesToMove = this.noteIdsToMove
|
||||
.map(noteId => targetRegion.getNotes().find(n => n.getId() === noteId))
|
||||
.filter(note => note !== undefined) as KGMidiNote[];
|
||||
|
||||
if (notesToMove.length === 0) {
|
||||
throw new Error(`No notes found to move from the provided note IDs`);
|
||||
}
|
||||
|
||||
// Verify the primary note is included
|
||||
const primaryNote = notesToMove.find(n => n.getId() === this.primaryNoteId);
|
||||
if (!primaryNote) {
|
||||
throw new Error(`Primary note with ID ${this.primaryNoteId} not found in notes to move`);
|
||||
}
|
||||
|
||||
// Store original positions and calculate new positions
|
||||
notesToMove.forEach(note => {
|
||||
const originalStartBeat = note.getStartBeat();
|
||||
const originalEndBeat = note.getEndBeat();
|
||||
const originalPitch = note.getPitch();
|
||||
|
||||
// Apply the deltas to calculate new positions
|
||||
const newStartBeat = originalStartBeat + this.startBeatDelta;
|
||||
const newEndBeat = originalEndBeat + this.startBeatDelta; // End beat moves by same amount as start
|
||||
const newPitch = originalPitch + this.pitchDelta;
|
||||
|
||||
// Store move data for undo
|
||||
this.noteMoveData.push({
|
||||
noteId: note.getId(),
|
||||
originalStartBeat,
|
||||
originalEndBeat,
|
||||
originalPitch,
|
||||
newStartBeat,
|
||||
newEndBeat,
|
||||
newPitch
|
||||
});
|
||||
});
|
||||
|
||||
// Apply all the move operations
|
||||
this.noteMoveData.forEach(data => {
|
||||
const note = targetRegion.getNotes().find(n => n.getId() === data.noteId);
|
||||
if (note) {
|
||||
note.setStartBeat(data.newStartBeat);
|
||||
note.setEndBeat(data.newEndBeat);
|
||||
note.setPitch(data.newPitch);
|
||||
}
|
||||
});
|
||||
|
||||
const beatDescription = this.startBeatDelta !== 0 ? `position: ${this.startBeatDelta > 0 ? '+' : ''}${this.startBeatDelta.toFixed(3)} beats` : '';
|
||||
const pitchDescription = this.pitchDelta !== 0 ? `pitch: ${this.pitchDelta > 0 ? '+' : ''}${this.pitchDelta} semitones` : '';
|
||||
const deltaDescription = [beatDescription, pitchDescription].filter(d => d).join(', ');
|
||||
|
||||
console.log(`Moved ${this.noteMoveData.length} notes (${deltaDescription}, primary note: ${this.primaryNoteId})`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.targetRegion || this.noteMoveData.length === 0) {
|
||||
throw new Error('Cannot undo: no notes were moved');
|
||||
}
|
||||
|
||||
// Restore all notes to their original positions
|
||||
this.noteMoveData.forEach(data => {
|
||||
const note = this.targetRegion!.getNotes().find(n => n.getId() === data.noteId);
|
||||
if (note) {
|
||||
note.setStartBeat(data.originalStartBeat);
|
||||
note.setEndBeat(data.originalEndBeat);
|
||||
note.setPitch(data.originalPitch);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Restored ${this.noteMoveData.length} notes to their original positions`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const noteCount = this.noteMoveData.length;
|
||||
|
||||
// Create a description based on the type of movement
|
||||
const movements: string[] = [];
|
||||
if (this.startBeatDelta !== 0) {
|
||||
const direction = this.startBeatDelta > 0 ? 'right' : 'left';
|
||||
movements.push(`${direction} ${Math.abs(this.startBeatDelta).toFixed(3)} beats`);
|
||||
}
|
||||
if (this.pitchDelta !== 0) {
|
||||
const direction = this.pitchDelta > 0 ? 'up' : 'down';
|
||||
movements.push(`${direction} ${Math.abs(this.pitchDelta)} semitones`);
|
||||
}
|
||||
|
||||
const movementDescription = movements.length > 0 ? ` ${movements.join(' and ')}` : '';
|
||||
|
||||
if (noteCount === 1) {
|
||||
return `Move note${movementDescription}`;
|
||||
} else {
|
||||
return `Move ${noteCount} notes${movementDescription}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary note ID
|
||||
*/
|
||||
public getPrimaryNoteId(): string {
|
||||
return this.primaryNoteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start beat delta
|
||||
*/
|
||||
public getStartBeatDelta(): number {
|
||||
return this.startBeatDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pitch delta
|
||||
*/
|
||||
public getPitchDelta(): number {
|
||||
return this.pitchDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the region ID
|
||||
*/
|
||||
public getRegionId(): string {
|
||||
return this.regionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note IDs that will be moved
|
||||
*/
|
||||
public getNoteIdsToMove(): string[] {
|
||||
return [...this.noteIdsToMove];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note move data (only available after execute)
|
||||
*/
|
||||
public getNoteMoveData(): NoteMoveData[] {
|
||||
return [...this.noteMoveData];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target region instance (only available after execute)
|
||||
*/
|
||||
public getTargetRegion(): KGMidiRegion | null {
|
||||
return this.targetRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent track instance (only available after execute)
|
||||
*/
|
||||
public getParentTrack(): KGTrack | null {
|
||||
return this.parentTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a move command from note drag parameters
|
||||
*/
|
||||
public static fromNoteDrag(
|
||||
primaryNoteId: string,
|
||||
originalStartBeat: number,
|
||||
originalPitch: number,
|
||||
newStartBeat: number,
|
||||
newPitch: number,
|
||||
regionId: string,
|
||||
noteIdsToMove: string[]
|
||||
): MoveNotesCommand {
|
||||
const startBeatDelta = newStartBeat - originalStartBeat;
|
||||
const pitchDelta = newPitch - originalPitch;
|
||||
|
||||
return new MoveNotesCommand(
|
||||
primaryNoteId,
|
||||
startBeatDelta,
|
||||
pitchDelta,
|
||||
regionId,
|
||||
noteIdsToMove
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
|
||||
/**
|
||||
* Interface for storing created note data for undo
|
||||
*/
|
||||
interface CreatedNoteData {
|
||||
noteId: string;
|
||||
startBeat: number;
|
||||
endBeat: number;
|
||||
pitch: number;
|
||||
velocity: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to paste notes to a target region
|
||||
* Handles creating new notes with new IDs and maintaining relative positions
|
||||
*/
|
||||
export class PasteNotesCommand extends KGCommand {
|
||||
private regionId: string;
|
||||
private pastePosition: number;
|
||||
private sourceNotes: KGMidiNote[];
|
||||
private createdNotes: CreatedNoteData[] = [];
|
||||
private targetRegion: KGMidiRegion | null = null;
|
||||
private parentTrack: KGTrack | null = null;
|
||||
|
||||
constructor(regionId: string, pastePosition: number, sourceNotes: KGMidiNote[]) {
|
||||
super();
|
||||
this.regionId = regionId;
|
||||
this.pastePosition = pastePosition;
|
||||
this.sourceNotes = [...sourceNotes]; // Create a copy to avoid reference issues
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
if (this.sourceNotes.length === 0) {
|
||||
throw new Error('No notes to paste');
|
||||
}
|
||||
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Find the target region
|
||||
let targetRegion: KGMidiRegion | null = null;
|
||||
let parentTrack: KGTrack | null = null;
|
||||
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === this.regionId);
|
||||
if (region && region instanceof KGMidiRegion) {
|
||||
targetRegion = region;
|
||||
parentTrack = track;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRegion || !parentTrack) {
|
||||
throw new Error(`Target MIDI region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
this.targetRegion = targetRegion;
|
||||
this.parentTrack = parentTrack;
|
||||
|
||||
// Clear any previously created notes (for re-execution)
|
||||
this.createdNotes = [];
|
||||
|
||||
// Calculate relative position within the region
|
||||
const relativeStartPosition = this.pastePosition - targetRegion.getStartFromBeat();
|
||||
|
||||
// Calculate the base position from the first note to maintain relative positions
|
||||
const basePosition = this.sourceNotes[0].getStartBeat();
|
||||
|
||||
// Create new notes at the target position
|
||||
this.sourceNotes.forEach((originalNote) => {
|
||||
// Generate new ID for the pasted note
|
||||
const newId = generateUniqueId('KGMidiNote');
|
||||
|
||||
// Calculate new position maintaining relative offset
|
||||
const noteOffset = originalNote.getStartBeat() - basePosition;
|
||||
const newStartBeat = relativeStartPosition + noteOffset;
|
||||
const duration = originalNote.getEndBeat() - originalNote.getStartBeat();
|
||||
const newEndBeat = newStartBeat + duration;
|
||||
|
||||
// Create a copy of the note with new position and ID
|
||||
const newNote = new KGMidiNote(
|
||||
newId,
|
||||
newStartBeat,
|
||||
newEndBeat,
|
||||
originalNote.getPitch(),
|
||||
originalNote.getVelocity()
|
||||
);
|
||||
|
||||
// Store created note data for undo
|
||||
this.createdNotes.push({
|
||||
noteId: newId,
|
||||
startBeat: newStartBeat,
|
||||
endBeat: newEndBeat,
|
||||
pitch: originalNote.getPitch(),
|
||||
velocity: originalNote.getVelocity()
|
||||
});
|
||||
|
||||
// Add the new note to the target region
|
||||
targetRegion.addNote(newNote);
|
||||
});
|
||||
|
||||
console.log(`Pasted ${this.sourceNotes.length} notes to region ${this.regionId} at position ${this.pastePosition}`);
|
||||
console.log(`Region now has ${targetRegion.getNotes().length} notes total`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.targetRegion || this.createdNotes.length === 0) {
|
||||
throw new Error('Cannot undo: no notes were pasted');
|
||||
}
|
||||
|
||||
// Remove all created notes from the target region
|
||||
const currentNotes = this.targetRegion.getNotes();
|
||||
const noteIdsToRemove = new Set(this.createdNotes.map(data => data.noteId));
|
||||
|
||||
const filteredNotes = currentNotes.filter(note => !noteIdsToRemove.has(note.getId()));
|
||||
this.targetRegion.setNotes(filteredNotes);
|
||||
|
||||
console.log(`Removed ${this.createdNotes.length} pasted notes from region ${this.regionId}`);
|
||||
console.log(`Region now has ${this.targetRegion.getNotes().length} notes total`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const noteCount = this.sourceNotes.length;
|
||||
const regionName = this.targetRegion ? this.targetRegion.getName() : `Region ${this.regionId}`;
|
||||
|
||||
if (noteCount === 1) {
|
||||
return `Paste note to "${regionName}"`;
|
||||
} else {
|
||||
return `Paste ${noteCount} notes to "${regionName}"`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target region ID
|
||||
*/
|
||||
public getRegionId(): string {
|
||||
return this.regionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the paste position
|
||||
*/
|
||||
public getPastePosition(): number {
|
||||
return this.pastePosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source notes being pasted
|
||||
*/
|
||||
public getSourceNotes(): KGMidiNote[] {
|
||||
return [...this.sourceNotes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the created note data (only available after execute)
|
||||
*/
|
||||
public getCreatedNotes(): CreatedNoteData[] {
|
||||
return [...this.createdNotes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target region instance (only available after execute)
|
||||
*/
|
||||
public getTargetRegion(): KGMidiRegion | null {
|
||||
return this.targetRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent track instance (only available after execute)
|
||||
*/
|
||||
public getParentTrack(): KGTrack | null {
|
||||
return this.parentTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a paste command from the clipboard
|
||||
*/
|
||||
public static fromClipboard(regionId: string, pastePosition: number): PasteNotesCommand | null {
|
||||
const core = KGCore.instance();
|
||||
const copiedItems = core.getCopiedItems();
|
||||
const notesToCreate = copiedItems.filter(item => item instanceof KGMidiNote) as KGMidiNote[];
|
||||
|
||||
if (notesToCreate.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PasteNotesCommand(regionId, pastePosition, notesToCreate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a paste command from specific notes
|
||||
*/
|
||||
public static fromNotes(regionId: string, pastePosition: number, notes: KGMidiNote[]): PasteNotesCommand {
|
||||
return new PasteNotesCommand(regionId, pastePosition, notes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiNote } from '../../midi/KGMidiNote';
|
||||
import { KGMidiRegion } from '../../region/KGMidiRegion';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
|
||||
/**
|
||||
* Interface for storing resize data for each note
|
||||
*/
|
||||
interface NoteResizeData {
|
||||
noteId: string;
|
||||
originalStartBeat: number;
|
||||
originalEndBeat: number;
|
||||
newStartBeat: number;
|
||||
newEndBeat: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to resize multiple notes simultaneously
|
||||
* Handles resizing notes from either start or end edge with delta application
|
||||
*/
|
||||
export class ResizeNotesCommand extends KGCommand {
|
||||
private primaryNoteId: string;
|
||||
private resizeEdge: 'start' | 'end';
|
||||
private primaryStartBeatDelta: number;
|
||||
private primaryEndBeatDelta: number;
|
||||
private regionId: string;
|
||||
private noteIdsToResize: string[]; // Store the specific note IDs to resize
|
||||
private noteResizeData: NoteResizeData[] = [];
|
||||
private targetRegion: KGMidiRegion | null = null;
|
||||
private parentTrack: KGTrack | null = null;
|
||||
|
||||
constructor(
|
||||
primaryNoteId: string,
|
||||
resizeEdge: 'start' | 'end',
|
||||
primaryStartBeatDelta: number,
|
||||
primaryEndBeatDelta: number,
|
||||
regionId: string,
|
||||
noteIdsToResize: string[]
|
||||
) {
|
||||
super();
|
||||
this.primaryNoteId = primaryNoteId;
|
||||
this.resizeEdge = resizeEdge;
|
||||
this.primaryStartBeatDelta = primaryStartBeatDelta;
|
||||
this.primaryEndBeatDelta = primaryEndBeatDelta;
|
||||
this.regionId = regionId;
|
||||
this.noteIdsToResize = [...noteIdsToResize]; // Create a copy to avoid reference issues
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Find the region containing the notes
|
||||
let targetRegion: KGMidiRegion | null = null;
|
||||
let parentTrack: KGTrack | null = null;
|
||||
|
||||
for (const track of tracks) {
|
||||
const regions = track.getRegions();
|
||||
const region = regions.find(r => r.getId() === this.regionId) as KGMidiRegion | undefined;
|
||||
if (region) {
|
||||
targetRegion = region;
|
||||
parentTrack = track;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRegion || !parentTrack) {
|
||||
throw new Error(`Region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
this.targetRegion = targetRegion;
|
||||
this.parentTrack = parentTrack;
|
||||
|
||||
// Clear any existing resize data (for re-execution)
|
||||
this.noteResizeData = [];
|
||||
|
||||
// Find all notes to resize based on the stored note IDs
|
||||
const notesToResize = this.noteIdsToResize
|
||||
.map(noteId => targetRegion.getNotes().find(n => n.getId() === noteId))
|
||||
.filter(note => note !== undefined) as KGMidiNote[];
|
||||
|
||||
if (notesToResize.length === 0) {
|
||||
throw new Error(`No notes found to resize from the provided note IDs`);
|
||||
}
|
||||
|
||||
// Verify the primary note is included
|
||||
const primaryNote = notesToResize.find(n => n.getId() === this.primaryNoteId);
|
||||
if (!primaryNote) {
|
||||
throw new Error(`Primary note with ID ${this.primaryNoteId} not found in notes to resize`);
|
||||
}
|
||||
|
||||
// Store original positions and calculate new positions
|
||||
notesToResize.forEach(note => {
|
||||
const originalStartBeat = note.getStartBeat();
|
||||
const originalEndBeat = note.getEndBeat();
|
||||
let newStartBeat = originalStartBeat;
|
||||
let newEndBeat = originalEndBeat;
|
||||
|
||||
if (note.getId() === this.primaryNoteId) {
|
||||
// Primary note: apply the exact deltas
|
||||
newStartBeat = originalStartBeat + this.primaryStartBeatDelta;
|
||||
newEndBeat = originalEndBeat + this.primaryEndBeatDelta;
|
||||
} else {
|
||||
// Other selected notes: apply the same delta as primary note
|
||||
if (this.resizeEdge === 'start') {
|
||||
newStartBeat = originalStartBeat + this.primaryStartBeatDelta;
|
||||
// End beat stays the same for start resize
|
||||
} else if (this.resizeEdge === 'end') {
|
||||
newEndBeat = originalEndBeat + this.primaryEndBeatDelta;
|
||||
// Start beat stays the same for end resize
|
||||
}
|
||||
}
|
||||
|
||||
// Store resize data for undo
|
||||
this.noteResizeData.push({
|
||||
noteId: note.getId(),
|
||||
originalStartBeat,
|
||||
originalEndBeat,
|
||||
newStartBeat,
|
||||
newEndBeat
|
||||
});
|
||||
});
|
||||
|
||||
// Apply all the resize operations
|
||||
this.noteResizeData.forEach(data => {
|
||||
const note = targetRegion.getNotes().find(n => n.getId() === data.noteId);
|
||||
if (note) {
|
||||
note.setStartBeat(data.newStartBeat);
|
||||
note.setEndBeat(data.newEndBeat);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Resized ${this.noteResizeData.length} notes (edge: ${this.resizeEdge}, primary note: ${this.primaryNoteId})`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.targetRegion || this.noteResizeData.length === 0) {
|
||||
throw new Error('Cannot undo: no notes were resized');
|
||||
}
|
||||
|
||||
// Restore all notes to their original positions
|
||||
this.noteResizeData.forEach(data => {
|
||||
const note = this.targetRegion!.getNotes().find(n => n.getId() === data.noteId);
|
||||
if (note) {
|
||||
note.setStartBeat(data.originalStartBeat);
|
||||
note.setEndBeat(data.originalEndBeat);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Restored ${this.noteResizeData.length} notes to their original positions`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const noteCount = this.noteResizeData.length;
|
||||
const edgeText = this.resizeEdge === 'start' ? 'start' : 'end';
|
||||
|
||||
if (noteCount === 1) {
|
||||
return `Resize note from ${edgeText}`;
|
||||
} else {
|
||||
return `Resize ${noteCount} notes from ${edgeText}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary note ID
|
||||
*/
|
||||
public getPrimaryNoteId(): string {
|
||||
return this.primaryNoteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resize edge
|
||||
*/
|
||||
public getResizeEdge(): 'start' | 'end' {
|
||||
return this.resizeEdge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the region ID
|
||||
*/
|
||||
public getRegionId(): string {
|
||||
return this.regionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note resize data (only available after execute)
|
||||
*/
|
||||
public getNoteResizeData(): NoteResizeData[] {
|
||||
return [...this.noteResizeData];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target region instance (only available after execute)
|
||||
*/
|
||||
public getTargetRegion(): KGMidiRegion | null {
|
||||
return this.targetRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent track instance (only available after execute)
|
||||
*/
|
||||
public getParentTrack(): KGTrack | null {
|
||||
return this.parentTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a resize command from note resize parameters
|
||||
*/
|
||||
public static fromNoteResize(
|
||||
primaryNoteId: string,
|
||||
resizeEdge: 'start' | 'end',
|
||||
originalStartBeat: number,
|
||||
originalEndBeat: number,
|
||||
newStartBeat: number,
|
||||
newEndBeat: number,
|
||||
regionId: string,
|
||||
noteIdsToResize: string[]
|
||||
): ResizeNotesCommand {
|
||||
const startBeatDelta = newStartBeat - originalStartBeat;
|
||||
const endBeatDelta = newEndBeat - originalEndBeat;
|
||||
|
||||
return new ResizeNotesCommand(
|
||||
primaryNoteId,
|
||||
resizeEdge,
|
||||
startBeatDelta,
|
||||
endBeatDelta,
|
||||
regionId,
|
||||
noteIdsToResize
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note IDs that will be resized
|
||||
*/
|
||||
public getNoteIdsToResize(): string[] {
|
||||
return [...this.noteIdsToResize];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user