feat: added tool write_chord_progression
This commit is contained in:
@@ -9,6 +9,7 @@ import { MoveGlobalRegionCommand } from './MoveGlobalRegionCommand';
|
||||
import { ReplaceChordRegionsInRangeCommand } from './ReplaceChordRegionsInRangeCommand';
|
||||
import { ResizeGlobalRegionCommand } from './ResizeGlobalRegionCommand';
|
||||
import { UpdateChordRegionCommand } from './UpdateChordRegionCommand';
|
||||
import { WriteChordProgressionCommand } from './WriteChordProgressionCommand';
|
||||
|
||||
describe('global chord region commands', () => {
|
||||
beforeEach(() => {
|
||||
@@ -147,4 +148,86 @@ describe('global chord region commands', () => {
|
||||
{ symbol: 'F', start: 8, length: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('writes a chord into the middle of an existing region and preserves both sides', () => {
|
||||
const chordTrack = getChordTrack();
|
||||
chordTrack.setRegions([
|
||||
new KGChordRegion('base', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 8),
|
||||
]);
|
||||
|
||||
const command = new WriteChordProgressionCommand([
|
||||
{ startBeat: 3, length: 2, symbol: 'C' },
|
||||
]);
|
||||
|
||||
command.execute();
|
||||
|
||||
expect((getChordTrack().getRegions() as KGChordRegion[]).map(region => ({
|
||||
symbol: region.getSymbol(),
|
||||
start: region.getStartFromBeat(),
|
||||
length: region.getLength(),
|
||||
}))).toEqual([
|
||||
{ symbol: 'Am', start: 0, length: 3 },
|
||||
{ symbol: 'C', start: 3, length: 2 },
|
||||
{ symbol: 'Am', start: 5, length: 3 },
|
||||
]);
|
||||
|
||||
command.undo();
|
||||
|
||||
expect((getChordTrack().getRegions() as KGChordRegion[]).map(region => ({
|
||||
symbol: region.getSymbol(),
|
||||
start: region.getStartFromBeat(),
|
||||
length: region.getLength(),
|
||||
}))).toEqual([
|
||||
{ symbol: 'Am', start: 0, length: 8 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('writes multiple non-contiguous chord spans while preserving untouched gaps', () => {
|
||||
const chordTrack = getChordTrack();
|
||||
chordTrack.setRegions([
|
||||
new KGChordRegion('left', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 12),
|
||||
]);
|
||||
|
||||
const command = new WriteChordProgressionCommand([
|
||||
{ startBeat: 2, length: 2, symbol: 'C' },
|
||||
{ startBeat: 8, length: 2, symbol: 'G' },
|
||||
]);
|
||||
|
||||
command.execute();
|
||||
|
||||
expect((getChordTrack().getRegions() as KGChordRegion[]).map(region => ({
|
||||
symbol: region.getSymbol(),
|
||||
start: region.getStartFromBeat(),
|
||||
length: region.getLength(),
|
||||
}))).toEqual([
|
||||
{ symbol: 'Am', start: 0, length: 2 },
|
||||
{ symbol: 'C', start: 2, length: 2 },
|
||||
{ symbol: 'Am', start: 4, length: 4 },
|
||||
{ symbol: 'G', start: 8, length: 2 },
|
||||
{ symbol: 'Am', start: 10, length: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('writes adjacent chord spans without introducing overlap', () => {
|
||||
const chordTrack = getChordTrack();
|
||||
chordTrack.setRegions([
|
||||
new KGChordRegion('base', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 8),
|
||||
]);
|
||||
|
||||
const command = new WriteChordProgressionCommand([
|
||||
{ startBeat: 0, length: 4, symbol: 'C' },
|
||||
{ startBeat: 4, length: 4, symbol: 'F' },
|
||||
]);
|
||||
|
||||
command.execute();
|
||||
|
||||
expect((getChordTrack().getRegions() as KGChordRegion[]).map(region => ({
|
||||
symbol: region.getSymbol(),
|
||||
start: region.getStartFromBeat(),
|
||||
length: region.getLength(),
|
||||
}))).toEqual([
|
||||
{ symbol: 'C', start: 0, length: 4 },
|
||||
{ symbol: 'F', start: 4, length: 4 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGChordRegion } from '../../region/KGChordRegion';
|
||||
import { findGlobalTrackByType } from '../../../util/globalTrackUtil';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
|
||||
export interface WriteChordProgressionEntry {
|
||||
startBeat: number;
|
||||
length: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
function cloneChordRegion(region: KGChordRegion): KGChordRegion {
|
||||
return new KGChordRegion(
|
||||
region.getId(),
|
||||
region.getTrackId(),
|
||||
region.getTrackIndex(),
|
||||
region.getSymbol(),
|
||||
region.getStartFromBeat(),
|
||||
region.getLength(),
|
||||
);
|
||||
}
|
||||
|
||||
function cloneChordRegions(regions: KGChordRegion[]): KGChordRegion[] {
|
||||
return regions.map(cloneChordRegion);
|
||||
}
|
||||
|
||||
export class WriteChordProgressionCommand extends KGCommand {
|
||||
private readonly replacements: WriteChordProgressionEntry[];
|
||||
private originalRegions: KGChordRegion[] | null = null;
|
||||
private nextRegions: KGChordRegion[] | null = null;
|
||||
|
||||
constructor(replacements: WriteChordProgressionEntry[]) {
|
||||
super();
|
||||
this.replacements = replacements.map(replacement => ({
|
||||
startBeat: replacement.startBeat,
|
||||
length: replacement.length,
|
||||
symbol: replacement.symbol,
|
||||
}));
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
if (!chordTrack) {
|
||||
throw new Error('Chord global track not found');
|
||||
}
|
||||
|
||||
if (this.nextRegions) {
|
||||
chordTrack.setRegions(cloneChordRegions(this.nextRegions));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRegions = chordTrack.getRegions()
|
||||
.filter((region): region is KGChordRegion => region instanceof KGChordRegion)
|
||||
.sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat());
|
||||
|
||||
const sortedReplacements = [...this.replacements].sort((left, right) => left.startBeat - right.startBeat);
|
||||
this.originalRegions = cloneChordRegions(currentRegions);
|
||||
|
||||
const preservedRegions: KGChordRegion[] = [];
|
||||
for (const region of currentRegions) {
|
||||
const regionStart = region.getStartFromBeat();
|
||||
const regionEnd = regionStart + region.getLength();
|
||||
const overlappingReplacements = sortedReplacements.filter(replacement => (
|
||||
replacement.startBeat < regionEnd
|
||||
&& replacement.startBeat + replacement.length > regionStart
|
||||
));
|
||||
|
||||
if (overlappingReplacements.length === 0) {
|
||||
preservedRegions.push(cloneChordRegion(region));
|
||||
continue;
|
||||
}
|
||||
|
||||
let cursor = regionStart;
|
||||
let fragmentIndex = 0;
|
||||
for (const replacement of overlappingReplacements) {
|
||||
const replacementStart = Math.max(regionStart, replacement.startBeat);
|
||||
const replacementEnd = Math.min(regionEnd, replacement.startBeat + replacement.length);
|
||||
if (replacementStart > cursor) {
|
||||
preservedRegions.push(new KGChordRegion(
|
||||
fragmentIndex === 0 ? region.getId() : generateUniqueId('KGChordRegion'),
|
||||
region.getTrackId(),
|
||||
region.getTrackIndex(),
|
||||
region.getSymbol(),
|
||||
cursor,
|
||||
replacementStart - cursor,
|
||||
));
|
||||
fragmentIndex += 1;
|
||||
}
|
||||
cursor = Math.max(cursor, replacementEnd);
|
||||
}
|
||||
|
||||
if (cursor < regionEnd) {
|
||||
preservedRegions.push(new KGChordRegion(
|
||||
fragmentIndex === 0 ? region.getId() : generateUniqueId('KGChordRegion'),
|
||||
region.getTrackId(),
|
||||
region.getTrackIndex(),
|
||||
region.getSymbol(),
|
||||
cursor,
|
||||
regionEnd - cursor,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const replacementRegions = sortedReplacements.map(replacement => new KGChordRegion(
|
||||
generateUniqueId('KGChordRegion'),
|
||||
chordTrack.getId(),
|
||||
chordTrack.getTrackIndex(),
|
||||
replacement.symbol,
|
||||
replacement.startBeat,
|
||||
replacement.length,
|
||||
));
|
||||
|
||||
this.nextRegions = [...preservedRegions, ...replacementRegions]
|
||||
.sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat());
|
||||
|
||||
chordTrack.setRegions(cloneChordRegions(this.nextRegions));
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.originalRegions) {
|
||||
throw new Error('Cannot undo chord progression write without original regions');
|
||||
}
|
||||
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
if (!chordTrack) {
|
||||
throw new Error('Chord global track not found during undo');
|
||||
}
|
||||
|
||||
chordTrack.setRegions(cloneChordRegions(this.originalRegions));
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Write chord progression';
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,10 @@ export {
|
||||
ReplaceChordRegionsInRangeCommand,
|
||||
type ChordRegionReplacementData,
|
||||
} from './global-region/ReplaceChordRegionsInRangeCommand';
|
||||
export {
|
||||
WriteChordProgressionCommand,
|
||||
type WriteChordProgressionEntry,
|
||||
} from './global-region/WriteChordProgressionCommand';
|
||||
export { CreateKeySignatureRegionCommand } from './global-region/CreateKeySignatureRegionCommand';
|
||||
export { CreateTempoRegionCommand } from './global-region/CreateTempoRegionCommand';
|
||||
export { MoveGlobalRegionCommand } from './global-region/MoveGlobalRegionCommand';
|
||||
|
||||
Reference in New Issue
Block a user