feat: implemented global tempo (bpm) track
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGTempoRegion } from '../../region/KGTempoRegion';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
import {
|
||||
cloneTempoRegions,
|
||||
findGlobalTrackByType,
|
||||
findTempoRegionAtBar,
|
||||
getEffectiveBpmAtBar,
|
||||
getSongEndBar,
|
||||
getSortedTempoRegions,
|
||||
} from '../../../util/globalTrackUtil';
|
||||
|
||||
export class CreateTempoRegionCommand extends KGCommand {
|
||||
private readonly startBar: number;
|
||||
private readonly regionId: string;
|
||||
private createdRegion: KGTempoRegion | null = null;
|
||||
private previousRegions: KGTempoRegion[] = [];
|
||||
|
||||
constructor(startBar: number, regionId?: string) {
|
||||
super();
|
||||
this.startBar = startBar;
|
||||
this.regionId = regionId ?? generateUniqueId('KGTempoRegion');
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found');
|
||||
}
|
||||
|
||||
const existingRegions = getSortedTempoRegions(track, beatsPerBar);
|
||||
this.previousRegions = cloneTempoRegions(existingRegions, beatsPerBar);
|
||||
|
||||
const songEndBar = getSongEndBar(project);
|
||||
const clampedStartBar = Math.max(0, Math.min(this.startBar, Math.max(0, songEndBar - 1)));
|
||||
|
||||
if (existingRegions.length === 0) {
|
||||
this.createdRegion = new KGTempoRegion(
|
||||
this.regionId,
|
||||
track.getId(),
|
||||
track.getTrackIndex(),
|
||||
getEffectiveBpmAtBar(project, clampedStartBar),
|
||||
0,
|
||||
Math.max(1, songEndBar),
|
||||
beatsPerBar
|
||||
);
|
||||
track.setRegions([this.createdRegion]);
|
||||
return;
|
||||
}
|
||||
|
||||
const containingRegion = findTempoRegionAtBar(project, clampedStartBar);
|
||||
if (!containingRegion) {
|
||||
throw new Error(`No tempo region covers bar ${clampedStartBar}`);
|
||||
}
|
||||
|
||||
const regionStartBar = containingRegion.getStartBar();
|
||||
const regionEndBar = containingRegion.getEndBar();
|
||||
if (clampedStartBar <= regionStartBar || clampedStartBar >= regionEndBar) {
|
||||
throw new Error(`Bar ${clampedStartBar} is not a valid split point`);
|
||||
}
|
||||
|
||||
containingRegion.setLengthBars(clampedStartBar - regionStartBar, beatsPerBar);
|
||||
this.createdRegion = new KGTempoRegion(
|
||||
this.regionId,
|
||||
track.getId(),
|
||||
track.getTrackIndex(),
|
||||
containingRegion.getBpm(),
|
||||
clampedStartBar,
|
||||
regionEndBar - clampedStartBar,
|
||||
beatsPerBar
|
||||
);
|
||||
track.setRegions([...existingRegions, this.createdRegion].sort((left, right) => left.getStartBar() - right.getStartBar()));
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found during undo');
|
||||
}
|
||||
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
track.setRegions(cloneTempoRegions(this.previousRegions, beatsPerBar));
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Create tempo change at bar ${this.startBar + 1}`;
|
||||
}
|
||||
|
||||
public getCreatedRegion(): KGTempoRegion | null {
|
||||
return this.createdRegion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGTempoRegion } from '../../region/KGTempoRegion';
|
||||
import {
|
||||
cloneTempoRegions,
|
||||
findGlobalTrackByType,
|
||||
getSortedTempoRegions,
|
||||
} from '../../../util/globalTrackUtil';
|
||||
|
||||
export class DeleteTempoRegionCommand extends KGCommand {
|
||||
private readonly regionId: string;
|
||||
private previousRegions: KGTempoRegion[] = [];
|
||||
private deletedBpm = '';
|
||||
|
||||
constructor(regionId: string) {
|
||||
super();
|
||||
this.regionId = regionId;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found');
|
||||
}
|
||||
|
||||
const regions = getSortedTempoRegions(track, beatsPerBar);
|
||||
this.previousRegions = cloneTempoRegions(regions, beatsPerBar);
|
||||
|
||||
const targetIndex = regions.findIndex(region => region.getId() === this.regionId);
|
||||
if (targetIndex === -1) {
|
||||
throw new Error(`Tempo region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
const targetRegion = regions[targetIndex];
|
||||
this.deletedBpm = `${targetRegion.getBpm()} BPM`;
|
||||
|
||||
if (regions.length === 1) {
|
||||
track.setRegions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRegions = [...regions];
|
||||
const deletedLengthBars = targetRegion.getLengthBars();
|
||||
|
||||
if (targetIndex === 0) {
|
||||
const nextRegion = nextRegions[1];
|
||||
nextRegion.setBarRange(0, nextRegion.getLengthBars() + deletedLengthBars, beatsPerBar);
|
||||
nextRegions.splice(0, 1);
|
||||
track.setRegions(nextRegions);
|
||||
return;
|
||||
}
|
||||
|
||||
const previousRegion = nextRegions[targetIndex - 1];
|
||||
previousRegion.setLengthBars(previousRegion.getLengthBars() + deletedLengthBars, beatsPerBar);
|
||||
nextRegions.splice(targetIndex, 1);
|
||||
track.setRegions(nextRegions);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found during undo');
|
||||
}
|
||||
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
track.setRegions(cloneTempoRegions(this.previousRegions, beatsPerBar));
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Delete tempo "${this.deletedBpm || this.regionId}"`;
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteMultipleTempoRegionsCommand extends KGCommand {
|
||||
private readonly regionIds: string[];
|
||||
private previousRegions: KGTempoRegion[] = [];
|
||||
|
||||
constructor(regionIds: string[]) {
|
||||
super();
|
||||
this.regionIds = regionIds;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found');
|
||||
}
|
||||
|
||||
const regions = getSortedTempoRegions(track, beatsPerBar);
|
||||
this.previousRegions = cloneTempoRegions(regions, beatsPerBar);
|
||||
|
||||
let workingRegions = cloneTempoRegions(regions, beatsPerBar);
|
||||
|
||||
for (const regionId of this.regionIds) {
|
||||
const targetIndex = workingRegions.findIndex(region => region.getId() === regionId);
|
||||
if (targetIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const deletedRegion = workingRegions[targetIndex];
|
||||
const deletedLengthBars = deletedRegion.getLengthBars();
|
||||
|
||||
if (workingRegions.length === 1) {
|
||||
workingRegions = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetIndex === 0) {
|
||||
const nextRegion = workingRegions[1];
|
||||
nextRegion.setBarRange(0, nextRegion.getLengthBars() + deletedLengthBars, beatsPerBar);
|
||||
workingRegions.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const previousRegion = workingRegions[targetIndex - 1];
|
||||
previousRegion.setLengthBars(previousRegion.getLengthBars() + deletedLengthBars, beatsPerBar);
|
||||
workingRegions.splice(targetIndex, 1);
|
||||
}
|
||||
|
||||
track.setRegions(workingRegions);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found during undo');
|
||||
}
|
||||
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
track.setRegions(cloneTempoRegions(this.previousRegions, beatsPerBar));
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return this.regionIds.length === 1 ? 'Delete tempo change' : `Delete ${this.regionIds.length} tempo changes`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGProject } from '../../KGProject';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGTempoRegion } from '../../region/KGTempoRegion';
|
||||
import { CreateTempoRegionCommand } from './CreateTempoRegionCommand';
|
||||
import { DeleteTempoRegionCommand } from './DeleteTempoRegionCommand';
|
||||
import { ResizeTempoRegionCommand } from './ResizeTempoRegionCommand';
|
||||
import { UpdateTempoRegionCommand } from './UpdateTempoRegionCommand';
|
||||
|
||||
describe('global tempo region commands', () => {
|
||||
beforeEach(() => {
|
||||
const project = new KGProject('Tempo', 8, 0, 120);
|
||||
const mockCore = KGCore.instance() as unknown as {
|
||||
getCurrentProject: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
mockCore.getCurrentProject.mockReturnValue(project);
|
||||
});
|
||||
|
||||
const getTempoTrack = () => {
|
||||
const tempoTrack = KGCore.instance().getCurrentProject().getGlobalTracks()
|
||||
.find(track => track.getType() === GlobalTrackType.Tempo);
|
||||
|
||||
if (!tempoTrack) {
|
||||
throw new Error('Tempo track missing in test setup');
|
||||
}
|
||||
|
||||
return tempoTrack;
|
||||
};
|
||||
|
||||
it('creates the first explicit region as full-song coverage', () => {
|
||||
const command = new CreateTempoRegionCommand(3);
|
||||
command.execute();
|
||||
|
||||
const tempoTrack = getTempoTrack();
|
||||
const regions = tempoTrack.getRegions() as KGTempoRegion[];
|
||||
|
||||
expect(regions).toHaveLength(1);
|
||||
expect(regions[0].getStartBar()).toBe(0);
|
||||
expect(regions[0].getLengthBars()).toBe(8);
|
||||
expect(regions[0].getBpm()).toBe(120);
|
||||
});
|
||||
|
||||
it('creates additional regions by splitting the covered span and inheriting BPM', () => {
|
||||
const tempoTrack = getTempoTrack();
|
||||
tempoTrack.setRegions([
|
||||
new KGTempoRegion('left', tempoTrack.getId(), tempoTrack.getTrackIndex(), 128, 0, 8, 4),
|
||||
]);
|
||||
|
||||
const command = new CreateTempoRegionCommand(5);
|
||||
command.execute();
|
||||
|
||||
const regions = tempoTrack.getRegions() as KGTempoRegion[];
|
||||
expect(regions).toHaveLength(2);
|
||||
expect(regions[0].getLengthBars()).toBe(5);
|
||||
expect(regions[1].getStartBar()).toBe(5);
|
||||
expect(regions[1].getLengthBars()).toBe(3);
|
||||
expect(regions[1].getBpm()).toBe(128);
|
||||
});
|
||||
|
||||
it('resizes a shared boundary and keeps the track gapless', () => {
|
||||
const tempoTrack = getTempoTrack();
|
||||
tempoTrack.setRegions([
|
||||
new KGTempoRegion('left', tempoTrack.getId(), tempoTrack.getTrackIndex(), 120, 0, 4, 4),
|
||||
new KGTempoRegion('right', tempoTrack.getId(), tempoTrack.getTrackIndex(), 140, 4, 4, 4),
|
||||
]);
|
||||
|
||||
const command = new ResizeTempoRegionCommand('left', 'end', 6);
|
||||
command.execute();
|
||||
|
||||
const regions = tempoTrack.getRegions() as KGTempoRegion[];
|
||||
expect(regions[0].getLengthBars()).toBe(6);
|
||||
expect(regions[1].getStartBar()).toBe(6);
|
||||
expect(regions[1].getLengthBars()).toBe(2);
|
||||
});
|
||||
|
||||
it('deletes a middle region by extending the previous region', () => {
|
||||
const tempoTrack = getTempoTrack();
|
||||
tempoTrack.setRegions([
|
||||
new KGTempoRegion('first', tempoTrack.getId(), tempoTrack.getTrackIndex(), 120, 0, 2, 4),
|
||||
new KGTempoRegion('middle', tempoTrack.getId(), tempoTrack.getTrackIndex(), 128, 2, 3, 4),
|
||||
new KGTempoRegion('last', tempoTrack.getId(), tempoTrack.getTrackIndex(), 140, 5, 3, 4),
|
||||
]);
|
||||
|
||||
const command = new DeleteTempoRegionCommand('middle');
|
||||
command.execute();
|
||||
|
||||
const regions = tempoTrack.getRegions() as KGTempoRegion[];
|
||||
expect(regions).toHaveLength(2);
|
||||
expect(regions[0].getLengthBars()).toBe(5);
|
||||
expect(regions[1].getStartBar()).toBe(5);
|
||||
});
|
||||
|
||||
it('allows deleting the last remaining region', () => {
|
||||
const tempoTrack = getTempoTrack();
|
||||
tempoTrack.setRegions([
|
||||
new KGTempoRegion('only', tempoTrack.getId(), tempoTrack.getTrackIndex(), 120, 0, 8, 4),
|
||||
]);
|
||||
|
||||
const command = new DeleteTempoRegionCommand('only');
|
||||
command.execute();
|
||||
|
||||
expect(tempoTrack.getRegions()).toHaveLength(0);
|
||||
command.undo();
|
||||
expect(tempoTrack.getRegions()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('updates the region tempo with undo support', () => {
|
||||
const tempoTrack = getTempoTrack();
|
||||
tempoTrack.setRegions([
|
||||
new KGTempoRegion('region', tempoTrack.getId(), tempoTrack.getTrackIndex(), 120, 0, 8, 4),
|
||||
]);
|
||||
|
||||
const command = new UpdateTempoRegionCommand('region', 150);
|
||||
command.execute();
|
||||
expect((tempoTrack.getRegions()[0] as KGTempoRegion).getBpm()).toBe(150);
|
||||
command.undo();
|
||||
expect((tempoTrack.getRegions()[0] as KGTempoRegion).getBpm()).toBe(120);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGTempoRegion } from '../../region/KGTempoRegion';
|
||||
import type { GlobalRegionResizeEdge } from './ResizeGlobalRegionCommand';
|
||||
import {
|
||||
cloneTempoRegions,
|
||||
findGlobalTrackByType,
|
||||
getSortedTempoRegions,
|
||||
} from '../../../util/globalTrackUtil';
|
||||
|
||||
export class ResizeTempoRegionCommand extends KGCommand {
|
||||
private readonly regionId: string;
|
||||
private readonly edge: GlobalRegionResizeEdge;
|
||||
private readonly desiredBar: number;
|
||||
private previousRegions: KGTempoRegion[] = [];
|
||||
|
||||
constructor(regionId: string, edge: GlobalRegionResizeEdge, desiredBar: number) {
|
||||
super();
|
||||
this.regionId = regionId;
|
||||
this.edge = edge;
|
||||
this.desiredBar = desiredBar;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found');
|
||||
}
|
||||
|
||||
const regions = getSortedTempoRegions(track, beatsPerBar);
|
||||
this.previousRegions = cloneTempoRegions(regions, beatsPerBar);
|
||||
|
||||
const targetIndex = regions.findIndex(region => region.getId() === this.regionId);
|
||||
if (targetIndex === -1) {
|
||||
throw new Error(`Tempo region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
const targetRegion = regions[targetIndex];
|
||||
if (this.edge === 'start') {
|
||||
if (targetIndex === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousRegion = regions[targetIndex - 1];
|
||||
const targetEndBar = targetRegion.getEndBar();
|
||||
const clampedBoundaryBar = Math.max(
|
||||
previousRegion.getStartBar() + 1,
|
||||
Math.min(this.desiredBar, targetEndBar - 1)
|
||||
);
|
||||
|
||||
previousRegion.setLengthBars(clampedBoundaryBar - previousRegion.getStartBar(), beatsPerBar);
|
||||
targetRegion.setBarRange(clampedBoundaryBar, targetEndBar - clampedBoundaryBar, beatsPerBar);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetIndex === regions.length - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRegion = regions[targetIndex + 1];
|
||||
const nextRegionEndBar = nextRegion.getEndBar();
|
||||
const clampedBoundaryBar = Math.max(
|
||||
targetRegion.getStartBar() + 1,
|
||||
Math.min(this.desiredBar, nextRegionEndBar - 1)
|
||||
);
|
||||
|
||||
targetRegion.setLengthBars(clampedBoundaryBar - targetRegion.getStartBar(), beatsPerBar);
|
||||
nextRegion.setBarRange(clampedBoundaryBar, nextRegionEndBar - clampedBoundaryBar, beatsPerBar);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found during undo');
|
||||
}
|
||||
|
||||
const beatsPerBar = project.getTimeSignature().numerator;
|
||||
track.setRegions(cloneTempoRegions(this.previousRegions, beatsPerBar));
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Resize tempo boundary for "${this.regionId}"`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGTempoRegion } from '../../region/KGTempoRegion';
|
||||
import { findGlobalTrackByType } from '../../../util/globalTrackUtil';
|
||||
|
||||
export class UpdateTempoRegionCommand extends KGCommand {
|
||||
private readonly regionId: string;
|
||||
private readonly nextBpm: number;
|
||||
private previousBpm: number | null = null;
|
||||
private targetRegion: KGTempoRegion | null = null;
|
||||
|
||||
constructor(regionId: string, nextBpm: number) {
|
||||
super();
|
||||
this.regionId = regionId;
|
||||
this.nextBpm = nextBpm;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const track = findGlobalTrackByType(project, GlobalTrackType.Tempo);
|
||||
if (!track) {
|
||||
throw new Error('Tempo global track not found');
|
||||
}
|
||||
|
||||
const region = track.getRegions().find(candidate => candidate.getId() === this.regionId);
|
||||
if (!(region instanceof KGTempoRegion)) {
|
||||
throw new Error(`Tempo region with ID ${this.regionId} not found`);
|
||||
}
|
||||
|
||||
this.targetRegion = region;
|
||||
this.previousBpm = region.getBpm();
|
||||
region.setBpm(this.nextBpm);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.targetRegion || this.previousBpm === null) {
|
||||
throw new Error('Cannot undo tempo update without previous state');
|
||||
}
|
||||
|
||||
this.targetRegion.setBpm(this.previousBpm);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Change tempo to "${this.nextBpm} BPM"`;
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,17 @@ export { MergeMidiRegionsCommand } from './region/MergeMidiRegionsCommand';
|
||||
// Global region commands
|
||||
export { CreateGlobalMarkerRegionCommand } from './global-region/CreateGlobalMarkerRegionCommand';
|
||||
export { CreateKeySignatureRegionCommand } from './global-region/CreateKeySignatureRegionCommand';
|
||||
export { CreateTempoRegionCommand } from './global-region/CreateTempoRegionCommand';
|
||||
export { MoveGlobalRegionCommand } from './global-region/MoveGlobalRegionCommand';
|
||||
export { ResizeGlobalRegionCommand, type GlobalRegionResizeEdge } from './global-region/ResizeGlobalRegionCommand';
|
||||
export { ResizeKeySignatureRegionCommand } from './global-region/ResizeKeySignatureRegionCommand';
|
||||
export { ResizeTempoRegionCommand } from './global-region/ResizeTempoRegionCommand';
|
||||
export { DeleteGlobalRegionCommand, DeleteMultipleGlobalRegionsCommand } from './global-region/DeleteGlobalRegionCommand';
|
||||
export { UpdateGlobalRegionTextCommand } from './global-region/UpdateGlobalRegionTextCommand';
|
||||
export { DeleteKeySignatureRegionCommand, DeleteMultipleKeySignatureRegionsCommand } from './global-region/DeleteKeySignatureRegionCommand';
|
||||
export { UpdateKeySignatureRegionCommand } from './global-region/UpdateKeySignatureRegionCommand';
|
||||
export { DeleteTempoRegionCommand, DeleteMultipleTempoRegionsCommand } from './global-region/DeleteTempoRegionCommand';
|
||||
export { UpdateTempoRegionCommand } from './global-region/UpdateTempoRegionCommand';
|
||||
|
||||
// Note commands
|
||||
export { CreateNoteCommand } from './note/CreateNoteCommand';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGProject, type KeySignature } from '../../KGProject';
|
||||
import type { TimeSignature } from '../../../types/projectTypes';
|
||||
import { normalizeTempoRegionsForProject } from '../../../util/globalTrackUtil';
|
||||
|
||||
/**
|
||||
* Interface defining properties that can be updated on a project
|
||||
@@ -59,6 +60,7 @@ export class ChangeProjectPropertyCommand extends KGCommand {
|
||||
// Update maxBars
|
||||
if (this.newProperties.maxBars !== undefined && this.newProperties.maxBars !== this.originalProperties.maxBars) {
|
||||
this.targetProject.setMaxBars(this.newProperties.maxBars);
|
||||
normalizeTempoRegionsForProject(this.targetProject);
|
||||
this.changedProperties.add('maxBars');
|
||||
updatedProperties.push(`maxBars: ${this.originalProperties.maxBars} → ${this.newProperties.maxBars}`);
|
||||
}
|
||||
@@ -85,6 +87,7 @@ export class ChangeProjectPropertyCommand extends KGCommand {
|
||||
// Compare time signatures
|
||||
if (originalTS.numerator !== newTS.numerator || originalTS.denominator !== newTS.denominator) {
|
||||
this.targetProject.setTimeSignature(newTS);
|
||||
normalizeTempoRegionsForProject(this.targetProject);
|
||||
this.changedProperties.add('timeSignature');
|
||||
updatedProperties.push(`timeSignature: ${originalTS.numerator}/${originalTS.denominator} → ${newTS.numerator}/${newTS.denominator}`);
|
||||
}
|
||||
@@ -128,6 +131,7 @@ export class ChangeProjectPropertyCommand extends KGCommand {
|
||||
// Restore maxBars (only if it was changed)
|
||||
if (this.changedProperties.has('maxBars') && this.originalProperties.maxBars !== undefined) {
|
||||
this.targetProject.setMaxBars(this.originalProperties.maxBars);
|
||||
normalizeTempoRegionsForProject(this.targetProject);
|
||||
restoredProperties.push(`maxBars: ${this.originalProperties.maxBars}`);
|
||||
}
|
||||
|
||||
@@ -146,6 +150,7 @@ export class ChangeProjectPropertyCommand extends KGCommand {
|
||||
// Restore time signature (only if it was changed)
|
||||
if (this.changedProperties.has('timeSignature') && this.originalProperties.timeSignature !== undefined) {
|
||||
this.targetProject.setTimeSignature(this.originalProperties.timeSignature);
|
||||
normalizeTempoRegionsForProject(this.targetProject);
|
||||
const ts = this.originalProperties.timeSignature;
|
||||
restoredProperties.push(`timeSignature: ${ts.numerator}/${ts.denominator}`);
|
||||
}
|
||||
@@ -226,4 +231,4 @@ export class ChangeProjectPropertyCommand extends KGCommand {
|
||||
public getChangedProperties(): Set<keyof ProjectUpdateProperties> {
|
||||
return new Set(this.changedProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user