253 lines
7.3 KiB
TypeScript
253 lines
7.3 KiB
TypeScript
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
|
|
);
|
|
}
|
|
} |