feat: implement global chord track
This commit is contained in:
@@ -58,7 +58,7 @@ export class KGProject {
|
||||
@WithDefault(0)
|
||||
private projectStructureVersion: number = 0;
|
||||
|
||||
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 14;
|
||||
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 15;
|
||||
|
||||
@Expose()
|
||||
@Type(() => KGTrack, {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGChordRegion } from '../../region/KGChordRegion';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
import { findGlobalTrackByType, findNonOverlappingNeighborBounds, getSongEndBeat } from '../../../util/globalTrackUtil';
|
||||
|
||||
export class CreateChordRegionCommand extends KGCommand {
|
||||
private readonly startBeat: number;
|
||||
private readonly preferredLength: number;
|
||||
private readonly symbol: string;
|
||||
private readonly regionId: string;
|
||||
private createdRegion: KGChordRegion | null = null;
|
||||
|
||||
constructor(startBeat: number, preferredLength: number, symbol: string = 'C', regionId?: string) {
|
||||
super();
|
||||
this.startBeat = startBeat;
|
||||
this.preferredLength = preferredLength;
|
||||
this.symbol = symbol;
|
||||
this.regionId = regionId ?? generateUniqueId('KGChordRegion');
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
if (!chordTrack) {
|
||||
throw new Error('Chord global track not found');
|
||||
}
|
||||
|
||||
const { maxEndBeat } = findNonOverlappingNeighborBounds(project, GlobalTrackType.Chord, null, this.startBeat);
|
||||
const songEndBeat = getSongEndBeat(project);
|
||||
const allowedEndBeat = Math.min(maxEndBeat, songEndBeat);
|
||||
const targetEndBeat = Math.min(this.startBeat + this.preferredLength, allowedEndBeat);
|
||||
const length = Math.max(1, targetEndBeat - this.startBeat);
|
||||
|
||||
this.createdRegion = new KGChordRegion(
|
||||
this.regionId,
|
||||
chordTrack.getId(),
|
||||
chordTrack.getTrackIndex(),
|
||||
this.symbol,
|
||||
this.startBeat,
|
||||
length
|
||||
);
|
||||
|
||||
chordTrack.setRegions(
|
||||
[...chordTrack.getRegions(), this.createdRegion]
|
||||
.sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat())
|
||||
);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
if (!chordTrack) {
|
||||
throw new Error('Chord global track not found during undo');
|
||||
}
|
||||
|
||||
chordTrack.removeRegion(this.regionId);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Create chord "${this.symbol}"`;
|
||||
}
|
||||
|
||||
public getCreatedRegion(): KGChordRegion | null {
|
||||
return this.createdRegion;
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class DeleteGlobalRegionCommand extends KGCommand {
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Delete marker "${this.deletedRegion?.getName() ?? this.regionId}"`;
|
||||
return `Delete global region "${this.deletedRegion?.getName() ?? this.regionId}"`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,6 @@ export class DeleteMultipleGlobalRegionsCommand extends KGCommand {
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return this.regionIds.length === 1 ? 'Delete marker' : `Delete ${this.regionIds.length} markers`;
|
||||
return this.regionIds.length === 1 ? 'Delete global region' : `Delete ${this.regionIds.length} global regions`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGProject } from '../../KGProject';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGChordRegion } from '../../region/KGChordRegion';
|
||||
import { CreateChordRegionCommand } from './CreateChordRegionCommand';
|
||||
import { InsertChordRegionAtBeatCommand } from './InsertChordRegionAtBeatCommand';
|
||||
import { MoveGlobalRegionCommand } from './MoveGlobalRegionCommand';
|
||||
import { ResizeGlobalRegionCommand } from './ResizeGlobalRegionCommand';
|
||||
import { UpdateChordRegionCommand } from './UpdateChordRegionCommand';
|
||||
|
||||
describe('global chord region commands', () => {
|
||||
beforeEach(() => {
|
||||
const project = new KGProject('Chords', 8, 0, 120);
|
||||
const mockCore = KGCore.instance() as unknown as {
|
||||
getCurrentProject: ReturnType<typeof vi.fn>;
|
||||
getSelectedItems: ReturnType<typeof vi.fn>;
|
||||
removeSelectedItem?: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
mockCore.getCurrentProject.mockReturnValue(project);
|
||||
mockCore.getSelectedItems.mockReturnValue([]);
|
||||
if (!mockCore.removeSelectedItem) {
|
||||
mockCore.removeSelectedItem = vi.fn();
|
||||
} else {
|
||||
mockCore.removeSelectedItem.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
const getChordTrack = () => {
|
||||
const chordTrack = KGCore.instance().getCurrentProject().getGlobalTracks()
|
||||
.find(track => track.getType() === GlobalTrackType.Chord);
|
||||
|
||||
if (!chordTrack) {
|
||||
throw new Error('Chord track missing in test setup');
|
||||
}
|
||||
|
||||
return chordTrack;
|
||||
};
|
||||
|
||||
it('creates a chord region with a default one-bar length clamped by the next region', () => {
|
||||
const chordTrack = getChordTrack();
|
||||
chordTrack.addRegion(new KGChordRegion('existing', chordTrack.getId(), chordTrack.getTrackIndex(), 'Fmaj7', 6, 2));
|
||||
|
||||
const command = new CreateChordRegionCommand(4, 4, 'Cmaj7');
|
||||
command.execute();
|
||||
|
||||
const created = command.getCreatedRegion();
|
||||
expect(created?.getStartFromBeat()).toBe(4);
|
||||
expect(created?.getLength()).toBe(2);
|
||||
});
|
||||
|
||||
it('moves and resizes chord regions with beat snapping and no overlap', () => {
|
||||
const chordTrack = getChordTrack();
|
||||
const region = new KGChordRegion('middle', chordTrack.getId(), chordTrack.getTrackIndex(), 'Dm7', 4, 2);
|
||||
chordTrack.setRegions([
|
||||
new KGChordRegion('left', chordTrack.getId(), chordTrack.getTrackIndex(), 'C', 0, 4),
|
||||
region,
|
||||
new KGChordRegion('right', chordTrack.getId(), chordTrack.getTrackIndex(), 'G7', 10, 2),
|
||||
]);
|
||||
|
||||
const moveCommand = new MoveGlobalRegionCommand('middle', 9);
|
||||
moveCommand.execute();
|
||||
expect(region.getStartFromBeat()).toBe(8);
|
||||
|
||||
const resizeCommand = new ResizeGlobalRegionCommand('middle', 'end', 12);
|
||||
resizeCommand.execute();
|
||||
expect(region.getLength()).toBe(2);
|
||||
|
||||
const resizeMinCommand = new ResizeGlobalRegionCommand('middle', 'start', 9);
|
||||
resizeMinCommand.execute();
|
||||
expect(region.getStartFromBeat()).toBe(9);
|
||||
expect(region.getLength()).toBe(1);
|
||||
});
|
||||
|
||||
it('updates chord symbols with undo support', () => {
|
||||
const chordTrack = getChordTrack();
|
||||
const region = new KGChordRegion('chord', chordTrack.getId(), chordTrack.getTrackIndex(), 'C', 0, 4);
|
||||
chordTrack.setRegions([region]);
|
||||
|
||||
const command = new UpdateChordRegionCommand('chord', 'Bm7b5');
|
||||
command.execute();
|
||||
expect(region.getSymbol()).toBe('Bm7b5');
|
||||
|
||||
command.undo();
|
||||
expect(region.getSymbol()).toBe('C');
|
||||
});
|
||||
|
||||
it('inserts a new chord inside an existing region and shortens the original', () => {
|
||||
const chordTrack = getChordTrack();
|
||||
const region = new KGChordRegion('chord', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 8);
|
||||
chordTrack.setRegions([region]);
|
||||
|
||||
const command = new InsertChordRegionAtBeatCommand(3, 'C');
|
||||
command.execute();
|
||||
|
||||
const created = command.getCreatedRegion();
|
||||
expect(created).not.toBeNull();
|
||||
expect(region.getLength()).toBe(3);
|
||||
expect(created?.getStartFromBeat()).toBe(3);
|
||||
expect(created?.getLength()).toBe(5);
|
||||
|
||||
command.undo();
|
||||
expect(chordTrack.getRegions()).toHaveLength(1);
|
||||
expect(region.getLength()).toBe(8);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGChordRegion } from '../../region/KGChordRegion';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
import { findChordRegionAtBeat, findGlobalTrackByType } from '../../../util/globalTrackUtil';
|
||||
|
||||
export class InsertChordRegionAtBeatCommand extends KGCommand {
|
||||
private readonly insertBeat: number;
|
||||
private readonly symbol: string;
|
||||
private readonly regionId: string;
|
||||
private createdRegion: KGChordRegion | null = null;
|
||||
private targetRegionId: string | null = null;
|
||||
private originalTargetLength = 0;
|
||||
|
||||
constructor(insertBeat: number, symbol: string = 'C', regionId?: string) {
|
||||
super();
|
||||
this.insertBeat = insertBeat;
|
||||
this.symbol = symbol;
|
||||
this.regionId = regionId ?? generateUniqueId('KGChordRegion');
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
if (!chordTrack) {
|
||||
throw new Error('Chord global track not found');
|
||||
}
|
||||
|
||||
const occupiedRegion = findChordRegionAtBeat(project, this.insertBeat);
|
||||
if (!occupiedRegion) {
|
||||
throw new Error(`No chord region found at beat ${this.insertBeat}`);
|
||||
}
|
||||
|
||||
const regionStart = occupiedRegion.getStartFromBeat();
|
||||
const regionEnd = regionStart + occupiedRegion.getLength();
|
||||
if (this.insertBeat <= regionStart || this.insertBeat >= regionEnd) {
|
||||
throw new Error(`Cannot insert chord at beat ${this.insertBeat} without shrinking region below minimum length`);
|
||||
}
|
||||
|
||||
this.targetRegionId = occupiedRegion.getId();
|
||||
this.originalTargetLength = occupiedRegion.getLength();
|
||||
occupiedRegion.setLength(this.insertBeat - regionStart);
|
||||
|
||||
this.createdRegion = new KGChordRegion(
|
||||
this.regionId,
|
||||
chordTrack.getId(),
|
||||
chordTrack.getTrackIndex(),
|
||||
this.symbol,
|
||||
this.insertBeat,
|
||||
regionEnd - this.insertBeat
|
||||
);
|
||||
|
||||
chordTrack.setRegions(
|
||||
[...chordTrack.getRegions(), this.createdRegion]
|
||||
.sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat())
|
||||
);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
if (!chordTrack || !this.targetRegionId || !this.createdRegion) {
|
||||
throw new Error('Cannot undo inserted chord region');
|
||||
}
|
||||
|
||||
chordTrack.removeRegion(this.createdRegion.getId());
|
||||
const targetRegion = chordTrack.getRegions().find((region): region is KGChordRegion => (
|
||||
region instanceof KGChordRegion && region.getId() === this.targetRegionId
|
||||
));
|
||||
if (!targetRegion) {
|
||||
throw new Error(`Chord region ${this.targetRegionId} not found during undo`);
|
||||
}
|
||||
|
||||
targetRegion.setLength(this.originalTargetLength);
|
||||
chordTrack.setRegions([...chordTrack.getRegions()].sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat()));
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Insert chord "${this.symbol}"`;
|
||||
}
|
||||
|
||||
public getCreatedRegion(): KGChordRegion | null {
|
||||
return this.createdRegion;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGGlobalRegion } from '../../region/KGGlobalRegion';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { findGlobalTrackContainingRegion, findMarkerNeighborBounds } from '../../../util/globalTrackUtil';
|
||||
import { findGlobalTrackContainingRegion, findNonOverlappingNeighborBounds } from '../../../util/globalTrackUtil';
|
||||
|
||||
export class MoveGlobalRegionCommand extends KGCommand {
|
||||
private readonly regionId: string;
|
||||
@@ -26,12 +26,17 @@ export class MoveGlobalRegionCommand extends KGCommand {
|
||||
this.targetRegion = result.region;
|
||||
this.originalStartBeat = result.region.getStartFromBeat();
|
||||
|
||||
if (result.track.getType() !== GlobalTrackType.Marker) {
|
||||
if (result.track.getType() !== GlobalTrackType.Marker && result.track.getType() !== GlobalTrackType.Chord) {
|
||||
result.region.setStartFromBeat(this.desiredStartBeat);
|
||||
return;
|
||||
}
|
||||
|
||||
const { minStartBeat, maxEndBeat } = findMarkerNeighborBounds(project, this.regionId, this.desiredStartBeat);
|
||||
const { minStartBeat, maxEndBeat } = findNonOverlappingNeighborBounds(
|
||||
project,
|
||||
result.track.getType() as GlobalTrackType.Marker | GlobalTrackType.Chord,
|
||||
this.regionId,
|
||||
this.desiredStartBeat
|
||||
);
|
||||
const maxStartBeat = Math.max(minStartBeat, maxEndBeat - result.region.getLength());
|
||||
const clampedStartBeat = Math.max(minStartBeat, Math.min(this.desiredStartBeat, maxStartBeat));
|
||||
result.region.setStartFromBeat(clampedStartBeat);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { findGlobalTrackContainingRegion, findMarkerNeighborBounds, getSongEndBeat } from '../../../util/globalTrackUtil';
|
||||
import { findGlobalTrackContainingRegion, findNonOverlappingNeighborBounds, getSongEndBeat } from '../../../util/globalTrackUtil';
|
||||
import { KGGlobalRegion } from '../../region/KGGlobalRegion';
|
||||
|
||||
export type GlobalRegionResizeEdge = 'start' | 'end';
|
||||
@@ -32,12 +32,17 @@ export class ResizeGlobalRegionCommand extends KGCommand {
|
||||
this.originalStartBeat = result.region.getStartFromBeat();
|
||||
this.originalLength = result.region.getLength();
|
||||
|
||||
if (result.track.getType() !== GlobalTrackType.Marker) {
|
||||
if (result.track.getType() !== GlobalTrackType.Marker && result.track.getType() !== GlobalTrackType.Chord) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalEndBeat = this.originalStartBeat + this.originalLength;
|
||||
const { minStartBeat, maxEndBeat } = findMarkerNeighborBounds(project, this.regionId, this.originalStartBeat);
|
||||
const { minStartBeat, maxEndBeat } = findNonOverlappingNeighborBounds(
|
||||
project,
|
||||
result.track.getType() as GlobalTrackType.Marker | GlobalTrackType.Chord,
|
||||
this.regionId,
|
||||
this.originalStartBeat
|
||||
);
|
||||
const songEndBeat = getSongEndBeat(project);
|
||||
const absoluteMaxEndBeat = Math.min(maxEndBeat, songEndBeat);
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { findGlobalTrackContainingRegion } from '../../../util/globalTrackUtil';
|
||||
import { KGChordRegion } from '../../region/KGChordRegion';
|
||||
|
||||
export class UpdateChordRegionCommand extends KGCommand {
|
||||
private readonly regionId: string;
|
||||
private readonly nextSymbol: string;
|
||||
private previousSymbol: string | null = null;
|
||||
|
||||
constructor(regionId: string, nextSymbol: string) {
|
||||
super();
|
||||
this.regionId = regionId;
|
||||
this.nextSymbol = nextSymbol;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const result = findGlobalTrackContainingRegion(KGCore.instance().getCurrentProject(), this.regionId);
|
||||
if (!result || !(result.region instanceof KGChordRegion)) {
|
||||
throw new Error(`Chord region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
this.previousSymbol = result.region.getSymbol();
|
||||
result.region.setSymbol(this.nextSymbol);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const result = findGlobalTrackContainingRegion(KGCore.instance().getCurrentProject(), this.regionId);
|
||||
if (!result || !(result.region instanceof KGChordRegion) || this.previousSymbol === null) {
|
||||
throw new Error(`Chord region with ID ${this.regionId} not found during undo`);
|
||||
}
|
||||
|
||||
result.region.setSymbol(this.previousSymbol);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Change chord to "${this.nextSymbol}"`;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ export { MergeMidiRegionsCommand } from './region/MergeMidiRegionsCommand';
|
||||
|
||||
// Global region commands
|
||||
export { CreateGlobalMarkerRegionCommand } from './global-region/CreateGlobalMarkerRegionCommand';
|
||||
export { CreateChordRegionCommand } from './global-region/CreateChordRegionCommand';
|
||||
export { InsertChordRegionAtBeatCommand } from './global-region/InsertChordRegionAtBeatCommand';
|
||||
export { CreateKeySignatureRegionCommand } from './global-region/CreateKeySignatureRegionCommand';
|
||||
export { CreateTempoRegionCommand } from './global-region/CreateTempoRegionCommand';
|
||||
export { MoveGlobalRegionCommand } from './global-region/MoveGlobalRegionCommand';
|
||||
@@ -44,6 +46,7 @@ export { ResizeKeySignatureRegionCommand } from './global-region/ResizeKeySignat
|
||||
export { ResizeTempoRegionCommand } from './global-region/ResizeTempoRegionCommand';
|
||||
export { DeleteGlobalRegionCommand, DeleteMultipleGlobalRegionsCommand } from './global-region/DeleteGlobalRegionCommand';
|
||||
export { UpdateGlobalRegionTextCommand } from './global-region/UpdateGlobalRegionTextCommand';
|
||||
export { UpdateChordRegionCommand } from './global-region/UpdateChordRegionCommand';
|
||||
export { DeleteKeySignatureRegionCommand, DeleteMultipleKeySignatureRegionsCommand } from './global-region/DeleteKeySignatureRegionCommand';
|
||||
export { UpdateKeySignatureRegionCommand } from './global-region/UpdateKeySignatureRegionCommand';
|
||||
export { DeleteTempoRegionCommand, DeleteMultipleTempoRegionsCommand } from './global-region/DeleteTempoRegionCommand';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Expose, Type } from 'class-transformer';
|
||||
import { KGGlobalRegion } from '../region/KGGlobalRegion';
|
||||
import { KGChordRegion } from '../region/KGChordRegion';
|
||||
import { KGKeySignatureRegion } from '../region/KGKeySignatureRegion';
|
||||
import { KGMarkerRegion } from '../region/KGMarkerRegion';
|
||||
import { KGTempoRegion } from '../region/KGTempoRegion';
|
||||
@@ -36,6 +37,7 @@ export class KGGlobalTrack {
|
||||
{ value: KGMarkerRegion, name: 'KGMarkerRegion' },
|
||||
{ value: KGTempoRegion, name: 'KGTempoRegion' },
|
||||
{ value: KGKeySignatureRegion, name: 'KGKeySignatureRegion' },
|
||||
{ value: KGChordRegion, name: 'KGChordRegion' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { KGProject } from '../KGProject';
|
||||
import { GlobalTrackType } from '../global-track';
|
||||
import { KGKeySignatureRegion } from '../region/KGKeySignatureRegion';
|
||||
import { KGMarkerRegion } from '../region/KGMarkerRegion';
|
||||
import { KGChordRegion } from '../region/KGChordRegion';
|
||||
import { KGTrack } from '../track/KGTrack';
|
||||
|
||||
// --- OPFS mock infrastructure ---
|
||||
@@ -199,6 +200,24 @@ describe('KGProjectStorage', () => {
|
||||
expect((loadedSignatureTrack?.getRegions()[0] as KGKeySignatureRegion).getStartBar()).toBe(4);
|
||||
});
|
||||
|
||||
it('preserves chord regions when saving and loading', async () => {
|
||||
const project = createTestProject('Chord Song');
|
||||
const chordTrack = project.getGlobalTracks().find(track => track.getType() === GlobalTrackType.Chord);
|
||||
|
||||
expect(chordTrack).toBeDefined();
|
||||
chordTrack?.addRegion(new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Bm7b5', 5, 3));
|
||||
|
||||
await storage.save('Chord Song', project);
|
||||
|
||||
const loaded = await storage.load('Chord Song');
|
||||
const loadedChordTrack = loaded?.getGlobalTracks().find(track => track.getType() === GlobalTrackType.Chord);
|
||||
|
||||
expect(loadedChordTrack).toBeDefined();
|
||||
expect(loadedChordTrack?.getRegions()).toHaveLength(1);
|
||||
expect(loadedChordTrack?.getRegions()[0]).toBeInstanceOf(KGChordRegion);
|
||||
expect((loadedChordTrack?.getRegions()[0] as KGChordRegion).getSymbol()).toBe('Bm7b5');
|
||||
});
|
||||
|
||||
it('creates meta.json and media/ directory on save', async () => {
|
||||
const project = createTestProject('My Song');
|
||||
await storage.save('My Song', project);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { upgradeToV11 } from './upgradeToV11';
|
||||
import { upgradeToV12 } from './upgradeToV12';
|
||||
import { upgradeToV13 } from './upgradeToV13';
|
||||
import { upgradeToV14 } from './upgradeToV14';
|
||||
import { upgradeToV15 } from './upgradeToV15';
|
||||
|
||||
/**
|
||||
* Upgrade the given project to the latest structure version, one version at a time.
|
||||
@@ -88,6 +89,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject {
|
||||
workingProject = upgradeToV14(workingProject);
|
||||
break;
|
||||
}
|
||||
case 15: {
|
||||
workingProject = upgradeToV15(workingProject);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// If an upgrader is missing, throw to prevent loading incompatible structures
|
||||
throw new Error(`No upgrader found for project structure version ${nextVersion}`);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { KGProject } from '../KGProject';
|
||||
import { upgradeProjectToLatest } from './KGProjectUpgrader';
|
||||
import { upgradeToV15 } from './upgradeToV15';
|
||||
|
||||
describe('upgradeToV15', () => {
|
||||
it('marks projects as upgraded and preserves default global tracks', () => {
|
||||
const project = new KGProject('Legacy', 32, 0, 125, undefined, undefined, undefined, undefined, [0, 0], 1, [], 14, 1, []);
|
||||
|
||||
upgradeToV15(project);
|
||||
|
||||
expect(project.getProjectStructureVersion()).toBe(15);
|
||||
expect(project.getGlobalTracks()).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('runs through the main upgrader path', () => {
|
||||
const project = new KGProject('Legacy', 32, 0, 125, undefined, undefined, undefined, undefined, [0, 0], 1, [], 14, 1, []);
|
||||
|
||||
const upgraded = upgradeProjectToLatest(project);
|
||||
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(KGProject.CURRENT_PROJECT_STRUCTURE_VERSION);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { KGProject } from '../KGProject';
|
||||
import { ensureDefaultGlobalTracks } from '../../util/globalTrackUtil';
|
||||
|
||||
export function upgradeToV15(project: KGProject): KGProject {
|
||||
try {
|
||||
ensureDefaultGlobalTracks(project);
|
||||
} finally {
|
||||
project.setProjectStructureVersion(15);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Expose } from 'class-transformer';
|
||||
import { KGGlobalRegion } from './KGGlobalRegion';
|
||||
|
||||
export class KGChordRegion extends KGGlobalRegion {
|
||||
@Expose()
|
||||
protected override __type: string = 'KGChordRegion';
|
||||
|
||||
@Expose()
|
||||
private symbol: string = 'C';
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
trackId: string,
|
||||
trackIndex: number,
|
||||
symbol: string,
|
||||
startFromBeat: number = 0,
|
||||
length: number = 0
|
||||
) {
|
||||
super(id, trackId, trackIndex, symbol, startFromBeat, length);
|
||||
this.__type = 'KGChordRegion';
|
||||
this.symbol = symbol;
|
||||
super.setName(symbol);
|
||||
}
|
||||
|
||||
public getSymbol(): string {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
public setSymbol(symbol: string): void {
|
||||
this.symbol = symbol;
|
||||
super.setName(symbol);
|
||||
}
|
||||
|
||||
public override getCurrentType(): string {
|
||||
return 'KGChordRegion';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user