feat: persist piano roll zoom level into project; fixed an issue in the horizontal auto-scrolling logic in piano roll window
This commit is contained in:
+15
-2
@@ -49,11 +49,15 @@ export class KGProject {
|
||||
@WithDefault(1)
|
||||
private barWidthMultiplier: number = 1;
|
||||
|
||||
@Expose()
|
||||
@WithDefault(1)
|
||||
private pianoRollZoom: number = 1;
|
||||
|
||||
@Expose()
|
||||
@WithDefault(0)
|
||||
private projectStructureVersion: number = 0;
|
||||
|
||||
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 11;
|
||||
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 12;
|
||||
|
||||
@Expose()
|
||||
@Type(() => KGTrack, {
|
||||
@@ -69,7 +73,7 @@ export class KGProject {
|
||||
private tracks: KGTrack[] = [];
|
||||
|
||||
// Constructor
|
||||
constructor(name: string = RESERVED_PROJECT_NAME, maxBars: number = 32, currentBars: number = 0, bpm: number = 125, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], barWidthMultiplier: number = 1, tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION) {
|
||||
constructor(name: string = RESERVED_PROJECT_NAME, maxBars: number = 32, currentBars: number = 0, bpm: number = 125, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], barWidthMultiplier: number = 1, tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION, pianoRollZoom: number = 1) {
|
||||
this.name = name;
|
||||
this.maxBars = maxBars;
|
||||
this.currentBars = currentBars;
|
||||
@@ -82,6 +86,7 @@ export class KGProject {
|
||||
this.barWidthMultiplier = barWidthMultiplier;
|
||||
this.tracks = tracks;
|
||||
this.projectStructureVersion = projectStructureVersion;
|
||||
this.pianoRollZoom = pianoRollZoom;
|
||||
}
|
||||
|
||||
// Getters
|
||||
@@ -181,4 +186,12 @@ export class KGProject {
|
||||
public setBarWidthMultiplier(barWidthMultiplier: number): void {
|
||||
this.barWidthMultiplier = barWidthMultiplier;
|
||||
}
|
||||
|
||||
public getPianoRollZoom(): number {
|
||||
return this.pianoRollZoom;
|
||||
}
|
||||
|
||||
public setPianoRollZoom(pianoRollZoom: number): void {
|
||||
this.pianoRollZoom = pianoRollZoom;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,13 @@ describe('KGProjectStorage', () => {
|
||||
return new KGProject(name, 16, 0, 120);
|
||||
}
|
||||
|
||||
it('defaults both zoom levels to 1 on a fresh project', () => {
|
||||
const project = new KGProject();
|
||||
|
||||
expect(project.getBarWidthMultiplier()).toBe(1);
|
||||
expect(project.getPianoRollZoom()).toBe(1);
|
||||
});
|
||||
|
||||
it('initializes and creates the projects directory', async () => {
|
||||
// The projects directory should exist after init
|
||||
const projects = await mockRoot.getDirectoryHandle('projects');
|
||||
@@ -138,6 +145,20 @@ describe('KGProjectStorage', () => {
|
||||
expect(loaded!.getTracks()[0].getSolo()).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves both main-grid and piano-roll zoom levels when saving and loading', async () => {
|
||||
const project = createTestProject('Zoom Song');
|
||||
project.setBarWidthMultiplier(3);
|
||||
project.setPianoRollZoom(5);
|
||||
|
||||
await storage.save('Zoom Song', project);
|
||||
|
||||
const loaded = await storage.load('Zoom Song');
|
||||
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded!.getBarWidthMultiplier()).toBe(3);
|
||||
expect(loaded!.getPianoRollZoom()).toBe(5);
|
||||
});
|
||||
|
||||
it('creates meta.json and media/ directory on save', async () => {
|
||||
const project = createTestProject('My Song');
|
||||
await storage.save('My Song', project);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { upgradeToV8 } from './upgradeToV8';
|
||||
import { upgradeToV9 } from './upgradeToV9';
|
||||
import { upgradeToV10 } from './upgradeToV10';
|
||||
import { upgradeToV11 } from './upgradeToV11';
|
||||
import { upgradeToV12 } from './upgradeToV12';
|
||||
|
||||
/**
|
||||
* Upgrade the given project to the latest structure version, one version at a time.
|
||||
@@ -73,6 +74,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject {
|
||||
workingProject = upgradeToV11(workingProject);
|
||||
break;
|
||||
}
|
||||
case 12: {
|
||||
workingProject = upgradeToV12(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}`);
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('upgradeToV10', () => {
|
||||
|
||||
const upgraded = upgradeProjectToLatest(project);
|
||||
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(11);
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(KGProject.CURRENT_PROJECT_STRUCTURE_VERSION);
|
||||
expect(upgraded.getTracks()[0].getVolumeAutomation()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('upgradeToV11', () => {
|
||||
|
||||
const upgraded = upgradeProjectToLatest(project);
|
||||
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(11);
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(KGProject.CURRENT_PROJECT_STRUCTURE_VERSION);
|
||||
expect(upgraded.getTracks()[0].getMuted()).toBe(false);
|
||||
expect(upgraded.getTracks()[0].getSolo()).toBe(false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { KGProject } from '../KGProject';
|
||||
import { upgradeProjectToLatest } from './KGProjectUpgrader';
|
||||
import { upgradeToV12 } from './upgradeToV12';
|
||||
|
||||
describe('upgradeToV12', () => {
|
||||
it('initializes missing zoom levels to 1', () => {
|
||||
const project = new KGProject('Legacy', 32, 0, 125, undefined, undefined, undefined, undefined, [0, 0], 1, [], 11);
|
||||
delete (project as unknown as { barWidthMultiplier?: unknown }).barWidthMultiplier;
|
||||
delete (project as unknown as { pianoRollZoom?: unknown }).pianoRollZoom;
|
||||
|
||||
upgradeToV12(project);
|
||||
|
||||
expect(project.getBarWidthMultiplier()).toBe(1);
|
||||
expect(project.getPianoRollZoom()).toBe(1);
|
||||
expect(project.getProjectStructureVersion()).toBe(12);
|
||||
});
|
||||
|
||||
it('preserves existing zoom levels', () => {
|
||||
const project = new KGProject('Legacy', 32, 0, 125, undefined, undefined, undefined, undefined, [0, 0], 4, [], 11, 6);
|
||||
|
||||
upgradeToV12(project);
|
||||
|
||||
expect(project.getBarWidthMultiplier()).toBe(4);
|
||||
expect(project.getPianoRollZoom()).toBe(6);
|
||||
});
|
||||
|
||||
it('upgrades legacy projects through the main upgrader path', () => {
|
||||
const project = new KGProject('Legacy', 32, 0, 125, undefined, undefined, undefined, undefined, [0, 0], 1, [], 11);
|
||||
delete (project as unknown as { barWidthMultiplier?: unknown }).barWidthMultiplier;
|
||||
delete (project as unknown as { pianoRollZoom?: unknown }).pianoRollZoom;
|
||||
|
||||
const upgraded = upgradeProjectToLatest(project);
|
||||
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(KGProject.CURRENT_PROJECT_STRUCTURE_VERSION);
|
||||
expect(upgraded.getBarWidthMultiplier()).toBe(1);
|
||||
expect(upgraded.getPianoRollZoom()).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { KGProject } from '../KGProject';
|
||||
|
||||
function isValidZoomLevel(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
||||
}
|
||||
|
||||
export function upgradeToV12(project: KGProject): KGProject {
|
||||
try {
|
||||
const barWidthMultiplier = project.getBarWidthMultiplier?.();
|
||||
if (!isValidZoomLevel(barWidthMultiplier)) {
|
||||
project.setBarWidthMultiplier(1);
|
||||
}
|
||||
|
||||
const pianoRollZoom = project.getPianoRollZoom?.();
|
||||
if (!isValidZoomLevel(pianoRollZoom)) {
|
||||
project.setPianoRollZoom(1);
|
||||
}
|
||||
} finally {
|
||||
project.setProjectStructureVersion(12);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ describe('upgradeToV8', () => {
|
||||
|
||||
const upgraded = upgradeProjectToLatest(project);
|
||||
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(11);
|
||||
expect(upgraded.getProjectStructureVersion()).toBe(KGProject.CURRENT_PROJECT_STRUCTURE_VERSION);
|
||||
expect((upgraded.getTracks()[0].getRegions()[0] as KGMidiRegion).getPitchBends()).toEqual([]);
|
||||
expect((upgraded.getTracks()[0].getRegions()[0] as KGMidiRegion).getControllerEventsByType()).toHaveLength(128);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ export class KGPianoRollState {
|
||||
private currentMode: string = "ionian"; // Default mode
|
||||
private automationViewEnabled: boolean = false;
|
||||
private currentAutomationType: string = "pitch-bend";
|
||||
private pianoRollZoom: number = 1;
|
||||
private sheetMusicViewEnabled: boolean = false;
|
||||
private sheetMusicTrackScopeEnabled: boolean = false;
|
||||
private sheetQuantization: string = '16,48';
|
||||
@@ -86,6 +87,14 @@ export class KGPianoRollState {
|
||||
this.currentAutomationType = type;
|
||||
}
|
||||
|
||||
public getPianoRollZoom(): number {
|
||||
return this.pianoRollZoom;
|
||||
}
|
||||
|
||||
public setPianoRollZoom(zoom: number): void {
|
||||
this.pianoRollZoom = zoom;
|
||||
}
|
||||
|
||||
public getSheetMusicViewEnabled(): boolean {
|
||||
return this.sheetMusicViewEnabled;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user