From a0fe283ab57fc8d99b3af29847a52c5c4ee9a051 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Mon, 18 May 2026 22:21:00 -0700 Subject: [PATCH] feat: persist piano roll zoom level into project; fixed an issue in the horizontal auto-scrolling logic in piano roll window --- src/components/piano-roll/PianoRoll.test.ts | 8 + src/components/piano-roll/PianoRoll.tsx | 30 ++-- .../PianoRollZoomPersistence.test.tsx | 162 ++++++++++++++++++ src/core/KGProject.ts | 17 +- src/core/io/KGProjectStorage.test.ts | 21 +++ .../project-upgrader/KGProjectUpgrader.ts | 5 + .../project-upgrader/upgradeToV10.test.ts | 2 +- .../project-upgrader/upgradeToV11.test.ts | 2 +- .../project-upgrader/upgradeToV12.test.ts | 39 +++++ src/core/project-upgrader/upgradeToV12.ts | 23 +++ src/core/project-upgrader/upgradeToV8.test.ts | 2 +- src/core/state/KGPianoRollState.ts | 9 + src/stores/projectStore.test.ts | 19 +- src/stores/projectStore.ts | 2 + src/test/utils/mock-data.ts | 2 +- 15 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 src/components/piano-roll/PianoRollZoomPersistence.test.tsx create mode 100644 src/core/project-upgrader/upgradeToV12.test.ts create mode 100644 src/core/project-upgrader/upgradeToV12.ts diff --git a/src/components/piano-roll/PianoRoll.test.ts b/src/components/piano-roll/PianoRoll.test.ts index c9c9c31..1a4b761 100644 --- a/src/components/piano-roll/PianoRoll.test.ts +++ b/src/components/piano-roll/PianoRoll.test.ts @@ -33,6 +33,7 @@ vi.mock('../../stores/projectStore', () => ({ import { createPendingModeSwitchRequest, + getRegionStartScrollLeft, getRegionPlayheadRelation, getScrollLeftForViewportRequest, } from './PianoRoll'; @@ -57,6 +58,13 @@ describe('PianoRoll viewport switch helpers', () => { expect(getRegionPlayheadRelation(25, 16, 24)).toBe('after'); }); + it('uses the zoomed beat width when scrolling to a different region in piano-roll view', () => { + document.documentElement.style.setProperty('--region-grid-beat-width', '80px'); + document.documentElement.style.setProperty('--region-grid-bar-width', 'calc(var(--region-grid-beat-width) * var(--time-signature-numerator))'); + + expect(getRegionStartScrollLeft(16)).toBe(1280); + }); + it('centers an in-region playhead when switching to region-scope sheet view', () => { const request = createPendingModeSwitchRequest({ playheadBeat: 20, diff --git a/src/components/piano-roll/PianoRoll.tsx b/src/components/piano-roll/PianoRoll.tsx index 85170b3..141fe35 100644 --- a/src/components/piano-roll/PianoRoll.tsx +++ b/src/components/piano-roll/PianoRoll.tsx @@ -96,7 +96,7 @@ const PianoRoll: React.FC = ({ useState(3); // Piano roll zoom (1x–8x); updates --region-grid-beat-width CSS variable - const [pianoRollZoom, setPianoRollZoom] = useState(1); + const [pianoRollZoom, setPianoRollZoom] = useState(() => KGPianoRollState.instance().getPianoRollZoom()); const [automationEnabled, setAutomationEnabled] = useState(false); const [automationType, setAutomationType] = useState('pitch-bend'); const [sheetMusicViewEnabled, setSheetMusicViewEnabled] = useState(false); @@ -785,6 +785,8 @@ const PianoRoll: React.FC = ({ } } + KGPianoRollState.instance().setPianoRollZoom(nextZoom); + KGCore.instance().getCurrentProject().setPianoRollZoom(nextZoom); setPianoRollZoom(nextZoom); }, [pianoRollZoom]); @@ -1027,7 +1029,7 @@ const PianoRoll: React.FC = ({ }; }, [pianoRollZoom]); - // Scroll horizontally to the active region's starting bar + // Scroll horizontally to the active region's starting position useEffect(() => { if (!pianoRollNoteScrollRef.current || !activeRegion) { previousActiveRegionIdRef.current = activeRegion?.getId() ?? null; @@ -1043,27 +1045,17 @@ const PianoRoll: React.FC = ({ // Get the starting beat of the region const startBeat = activeRegion.getStartFromBeat(); - // Get the time signature to calculate beats per bar - const beatsPerBar = timeSignature.numerator; - - // Calculate the bar number (0-indexed) - const barNumber = Math.floor(startBeat / beatsPerBar); - if (DEBUG_MODE.PIANO_ROLL) { - console.log(`Scrolling to region's starting bar: ${barNumber + 1} (startBeat: ${startBeat}, beatsPerBar: ${beatsPerBar})`); + console.log(`Scrolling to region's starting position: startBeat=${startBeat}`); } - // Calculate the pixel position (each bar is --region-grid-bar-width wide, which is 160px by default) - const barWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-bar-width')) || 160; - - // Calculate the scroll position to scroll to the starting bar - const scrollPosition = barNumber * barWidth; + const scrollPosition = getRegionStartScrollLeft(startBeat); // Scroll to the calculated position pianoRollNoteScrollRef.current.scrollLeft = Math.max(0, scrollPosition); previousActiveRegionIdRef.current = activeRegion.getId(); } - }, [activeRegion, timeSignature]); + }, [activeRegion]); useLayoutEffect(() => { const request = pendingModeSwitchRequestRef.current; @@ -1589,3 +1581,11 @@ export function getScrollLeftForViewportRequest({ container, }); } + +export function getRegionStartScrollLeft(startBeat: number): number { + const beatWidth = parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width') + ) || TOOLBAR_CONSTANTS.BASE_BAR_WIDTH; + + return Math.max(0, startBeat * beatWidth); +} diff --git a/src/components/piano-roll/PianoRollZoomPersistence.test.tsx b/src/components/piano-roll/PianoRollZoomPersistence.test.tsx new file mode 100644 index 0000000..f9df01b --- /dev/null +++ b/src/components/piano-roll/PianoRollZoomPersistence.test.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { describe, expect, it, beforeEach, vi } from 'vitest'; +import { render } from '@testing-library/react'; +import PianoRoll from './PianoRoll'; +import { createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data'; + +const pianoRollState = { + zoom: 1, + getCurrentSnap: vi.fn(() => 'NO SNAP'), + getActiveTool: vi.fn(() => 'pointer'), + getAutomationViewEnabled: vi.fn(() => false), + getCurrentAutomationType: vi.fn(() => 'pitch-bend'), + getPianoRollZoom: vi.fn(() => pianoRollState.zoom), + setPianoRollZoom: vi.fn((zoom: number) => { + pianoRollState.zoom = zoom; + }), + getSheetMusicViewEnabled: vi.fn(() => false), + getSheetMusicTrackScopeEnabled: vi.fn(() => false), + getSheetQuantization: vi.fn(() => '16,48'), + setSheetMusicViewEnabled: vi.fn(), + setActiveTool: vi.fn(), + setAutomationViewEnabled: vi.fn(), + setCurrentAutomationType: vi.fn(), + setSheetQuantization: vi.fn(), + setSheetMusicTrackScopeEnabled: vi.fn(), + setCurrentSnap: vi.fn(), + setCurrentSuitableChords: vi.fn(), + setCurrentSuitableChordsPitchClasses: vi.fn(), +}; + +const mockProject = { + setPianoRollZoom: vi.fn(), +}; + +const region = createMockMidiRegion({ id: 'region-1', trackId: '1', trackIndex: 0 }); +const track = createMockMidiTrack({ id: 1, regions: [region] }); + +const storeState = { + maxBars: 8, + tracks: [track], + updateTrack: vi.fn(), + timeSignature: { numerator: 4, denominator: 4 }, + showChatBox: false, + showKGOnePanel: false, + showEventListPanel: false, + showInstrumentSelection: false, + keySignature: 'C major', + selectedMode: 'ionian', + setSelectedMode: vi.fn(), + playheadPosition: 0, + isPlaying: false, + autoScrollEnabled: false, + bpm: 120, + pianoRollScrollRequest: null, + selectedNoteIds: [], + automationRedrawVersion: 0, +}; + +let latestToolbarProps: { zoom: number; onZoomChange: (value: number) => void } | null = null; + +vi.mock('../../stores/projectStore', () => ({ + useProjectStore: Object.assign( + (selector?: (state: typeof storeState) => unknown) => selector ? selector(storeState) : storeState, + { + getState: () => ({ setAutoScrollEnabled: vi.fn() }), + setState: vi.fn(), + } + ), +})); + +vi.mock('../../core/state/KGPianoRollState', () => ({ + KGPianoRollState: { + instance: () => pianoRollState, + SNAP_OPTIONS: ['NO SNAP'], + QUANT_POS_OPTIONS: ['1/8'], + QUANT_LEN_OPTIONS: ['1/8'], + }, +})); + +vi.mock('../../core/KGCore', () => ({ + KGCore: { + FUNCTIONAL_CHORDS_DATA: { ionian: { name: 'Ionian' } }, + instance: () => ({ + getCurrentProject: () => mockProject, + getSelectedItems: () => [], + executeCommand: vi.fn(), + }), + }, +})); + +vi.mock('../../core/config/ConfigManager', () => ({ + ConfigManager: { + instance: () => ({ + getIsInitialized: () => true, + get: vi.fn(), + addChangeListener: () => () => undefined, + }), + }, +})); + +vi.mock('../../util/scaleUtil', () => ({ + getSuitableChords: vi.fn(() => ({})), + noteNameToPitchClass: vi.fn(), +})); + +vi.mock('../../util/dialogUtil', () => ({ + showAlert: vi.fn(), + showPrompt: vi.fn(), +})); + +vi.mock('./PianoRollHeader', () => ({ default: () =>
})); +vi.mock('./NoteAttributeBar', () => ({ default: () =>
})); +vi.mock('./PianoRollContent', () => ({ default: () =>
})); +vi.mock('./PianoRollToolbar', () => ({ + default: (props: { zoom: number; onZoomChange: (value: number) => void }) => { + latestToolbarProps = props; + return
{props.zoom}x
; + }, +})); + +describe('PianoRoll zoom persistence', () => { + beforeEach(() => { + latestToolbarProps = null; + pianoRollState.zoom = 1; + mockProject.setPianoRollZoom.mockReset(); + pianoRollState.getPianoRollZoom.mockClear(); + pianoRollState.setPianoRollZoom.mockClear(); + document.documentElement.style.setProperty('--region-piano-key-width', '60px'); + document.documentElement.style.setProperty('--region-grid-beat-width', '40px'); + }); + + it('restores the previous zoom after closing and reopening the piano roll', () => { + const firstRender = render( + + ); + + expect(latestToolbarProps?.zoom).toBe(1); + + latestToolbarProps?.onZoomChange(3); + + expect(pianoRollState.setPianoRollZoom).toHaveBeenCalledWith(3); + expect(mockProject.setPianoRollZoom).toHaveBeenCalledWith(3); + + firstRender.unmount(); + + render( + + ); + + expect(latestToolbarProps?.zoom).toBe(3); + }); +}); diff --git a/src/core/KGProject.ts b/src/core/KGProject.ts index c3d28aa..6f7bf12 100644 --- a/src/core/KGProject.ts +++ b/src/core/KGProject.ts @@ -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; + } } diff --git a/src/core/io/KGProjectStorage.test.ts b/src/core/io/KGProjectStorage.test.ts index a20a707..0e722a9 100644 --- a/src/core/io/KGProjectStorage.test.ts +++ b/src/core/io/KGProjectStorage.test.ts @@ -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); diff --git a/src/core/project-upgrader/KGProjectUpgrader.ts b/src/core/project-upgrader/KGProjectUpgrader.ts index b70b365..5a13d7a 100644 --- a/src/core/project-upgrader/KGProjectUpgrader.ts +++ b/src/core/project-upgrader/KGProjectUpgrader.ts @@ -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}`); diff --git a/src/core/project-upgrader/upgradeToV10.test.ts b/src/core/project-upgrader/upgradeToV10.test.ts index 643c18d..0b98d81 100644 --- a/src/core/project-upgrader/upgradeToV10.test.ts +++ b/src/core/project-upgrader/upgradeToV10.test.ts @@ -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); }); }); diff --git a/src/core/project-upgrader/upgradeToV11.test.ts b/src/core/project-upgrader/upgradeToV11.test.ts index abb1832..325a918 100644 --- a/src/core/project-upgrader/upgradeToV11.test.ts +++ b/src/core/project-upgrader/upgradeToV11.test.ts @@ -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); }); diff --git a/src/core/project-upgrader/upgradeToV12.test.ts b/src/core/project-upgrader/upgradeToV12.test.ts new file mode 100644 index 0000000..8b9cf8b --- /dev/null +++ b/src/core/project-upgrader/upgradeToV12.test.ts @@ -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); + }); +}); diff --git a/src/core/project-upgrader/upgradeToV12.ts b/src/core/project-upgrader/upgradeToV12.ts new file mode 100644 index 0000000..5b4682c --- /dev/null +++ b/src/core/project-upgrader/upgradeToV12.ts @@ -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; +} diff --git a/src/core/project-upgrader/upgradeToV8.test.ts b/src/core/project-upgrader/upgradeToV8.test.ts index ee13acf..076155b 100644 --- a/src/core/project-upgrader/upgradeToV8.test.ts +++ b/src/core/project-upgrader/upgradeToV8.test.ts @@ -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); }); diff --git a/src/core/state/KGPianoRollState.ts b/src/core/state/KGPianoRollState.ts index f426f95..8d15a7c 100644 --- a/src/core/state/KGPianoRollState.ts +++ b/src/core/state/KGPianoRollState.ts @@ -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; } diff --git a/src/stores/projectStore.test.ts b/src/stores/projectStore.test.ts index 9788284..40bef52 100644 --- a/src/stores/projectStore.test.ts +++ b/src/stores/projectStore.test.ts @@ -5,6 +5,7 @@ import { KGMidiTrack } from '../core/track/KGMidiTrack'; const pianoRollStateMocks = vi.hoisted(() => ({ setSheetMusicViewEnabled: vi.fn(), + setPianoRollZoom: vi.fn(), })); let mockTracks: KGTrack[] = [new KGMidiTrack('Track 1', 0, 'acoustic_grand_piano')]; @@ -19,13 +20,23 @@ const mockProject = { getSelectedMode: () => 'major', getIsLooping: () => false, getLoopingRange: () => [0, 0] as [number, number], + getPianoRollZoom: () => 1, }; +let currentProject = mockProject; const mockAudioInterface = { getTransportPosition: vi.fn().mockReturnValue(8), startAudioRecording: vi.fn().mockResolvedValue({ usedDeviceId: 'default', fellBackToDefault: false }), stopAudioRecording: vi.fn().mockResolvedValue(null), cancelAudioRecording: vi.fn().mockResolvedValue(undefined), + removeTrackSynth: vi.fn(), + removeTrackAudioPlayerBus: vi.fn(), + createTrackAudioPlayerBus: vi.fn().mockResolvedValue(undefined), + loadAudioBufferForTrack: vi.fn(), + createTrackSynth: vi.fn(), + setTrackVolume: vi.fn(), + setTrackMute: vi.fn(), + setTrackSolo: vi.fn(), }; const configValues = new Map([ @@ -33,7 +44,10 @@ const configValues = new Map([ ]); const mockCore = { - getCurrentProject: () => mockProject, + getCurrentProject: () => currentProject, + setCurrentProject: vi.fn((project: typeof mockProject) => { + currentProject = project; + }), setPlayheadUpdateCallback: vi.fn(), setPlaybackStateChangeCallback: vi.fn(), setLoopBoundaryReachedCallback: vi.fn(), @@ -49,6 +63,7 @@ const mockCore = { redo: vi.fn(() => true), clearSelectedItems: vi.fn(), getStatus: () => 'Ready', + setStatus: vi.fn(), getPlayheadPosition: () => 0, setPlayheadPosition: vi.fn(), getIsPlaying: () => false, @@ -91,7 +106,9 @@ describe('projectStore piano roll state', () => { vi.useFakeTimers(); vi.resetModules(); pianoRollStateMocks.setSheetMusicViewEnabled.mockReset(); + pianoRollStateMocks.setPianoRollZoom.mockReset(); mockTracks = [new KGMidiTrack('Track 1', 0, 'acoustic_grand_piano')]; + currentProject = mockProject; mockCore.startPlaying.mockReset(); mockCore.startPlaying.mockResolvedValue(undefined); mockCore.stopPlaying.mockReset(); diff --git a/src/stores/projectStore.ts b/src/stores/projectStore.ts index eafac14..d1418e6 100644 --- a/src/stores/projectStore.ts +++ b/src/stores/projectStore.ts @@ -308,6 +308,7 @@ export const useProjectStore = create((set, get) => { // Get initial ChatBox state from config const configManager = ConfigManager.instance(); + KGPianoRollState.instance().setPianoRollZoom(currentProject.getPianoRollZoom()); const initialChatBoxState = configManager.getIsInitialized() ? (configManager.get('chatbox.default_open') as boolean) ?? false : false; @@ -942,6 +943,7 @@ export const useProjectStore = create((set, get) => { // Reset piano roll state for new/loaded project KGPianoRollState.instance().setLastEditedNoteLength(1); + KGPianoRollState.instance().setPianoRollZoom(projectToLoad.getPianoRollZoom()); // Add a status message KGCore.instance().setStatus(`Project "${projectToLoad.getName()}" loaded with audio setup`); diff --git a/src/test/utils/mock-data.ts b/src/test/utils/mock-data.ts index 829bb29..9024bcf 100644 --- a/src/test/utils/mock-data.ts +++ b/src/test/utils/mock-data.ts @@ -165,7 +165,7 @@ export const createMockProject = (overrides: Partial<{ [0, 0], // loopingRange 1, // barWidthMultiplier defaults.tracks, // tracks - 5 // projectStructureVersion + KGProject.CURRENT_PROJECT_STRUCTURE_VERSION // projectStructureVersion ); return project;