From e674e8b6099f62d636c41b692a8b7deec93d5bec Mon Sep 17 00:00:00 2001 From: andorthehood Date: Fri, 2 Jan 2026 21:14:31 +0000 Subject: [PATCH 1/9] feat(editor-state): add manual compilation --- .../packages/editor-state/src/configSchema.ts | 2 +- .../editor-state/src/effects/compiler.ts | 40 ++++++----- .../editor-state/src/effects/config.ts | 6 +- .../src/effects/disableCompilation.test.ts | 71 ++++++++++--------- .../editor-state/src/effects/menu/menus.ts | 7 ++ .../src/effects/projectImport.test.ts | 4 +- .../src/effects/runtimeReadyProject.test.ts | 14 ++-- .../config/applyConfigToState.test.ts | 14 ++-- .../config/applyConfigToState.ts | 10 +-- .../pureHelpers/state/createDefaultState.ts | 2 +- .../src/pureHelpers/testingUtils/testUtils.ts | 2 +- .../editor/packages/editor-state/src/types.ts | 4 +- src/compiler-callback.ts | 2 +- src/editor.ts | 4 +- 14 files changed, 98 insertions(+), 84 deletions(-) diff --git a/packages/editor/packages/editor-state/src/configSchema.ts b/packages/editor/packages/editor-state/src/configSchema.ts index cae1a0a6a..77b43b620 100644 --- a/packages/editor/packages/editor-state/src/configSchema.ts +++ b/packages/editor/packages/editor-state/src/configSchema.ts @@ -101,7 +101,7 @@ const configSchema: ConfigSchema = { type: 'array', items: runtimeSettingsSchema, }, - disableCompilation: { type: 'boolean' }, + disableAutoCompilation: { type: 'boolean' }, }, additionalProperties: false, }; diff --git a/packages/editor/packages/editor-state/src/effects/compiler.ts b/packages/editor/packages/editor-state/src/effects/compiler.ts index f1de460b5..3b4b3bf8e 100644 --- a/packages/editor/packages/editor-state/src/effects/compiler.ts +++ b/packages/editor/packages/editor-state/src/effects/compiler.ts @@ -27,24 +27,8 @@ export function flattenProjectForCompiler(codeBlocks: Set) export default async function compiler(store: StateManager, events: EventDispatcher) { const state = store.getState(); - async function onRecompile() { - // Check if compilation is disabled by config - if (state.compiler.disableCompilation) { - log(state, 'Compilation skipped: disableCompilation flag is set', 'Compiler'); - store.set('compiler.isCompiling', false); - store.set('codeErrors.compilationErrors', []); - return; - } - - // Check if project has pre-compiled WASM already loaded (runtime-ready project) - // If codeBuffer is populated and we don't have a compiler, skip compilation - if (state.compiler.codeBuffer.length > 0 && !state.callbacks.compileProject) { - log(state, 'Using pre-compiled WASM from runtime-ready project', 'Compiler'); - store.set('compiler.isCompiling', false); - store.set('codeErrors.compilationErrors', []); - return; - } + async function onForceCompile() { const { modules, functions } = flattenProjectForCompiler(state.graphicHelper.codeBlocks); store.set('compiler.isCompiling', true); @@ -68,7 +52,7 @@ export default async function compiler(store: StateManager, events: Event }, }; - const result = await state.callbacks.compileProject?.(modules, compilerOptions, functions); + const result = await state.callbacks.compileCode?.(modules, compilerOptions, functions); if (!result) { return; @@ -109,6 +93,26 @@ export default async function compiler(store: StateManager, events: Event } } + function onRecompile() { + // Check if compilation is disabled by config + if (state.compiler.disableAutoCompilation) { + log(state, 'Compilation skipped: disableAutoCompilation flag is set', 'Compiler'); + return; + } + + // Check if project has pre-compiled WASM already loaded (runtime-ready project) + // If codeBuffer is populated and we don't have a compiler, skip compilation + if (state.compiler.codeBuffer.length > 0 && !state.callbacks.compileCode) { + log(state, 'Using pre-compiled WASM from runtime-ready project', 'Compiler'); + store.set('compiler.isCompiling', false); + store.set('codeErrors.compilationErrors', []); + return; + } + + onForceCompile(); + } + + events.on('compileCode', onForceCompile); events.on('codeBlockAdded', onRecompile); events.on('deleteCodeBlock', onRecompile); store.subscribe('compiler.compilerOptions', onRecompile); diff --git a/packages/editor/packages/editor-state/src/effects/config.ts b/packages/editor/packages/editor-state/src/effects/config.ts index 64090e664..31504ba73 100644 --- a/packages/editor/packages/editor-state/src/effects/config.ts +++ b/packages/editor/packages/editor-state/src/effects/config.ts @@ -72,8 +72,8 @@ export default function configEffect(store: StateManager, events: EventDi } // Check if compilation is disabled by config - if (state.compiler.disableCompilation) { - log(state, 'Config compilation skipped: disableCompilation flag is set', 'Config'); + if (state.compiler.disableAutoCompilation) { + log(state, 'Config compilation skipped: disableAutoCompilation flag is set', 'Config'); return; } @@ -118,7 +118,7 @@ export default function configEffect(store: StateManager, events: EventDi */ export async function compileConfigForExport(state: State): Promise> { // If compilation is disabled, return the stored compiled config if available - if (state.compiler.disableCompilation) { + if (state.compiler.disableAutoCompilation) { return state.compiler.compiledConfig || {}; } diff --git a/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts b/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts index e4e5d0fdc..dd06458cc 100644 --- a/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts +++ b/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts @@ -9,15 +9,15 @@ import { createMockEventDispatcherWithVitest } from '../pureHelpers/testingUtils import type { State } from '../types'; -describe('disableCompilation feature', () => { +describe('disableAutoCompilation feature', () => { let mockState: State; let store: ReturnType>; let mockEvents: ReturnType; - let mockCompileProject: MockInstance; + let mockCompileCode: MockInstance; let mockCompileConfig: MockInstance; beforeEach(() => { - mockCompileProject = vi.fn().mockResolvedValue({ + mockCompileCode = vi.fn().mockResolvedValue({ compiledModules: {}, codeBuffer: new Uint8Array([1, 2, 3]), allocatedMemorySize: 1024, @@ -47,10 +47,10 @@ describe('disableCompilation feature', () => { mockState = createMockState({ compiler: { - disableCompilation: false, + disableAutoCompilation: false, }, callbacks: { - compileProject: mockCompileProject, + compileCode: mockCompileCode, compileConfig: mockCompileConfig, }, }); @@ -63,8 +63,8 @@ describe('disableCompilation feature', () => { }); describe('Project compilation', () => { - it('should skip compilation when disableCompilation is true', async () => { - store.set('compiler.disableCompilation', true); + it('should skip compilation when disableAutoCompilation is true', async () => { + store.set('compiler.disableAutoCompilation', true); compiler(store, mockEvents); @@ -75,19 +75,20 @@ describe('disableCompilation feature', () => { const onRecompileCallback = compileCall![1]; await onRecompileCallback(); - expect(mockCompileProject).not.toHaveBeenCalled(); + expect(mockCompileCode).not.toHaveBeenCalled(); expect(mockState.compiler.isCompiling).toBe(false); expect(mockState.codeErrors.compilationErrors).toEqual([]); expect( mockState.console.logs.some( log => - log.message.includes('Compilation skipped: disableCompilation flag is set') && log.category === '[Compiler]' + log.message.includes('Compilation skipped: disableAutoCompilation flag is set') && + log.category === '[Compiler]' ) ).toBe(true); }); - it('should compile normally when disableCompilation is false', async () => { - store.set('compiler.disableCompilation', false); + it('should compile normally when disableAutoCompilation is false', async () => { + store.set('compiler.disableAutoCompilation', false); compiler(store, mockEvents); @@ -97,13 +98,13 @@ describe('disableCompilation feature', () => { await onRecompileCallback(); - expect(mockCompileProject).toHaveBeenCalled(); + expect(mockCompileCode).toHaveBeenCalled(); }); - it('should prioritize disableCompilation check over pre-compiled WASM check', async () => { - store.set('compiler.disableCompilation', true); + it('should prioritize disableAutoCompilation check over pre-compiled WASM check', async () => { + store.set('compiler.disableAutoCompilation', true); store.set('compiler.codeBuffer', new Uint8Array([1, 2, 3, 4, 5])); - mockState.callbacks.compileProject = undefined; + mockState.callbacks.compileCode = undefined; compiler(store, mockEvents); @@ -114,15 +115,17 @@ describe('disableCompilation feature', () => { await onRecompileCallback(); expect( - mockState.console.logs.some(log => log.message.includes('Compilation skipped: disableCompilation flag is set')) + mockState.console.logs.some(log => + log.message.includes('Compilation skipped: disableAutoCompilation flag is set') + ) ).toBe(true); expect(mockState.console.logs.some(log => log.message.includes('Using pre-compiled WASM'))).toBe(false); }); }); describe('Config compilation', () => { - it('should skip config compilation when disableCompilation is true', async () => { - store.set('compiler.disableCompilation', true); + it('should skip config compilation when disableAutoCompilation is true', async () => { + store.set('compiler.disableAutoCompilation', true); configEffect(store, mockEvents); @@ -137,14 +140,14 @@ describe('disableCompilation feature', () => { expect( mockState.console.logs.some( log => - log.message.includes('Config compilation skipped: disableCompilation flag is set') && + log.message.includes('Config compilation skipped: disableAutoCompilation flag is set') && log.category === '[Config]' ) ).toBe(true); }); - it('should compile config normally when disableCompilation is false', async () => { - store.set('compiler.disableCompilation', false); + it('should compile config normally when disableAutoCompilation is false', async () => { + store.set('compiler.disableAutoCompilation', false); configEffect(store, mockEvents); @@ -159,8 +162,8 @@ describe('disableCompilation feature', () => { }); describe('Runtime-ready export', () => { - it('should skip config compilation for export when disableCompilation is true', async () => { - mockState.compiler.disableCompilation = true; + it('should skip config compilation for export when disableAutoCompilation is true', async () => { + mockState.compiler.disableAutoCompilation = true; const result = await compileConfigForExport(mockState); @@ -168,8 +171,8 @@ describe('disableCompilation feature', () => { expect(result).toEqual({}); }); - it('should return stored compiledConfig when disableCompilation is true and config exists', async () => { - mockState.compiler.disableCompilation = true; + it('should return stored compiledConfig when disableAutoCompilation is true and config exists', async () => { + mockState.compiler.disableAutoCompilation = true; mockState.compiler.compiledConfig = { memorySizeBytes: 2097152, selectedRuntime: 1, @@ -184,8 +187,8 @@ describe('disableCompilation feature', () => { }); }); - it('should compile config for export when disableCompilation is false', async () => { - mockState.compiler.disableCompilation = false; + it('should compile config for export when disableAutoCompilation is false', async () => { + mockState.compiler.disableAutoCompilation = false; const result = await compileConfigForExport(mockState); @@ -194,7 +197,7 @@ describe('disableCompilation feature', () => { }); it('should return empty config when no compileConfig callback is provided', async () => { - mockState.compiler.disableCompilation = false; + mockState.compiler.disableAutoCompilation = false; mockState.callbacks.compileConfig = undefined; const result = await compileConfigForExport(mockState); @@ -204,9 +207,9 @@ describe('disableCompilation feature', () => { }); describe('applyConfigToState integration', () => { - it('should set disableCompilation flag from config', async () => { + it('should set disableAutoCompilation flag from config', async () => { mockCompileConfig.mockResolvedValue({ - config: { disableCompilation: true }, + config: { disableAutoCompilation: true }, errors: [], }); @@ -218,11 +221,11 @@ describe('disableCompilation feature', () => { await rebuildConfigCallback(); - expect(mockState.compiler.disableCompilation).toBe(true); + expect(mockState.compiler.disableAutoCompilation).toBe(true); }); - it('should not change disableCompilation flag when not in config', async () => { - store.set('compiler.disableCompilation', false); + it('should not change disableAutoCompilation flag when not in config', async () => { + store.set('compiler.disableAutoCompilation', false); mockCompileConfig.mockResolvedValue({ config: { memorySizeBytes: 2097152 }, @@ -237,7 +240,7 @@ describe('disableCompilation feature', () => { await rebuildConfigCallback(); - expect(mockState.compiler.disableCompilation).toBe(false); + expect(mockState.compiler.disableAutoCompilation).toBe(false); }); }); }); diff --git a/packages/editor/packages/editor-state/src/effects/menu/menus.ts b/packages/editor/packages/editor-state/src/effects/menu/menus.ts index 03f106e47..74c6cfe83 100644 --- a/packages/editor/packages/editor-state/src/effects/menu/menus.ts +++ b/packages/editor/packages/editor-state/src/effects/menu/menus.ts @@ -67,6 +67,13 @@ export const mainMenu: MenuGenerator = state => [ disabled: !state.callbacks.getListOfProjects, }, { divider: true }, + { + title: 'Compile Code', + action: 'compileCode', + close: true, + disabled: !state.callbacks.compileCode || !state.callbacks.compileConfig, + }, + { divider: true }, { title: 'Export Project', action: 'exportProject', close: true, disabled: !state.callbacks.exportProject }, { title: 'Export Runtime-Ready Project', diff --git a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts index 306ee27ff..2f70efe5e 100644 --- a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts +++ b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts @@ -317,7 +317,7 @@ describe('projectImport', () => { compiledConfig: { memorySizeBytes: 2097152, selectedRuntime: 1, - disableCompilation: true, + disableAutoCompilation: true, }, compiledWasm: 'base64encodedwasm', memorySnapshot: 'base64encodedmemory', @@ -334,7 +334,7 @@ describe('projectImport', () => { expect(mockState.compiler.compiledConfig).toEqual({ memorySizeBytes: 2097152, selectedRuntime: 1, - disableCompilation: true, + disableAutoCompilation: true, }); }); diff --git a/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts b/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts index dd688e9e3..6b300f9a4 100644 --- a/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts +++ b/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts @@ -83,7 +83,7 @@ describe('Runtime-ready project functionality', () => { getModule: vi.fn(), getListOfProjects: vi.fn(), getProject: vi.fn(), - compileProject: vi.fn(), + compileCode: vi.fn(), loadSession: vi.fn(), }, editorSettings: { @@ -238,8 +238,8 @@ describe('Runtime-ready project functionality', () => { mockState.compiler.memoryBuffer = expectedIntMemory; mockState.compiler.memoryBufferFloat = expectedFloatMemory; mockState.compiler.allocatedMemorySize = expectedIntMemory.byteLength; - // No compileProject callback for runtime-only projects - mockState.callbacks.compileProject = undefined; + // No compileCode callback for runtime-only projects + mockState.callbacks.compileCode = undefined; // Set up compiler functionality compiler(store, mockEvents); @@ -276,13 +276,13 @@ describe('Runtime-ready project functionality', () => { // Set up a project without pre-compiled WASM (normal project) mockState.compiler.codeBuffer = new Uint8Array(0); // No pre-compiled data - // Mock the compileProject function - const mockCompileProject = vi.fn().mockResolvedValue({ + // Mock the compileCode function + const mockCompileCode = vi.fn().mockResolvedValue({ compiledModules: {}, codeBuffer: new Uint8Array([100, 200]), allocatedMemorySize: 1024, }); - mockState.callbacks.compileProject = mockCompileProject; + mockState.callbacks.compileCode = mockCompileCode; // Set up compiler functionality compiler(store, mockEvents); @@ -298,7 +298,7 @@ describe('Runtime-ready project functionality', () => { await onRecompileCallback(); // Verify regular compilation was attempted - expect(mockCompileProject).toHaveBeenCalled(); + expect(mockCompileCode).toHaveBeenCalled(); }); }); diff --git a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.test.ts b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.test.ts index 32e9ee6e5..2ebd36dfe 100644 --- a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.test.ts +++ b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.test.ts @@ -71,19 +71,19 @@ describe('applyConfigToState', () => { expect(state.runtime.selectedRuntime).toBe(originalSelectedRuntime); }); - it('should apply disableCompilation flag', () => { + it('should apply disableAutoCompilation flag', () => { const state = createMockState(); const store = createStateManager(state); - expect(state.compiler.disableCompilation).toBe(false); - applyConfigToState(store, { disableCompilation: true }); - expect(state.compiler.disableCompilation).toBe(true); + expect(state.compiler.disableAutoCompilation).toBe(false); + applyConfigToState(store, { disableAutoCompilation: true }); + expect(state.compiler.disableAutoCompilation).toBe(true); }); - it('should not change disableCompilation when not in config', () => { + it('should not change disableAutoCompilation when not in config', () => { const state = createMockState(); const store = createStateManager(state); - state.compiler.disableCompilation = true; + state.compiler.disableAutoCompilation = true; applyConfigToState(store, { memorySizeBytes: 65536 }); - expect(state.compiler.disableCompilation).toBe(true); + expect(state.compiler.disableAutoCompilation).toBe(true); }); }); diff --git a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts index 8d6c901db..e90f02775 100644 --- a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts +++ b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts @@ -11,7 +11,7 @@ export interface ConfigObject { memorySizeBytes?: number; selectedRuntime?: number; runtimeSettings?: Runtimes[]; - disableCompilation?: boolean; + disableAutoCompilation?: boolean; } /** @@ -50,11 +50,11 @@ export function applyConfigToState(store: StateManager, config: ConfigObj } } - if (typeof config.memorySizeBytes === 'number') { - store.set('compiler.compilerOptions.memorySizeBytes', config.memorySizeBytes); + if (typeof config.disableAutoCompilation === 'boolean') { + store.set('compiler.disableAutoCompilation', config.disableAutoCompilation); } - if (typeof config.disableCompilation === 'boolean') { - store.set('compiler.disableCompilation', config.disableCompilation); + if (typeof config.memorySizeBytes === 'number') { + store.set('compiler.compilerOptions.memorySizeBytes', config.memorySizeBytes); } } diff --git a/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts b/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts index 6da170e20..3b7d2ac4c 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts @@ -23,7 +23,7 @@ export default function createDefaultState() { ignoredKeywords: ['debug', 'button', 'switch', 'offset', 'plot', 'piano'], }, }, - disableCompilation: false, + disableAutoCompilation: false, }, midi: { inputs: [], diff --git a/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts b/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts index 674eeb49c..bb7f251bd 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts @@ -226,7 +226,7 @@ export function createMockState(overrides: DeepPartial = {}): State { ignoredKeywords: [], }, }, - disableCompilation: false, + disableAutoCompilation: false, }, callbacks: { requestRuntime: createMockAsyncFunction(() => () => {}), diff --git a/packages/editor/packages/editor-state/src/types.ts b/packages/editor/packages/editor-state/src/types.ts index 11f035904..ef2b3471e 100644 --- a/packages/editor/packages/editor-state/src/types.ts +++ b/packages/editor/packages/editor-state/src/types.ts @@ -174,7 +174,7 @@ export interface Compiler { compilerOptions: CompileOptions; allocatedMemorySize: number; compiledFunctions?: CompiledFunctionLookup; - disableCompilation: boolean; + disableAutoCompilation: boolean; compiledConfig?: Record; } @@ -536,7 +536,7 @@ export interface Callbacks { getProject?: (slug: string) => Promise; // Compilation callback - compileProject?: ( + compileCode?: ( modules: Module[], compilerOptions: CompileOptions, functions?: Module[] diff --git a/src/compiler-callback.ts b/src/compiler-callback.ts index a8666a696..493dca3fa 100644 --- a/src/compiler-callback.ts +++ b/src/compiler-callback.ts @@ -8,7 +8,7 @@ const compilerWorker = new CompilerWorker(); let memoryRef: WebAssembly.Memory | null = null; -export async function compileProject( +export async function compileCode( modules: Module[], compilerOptions: CompileOptions, functions?: Module[] diff --git a/src/editor.ts b/src/editor.ts index 5fd761b4b..b26dc333d 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -14,7 +14,7 @@ import { exportBinaryFile, getStorageQuota, } from './storage-callbacks'; -import { compileProject } from './compiler-callback'; +import { compileCode } from './compiler-callback'; import compileConfig from './config-callback'; async function getListOfColorSchemes(): Promise { @@ -41,7 +41,7 @@ async function init() { getListOfProjects, getProject, requestRuntime, - compileProject, + compileCode, compileConfig, loadSession, saveSession, From 79d954a131dcb2b6fcc20dd56657d8b506d925d0 Mon Sep 17 00:00:00 2001 From: andorthehood Date: Fri, 2 Jan 2026 21:20:29 +0000 Subject: [PATCH 2/9] chore(editor-state): remove obsolete project settings menu --- .../editor-state/src/effects/menu/menus.ts | 42 ++++--------------- .../editor-state/src/effects/sampleRate.ts | 12 ------ .../editor/packages/editor-state/src/index.ts | 2 - .../utils/generateContextMenuMock.ts | 8 ---- 4 files changed, 7 insertions(+), 57 deletions(-) delete mode 100644 packages/editor/packages/editor-state/src/effects/sampleRate.ts diff --git a/packages/editor/packages/editor-state/src/effects/menu/menus.ts b/packages/editor/packages/editor-state/src/effects/menu/menus.ts index 74c6cfe83..cc61a97b4 100644 --- a/packages/editor/packages/editor-state/src/effects/menu/menus.ts +++ b/packages/editor/packages/editor-state/src/effects/menu/menus.ts @@ -67,11 +67,17 @@ export const mainMenu: MenuGenerator = state => [ disabled: !state.callbacks.getListOfProjects, }, { divider: true }, + { + title: 'Compile Config', + action: 'compileConfig', + close: true, + disabled: !state.callbacks.compileConfig, + }, { title: 'Compile Code', action: 'compileCode', close: true, - disabled: !state.callbacks.compileCode || !state.callbacks.compileConfig, + disabled: !state.callbacks.compileCode, }, { divider: true }, { title: 'Export Project', action: 'exportProject', close: true, disabled: !state.callbacks.exportProject }, @@ -89,7 +95,6 @@ export const mainMenu: MenuGenerator = state => [ }, { divider: true }, { title: 'Editor Settings', action: 'openSubMenu', payload: { menu: 'editorSettingsMenu' }, close: false }, - { title: 'Project Settings', action: 'openSubMenu', payload: { menu: 'projectSettingsMenu' }, close: false }, { divider: true }, { title: 'MIDI Info', action: 'openSubMenu', payload: { menu: 'midiInfoMenu' }, close: false }, ]; @@ -199,39 +204,6 @@ export const builtInModuleMenu: MenuGenerator = async (state, payload = {}) => { return menuItems; }; -export const sampleRateMenu: MenuGenerator = () => [ - { - title: '44100 Hz (buffered, for audio and MIDI CC)', - action: 'setSampleRate', - payload: { sampleRate: 44100 }, - close: true, - }, - { - title: '22050 Hz (buffered, for audio and MIDI CC)', - action: 'setSampleRate', - payload: { sampleRate: 22050 }, - close: true, - }, - { - title: '100 Hz (real time, for high precision MIDI timing)', - action: 'setSampleRate', - payload: { sampleRate: 100 }, - close: true, - }, - { - title: '50 Hz (real time, for high precision MIDI timing)', - action: 'setSampleRate', - payload: { sampleRate: 50 }, - close: true, - }, - { title: '1 Hz (real time, for debugging)', action: 'setSampleRate', payload: { sampleRate: 1 }, close: true }, -]; - -export const projectSettingsMenu: MenuGenerator = () => [ - { title: 'Set Sample Rate', action: 'openSubMenu', payload: { menu: 'sampleRateMenu' }, close: false }, - { title: 'Configure Audio I/O', action: 'openSubMenu', payload: { menu: 'configureAudioIO' }, close: false }, -]; - export const editorSettingsMenu: MenuGenerator = state => [ { title: 'Theme', diff --git a/packages/editor/packages/editor-state/src/effects/sampleRate.ts b/packages/editor/packages/editor-state/src/effects/sampleRate.ts deleted file mode 100644 index 499cc4511..000000000 --- a/packages/editor/packages/editor-state/src/effects/sampleRate.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EventDispatcher } from '../types'; - -import type { State } from '../types'; - -export default async function sampleRate(state: State, events: EventDispatcher): Promise { - function onSetSampleRate({ sampleRate }: { sampleRate: number }) { - state.runtime.runtimeSettings[state.runtime.selectedRuntime].sampleRate = sampleRate; - events.dispatch('initRuntime'); - } - - events.on('setSampleRate', onSetSampleRate); -} diff --git a/packages/editor/packages/editor-state/src/index.ts b/packages/editor/packages/editor-state/src/index.ts index cdcdfa3a3..315e265d2 100644 --- a/packages/editor/packages/editor-state/src/index.ts +++ b/packages/editor/packages/editor-state/src/index.ts @@ -17,7 +17,6 @@ import editorSettings from './effects/editorSettings'; import projectImport from './effects/projectImport'; import projectExport from './effects/projectExport'; import pianoKeyboard from './effects/codeBlocks/codeBlockDecorators/pianoKeyboard/interaction'; -import sampleRate from './effects/sampleRate'; import exportWasm from './effects/exportWasm'; import viewport from './effects/viewport'; import binaryAsset from './effects/binaryAssets'; @@ -44,7 +43,6 @@ export default function init(events: EventDispatcher, options: Options): StateMa editorSettings(store, events, state); runtime(store, events); - sampleRate(state, events); projectImport(store, events, state); codeBlockDragger(state, events); codeBlockNavigation(state, events); diff --git a/packages/editor/packages/web-ui/screenshot-tests/utils/generateContextMenuMock.ts b/packages/editor/packages/web-ui/screenshot-tests/utils/generateContextMenuMock.ts index 56fe9d03b..c38954b24 100644 --- a/packages/editor/packages/web-ui/screenshot-tests/utils/generateContextMenuMock.ts +++ b/packages/editor/packages/web-ui/screenshot-tests/utils/generateContextMenuMock.ts @@ -105,14 +105,6 @@ export default function generateContextMenuMock(): ContextMenu { }, close: false, }, - { - title: '............ Project Settings >', - action: 'openSubMenu', - payload: { - menu: 'projectSettingsMenu', - }, - close: false, - }, { divider: true, }, From 78682a9548cf807d817c9d126eee3a9f2db73223 Mon Sep 17 00:00:00 2001 From: andorthehood Date: Sat, 3 Jan 2026 16:39:15 +0000 Subject: [PATCH 3/9] chore(editor-state): refactor initialization flow and change codeBlocks type to array --- .../src/effects/codeBlockNavigation.test.ts | 4 +- .../__tests__/gridCoordinates.test.ts | 10 +- .../effects/codeBlocks/blockTypeUpdater.ts | 23 +--- .../effects/codeBlocks/codeBlockCreator.ts | 18 +-- .../errorMessages/errorMessages.test.ts | 2 +- .../errorMessages/errorMessages.ts | 35 ------ .../effects/codeBlocks/codeBlockDragger.ts | 10 +- .../effects/codeBlocks/creationIndex.test.ts | 25 ++--- .../src/effects/codeBlocks/graphicHelper.ts | 90 ++++++++++++++- .../editor-state/src/effects/compiler.test.ts | 12 +- .../editor-state/src/effects/compiler.ts | 44 +++++++- .../editor-state/src/effects/config.ts | 27 +++-- .../src/effects/demoModeNavigation.test.ts | 6 +- .../src/effects/demoModeNavigation.ts | 4 +- .../src/effects/disableCompilation.test.ts | 20 ++-- .../src/effects/projectImport.test.ts | 42 +++---- .../editor-state/src/effects/projectImport.ts | 106 +----------------- .../src/effects/runtimeReadyProject.test.ts | 10 +- .../effects/shaders/shaderEffectsDeriver.ts | 18 +-- .../config/applyConfigToState.ts | 2 + .../editor/packages/editor-state/src/index.ts | 4 +- .../pureHelpers/config/collectConfigBlocks.ts | 14 +-- .../findClosestCodeBlockInDirection.test.ts | 80 ++++++------- .../findClosestCodeBlockInDirection.ts | 6 +- .../findCodeBlockAtViewportCoordinates.ts | 3 +- .../projectSerializing/serializeToProject.ts | 8 +- .../serializeToRuntimeReadyProject.ts | 2 +- .../pureHelpers/state/createDefaultState.ts | 4 +- .../src/pureHelpers/testingUtils/testUtils.ts | 2 +- .../editor/packages/editor-state/src/types.ts | 8 +- .../screenshot-tests/dragged-modules.test.ts | 2 +- .../font-color-rendering.test.ts | 2 +- .../web-ui/screenshot-tests/switches.test.ts | 4 +- src/colorSchemes/hackerman.ts | 4 +- 34 files changed, 305 insertions(+), 346 deletions(-) delete mode 100644 packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.ts diff --git a/packages/editor/packages/editor-state/src/effects/codeBlockNavigation.test.ts b/packages/editor/packages/editor-state/src/effects/codeBlockNavigation.test.ts index 0dc5c7187..e8154de4f 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlockNavigation.test.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlockNavigation.test.ts @@ -43,7 +43,7 @@ describe('codeBlockNavigation', () => { }, graphicHelper: { selectedCodeBlock: selectedBlock, - codeBlocks: new Set([selectedBlock, leftBlock, rightBlock, upBlock, downBlock]), + codeBlocks: [selectedBlock, leftBlock, rightBlock, upBlock, downBlock], viewport: { x: 0, y: 0, width: 800, height: 600, vGrid: 8, hGrid: 16 }, }, }); @@ -146,7 +146,7 @@ describe('codeBlockNavigation', () => { codeBlockNavigation(state, events); // Remove all blocks except selected - state.graphicHelper.codeBlocks = new Set([selectedBlock]); + state.graphicHelper.codeBlocks = [selectedBlock]; onKeydownHandler({ key: 'ArrowRight', metaKey: true }); expect(state.graphicHelper.selectedCodeBlock).toBe(selectedBlock); diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/__tests__/gridCoordinates.test.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/__tests__/gridCoordinates.test.ts index 500c5391b..00571e901 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/__tests__/gridCoordinates.test.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlocks/__tests__/gridCoordinates.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { createMockCodeBlock } from '../../../pureHelpers/testingUtils/testUtils'; -import type { CodeBlockGraphicData, State } from '../../../types'; +import type { State } from '../../../types'; describe('Grid Coordinates Integration', () => { let mockState: Pick; @@ -16,7 +16,7 @@ describe('Grid Coordinates Integration', () => { x: 0, y: 0, }, - codeBlocks: new Set(), + codeBlocks: [], } as State['graphicHelper'], }; }); @@ -88,7 +88,7 @@ describe('Grid Coordinates Integration', () => { y: 160, }); - mockState.graphicHelper.codeBlocks.add(codeBlock); + mockState.graphicHelper.codeBlocks.push(codeBlock); // Verify initial state with 8x16 font expect(codeBlock.x).toBe(80); @@ -132,8 +132,8 @@ describe('Grid Coordinates Integration', () => { y: 240, }); - mockState.graphicHelper.codeBlocks.add(block1); - mockState.graphicHelper.codeBlocks.add(block2); + mockState.graphicHelper.codeBlocks.push(block1); + mockState.graphicHelper.codeBlocks.push(block2); // Calculate initial grid spacing const initialGridSpacingX = block2.gridX - block1.gridX; diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/blockTypeUpdater.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/blockTypeUpdater.ts index 86fa3819f..a9d64b746 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/blockTypeUpdater.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlocks/blockTypeUpdater.ts @@ -2,20 +2,13 @@ import { StateManager } from '@8f4e/state-manager'; import getBlockType from '../../pureHelpers/codeParsers/getBlockType'; -import type { CodeBlockGraphicData, EventDispatcher, State } from '../../types'; - -interface CodeBlockAddedEvent { - codeBlock: CodeBlockGraphicData; -} +import type { CodeBlockGraphicData, State } from '../../types'; /** * Effect that keeps the blockType field in sync with code block contents. - * Updates blockType on: - * - codeBlockAdded: When a new code block is created - * - projectLoaded: When a project is loaded (recompute all block types) - * - code changes: When the selected code block's code changes + * Updates blockType when the selected code block's code changes. */ -export default function blockTypeUpdater(store: StateManager, events: EventDispatcher): void { +export default function blockTypeUpdater(store: StateManager): void { const state = store.getState(); /** @@ -34,13 +27,6 @@ export default function blockTypeUpdater(store: StateManager, events: Eve } } - /** - * Update blockType when a new code block is added - */ - function onCodeBlockAdded({ codeBlock }: CodeBlockAddedEvent): void { - updateBlockType(codeBlock); - } - /** * Update blockType when the selected code block's code changes */ @@ -50,7 +36,6 @@ export default function blockTypeUpdater(store: StateManager, events: Eve } } - events.on('codeBlockAdded', onCodeBlockAdded); - events.on('projectLoaded', updateAllBlockTypes); + store.subscribe('graphicHelper.codeBlocks', updateAllBlockTypes); store.subscribe('graphicHelper.selectedCodeBlock.code', onSelectedCodeBlockCodeChange); } diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockCreator.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockCreator.ts index 1c5928982..f27322405 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockCreator.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockCreator.ts @@ -6,12 +6,9 @@ import getVertexShaderId from '../../pureHelpers/shaderUtils/getVertexShaderId'; import getFragmentShaderId from '../../pureHelpers/shaderUtils/getFragmentShaderId'; import { EventDispatcher } from '../../types'; +import type { StateManager } from '@8f4e/state-manager'; import type { CodeBlockGraphicData, State } from '../../types'; -export interface CodeBlockAddedEvent { - codeBlock: CodeBlockGraphicData; -} - const nameList = [ 'quark', 'electron', @@ -56,7 +53,7 @@ function getRandomCodeBlockId() { } function checkIfCodeBlockIdIsTaken(state: State, id: string) { - return Array.from(state.graphicHelper.codeBlocks).some(codeBlock => { + return state.graphicHelper.codeBlocks.some(codeBlock => { return codeBlock.id === id; }); } @@ -95,7 +92,8 @@ function incrementCodeBlockIdUntilUnique(state: State, blockId: string) { return blockId; } -export default function codeBlockCreator(state: State, events: EventDispatcher): void { +export default function codeBlockCreator(store: StateManager, events: EventDispatcher): void { + const state = store.getState(); async function onAddCodeBlock({ x, y, @@ -181,8 +179,7 @@ export default function codeBlockCreator(state: State, events: EventDispatcher): blockType: 'unknown', // Will be updated by blockTypeUpdater effect }; - state.graphicHelper.codeBlocks.add(codeBlock); - events.dispatch('codeBlockAdded', { codeBlock }); + store.set('graphicHelper.codeBlocks', [...state.graphicHelper.codeBlocks, codeBlock]); } function onDeleteCodeBlock({ codeBlock }: { codeBlock: CodeBlockGraphicData }): void { @@ -190,7 +187,10 @@ export default function codeBlockCreator(state: State, events: EventDispatcher): return; } - state.graphicHelper.codeBlocks.delete(codeBlock); + store.set( + 'graphicHelper.codeBlocks', + state.graphicHelper.codeBlocks.filter(block => block !== codeBlock) + ); } function onCopyCodeBlock({ codeBlock }: { codeBlock: CodeBlockGraphicData }): void { diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.test.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.test.ts index 448b9da25..122947d74 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.test.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.test.ts @@ -30,7 +30,7 @@ describe('errorMessages', () => { mockState = createMockState({ graphicHelper: { - codeBlocks: new Set([codeBlock1, codeBlock2]), + codeBlocks: [codeBlock1, codeBlock2], viewport: { vGrid: 8, hGrid: 16, diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.ts deleted file mode 100644 index d951772b2..000000000 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { StateManager } from '@8f4e/state-manager'; - -import wrapText from '../../../../pureHelpers/wrapText'; -import gapCalculator from '../../../../pureHelpers/codeEditing/gapCalculator'; -import { State } from '../../../../types'; - -export default function errorMessages(store: StateManager) { - const state = store.getState(); - - function updateErrorMessages() { - const codeErrors = [...state.codeErrors.compilationErrors, ...state.codeErrors.configErrors]; - state.graphicHelper.codeBlocks.forEach(codeBlock => { - codeBlock.extras.errorMessages = []; - codeErrors.forEach(codeError => { - if (codeBlock.creationIndex === codeError.codeBlockId || codeBlock.id === codeError.codeBlockId) { - const message = wrapText(codeError.message, codeBlock.width / state.graphicHelper.viewport.vGrid - 1); - - codeBlock.extras.errorMessages.push({ - x: 0, - y: (gapCalculator(codeError.lineNumber, codeBlock.gaps) + 1) * state.graphicHelper.viewport.hGrid, - message: ['Error:', ...message], - lineNumber: codeError.lineNumber, - }); - } - }); - }); - - // To trigger rerender TODO: refactor this - store.set('graphicHelper.codeBlocks', state.graphicHelper.codeBlocks); - } - - updateErrorMessages(); - - store.subscribe('codeErrors', updateErrorMessages); -} diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDragger.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDragger.ts index 75f72944f..ad724939b 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDragger.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDragger.ts @@ -2,6 +2,7 @@ import { EventDispatcher } from '../../types'; import { InternalMouseEvent } from '../../types'; import findCodeBlockAtViewportCoordinates from '../../pureHelpers/finders/findCodeBlockAtViewportCoordinates'; +import type { StateManager } from '@8f4e/state-manager'; import type { CodeBlockGraphicData, State } from '../../types'; export interface CodeBlockClickEvent { @@ -12,7 +13,8 @@ export interface CodeBlockClickEvent { codeBlock: CodeBlockGraphicData; } -export default function codeBlockDragger(state: State, events: EventDispatcher): () => void { +export default function codeBlockDragger(store: StateManager, events: EventDispatcher): () => void { + const state = store.getState(); function onMouseDown({ x, y }: InternalMouseEvent) { if (!state.featureFlags.moduleDragging) { return; @@ -38,8 +40,10 @@ export default function codeBlockDragger(state: State, events: EventDispatcher): }); // Bring dragged module forward. - state.graphicHelper.codeBlocks.delete(draggedCodeBlock); - state.graphicHelper.codeBlocks.add(draggedCodeBlock); + state.graphicHelper.codeBlocks = [ + ...state.graphicHelper.codeBlocks.filter(block => block !== draggedCodeBlock), + draggedCodeBlock, + ]; } function onMouseMove(event: InternalMouseEvent) { diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/creationIndex.test.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/creationIndex.test.ts index d88d8f63c..08cd412bf 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/creationIndex.test.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlocks/creationIndex.test.ts @@ -25,7 +25,7 @@ describe('creationIndex', () => { describe('codeBlockCreator', () => { it('should assign creationIndex to new code blocks', () => { // Initialize the codeBlockCreator - codeBlockCreator(mockState, mockEvents); + codeBlockCreator(store, mockEvents); // Find the addCodeBlock handler const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; @@ -41,14 +41,14 @@ describe('creationIndex', () => { addCodeBlockHandler({ x: 0, y: 0, isNew: true }); // Verify first code block got creationIndex 0 - const codeBlocks = Array.from(mockState.graphicHelper.codeBlocks); + const codeBlocks = mockState.graphicHelper.codeBlocks; expect(codeBlocks.length).toBe(1); expect(codeBlocks[0].creationIndex).toBe(0); expect(mockState.graphicHelper.nextCodeBlockCreationIndex).toBe(1); }); it('should increment creationIndex for each new code block', () => { - codeBlockCreator(mockState, mockEvents); + codeBlockCreator(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const addCodeBlockCall = onCalls.find(call => call[0] === 'addCodeBlock'); @@ -59,7 +59,7 @@ describe('creationIndex', () => { addCodeBlockHandler({ x: 100, y: 0, isNew: true }); addCodeBlockHandler({ x: 200, y: 0, isNew: true }); - const codeBlocks = Array.from(mockState.graphicHelper.codeBlocks); + const codeBlocks = mockState.graphicHelper.codeBlocks; expect(codeBlocks.length).toBe(3); // Verify each block has a unique, incrementing creationIndex @@ -69,7 +69,7 @@ describe('creationIndex', () => { }); it('should leave gaps in creationIndex when blocks are deleted', () => { - codeBlockCreator(mockState, mockEvents); + codeBlockCreator(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const addCodeBlockCall = onCalls.find(call => call[0] === 'addCodeBlock'); @@ -83,7 +83,7 @@ describe('creationIndex', () => { addCodeBlockHandler({ x: 200, y: 0, isNew: true }); // Delete the middle one (creationIndex 1) - const codeBlocks = Array.from(mockState.graphicHelper.codeBlocks); + const codeBlocks = mockState.graphicHelper.codeBlocks; const middleBlock = codeBlocks.find(b => b.creationIndex === 1); deleteCodeBlockHandler({ codeBlock: middleBlock }); @@ -91,7 +91,7 @@ describe('creationIndex', () => { addCodeBlockHandler({ x: 300, y: 0, isNew: true }); // Verify the new block gets creationIndex 3 (not 1, leaving a gap) - const updatedCodeBlocks = Array.from(mockState.graphicHelper.codeBlocks); + const updatedCodeBlocks = mockState.graphicHelper.codeBlocks; const creationIndexes = updatedCodeBlocks.map(b => b.creationIndex).sort((a, b) => a - b); expect(creationIndexes).toEqual([0, 2, 3]); expect(mockState.graphicHelper.nextCodeBlockCreationIndex).toBe(4); @@ -117,7 +117,7 @@ describe('creationIndex', () => { loadProjectCallback({ project: projectWithBlocks }); - const codeBlocks = Array.from(mockState.graphicHelper.codeBlocks); + const codeBlocks = mockState.graphicHelper.codeBlocks; expect(codeBlocks.length).toBe(3); // Verify each block has a creationIndex @@ -172,11 +172,10 @@ describe('creationIndex', () => { blockType: 'module', }); - // Create a Set to simulate graphicHelper.codeBlocks (insertion order) - const codeBlocksSet = new Set([block1, block2, block3]); + const codeBlocksArray = [block1, block2, block3]; // Use the actual flattenProjectForCompiler function - const { modules } = flattenProjectForCompiler(codeBlocksSet); + const { modules } = flattenProjectForCompiler(codeBlocksArray); // Verify blocks are sorted by creationIndex expect(modules[0].code).toEqual(['module b', 'moduleEnd']); // creationIndex 0 @@ -204,9 +203,9 @@ describe('creationIndex', () => { blockType: 'config', }); - const codeBlocksSet = new Set([moduleBlock, functionBlock, configBlock]); + const codeBlocksArray = [moduleBlock, functionBlock, configBlock]; - const { modules, functions } = flattenProjectForCompiler(codeBlocksSet); + const { modules, functions } = flattenProjectForCompiler(codeBlocksArray); expect(modules.length).toBe(1); expect(modules[0].code).toEqual(['module testModule', 'moduleEnd']); diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/graphicHelper.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/graphicHelper.ts index dec91402b..1df6f083f 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/graphicHelper.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlocks/graphicHelper.ts @@ -4,7 +4,6 @@ import { getModuleId } from '@8f4e/compiler/syntax'; import bufferPlotters from './codeBlockDecorators/bufferPlotters/updateGraphicData'; import buttons from './codeBlockDecorators/buttons/updateGraphicData'; import debuggers from './codeBlockDecorators/debuggers/updateGraphicData'; -import errorMessages from './codeBlockDecorators/errorMessages/errorMessages'; import gaps from './gaps'; import inputs from './codeBlockDecorators/inputs/updateGraphicData'; import outputs from './codeBlockDecorators/outputs/updateGraphicData'; @@ -13,7 +12,6 @@ import positionOffsetters from './positionOffsetters'; import switches from './codeBlockDecorators/switches/updateGraphicData'; import blockHighlights from './codeBlockDecorators/blockHighlights/updateGraphicData'; import { CodeBlockClickEvent } from './codeBlockDragger'; -import { CodeBlockAddedEvent } from './codeBlockCreator'; import { EventDispatcher } from '../../types'; import gapCalculator from '../../pureHelpers/codeEditing/gapCalculator'; @@ -21,6 +19,7 @@ import generateCodeColorMap from '../../pureHelpers/codeEditing/generateCodeColo import { moveCaret } from '../../pureHelpers/codeEditing/moveCaret'; import reverseGapCalculator from '../../pureHelpers/codeEditing/reverseGapCalculator'; import getLongestLineLength from '../../pureHelpers/codeParsers/getLongestLineLength'; +import wrapText from '../../pureHelpers/wrapText'; import type { CodeBlockGraphicData, State } from '../../types'; @@ -200,14 +199,95 @@ export default function graphicHelper(store: StateManager, events: EventD updateGraphics(state.graphicHelper.selectedCodeBlock); }; - errorMessages(store); + const populateCodeBlocks = function () { + if (!state.initialProjectState) { + return; + } + + state.graphicHelper.outputsByWordAddress.clear(); + state.graphicHelper.selectedCodeBlock = undefined; + state.graphicHelper.draggedCodeBlock = undefined; + state.graphicHelper.nextCodeBlockCreationIndex = 0; + state.graphicHelper.viewport.x = + state.initialProjectState.viewport.gridCoordinates.x * state.graphicHelper.viewport.vGrid; + state.graphicHelper.viewport.y = + state.initialProjectState.viewport.gridCoordinates.y * state.graphicHelper.viewport.hGrid; + const codeBlocks = state.initialProjectState.codeBlocks.map(codeBlock => { + const creationIndex = state.graphicHelper.nextCodeBlockCreationIndex; + state.graphicHelper.nextCodeBlockCreationIndex++; + // Compute grid coordinates first as source of truth + const gridX = codeBlock.gridCoordinates.x; + const gridY = codeBlock.gridCoordinates.y; + // Compute pixel coordinates from grid coordinates + const pixelX = gridX * state.graphicHelper.viewport.vGrid; + const pixelY = gridY * state.graphicHelper.viewport.hGrid; + + return { + width: 0, + minGridWidth: 32, + height: 0, + code: codeBlock.code, + codeColors: [], + codeToRender: [], + extras: { + blockHighlights: [], + inputs: [], + outputs: [], + debuggers: [], + switches: [], + buttons: [], + pianoKeyboards: [], + bufferPlotters: [], + errorMessages: [], + }, + cursor: { col: 0, row: 0, x: 0, y: 0 }, + id: getModuleId(codeBlock.code) || '', + gaps: new Map(), + gridX, + gridY, + x: pixelX, + y: pixelY, + offsetX: 0, + offsetY: 0, + lineNumberColumnWidth: 1, + lastUpdated: Date.now(), + creationIndex, + blockType: 'unknown', // Will be updated by blockTypeUpdater effect + } as CodeBlockGraphicData; + }); + + store.set('graphicHelper.codeBlocks', codeBlocks); + }; + + function updateErrorMessages() { + const codeErrors = [...state.codeErrors.compilationErrors, ...state.codeErrors.configErrors]; + state.graphicHelper.codeBlocks.forEach(codeBlock => { + codeBlock.extras.errorMessages = []; + codeErrors.forEach(codeError => { + if (codeBlock.creationIndex === codeError.codeBlockId || codeBlock.id === codeError.codeBlockId) { + const message = wrapText(codeError.message, codeBlock.width / state.graphicHelper.viewport.vGrid - 1); + + codeBlock.extras.errorMessages.push({ + x: 0, + y: (gapCalculator(codeError.lineNumber, codeBlock.gaps) + 1) * state.graphicHelper.viewport.hGrid, + message: ['Error:', ...message], + lineNumber: codeError.lineNumber, + }); + + updateGraphics(codeBlock); + } + }); + }); + } + + updateErrorMessages(); events.on('codeBlockClick', onCodeBlockClick); events.on('codeBlockClick', ({ codeBlock }) => updateGraphics(codeBlock)); events.on('runtimeInitialized', updateGraphicsAll); - events.on('codeBlockAdded', ({ codeBlock }) => updateGraphics(codeBlock)); - events.on('init', updateGraphicsAll); events.on('spriteSheetRerendered', recomputePixelCoordinatesAndUpdateGraphics); + store.subscribe('codeErrors', updateErrorMessages); + store.subscribe('initialProjectState', populateCodeBlocks); store.subscribe('graphicHelper.codeBlocks', updateGraphicsAll); store.subscribe('graphicHelper.selectedCodeBlock.code', updateSelectedCodeBlock); store.subscribe('graphicHelper.selectedCodeBlock.cursor', updateSelectedCodeBlock); diff --git a/packages/editor/packages/editor-state/src/effects/compiler.test.ts b/packages/editor/packages/editor-state/src/effects/compiler.test.ts index 019592b3e..15e8dcd47 100644 --- a/packages/editor/packages/editor-state/src/effects/compiler.test.ts +++ b/packages/editor/packages/editor-state/src/effects/compiler.test.ts @@ -6,7 +6,7 @@ import type { CodeBlockGraphicData } from '../types'; describe('flattenProjectForCompiler', () => { it('should exclude comment blocks from compilation', () => { - const mockCodeBlocks = new Set([ + const mockCodeBlocks: CodeBlockGraphicData[] = [ { code: ['module test', 'moduleEnd'], blockType: 'module', @@ -27,7 +27,7 @@ describe('flattenProjectForCompiler', () => { blockType: 'comment', creationIndex: 3, } as CodeBlockGraphicData, - ]); + ]; const result = flattenProjectForCompiler(mockCodeBlocks); @@ -38,7 +38,7 @@ describe('flattenProjectForCompiler', () => { }); it('should include constants blocks but not comment blocks', () => { - const mockCodeBlocks = new Set([ + const mockCodeBlocks: CodeBlockGraphicData[] = [ { code: ['constants', 'constantsEnd'], blockType: 'constants', @@ -49,7 +49,7 @@ describe('flattenProjectForCompiler', () => { blockType: 'comment', creationIndex: 1, } as CodeBlockGraphicData, - ]); + ]; const result = flattenProjectForCompiler(mockCodeBlocks); @@ -59,7 +59,7 @@ describe('flattenProjectForCompiler', () => { }); it('should handle only comment blocks', () => { - const mockCodeBlocks = new Set([ + const mockCodeBlocks: CodeBlockGraphicData[] = [ { code: ['comment', 'Comment 1', 'commentEnd'], blockType: 'comment', @@ -70,7 +70,7 @@ describe('flattenProjectForCompiler', () => { blockType: 'comment', creationIndex: 1, } as CodeBlockGraphicData, - ]); + ]; const result = flattenProjectForCompiler(mockCodeBlocks); diff --git a/packages/editor/packages/editor-state/src/effects/compiler.ts b/packages/editor/packages/editor-state/src/effects/compiler.ts index 3b4b3bf8e..e333e6490 100644 --- a/packages/editor/packages/editor-state/src/effects/compiler.ts +++ b/packages/editor/packages/editor-state/src/effects/compiler.ts @@ -6,18 +6,18 @@ import { log } from '../impureHelpers/logger/logger'; import type { CodeBlockGraphicData, State } from '../types'; /** - * Converts code blocks from a Set to separate arrays for modules and functions, sorted by creationIndex. + * Converts code blocks into separate arrays for modules and functions, sorted by creationIndex. * - * @param codeBlocks - Set of code blocks to filter and sort + * @param codeBlocks - List of code blocks to filter and sort * @returns Object containing modules and functions arrays, each sorted by creationIndex. * Config blocks and comment blocks are excluded from the WASM compilation pipeline. * Constants blocks are included in modules array. */ -export function flattenProjectForCompiler(codeBlocks: Set): { +export function flattenProjectForCompiler(codeBlocks: CodeBlockGraphicData[]): { modules: { code: string[] }[]; functions: { code: string[] }[]; } { - const allBlocks = Array.from(codeBlocks).sort((a, b) => a.creationIndex - b.creationIndex); + const allBlocks = [...codeBlocks].sort((a, b) => a.creationIndex - b.creationIndex); return { modules: allBlocks.filter(block => block.blockType === 'module' || block.blockType === 'constants'), @@ -76,6 +76,7 @@ export default async function compiler(store: StateManager, events: Event log(state, 'Compilation succeeded in ' + state.compiler.compilationTime.toFixed(2) + 'ms', 'Compiler'); } catch (error) { + console.log(error); store.set('compiler.isCompiling', false); const errorObject = error as Error & { line?: { lineNumber: number }; @@ -113,9 +114,42 @@ export default async function compiler(store: StateManager, events: Event } events.on('compileCode', onForceCompile); - events.on('codeBlockAdded', onRecompile); events.on('deleteCodeBlock', onRecompile); store.subscribe('compiler.compilerOptions', onRecompile); + store.subscribe('graphicHelper.codeBlocks', () => { + // state.compiler.compilerOptions.memorySizeBytes = defaultState.compiler.compilerOptions.memorySizeBytes; + // state.compiler.memoryBuffer = new Int32Array(); + // state.compiler.memoryBufferFloat = new Float32Array(); + // state.compiler.codeBuffer = new Uint8Array(); + // state.compiler.compiledModules = {}; + // state.compiler.allocatedMemorySize = 0; + // store.set('codeErrors.compilationErrors', []); + // state.compiler.isCompiling = false; + // state.binaryAssets = newProject.binaryAssets || []; + // state.runtime.runtimeSettings = defaultState.runtime.runtimeSettings; + // state.runtime.selectedRuntime = defaultState.runtime.selectedRuntime; + // // postProcessEffects are now derived from shader code blocks, not loaded from project data + // if (newProject.compiledWasm && newProject.memorySnapshot) { + // try { + // state.compiler.codeBuffer = decodeBase64ToUint8Array(newProject.compiledWasm); + // state.compiler.memoryBuffer = decodeBase64ToInt32Array(newProject.memorySnapshot); + // state.compiler.memoryBufferFloat = decodeBase64ToFloat32Array(newProject.memorySnapshot); + // state.compiler.allocatedMemorySize = state.compiler.memoryBuffer.byteLength; + // if (newProject.compiledModules) { + // state.compiler.compiledModules = newProject.compiledModules; + // } + // log(state, 'Pre-compiled WASM loaded and decoded successfully', 'Loader'); + // } catch (err) { + // console.error('[Loader] Failed to decode pre-compiled WASM:', err); + // error(state, 'Failed to decode pre-compiled WASM', 'Loader'); + // state.compiler.codeBuffer = new Uint8Array(); + // state.compiler.memoryBuffer = new Int32Array(); + // state.compiler.memoryBufferFloat = new Float32Array(); + // } + // } else if (newProject.compiledModules) { + // state.compiler.compiledModules = newProject.compiledModules; + // } + }); store.subscribe('graphicHelper.selectedCodeBlock.code', () => { if ( state.graphicHelper.selectedCodeBlock?.blockType !== 'module' && diff --git a/packages/editor/packages/editor-state/src/effects/config.ts b/packages/editor/packages/editor-state/src/effects/config.ts index 31504ba73..1c28d34b6 100644 --- a/packages/editor/packages/editor-state/src/effects/config.ts +++ b/packages/editor/packages/editor-state/src/effects/config.ts @@ -65,18 +65,12 @@ export default function configEffect(store: StateManager, events: EventDi * Each config block is compiled independently to allow proper error mapping. * Errors are saved to codeErrors.configErrors with the creationIndex of the source block. */ - async function rebuildConfig(): Promise { + async function forceCompileConfig(): Promise { const compileConfig = state.callbacks.compileConfig; if (!compileConfig) { return; } - // Check if compilation is disabled by config - if (state.compiler.disableAutoCompilation) { - log(state, 'Config compilation skipped: disableAutoCompilation flag is set', 'Config'); - return; - } - const configBlocks = collectConfigBlocks(state.graphicHelper.codeBlocks); if (configBlocks.length === 0) { @@ -97,11 +91,20 @@ export default function configEffect(store: StateManager, events: EventDi } } + function rebuildConfig() { + // Check if compilation is disabled by config + if (state.compiler.disableAutoCompilation) { + log(state, 'Config compilation skipped: disableAutoCompilation flag is set', 'Config'); + return; + } + + forceCompileConfig(); + } + // Wire up event handlers // rebuildConfig runs BEFORE module compilation because blockTypeUpdater runs first - events.on('codeBlockAdded', rebuildConfig); - events.on('deleteCodeBlock', rebuildConfig); - events.on('projectLoaded', rebuildConfig); + events.on('compileConfig', forceCompileConfig); + store.subscribe('graphicHelper.codeBlocks', rebuildConfig); store.subscribe('graphicHelper.selectedCodeBlock.code', () => { if (state.graphicHelper.selectedCodeBlock?.blockType !== 'config') { return; @@ -116,10 +119,10 @@ export default function configEffect(store: StateManager, events: EventDi * @param state The current editor state * @returns Promise resolving to the merged config object */ -export async function compileConfigForExport(state: State): Promise> { +export async function compileConfigForExport(state: State): Promise { // If compilation is disabled, return the stored compiled config if available if (state.compiler.disableAutoCompilation) { - return state.compiler.compiledConfig || {}; + return state.compiledConfig || {}; } // If no compileConfig callback, return empty object diff --git a/packages/editor/packages/editor-state/src/effects/demoModeNavigation.test.ts b/packages/editor/packages/editor-state/src/effects/demoModeNavigation.test.ts index 47736bd33..c0f35560d 100644 --- a/packages/editor/packages/editor-state/src/effects/demoModeNavigation.test.ts +++ b/packages/editor/packages/editor-state/src/effects/demoModeNavigation.test.ts @@ -52,7 +52,7 @@ describe('demoModeNavigation', () => { }, graphicHelper: { selectedCodeBlock: undefined, - codeBlocks: new Set([selectedBlock, leftBlock, rightBlock, upBlock, downBlock]), + codeBlocks: [selectedBlock, leftBlock, rightBlock, upBlock, downBlock], viewport: { x: 0, y: 0, width: 800, height: 600, vGrid: 8, hGrid: 16 }, }, }); @@ -94,7 +94,7 @@ describe('demoModeNavigation', () => { // A code block should now be selected expect(state.graphicHelper.selectedCodeBlock).toBeDefined(); - expect(state.graphicHelper.codeBlocks.has(state.graphicHelper.selectedCodeBlock!)).toBe(true); + expect(state.graphicHelper.codeBlocks.includes(state.graphicHelper.selectedCodeBlock!)).toBe(true); }); it('should not change selection on init when a block is already selected', () => { @@ -123,7 +123,7 @@ describe('demoModeNavigation', () => { }); it('should handle empty code blocks gracefully during demo navigation', () => { - state.graphicHelper.codeBlocks = new Set(); + state.graphicHelper.codeBlocks = []; state.graphicHelper.selectedCodeBlock = undefined; demoModeNavigation(state, events); diff --git a/packages/editor/packages/editor-state/src/effects/demoModeNavigation.ts b/packages/editor/packages/editor-state/src/effects/demoModeNavigation.ts index 06c04afc9..934f2bd70 100644 --- a/packages/editor/packages/editor-state/src/effects/demoModeNavigation.ts +++ b/packages/editor/packages/editor-state/src/effects/demoModeNavigation.ts @@ -12,7 +12,7 @@ import type { Direction } from '../pureHelpers/finders/findClosestCodeBlockInDir * @returns true if a block was selected, false otherwise */ function selectRandomCodeBlock(state: State): boolean { - const codeBlocks = Array.from(state.graphicHelper.codeBlocks); + const codeBlocks = state.graphicHelper.codeBlocks; if (codeBlocks.length === 0) { return false; @@ -64,7 +64,7 @@ export default function demoModeNavigation(state: State, events: EventDispatcher // Start the demo navigation interval demoInterval = setInterval(() => { // Check if we still have code blocks available - if (state.graphicHelper.codeBlocks.size === 0) { + if (state.graphicHelper.codeBlocks.length === 0) { return; } diff --git a/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts b/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts index dd06458cc..b85220c48 100644 --- a/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts +++ b/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts @@ -55,8 +55,8 @@ describe('disableAutoCompilation feature', () => { }, }); - mockState.graphicHelper.codeBlocks.add(moduleBlock); - mockState.graphicHelper.codeBlocks.add(configBlock); + mockState.graphicHelper.codeBlocks.push(moduleBlock); + mockState.graphicHelper.codeBlocks.push(configBlock); mockEvents = createMockEventDispatcherWithVitest(); store = createStateManager(mockState); @@ -69,7 +69,7 @@ describe('disableAutoCompilation feature', () => { compiler(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find(call => call[0] === 'codeBlockAdded'); + const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); expect(compileCall).toBeDefined(); const onRecompileCallback = compileCall![1]; @@ -93,7 +93,7 @@ describe('disableAutoCompilation feature', () => { compiler(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find(call => call[0] === 'codeBlockAdded'); + const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); const onRecompileCallback = compileCall![1]; await onRecompileCallback(); @@ -109,7 +109,7 @@ describe('disableAutoCompilation feature', () => { compiler(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find(call => call[0] === 'codeBlockAdded'); + const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); const onRecompileCallback = compileCall![1]; await onRecompileCallback(); @@ -130,7 +130,7 @@ describe('disableAutoCompilation feature', () => { configEffect(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const configCall = onCalls.find(call => call[0] === 'codeBlockAdded'); + const configCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); expect(configCall).toBeDefined(); const rebuildConfigCallback = configCall![1]; @@ -152,7 +152,7 @@ describe('disableAutoCompilation feature', () => { configEffect(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const configCall = onCalls.find(call => call[0] === 'codeBlockAdded'); + const configCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); const rebuildConfigCallback = configCall![1]; await rebuildConfigCallback(); @@ -173,7 +173,7 @@ describe('disableAutoCompilation feature', () => { it('should return stored compiledConfig when disableAutoCompilation is true and config exists', async () => { mockState.compiler.disableAutoCompilation = true; - mockState.compiler.compiledConfig = { + mockState.compiledConfig = { memorySizeBytes: 2097152, selectedRuntime: 1, }; @@ -216,7 +216,7 @@ describe('disableAutoCompilation feature', () => { configEffect(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const configCall = onCalls.find(call => call[0] === 'codeBlockAdded'); + const configCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); const rebuildConfigCallback = configCall![1]; await rebuildConfigCallback(); @@ -235,7 +235,7 @@ describe('disableAutoCompilation feature', () => { configEffect(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const configCall = onCalls.find(call => call[0] === 'codeBlockAdded'); + const configCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); const rebuildConfigCallback = configCall![1]; await rebuildConfigCallback(); diff --git a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts index 2f70efe5e..fde5dff17 100644 --- a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts +++ b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts @@ -22,7 +22,7 @@ describe('projectImport', () => { describe('Event wiring', () => { it('should register importProject event handler', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const importProjectCall = onCalls.find(call => call[0] === 'importProject'); @@ -30,7 +30,7 @@ describe('projectImport', () => { }); it('should register loadProject event handler', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -38,7 +38,7 @@ describe('projectImport', () => { }); it('should register loadProjectBySlug event handler', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectBySlugCall = onCalls.find(call => call[0] === 'loadProjectBySlug'); @@ -49,7 +49,7 @@ describe('projectImport', () => { describe('Initial session loading', () => { it('should load empty project when persistentStorage is disabled', async () => { mockState.featureFlags.persistentStorage = false; - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); // Give time for promises to resolve await new Promise(resolve => setTimeout(resolve, 10)); @@ -67,7 +67,7 @@ describe('projectImport', () => { mockState.featureFlags.persistentStorage = true; mockState.callbacks.loadSession = vi.fn().mockResolvedValue(mockProject); - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); // Give time for promises to resolve await new Promise(resolve => setTimeout(resolve, 10)); @@ -81,7 +81,7 @@ describe('projectImport', () => { mockState.featureFlags.persistentStorage = true; mockState.callbacks.loadSession = vi.fn().mockRejectedValue(new Error('Storage error')); - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); // Give time for promises to resolve await new Promise(resolve => setTimeout(resolve, 10)); @@ -94,7 +94,7 @@ describe('projectImport', () => { describe('loadProject', () => { it('should use default memory settings when loading project', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -118,7 +118,7 @@ describe('projectImport', () => { // Set custom memory first mockState.compiler.compilerOptions.memorySizeBytes = 500 * 65536; - projectImport(store, mockEvents, originalDefault); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -135,7 +135,7 @@ describe('projectImport', () => { }); it('should reset compiler state when loading a project', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -153,7 +153,7 @@ describe('projectImport', () => { }); it('should dispatch projectLoaded event after loading', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -167,7 +167,7 @@ describe('projectImport', () => { }); it('should load runtime-ready project with pre-compiled WASM', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -190,7 +190,7 @@ describe('projectImport', () => { }); it('should handle decoding errors gracefully', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -220,7 +220,7 @@ describe('projectImport', () => { }; mockState.callbacks.importProject = vi.fn().mockResolvedValue(mockProject); - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const importProjectCall = onCalls.find(call => call[0] === 'importProject'); @@ -235,7 +235,7 @@ describe('projectImport', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); mockState.callbacks.importProject = undefined; - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const importProjectCall = onCalls.find(call => call[0] === 'importProject'); @@ -252,7 +252,7 @@ describe('projectImport', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); mockState.callbacks.importProject = vi.fn().mockRejectedValue(new Error('Import failed')); - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const importProjectCall = onCalls.find(call => call[0] === 'importProject'); @@ -276,7 +276,7 @@ describe('projectImport', () => { }; mockState.callbacks.getProject = vi.fn().mockResolvedValue(mockProject); - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectBySlugCall = onCalls.find(call => call[0] === 'loadProjectBySlug'); @@ -296,7 +296,7 @@ describe('projectImport', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); mockState.callbacks.getProject = undefined; - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectBySlugCall = onCalls.find(call => call[0] === 'loadProjectBySlug'); @@ -323,7 +323,7 @@ describe('projectImport', () => { memorySnapshot: 'base64encodedmemory', }; - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -331,7 +331,7 @@ describe('projectImport', () => { loadProjectCallback({ project: runtimeReadyProject }); - expect(mockState.compiler.compiledConfig).toEqual({ + expect(mockState.compiledConfig).toEqual({ memorySizeBytes: 2097152, selectedRuntime: 1, disableAutoCompilation: true, @@ -343,7 +343,7 @@ describe('projectImport', () => { ...EMPTY_DEFAULT_PROJECT, }; - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -351,7 +351,7 @@ describe('projectImport', () => { loadProjectCallback({ project: regularProject }); - expect(mockState.compiler.compiledConfig).toBeUndefined(); + expect(mockState.compiledConfig).toBeUndefined(); }); }); }); diff --git a/packages/editor/packages/editor-state/src/effects/projectImport.ts b/packages/editor/packages/editor-state/src/effects/projectImport.ts index 2a67dcdac..c829b1ee6 100644 --- a/packages/editor/packages/editor-state/src/effects/projectImport.ts +++ b/packages/editor/packages/editor-state/src/effects/projectImport.ts @@ -1,16 +1,12 @@ import { StateManager } from '@8f4e/state-manager'; -import { getModuleId } from '@8f4e/compiler/syntax'; import { EventDispatcher } from '../types'; import { EMPTY_DEFAULT_PROJECT } from '../types'; -import decodeBase64ToUint8Array from '../pureHelpers/base64/decodeBase64ToUint8Array'; -import decodeBase64ToInt32Array from '../pureHelpers/base64/decodeBase64ToInt32Array'; -import decodeBase64ToFloat32Array from '../pureHelpers/base64/decodeBase64ToFloat32Array'; -import { log, warn, error } from '../impureHelpers/logger/logger'; +import { warn, error } from '../impureHelpers/logger/logger'; import type { Project, State } from '../types'; -export default function projectImport(store: StateManager, events: EventDispatcher, defaultState: State): void { +export default function projectImport(store: StateManager, events: EventDispatcher): void { const state = store.getState(); const projectPromise = Promise.resolve().then(() => { @@ -41,103 +37,7 @@ export default function projectImport(store: StateManager, events: EventD } function loadProject({ project: newProject }: { project: Project }) { - state.compiler.compilerOptions.memorySizeBytes = defaultState.compiler.compilerOptions.memorySizeBytes; - state.compiler.memoryBuffer = new Int32Array(); - state.compiler.memoryBufferFloat = new Float32Array(); - state.compiler.codeBuffer = new Uint8Array(); - state.compiler.compiledModules = {}; - state.compiler.allocatedMemorySize = 0; - store.set('codeErrors.compilationErrors', []); - state.compiler.isCompiling = false; - - state.binaryAssets = newProject.binaryAssets || []; - state.runtime.runtimeSettings = defaultState.runtime.runtimeSettings; - state.runtime.selectedRuntime = defaultState.runtime.selectedRuntime; - // postProcessEffects are now derived from shader code blocks, not loaded from project data - - if (newProject.compiledWasm && newProject.memorySnapshot) { - try { - state.compiler.codeBuffer = decodeBase64ToUint8Array(newProject.compiledWasm); - state.compiler.memoryBuffer = decodeBase64ToInt32Array(newProject.memorySnapshot); - state.compiler.memoryBufferFloat = decodeBase64ToFloat32Array(newProject.memorySnapshot); - state.compiler.allocatedMemorySize = state.compiler.memoryBuffer.byteLength; - if (newProject.compiledModules) { - state.compiler.compiledModules = newProject.compiledModules; - } - log(state, 'Pre-compiled WASM loaded and decoded successfully', 'Loader'); - } catch (err) { - console.error('[Loader] Failed to decode pre-compiled WASM:', err); - error(state, 'Failed to decode pre-compiled WASM', 'Loader'); - state.compiler.codeBuffer = new Uint8Array(); - state.compiler.memoryBuffer = new Int32Array(); - state.compiler.memoryBufferFloat = new Float32Array(); - } - } else if (newProject.compiledModules) { - state.compiler.compiledModules = newProject.compiledModules; - } - - // Store compiled config from runtime-ready projects - if (newProject.compiledConfig) { - state.compiler.compiledConfig = newProject.compiledConfig; - } - - state.graphicHelper.outputsByWordAddress.clear(); - state.graphicHelper.selectedCodeBlock = undefined; - state.graphicHelper.draggedCodeBlock = undefined; - - state.graphicHelper.codeBlocks.clear(); - state.graphicHelper.nextCodeBlockCreationIndex = 0; - state.graphicHelper.viewport.x = newProject.viewport.gridCoordinates.x * state.graphicHelper.viewport.vGrid; - state.graphicHelper.viewport.y = newProject.viewport.gridCoordinates.y * state.graphicHelper.viewport.hGrid; - - newProject.codeBlocks.forEach(codeBlock => { - const creationIndex = state.graphicHelper.nextCodeBlockCreationIndex; - state.graphicHelper.nextCodeBlockCreationIndex++; - - // Compute grid coordinates first as source of truth - const gridX = codeBlock.gridCoordinates.x; - const gridY = codeBlock.gridCoordinates.y; - - // Compute pixel coordinates from grid coordinates - const pixelX = gridX * state.graphicHelper.viewport.vGrid; - const pixelY = gridY * state.graphicHelper.viewport.hGrid; - - state.graphicHelper.codeBlocks.add({ - width: 0, - minGridWidth: 32, - height: 0, - code: codeBlock.code, - codeColors: [], - codeToRender: [], - extras: { - blockHighlights: [], - inputs: [], - outputs: [], - debuggers: [], - switches: [], - buttons: [], - pianoKeyboards: [], - bufferPlotters: [], - errorMessages: [], - }, - cursor: { col: 0, row: 0, x: 0, y: 0 }, - id: getModuleId(codeBlock.code) || '', - gaps: new Map(), - gridX, - gridY, - x: pixelX, - y: pixelY, - offsetX: 0, - offsetY: 0, - lineNumberColumnWidth: 1, - lastUpdated: Date.now(), - creationIndex, - blockType: 'unknown', // Will be updated by blockTypeUpdater effect - }); - }); - events.dispatch('init'); - events.dispatch('projectLoaded'); - // loadPostProcessEffects will be dispatched by shaderEffectsDeriver + store.set('initialProjectState', newProject); } void projectPromise; diff --git a/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts b/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts index 6b300f9a4..a13271003 100644 --- a/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts +++ b/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts @@ -103,7 +103,7 @@ describe('Runtime-ready project functionality', () => { }); // Add the config block to the state - mockState.graphicHelper.codeBlocks.add(configBlock); + mockState.graphicHelper.codeBlocks.push(configBlock); mockEvents = createMockEventDispatcherWithVitest(); store = createStateManager(mockState); @@ -246,9 +246,7 @@ describe('Runtime-ready project functionality', () => { // Get the onRecompile callback const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find( - call => call[0] === 'codeBlockAdded' || call[0] === 'deleteCodeBlock' || call[0] === 'projectLoaded' - ); + const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); expect(compileCall).toBeDefined(); const onRecompileCallback = compileCall![1]; @@ -289,9 +287,7 @@ describe('Runtime-ready project functionality', () => { // Get the onRecompile callback const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find( - call => call[0] === 'codeBlockAdded' || call[0] === 'deleteCodeBlock' || call[0] === 'projectLoaded' - ); + const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); const onRecompileCallback = compileCall![1]; // Trigger recompilation diff --git a/packages/editor/packages/editor-state/src/effects/shaders/shaderEffectsDeriver.ts b/packages/editor/packages/editor-state/src/effects/shaders/shaderEffectsDeriver.ts index 986c75065..cd0c0ef5f 100644 --- a/packages/editor/packages/editor-state/src/effects/shaders/shaderEffectsDeriver.ts +++ b/packages/editor/packages/editor-state/src/effects/shaders/shaderEffectsDeriver.ts @@ -9,9 +9,7 @@ import type { EventDispatcher, State } from '../../types'; * Effect that keeps post-process effects in sync with shader code blocks. * Recomputes effects when: * - projectLoaded: When a project is loaded - * - codeBlockAdded: When a new shader block is added * - code changes: When shader block's code changes - * - deleteCodeBlock: When a shader block is deleted */ export default function shaderEffectsDeriver(store: StateManager, events: EventDispatcher): void { const state = store.getState(); @@ -20,8 +18,7 @@ export default function shaderEffectsDeriver(store: StateManager, events: * Recompute post-process effects from all shader blocks */ function recomputeShaderEffects(): void { - const codeBlocksArray = Array.from(state.graphicHelper.codeBlocks); - const { effects, errors } = derivePostProcessEffects(codeBlocksArray); + const { effects, errors } = derivePostProcessEffects(state.graphicHelper.codeBlocks); log(state, 'Recomputed shader effects', 'Shaders'); @@ -44,16 +41,9 @@ export default function shaderEffectsDeriver(store: StateManager, events: events.dispatch('loadPostProcessEffects', effects); } - // Recompute on project load - events.on('projectLoaded', recomputeShaderEffects); - - // Recompute when a code block is added - events.on('codeBlockAdded', recomputeShaderEffects); - - // Recompute when a code block is deleted - events.on('deleteCodeBlock', recomputeShaderEffects); - - // Recompute when shader block's code changes + store.subscribe('graphicHelper.codeBlocks', () => { + recomputeShaderEffects(); + }); store.subscribe('graphicHelper.selectedCodeBlock.code', () => { if ( state.graphicHelper.selectedCodeBlock?.blockType === 'fragmentShader' || diff --git a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts index e90f02775..f57eda1af 100644 --- a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts +++ b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts @@ -21,6 +21,8 @@ export interface ConfigObject { export function applyConfigToState(store: StateManager, config: ConfigObject): void { const state = store.getState(); + state.compiledConfig = config; + if (Array.isArray(config.runtimeSettings)) { const validRuntimeTypes = [ 'WebWorkerLogicRuntime', diff --git a/packages/editor/packages/editor-state/src/index.ts b/packages/editor/packages/editor-state/src/index.ts index 315e265d2..f898ac11a 100644 --- a/packages/editor/packages/editor-state/src/index.ts +++ b/packages/editor/packages/editor-state/src/index.ts @@ -44,7 +44,7 @@ export default function init(events: EventDispatcher, options: Options): StateMa runtime(store, events); projectImport(store, events, state); - codeBlockDragger(state, events); + codeBlockDragger(store, events); codeBlockNavigation(state, events); demoModeNavigation(state, events); _switch(state, events); @@ -52,7 +52,7 @@ export default function init(events: EventDispatcher, options: Options): StateMa pianoKeyboard(store, events); viewport(state, events); contextMenu(store, events); - codeBlockCreator(state, events); + codeBlockCreator(store, events); blockTypeUpdater(store, events); // Must run before compiler to classify blocks first shaderEffectsDeriver(store, events); // Must run after blockTypeUpdater to derive shader effects configEffect(store, events); diff --git a/packages/editor/packages/editor-state/src/pureHelpers/config/collectConfigBlocks.ts b/packages/editor/packages/editor-state/src/pureHelpers/config/collectConfigBlocks.ts index e0e771801..16bee54df 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/config/collectConfigBlocks.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/config/collectConfigBlocks.ts @@ -15,8 +15,8 @@ export interface ConfigBlockSource { * Config blocks are sorted in creation order. * Each config block is compiled independently to allow proper error mapping. */ -export function collectConfigBlocks(codeBlocks: Set): ConfigBlockSource[] { - return Array.from(codeBlocks) +export function collectConfigBlocks(codeBlocks: CodeBlockGraphicData[]): ConfigBlockSource[] { + return codeBlocks .filter(block => block.blockType === 'config') .sort((a, b) => a.creationIndex - b.creationIndex) .map(block => { @@ -45,7 +45,7 @@ if (import.meta.vitest) { blockType: 'config', creationIndex: 1, }); - const codeBlocks = new Set([block1, block2]); + const codeBlocks = [block1, block2]; const result = collectConfigBlocks(codeBlocks); expect(result).toHaveLength(2); @@ -68,7 +68,7 @@ if (import.meta.vitest) { blockType: 'config', creationIndex: 0, }); - const codeBlocks = new Set([block1, block2]); + const codeBlocks = [block1, block2]; const result = collectConfigBlocks(codeBlocks); expect(result[0].block.id).toBe('second'); @@ -86,7 +86,7 @@ if (import.meta.vitest) { blockType: 'module', creationIndex: 1, }); - const codeBlocks = new Set([configBlock, moduleBlock]); + const codeBlocks = [configBlock, moduleBlock]; const result = collectConfigBlocks(codeBlocks); expect(result).toHaveLength(1); @@ -99,7 +99,7 @@ if (import.meta.vitest) { blockType: 'module', creationIndex: 0, }); - const codeBlocks = new Set([moduleBlock]); + const codeBlocks = [moduleBlock]; const result = collectConfigBlocks(codeBlocks); expect(result).toHaveLength(0); @@ -116,7 +116,7 @@ if (import.meta.vitest) { blockType: 'config', creationIndex: 1, }); - const codeBlocks = new Set([emptyBlock, contentBlock]); + const codeBlocks = [emptyBlock, contentBlock]; const result = collectConfigBlocks(codeBlocks); expect(result).toHaveLength(1); diff --git a/packages/editor/packages/editor-state/src/pureHelpers/finders/findClosestCodeBlockInDirection.test.ts b/packages/editor/packages/editor-state/src/pureHelpers/finders/findClosestCodeBlockInDirection.test.ts index 4b451b373..c694019ea 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/finders/findClosestCodeBlockInDirection.test.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/finders/findClosestCodeBlockInDirection.test.ts @@ -12,7 +12,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0 }); const right1 = createMockCodeBlock({ id: 'right1', x: 200, y: 0 }); const right2 = createMockCodeBlock({ id: 'right2', x: 400, y: 0 }); - const codeBlocks = new Set([selected, right1, right2]); + const codeBlocks = [selected, right1, right2]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -23,7 +23,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0 }); // cursor at (50, 50) const closeButMisaligned = createMockCodeBlock({ id: 'close', x: 150, y: 200 }); // center at (200, 250), Y distance = 200 const alignedButFarther = createMockCodeBlock({ id: 'aligned', x: 300, y: 10 }); // center at (350, 60), Y distance = 10 - const codeBlocks = new Set([selected, closeButMisaligned, alignedButFarther]); + const codeBlocks = [selected, closeButMisaligned, alignedButFarther]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -36,7 +36,7 @@ describe('findClosestCodeBlockInDirection', () => { it('should return selected block if no blocks to the right', () => { const selected = createMockCodeBlock({ id: 'selected', x: 100, y: 0 }); const left = createMockCodeBlock({ id: 'left', x: 0, y: 0 }); - const codeBlocks = new Set([selected, left]); + const codeBlocks = [selected, left]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -49,7 +49,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 400, y: 0 }); const left1 = createMockCodeBlock({ id: 'left1', x: 200, y: 0 }); const left2 = createMockCodeBlock({ id: 'left2', x: 0, y: 0 }); - const codeBlocks = new Set([selected, left1, left2]); + const codeBlocks = [selected, left1, left2]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'left'); @@ -59,7 +59,7 @@ describe('findClosestCodeBlockInDirection', () => { it('should return selected block if no blocks to the left', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0 }); const right = createMockCodeBlock({ id: 'right', x: 200, y: 0 }); - const codeBlocks = new Set([selected, right]); + const codeBlocks = [selected, right]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'left'); @@ -72,7 +72,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 400 }); const up1 = createMockCodeBlock({ id: 'up1', x: 0, y: 200 }); const up2 = createMockCodeBlock({ id: 'up2', x: 0, y: 0 }); - const codeBlocks = new Set([selected, up1, up2]); + const codeBlocks = [selected, up1, up2]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'up'); @@ -83,7 +83,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 400 }); const closeButMisaligned = createMockCodeBlock({ id: 'close', x: 200, y: 250 }); // Closer but far horizontally const alignedButFarther = createMockCodeBlock({ id: 'aligned', x: 10, y: 100 }); // Farther but more aligned - const codeBlocks = new Set([selected, closeButMisaligned, alignedButFarther]); + const codeBlocks = [selected, closeButMisaligned, alignedButFarther]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'up'); @@ -96,7 +96,7 @@ describe('findClosestCodeBlockInDirection', () => { it('should return selected block if no blocks above', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0 }); const down = createMockCodeBlock({ id: 'down', x: 0, y: 200 }); - const codeBlocks = new Set([selected, down]); + const codeBlocks = [selected, down]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'up'); @@ -109,7 +109,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0 }); const down1 = createMockCodeBlock({ id: 'down1', x: 0, y: 200 }); const down2 = createMockCodeBlock({ id: 'down2', x: 0, y: 400 }); - const codeBlocks = new Set([selected, down1, down2]); + const codeBlocks = [selected, down1, down2]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'down'); @@ -119,7 +119,7 @@ describe('findClosestCodeBlockInDirection', () => { it('should return selected block if no blocks below', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 200 }); const up = createMockCodeBlock({ id: 'up', x: 0, y: 0 }); - const codeBlocks = new Set([selected, up]); + const codeBlocks = [selected, up]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'down'); @@ -130,7 +130,7 @@ describe('findClosestCodeBlockInDirection', () => { describe('edge cases', () => { it('should handle only the selected block', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0 }); - const codeBlocks = new Set([selected]); + const codeBlocks = [selected]; expect(findClosestCodeBlockInDirection(codeBlocks, selected, 'right').id).toBe('selected'); expect(findClosestCodeBlockInDirection(codeBlocks, selected, 'left').id).toBe('selected'); @@ -138,10 +138,10 @@ describe('findClosestCodeBlockInDirection', () => { expect(findClosestCodeBlockInDirection(codeBlocks, selected, 'down').id).toBe('selected'); }); - it('should handle empty set by returning selected block', () => { + it('should handle empty list by returning selected block', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0 }); - const codeBlocks = new Set(); - codeBlocks.add(selected); + const codeBlocks: CodeBlockGraphicData[] = []; + codeBlocks.push(selected); const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -167,7 +167,7 @@ describe('findClosestCodeBlockInDirection', () => { offsetX: 20, offsetY: 20, }); - const codeBlocks = new Set([selected, right]); + const codeBlocks = [selected, right]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -178,7 +178,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 100, y: 100 }); const overlap = createMockCodeBlock({ id: 'overlap', x: 100, y: 100 }); const right = createMockCodeBlock({ id: 'right', x: 300, y: 100 }); - const codeBlocks = new Set([selected, overlap, right]); + const codeBlocks = [selected, overlap, right]; // Even though overlap is at same position, center calculation means it won't be selected // when looking right, the 'right' block should be found @@ -191,7 +191,7 @@ describe('findClosestCodeBlockInDirection', () => { // Test that blocks are compared using their center points const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0, width: 100, height: 100 }); // center at (50, 50) const right = createMockCodeBlock({ id: 'right', x: 150, y: 0, width: 100, height: 100 }); // center at (200, 50) - const codeBlocks = new Set([selected, right]); + const codeBlocks = [selected, right]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -202,7 +202,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0, width: 50, height: 50 }); const largeRight = createMockCodeBlock({ id: 'largeRight', x: 200, y: 0, width: 200, height: 200 }); const smallRight = createMockCodeBlock({ id: 'smallRight', x: 400, y: 0, width: 25, height: 25 }); - const codeBlocks = new Set([selected, largeRight, smallRight]); + const codeBlocks = [selected, largeRight, smallRight]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -229,7 +229,7 @@ describe('findClosestCodeBlockInDirection', () => { const bottom = createMockCodeBlock({ id: 'bottom', x: 200, y: 400 }); const bottomRight = createMockCodeBlock({ id: 'bottomRight', x: 400, y: 400 }); - const codeBlocks = new Set([selected, topLeft, top, topRight, left, right, bottomLeft, bottom, bottomRight]); + const codeBlocks = [selected, topLeft, top, topRight, left, right, bottomLeft, bottom, bottomRight]; expect(findClosestCodeBlockInDirection(codeBlocks, selected, 'up').id).toBe('top'); expect(findClosestCodeBlockInDirection(codeBlocks, selected, 'down').id).toBe('bottom'); @@ -241,7 +241,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 200, y: 200 }); const right1 = createMockCodeBlock({ id: 'right1', x: 400, y: 200 }); const right2 = createMockCodeBlock({ id: 'right2', x: 400, y: 200 }); - const codeBlocks = new Set([selected, right1, right2]); + const codeBlocks = [selected, right1, right2]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -257,7 +257,7 @@ describe('findClosestCodeBlockInDirection', () => { const down = createMockCodeBlock({ id: 'down', x: 200, y: 400 }); const left = createMockCodeBlock({ id: 'left', x: 0, y: 200 }); const right = createMockCodeBlock({ id: 'right', x: 400, y: 200 }); - const codeBlocks = new Set([selected, up, down, left, right]); + const codeBlocks = [selected, up, down, left, right]; expect(findClosestCodeBlockInDirection(codeBlocks, selected, 'up').id).toBe('up'); expect(findClosestCodeBlockInDirection(codeBlocks, selected, 'down').id).toBe('down'); @@ -276,7 +276,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0, width: 100, height: 100 }); const directlyBelow = createMockCodeBlock({ id: 'directlyBelow', x: 0, y: 200, width: 100, height: 100 }); const diagonalCloser = createMockCodeBlock({ id: 'diagonalCloser', x: 80, y: 150, width: 100, height: 100 }); - const codeBlocks = new Set([selected, directlyBelow, diagonalCloser]); + const codeBlocks = [selected, directlyBelow, diagonalCloser]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'down'); @@ -288,7 +288,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 300, width: 100, height: 100 }); const directlyAbove = createMockCodeBlock({ id: 'directlyAbove', x: 0, y: 100, width: 100, height: 100 }); const diagonalCloser = createMockCodeBlock({ id: 'diagonalCloser', x: 80, y: 150, width: 100, height: 100 }); - const codeBlocks = new Set([selected, directlyAbove, diagonalCloser]); + const codeBlocks = [selected, directlyAbove, diagonalCloser]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'up'); @@ -299,7 +299,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0, width: 100, height: 100 }); // cursor at (50, 50) const directlyRight = createMockCodeBlock({ id: 'directlyRight', x: 200, y: 0, width: 100, height: 100 }); // center at (250, 50), Y distance=0 const diagonalCloser = createMockCodeBlock({ id: 'diagonalCloser', x: 150, y: 80, width: 100, height: 100 }); // center at (200, 130), Y distance=80 - const codeBlocks = new Set([selected, directlyRight, diagonalCloser]); + const codeBlocks = [selected, directlyRight, diagonalCloser]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -311,7 +311,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 300, y: 0, width: 100, height: 100 }); // cursor at (350, 50) const directlyLeft = createMockCodeBlock({ id: 'directlyLeft', x: 100, y: 0, width: 100, height: 100 }); // center at (150, 50), Y distance=0 const diagonalCloser = createMockCodeBlock({ id: 'diagonalCloser', x: 150, y: 80, width: 100, height: 100 }); // center at (200, 130), Y distance=80 - const codeBlocks = new Set([selected, directlyLeft, diagonalCloser]); + const codeBlocks = [selected, directlyLeft, diagonalCloser]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'left'); @@ -329,7 +329,7 @@ describe('findClosestCodeBlockInDirection', () => { const blockA = createMockCodeBlock({ id: 'A', x: 50, y: 0, width: 100, height: 80 }); const blockC = createMockCodeBlock({ id: 'C', x: 50, y: 200, width: 100, height: 80 }); const blockD = createMockCodeBlock({ id: 'D', x: 200, y: 300, width: 100, height: 80 }); - const codeBlocks = new Set([selected, blockA, blockC, blockD]); + const codeBlocks = [selected, blockA, blockC, blockD]; // Moving up should go to A (even though it's offset) const upResult = findClosestCodeBlockInDirection(codeBlocks, selected, 'up'); @@ -348,7 +348,7 @@ describe('findClosestCodeBlockInDirection', () => { const blockA = createMockCodeBlock({ id: 'A', x: 0, y: 50, width: 100, height: 80 }); const blockC = createMockCodeBlock({ id: 'C', x: 400, y: 50, width: 100, height: 80 }); const blockD = createMockCodeBlock({ id: 'D', x: 600, y: 100, width: 100, height: 80 }); - const codeBlocks = new Set([selected, blockA, blockC, blockD]); + const codeBlocks = [selected, blockA, blockC, blockD]; // Moving left with cursor Y=140 (100 + 40): // A: Y range [50, 130], cursor 140 is NOT in range - no movement @@ -376,7 +376,7 @@ describe('findClosestCodeBlockInDirection', () => { const selected = createMockCodeBlock({ id: 'selected', x: 100, y: 100, width: 100, height: 100 }); const overlapping = createMockCodeBlock({ id: 'overlapping', x: 150, y: 150, width: 100, height: 100 }); const properlyBelow = createMockCodeBlock({ id: 'properlyBelow', x: 100, y: 250, width: 100, height: 100 }); - const codeBlocks = new Set([selected, overlapping, properlyBelow]); + const codeBlocks = [selected, overlapping, properlyBelow]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'down'); @@ -390,7 +390,7 @@ describe('findClosestCodeBlockInDirection', () => { // Blocks that touch edge-to-edge (no gap between them) const selected = createMockCodeBlock({ id: 'selected', x: 0, y: 0, width: 100, height: 100 }); const adjacent = createMockCodeBlock({ id: 'adjacent', x: 0, y: 100, width: 100, height: 100 }); // top edge exactly at selected's bottom - const codeBlocks = new Set([selected, adjacent]); + const codeBlocks = [selected, adjacent]; const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'down'); @@ -405,7 +405,7 @@ describe('findClosestCodeBlockInDirection', () => { const farAligned = createMockCodeBlock({ id: 'farAligned', x: 100, y: 300, width: 100, height: 100 }); // closeMisaligned: closer but significantly misaligned const closeMisaligned = createMockCodeBlock({ id: 'closeMisaligned', x: 250, y: 220, width: 100, height: 100 }); - const codeBlocks = new Set([selected, farAligned, closeMisaligned]); + const codeBlocks = [selected, farAligned, closeMisaligned]; // farAligned: primary = 300-200 = 100, secondary = 0, score = 100 // closeMisaligned: primary = 220-200 = 20, secondary = 200, score = 20 + 200*2 = 420 @@ -431,7 +431,7 @@ describe('findClosestCodeBlockInDirection', () => { // Two neighbors to the right at different heights const topNeighbor = createMockCodeBlock({ id: 'topNeighbor', x: 200, y: 50, width: 100, height: 100 }); const bottomNeighbor = createMockCodeBlock({ id: 'bottomNeighbor', x: 200, y: 200, width: 100, height: 100 }); - const codeBlocks = new Set([selected, topNeighbor, bottomNeighbor]); + const codeBlocks = [selected, topNeighbor, bottomNeighbor]; // Cursor at Y=110, topNeighbor center at Y=100, bottomNeighbor center at Y=250 // topNeighbor: distance from cursor = |100 - 110| = 10 @@ -457,7 +457,7 @@ describe('findClosestCodeBlockInDirection', () => { // Two neighbors to the right at different heights const topNeighbor = createMockCodeBlock({ id: 'topNeighbor', x: 200, y: 50, width: 100, height: 100 }); const bottomNeighbor = createMockCodeBlock({ id: 'bottomNeighbor', x: 200, y: 200, width: 100, height: 100 }); - const codeBlocks = new Set([selected, topNeighbor, bottomNeighbor]); + const codeBlocks = [selected, topNeighbor, bottomNeighbor]; // Cursor at Y=280, topNeighbor center at Y=100, bottomNeighbor center at Y=250 // topNeighbor: distance from cursor = |100 - 280| = 180 @@ -483,7 +483,7 @@ describe('findClosestCodeBlockInDirection', () => { // Two neighbors to the left at different heights const topNeighbor = createMockCodeBlock({ id: 'topNeighbor', x: 200, y: 50, width: 100, height: 100 }); const bottomNeighbor = createMockCodeBlock({ id: 'bottomNeighbor', x: 200, y: 200, width: 100, height: 100 }); - const codeBlocks = new Set([selected, topNeighbor, bottomNeighbor]); + const codeBlocks = [selected, topNeighbor, bottomNeighbor]; // Cursor at Y=110, topNeighbor center at Y=100, bottomNeighbor center at Y=250 // topNeighbor should win due to cursor proximity @@ -510,7 +510,7 @@ describe('findClosestCodeBlockInDirection', () => { const middle = createMockCodeBlock({ id: 'middle', x: 200, y: 180, width: 100, height: 80 }); // Center at Y=220 const lowerMiddle = createMockCodeBlock({ id: 'lowerMiddle', x: 200, y: 260, width: 100, height: 80 }); const bottom = createMockCodeBlock({ id: 'bottom', x: 200, y: 340, width: 100, height: 80 }); - const codeBlocks = new Set([selected, top, upperMiddle, middle, lowerMiddle, bottom]); + const codeBlocks = [selected, top, upperMiddle, middle, lowerMiddle, bottom]; // Cursor at Y=200, middle center at Y=220 (distance=20) // All others are farther from Y=200 @@ -525,7 +525,7 @@ describe('findClosestCodeBlockInDirection', () => { // Two neighbors at different heights const topNeighbor = createMockCodeBlock({ id: 'topNeighbor', x: 200, y: 50, width: 100, height: 100 }); const bottomNeighbor = createMockCodeBlock({ id: 'bottomNeighbor', x: 200, y: 200, width: 100, height: 100 }); - const codeBlocks = new Set([selected, topNeighbor, bottomNeighbor]); + const codeBlocks = [selected, topNeighbor, bottomNeighbor]; // Without cursor, selected center is at Y=150 // topNeighbor center at Y=100 (distance=50) @@ -551,7 +551,7 @@ describe('findClosestCodeBlockInDirection', () => { // Blocks above and below const above = createMockCodeBlock({ id: 'above', x: 0, y: 0, width: 100, height: 100 }); const below = createMockCodeBlock({ id: 'below', x: 0, y: 400, width: 100, height: 100 }); - const codeBlocks = new Set([selected, above, below]); + const codeBlocks = [selected, above, below]; // Vertical navigation should use block centers, not cursor const upResult = findClosestCodeBlockInDirection(codeBlocks, selected, 'up'); @@ -577,7 +577,7 @@ describe('findClosestCodeBlockInDirection', () => { const veryTop = createMockCodeBlock({ id: 'veryTop', x: 200, y: 0, width: 100, height: 100 }); const middle = createMockCodeBlock({ id: 'middle', x: 200, y: 200, width: 100, height: 100 }); const bottom = createMockCodeBlock({ id: 'bottom', x: 200, y: 400, width: 100, height: 100 }); - const codeBlocks = new Set([selected, veryTop, middle, bottom]); + const codeBlocks = [selected, veryTop, middle, bottom]; // Cursor at Y=10, veryTop center at Y=50 (distance=40) // middle and bottom are much farther @@ -602,7 +602,7 @@ describe('findClosestCodeBlockInDirection', () => { const perfectMatch = createMockCodeBlock({ id: 'perfectMatch', x: 200, y: 100, width: 100, height: 100 }); // Other neighbor const other = createMockCodeBlock({ id: 'other', x: 200, y: 250, width: 100, height: 100 }); - const codeBlocks = new Set([selected, perfectMatch, other]); + const codeBlocks = [selected, perfectMatch, other]; // Cursor at Y=150 perfectly matches perfectMatch center const result = findClosestCodeBlockInDirection(codeBlocks, selected, 'right'); @@ -626,7 +626,7 @@ describe('findClosestCodeBlockInDirection', () => { const closeButMisaligned = createMockCodeBlock({ id: 'close', x: 150, y: 300, width: 100, height: 100 }); // Center at (200, 350), Y distance = 250 // Farther but vertically aligned neighbor const farButAligned = createMockCodeBlock({ id: 'aligned', x: 300, y: 80, width: 100, height: 100 }); // Center at (350, 130), Y distance = 30 - const codeBlocks = new Set([selected, closeButMisaligned, farButAligned]); + const codeBlocks = [selected, closeButMisaligned, farButAligned]; // With Y-only distance, aligned is much closer vertically // close: Y distance = |350 - 100| = 250 @@ -652,7 +652,7 @@ describe('findClosestCodeBlockInDirection', () => { const top = createMockCodeBlock({ id: 'top', x: 200, y: 0, width: 100, height: 100 }); // Range [0, 100] const middle = createMockCodeBlock({ id: 'middle', x: 200, y: 150, width: 100, height: 100 }); // Range [150, 250] - overlaps cursor at 200 const bottom = createMockCodeBlock({ id: 'bottom', x: 200, y: 300, width: 100, height: 100 }); // Range [300, 400] - const codeBlocks = new Set([selected, top, middle, bottom]); + const codeBlocks = [selected, top, middle, bottom]; // Cursor at Y=200 // top: range [0,100], cursor not in range, distance = 200-100 = 100 diff --git a/packages/editor/packages/editor-state/src/pureHelpers/finders/findClosestCodeBlockInDirection.ts b/packages/editor/packages/editor-state/src/pureHelpers/finders/findClosestCodeBlockInDirection.ts index a5b712bdd..a1bdf264d 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/finders/findClosestCodeBlockInDirection.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/finders/findClosestCodeBlockInDirection.ts @@ -122,7 +122,7 @@ function calculateSecondaryDistance( * diagonal layouts, by preferring blocks that are truly in the requested direction * rather than just diagonally closer by center-to-center distance. * - * @param codeBlocks - The set of all code blocks to search through + * @param codeBlocks - The list of all code blocks to search through * @param selectedBlock - The currently selected code block to navigate from * @param direction - The direction to navigate: 'left', 'right', 'up', or 'down' * @returns The closest code block in the specified direction, or the selected block if none found @@ -137,13 +137,13 @@ function calculateSecondaryDistance( * ``` */ export default function findClosestCodeBlockInDirection( - codeBlocks: Set, + codeBlocks: CodeBlockGraphicData[], selectedBlock: CodeBlockGraphicData, direction: Direction ): CodeBlockGraphicData { const selectedBounds = getBlockBounds(selectedBlock); - const candidates = Array.from(codeBlocks).filter(block => { + const candidates = codeBlocks.filter(block => { if (block === selectedBlock) return false; const candidateBounds = getBlockBounds(block); diff --git a/packages/editor/packages/editor-state/src/pureHelpers/finders/findCodeBlockAtViewportCoordinates.ts b/packages/editor/packages/editor-state/src/pureHelpers/finders/findCodeBlockAtViewportCoordinates.ts index 942d79892..ad48bcf75 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/finders/findCodeBlockAtViewportCoordinates.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/finders/findCodeBlockAtViewportCoordinates.ts @@ -14,7 +14,8 @@ export default function findCodeBlockAtViewportCoordinates( searchX: number, searchY: number ): CodeBlockGraphicData | undefined { - for (const graphicData of Array.from(graphicHelper.codeBlocks).reverse()) { + for (let index = graphicHelper.codeBlocks.length - 1; index >= 0; index -= 1) { + const graphicData = graphicHelper.codeBlocks[index]; const { width, height, x, y, offsetX, offsetY } = graphicData; if ( searchX >= x + offsetX - graphicHelper.viewport.x && diff --git a/packages/editor/packages/editor-state/src/pureHelpers/projectSerializing/serializeToProject.ts b/packages/editor/packages/editor-state/src/pureHelpers/projectSerializing/serializeToProject.ts index d807bdc45..125bcbf7d 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/projectSerializing/serializeToProject.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/projectSerializing/serializeToProject.ts @@ -21,7 +21,7 @@ export default function serializeToProject( const { graphicHelper, compiler } = state; const project: Project = { - codeBlocks: convertGraphicDataToProjectStructure(Array.from(graphicHelper.codeBlocks)), + codeBlocks: convertGraphicDataToProjectStructure(graphicHelper.codeBlocks), viewport: { // Convert pixel coordinates to grid coordinates for persistent storage gridCoordinates: { @@ -59,14 +59,14 @@ if (import.meta.vitest) { it('serializes basic project state without compiled data', () => { const state = createMockState({ graphicHelper: { - codeBlocks: new Set([ + codeBlocks: [ createMockCodeBlock({ id: 'block-1', code: ['10 example'], x: 20, y: 30, }), - ]), + ], viewport: { x: 40, y: 50, @@ -88,7 +88,7 @@ if (import.meta.vitest) { it('includes compiled modules, wasm and memory snapshot when requested', () => { const state = createMockState({ graphicHelper: { - codeBlocks: new Set(), + codeBlocks: [], viewport: { x: 0, y: 0, diff --git a/packages/editor/packages/editor-state/src/pureHelpers/projectSerializing/serializeToRuntimeReadyProject.ts b/packages/editor/packages/editor-state/src/pureHelpers/projectSerializing/serializeToRuntimeReadyProject.ts index fbcf8929d..8b0df9b6c 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/projectSerializing/serializeToRuntimeReadyProject.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/projectSerializing/serializeToRuntimeReadyProject.ts @@ -43,7 +43,7 @@ if (import.meta.vitest) { const state = createMockState({ graphicHelper: { - codeBlocks: new Set([configBlock]), + codeBlocks: [configBlock], }, compiler: { compiledModules: { mod: {} }, diff --git a/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts b/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts index 3b7d2ac4c..f25db25f8 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/state/createDefaultState.ts @@ -2,8 +2,6 @@ import { Font } from '@8f4e/sprite-generator'; import { defaultFeatureFlags } from './featureFlags'; -import type { CodeBlockGraphicData } from '../../types'; - export default function createDefaultState() { return { compiler: { @@ -36,7 +34,7 @@ export default function createDefaultState() { title: 'Dialog', buttons: [{ title: 'Close', action: 'close' }], }, - codeBlocks: new Set(), + codeBlocks: [], nextCodeBlockCreationIndex: 0, outputsByWordAddress: new Map(), viewport: { diff --git a/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts b/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts index bb7f251bd..dae373a23 100644 --- a/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts +++ b/packages/editor/packages/editor-state/src/pureHelpers/testingUtils/testUtils.ts @@ -233,7 +233,7 @@ export function createMockState(overrides: DeepPartial = {}): State { loadSession: createMockAsyncFunction(null), }, graphicHelper: { - codeBlocks: new Set(), + codeBlocks: [], nextCodeBlockCreationIndex: 0, viewport: { x: 0, diff --git a/packages/editor/packages/editor-state/src/types.ts b/packages/editor/packages/editor-state/src/types.ts index ef2b3471e..587c93abf 100644 --- a/packages/editor/packages/editor-state/src/types.ts +++ b/packages/editor/packages/editor-state/src/types.ts @@ -1,4 +1,5 @@ import { StateManager } from '@8f4e/state-manager'; +import { ConfigObject } from 'impureHelpers/config/applyConfigToState'; import type { Font, SpriteLookups, ColorScheme } from '@8f4e/sprite-generator'; import type { SpriteLookup, PostProcessEffect } from 'glugglug'; @@ -175,7 +176,6 @@ export interface Compiler { allocatedMemorySize: number; compiledFunctions?: CompiledFunctionLookup; disableAutoCompilation: boolean; - compiledConfig?: Record; } export interface Midi { @@ -369,7 +369,7 @@ export type GraphicHelper = { y: number; animationDurationMs?: number; }; - codeBlocks: Set; + codeBlocks: CodeBlockGraphicData[]; /** * Monotonically increasing counter for assigning creationIndex to new code blocks. * Incremented each time a new code block is created. @@ -484,7 +484,7 @@ export interface Project { compiledModules?: CompiledModuleLookup; memorySnapshot?: string; /** Compiled configuration from config blocks for runtime-only execution */ - compiledConfig?: Record; + compiledConfig?: ConfigObject; /** Post-process effects configuration for custom visual effects */ postProcessEffects?: PostProcessEffect[]; } @@ -644,6 +644,8 @@ export interface State { colorSchemes: string[]; colorScheme?: ColorScheme; historyStack: Project[]; + initialProjectState?: Project; + compiledConfig?: ConfigObject; redoStack: Project[]; storageQuota: { usedBytes: number; totalBytes: number }; binaryAssets: BinaryAsset[]; diff --git a/packages/editor/packages/web-ui/screenshot-tests/dragged-modules.test.ts b/packages/editor/packages/web-ui/screenshot-tests/dragged-modules.test.ts index e915a1637..d021c2934 100644 --- a/packages/editor/packages/web-ui/screenshot-tests/dragged-modules.test.ts +++ b/packages/editor/packages/web-ui/screenshot-tests/dragged-modules.test.ts @@ -34,7 +34,7 @@ test('dragged module', async () => { mockState.graphicHelper.draggedCodeBlock = codeBlockMock; - mockState.graphicHelper.codeBlocks.add(codeBlockMock); + mockState.graphicHelper.codeBlocks.push(codeBlockMock); } await expect(canvas).toMatchScreenshot(); diff --git a/packages/editor/packages/web-ui/screenshot-tests/font-color-rendering.test.ts b/packages/editor/packages/web-ui/screenshot-tests/font-color-rendering.test.ts index 17d7b3517..88659c076 100644 --- a/packages/editor/packages/web-ui/screenshot-tests/font-color-rendering.test.ts +++ b/packages/editor/packages/web-ui/screenshot-tests/font-color-rendering.test.ts @@ -45,7 +45,7 @@ test('font color rendering', async () => { const codeLines = ['', colorName, ...lines.map(line => line.join('')), '']; const codeToRender = codeLines.map(line => line.split('').map(char => char.charCodeAt(0))); - mockState.graphicHelper.codeBlocks.add( + mockState.graphicHelper.codeBlocks.push( createMockCodeBlock({ id: `codeBlock${index}`, x: (index % 4) * 8 * 32, diff --git a/packages/editor/packages/web-ui/screenshot-tests/switches.test.ts b/packages/editor/packages/web-ui/screenshot-tests/switches.test.ts index 590e91840..7ee521f2c 100644 --- a/packages/editor/packages/web-ui/screenshot-tests/switches.test.ts +++ b/packages/editor/packages/web-ui/screenshot-tests/switches.test.ts @@ -49,12 +49,12 @@ test('switches', async () => { mockState.graphicHelper.selectedCodeBlock = codeBlockMock; - mockState.graphicHelper.codeBlocks.add(codeBlockMock); + mockState.graphicHelper.codeBlocks.push(codeBlockMock); const lines2 = ['not selected code block', '', '', '', '', '', '', '', '']; const codeToRender2 = lines2.map(line => line.split('').map(char => char.charCodeAt(0))); - mockState.graphicHelper.codeBlocks.add( + mockState.graphicHelper.codeBlocks.push( createMockCodeBlock({ x: 288, y: 16, diff --git a/src/colorSchemes/hackerman.ts b/src/colorSchemes/hackerman.ts index d9bf22f3c..2abdf3de4 100644 --- a/src/colorSchemes/hackerman.ts +++ b/src/colorSchemes/hackerman.ts @@ -18,8 +18,8 @@ export default { background: '#000000', backgroundDots: '#004400', backgroundDots2: '#006600', - moduleBackground: 'rgba(0,0,0,0.9)', - moduleBackgroundDragged: 'rgba(0,0,0,0.8)', + moduleBackground: 'rgba(0,0,0,0.85)', + moduleBackgroundDragged: 'rgba(0,0,0,0.7)', wire: 'rgba(153,255,153,0.6)', wireHighlighted: 'rgba(204,255,204,0.8)', errorMessageBackground: '#cc0000', From 2de1b5b9cfbd1847b2cf68bfd65b39c1d639715c Mon Sep 17 00:00:00 2001 From: andorthehood Date: Sat, 3 Jan 2026 16:41:02 +0000 Subject: [PATCH 4/9] chore(editor-state): fix type errors --- packages/editor/packages/editor-state/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/packages/editor-state/src/index.ts b/packages/editor/packages/editor-state/src/index.ts index f898ac11a..ad239edfd 100644 --- a/packages/editor/packages/editor-state/src/index.ts +++ b/packages/editor/packages/editor-state/src/index.ts @@ -43,7 +43,7 @@ export default function init(events: EventDispatcher, options: Options): StateMa editorSettings(store, events, state); runtime(store, events); - projectImport(store, events, state); + projectImport(store, events); codeBlockDragger(store, events); codeBlockNavigation(state, events); demoModeNavigation(state, events); @@ -53,7 +53,7 @@ export default function init(events: EventDispatcher, options: Options): StateMa viewport(state, events); contextMenu(store, events); codeBlockCreator(store, events); - blockTypeUpdater(store, events); // Must run before compiler to classify blocks first + blockTypeUpdater(store); // Must run before compiler to classify blocks first shaderEffectsDeriver(store, events); // Must run after blockTypeUpdater to derive shader effects configEffect(store, events); compiler(store, events); From 8f0c83ede53a02189ca447b04ccef02f80f597f6 Mon Sep 17 00:00:00 2001 From: andorthehood Date: Sat, 3 Jan 2026 16:58:49 +0000 Subject: [PATCH 5/9] chore(editor-state): fix failing tests --- .../errorMessages/errorMessages.test.ts | 294 ------------------ .../editor-state/src/effects/compiler.ts | 1 - 2 files changed, 295 deletions(-) delete mode 100644 packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.test.ts diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.test.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.test.ts deleted file mode 100644 index 122947d74..000000000 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/codeBlockDecorators/errorMessages/errorMessages.test.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import createStateManager from '@8f4e/state-manager'; - -import errorMessages from './errorMessages'; - -import { createMockCodeBlock, createMockState } from '../../../../pureHelpers/testingUtils/testUtils'; - -import type { CodeBlockGraphicData, State, CodeError } from '../../../../types'; - -describe('errorMessages', () => { - let mockState: State; - let store: ReturnType>; - let codeBlock1: CodeBlockGraphicData; - let codeBlock2: CodeBlockGraphicData; - - beforeEach(() => { - codeBlock1 = createMockCodeBlock({ - id: 'block-1', - creationIndex: 1, - width: 160, - gaps: new Map(), - }); - - codeBlock2 = createMockCodeBlock({ - id: 'block-2', - creationIndex: 2, - width: 160, - gaps: new Map(), - }); - - mockState = createMockState({ - graphicHelper: { - codeBlocks: [codeBlock1, codeBlock2], - viewport: { - vGrid: 8, - hGrid: 16, - }, - }, - codeErrors: { - compilationErrors: [], - configErrors: [], - }, - }); - - store = createStateManager(mockState); - }); - - describe('error filtering by codeBlockId', () => { - it('should add error messages only to matching code block by id', () => { - const compilationError: CodeError = { - lineNumber: 5, - message: 'Undefined variable', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [compilationError]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages.length).toBe(1); - expect(codeBlock2.extras.errorMessages.length).toBe(0); - }); - - it('should add error messages to matching code block by creationIndex', () => { - const configError: CodeError = { - lineNumber: 3, - message: 'Invalid config value', - codeBlockId: 2, - }; - mockState.codeErrors.configErrors = [configError]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages.length).toBe(0); - expect(codeBlock2.extras.errorMessages.length).toBe(1); - }); - - it('should not add error messages when codeBlockId does not match', () => { - const compilationError: CodeError = { - lineNumber: 1, - message: 'Some error', - codeBlockId: 'non-existent-block', - }; - mockState.codeErrors.compilationErrors = [compilationError]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages.length).toBe(0); - expect(codeBlock2.extras.errorMessages.length).toBe(0); - }); - }); - - describe('text wrapping', () => { - it('should wrap long error messages into multiple lines', () => { - const longMessage = - 'This is a very long error message that should be wrapped into multiple lines based on the width'; - const compilationError: CodeError = { - lineNumber: 1, - message: longMessage, - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [compilationError]; - - errorMessages(store); - - const errorMsg = codeBlock1.extras.errorMessages[0]; - expect(errorMsg.message[0]).toBe('Error:'); - expect(errorMsg.message.length).toBeGreaterThan(2); - }); - - it('should include Error: prefix in wrapped message', () => { - const compilationError: CodeError = { - lineNumber: 1, - message: 'Short error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [compilationError]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages[0].message[0]).toBe('Error:'); - }); - }); - - describe('both compilation and config errors', () => { - it('should display both compilation errors and config errors', () => { - const compilationError: CodeError = { - lineNumber: 1, - message: 'Compilation error', - codeBlockId: 'block-1', - }; - const configError: CodeError = { - lineNumber: 2, - message: 'Config error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [compilationError]; - mockState.codeErrors.configErrors = [configError]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages.length).toBe(2); - }); - - it('should handle multiple errors on different blocks', () => { - const error1: CodeError = { - lineNumber: 1, - message: 'Error 1', - codeBlockId: 'block-1', - }; - const error2: CodeError = { - lineNumber: 2, - message: 'Error 2', - codeBlockId: 'block-2', - }; - mockState.codeErrors.compilationErrors = [error1, error2]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages.length).toBe(1); - expect(codeBlock2.extras.errorMessages.length).toBe(1); - }); - }); - - describe('subscription updates', () => { - it('should subscribe to codeErrors changes', () => { - const subscribeSpy = vi.spyOn(store, 'subscribe'); - errorMessages(store); - - expect(subscribeSpy).toHaveBeenCalledWith('codeErrors', expect.any(Function)); - subscribeSpy.mockRestore(); - }); - - it('should update error messages when codeErrors changes', () => { - errorMessages(store); - - expect(codeBlock1.extras.errorMessages.length).toBe(0); - - const newError: CodeError = { - lineNumber: 1, - message: 'New error', - codeBlockId: 'block-1', - }; - store.set('codeErrors.compilationErrors', [newError]); - - expect(codeBlock1.extras.errorMessages.length).toBe(1); - }); - - it('should clear previous error messages when errors change', () => { - const error: CodeError = { - lineNumber: 1, - message: 'Original error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [error]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages.length).toBe(1); - - store.set('codeErrors.compilationErrors', []); - - expect(codeBlock1.extras.errorMessages.length).toBe(0); - }); - }); - - describe('error message positioning', () => { - it('should set correct x coordinate', () => { - const error: CodeError = { - lineNumber: 1, - message: 'Error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [error]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages[0].x).toBe(0); - }); - - it('should preserve lineNumber in error message data', () => { - const error: CodeError = { - lineNumber: 5, - message: 'Error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [error]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages[0].lineNumber).toBe(5); - }); - - it('should calculate y position based on line number and hGrid when no gaps', () => { - const error: CodeError = { - lineNumber: 3, - message: 'Error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [error]; - - errorMessages(store); - - const expectedY = (3 + 1) * mockState.graphicHelper.viewport.hGrid; - expect(codeBlock1.extras.errorMessages[0].y).toBe(expectedY); - }); - - it('should calculate y position accounting for gaps', () => { - codeBlock1.gaps.set(1, { size: 2 }); - - const error: CodeError = { - lineNumber: 3, - message: 'Error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [error]; - - errorMessages(store); - - const physicalRow = 3 + 2; - const expectedY = (physicalRow + 1) * mockState.graphicHelper.viewport.hGrid; - expect(codeBlock1.extras.errorMessages[0].y).toBe(expectedY); - }); - }); - - describe('initial update', () => { - it('should process existing errors on initialization', () => { - const error: CodeError = { - lineNumber: 1, - message: 'Initial error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [error]; - - errorMessages(store); - - expect(codeBlock1.extras.errorMessages.length).toBe(1); - }); - - it('should trigger rerender after processing errors', () => { - const error: CodeError = { - lineNumber: 1, - message: 'Error', - codeBlockId: 'block-1', - }; - mockState.codeErrors.compilationErrors = [error]; - - const setSpy = vi.spyOn(store, 'set'); - errorMessages(store); - - expect(setSpy).toHaveBeenCalledWith('graphicHelper.codeBlocks', mockState.graphicHelper.codeBlocks); - setSpy.mockRestore(); - }); - }); -}); diff --git a/packages/editor/packages/editor-state/src/effects/compiler.ts b/packages/editor/packages/editor-state/src/effects/compiler.ts index e333e6490..f77b312e7 100644 --- a/packages/editor/packages/editor-state/src/effects/compiler.ts +++ b/packages/editor/packages/editor-state/src/effects/compiler.ts @@ -114,7 +114,6 @@ export default async function compiler(store: StateManager, events: Event } events.on('compileCode', onForceCompile); - events.on('deleteCodeBlock', onRecompile); store.subscribe('compiler.compilerOptions', onRecompile); store.subscribe('graphicHelper.codeBlocks', () => { // state.compiler.compilerOptions.memorySizeBytes = defaultState.compiler.compilerOptions.memorySizeBytes; From 4fa75193eccd87bcff4e37d07a99917926b462b6 Mon Sep 17 00:00:00 2001 From: andorthehood Date: Sat, 3 Jan 2026 18:33:29 +0000 Subject: [PATCH 6/9] chore(editor-state): restore default compiler settings when changing project --- .../editor-state/src/effects/compiler.ts | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/packages/editor/packages/editor-state/src/effects/compiler.ts b/packages/editor/packages/editor-state/src/effects/compiler.ts index f77b312e7..a287f0406 100644 --- a/packages/editor/packages/editor-state/src/effects/compiler.ts +++ b/packages/editor/packages/editor-state/src/effects/compiler.ts @@ -1,7 +1,10 @@ import { StateManager } from '@8f4e/state-manager'; +import decodeBase64ToUint8Array from '../pureHelpers/base64/decodeBase64ToUint8Array'; +import decodeBase64ToInt32Array from '../pureHelpers/base64/decodeBase64ToInt32Array'; +import decodeBase64ToFloat32Array from '../pureHelpers/base64/decodeBase64ToFloat32Array'; import { EventDispatcher } from '../types'; -import { log } from '../impureHelpers/logger/logger'; +import { error, log } from '../impureHelpers/logger/logger'; import type { CodeBlockGraphicData, State } from '../types'; @@ -115,40 +118,48 @@ export default async function compiler(store: StateManager, events: Event events.on('compileCode', onForceCompile); store.subscribe('compiler.compilerOptions', onRecompile); - store.subscribe('graphicHelper.codeBlocks', () => { - // state.compiler.compilerOptions.memorySizeBytes = defaultState.compiler.compilerOptions.memorySizeBytes; - // state.compiler.memoryBuffer = new Int32Array(); - // state.compiler.memoryBufferFloat = new Float32Array(); - // state.compiler.codeBuffer = new Uint8Array(); - // state.compiler.compiledModules = {}; - // state.compiler.allocatedMemorySize = 0; - // store.set('codeErrors.compilationErrors', []); - // state.compiler.isCompiling = false; - // state.binaryAssets = newProject.binaryAssets || []; - // state.runtime.runtimeSettings = defaultState.runtime.runtimeSettings; - // state.runtime.selectedRuntime = defaultState.runtime.selectedRuntime; - // // postProcessEffects are now derived from shader code blocks, not loaded from project data - // if (newProject.compiledWasm && newProject.memorySnapshot) { - // try { - // state.compiler.codeBuffer = decodeBase64ToUint8Array(newProject.compiledWasm); - // state.compiler.memoryBuffer = decodeBase64ToInt32Array(newProject.memorySnapshot); - // state.compiler.memoryBufferFloat = decodeBase64ToFloat32Array(newProject.memorySnapshot); - // state.compiler.allocatedMemorySize = state.compiler.memoryBuffer.byteLength; - // if (newProject.compiledModules) { - // state.compiler.compiledModules = newProject.compiledModules; - // } - // log(state, 'Pre-compiled WASM loaded and decoded successfully', 'Loader'); - // } catch (err) { - // console.error('[Loader] Failed to decode pre-compiled WASM:', err); - // error(state, 'Failed to decode pre-compiled WASM', 'Loader'); - // state.compiler.codeBuffer = new Uint8Array(); - // state.compiler.memoryBuffer = new Int32Array(); - // state.compiler.memoryBufferFloat = new Float32Array(); - // } - // } else if (newProject.compiledModules) { - // state.compiler.compiledModules = newProject.compiledModules; - // } + store.subscribe('initialProjectState', () => { + store.set('codeErrors.compilationErrors', []); + state.binaryAssets = state.initialProjectState?.binaryAssets || []; + + // Return to default compiler state + state.compiler.isCompiling = false; + state.compiler.compilerOptions.memorySizeBytes = 1048576; // Default to 1MB if not specified + state.compiler.codeBuffer = new Uint8Array(); + state.compiler.compiledModules = {}; + state.compiler.memoryBuffer = new Int32Array(); + state.compiler.memoryBufferFloat = new Float32Array(); + state.compiler.allocatedMemorySize = 0; + + if (state.initialProjectState?.compiledWasm && state.initialProjectState.compiledModules) { + try { + state.compiler.codeBuffer = decodeBase64ToUint8Array(state.initialProjectState.compiledWasm); + state.compiler.compiledModules = state.initialProjectState.compiledModules; + log(state, 'Pre-compiled WASM loaded and decoded successfully', 'Loader'); + } catch (err) { + state.compiler.codeBuffer = new Uint8Array(); + state.compiler.compiledModules = {}; + console.error('[Loader] Failed to decode pre-compiled WASM:', err); + error(state, 'Failed to decode pre-compiled WASM', 'Loader'); + } + } + + if (state.initialProjectState?.memorySnapshot) { + try { + state.compiler.memoryBuffer = decodeBase64ToInt32Array(state.initialProjectState.memorySnapshot); + state.compiler.memoryBufferFloat = decodeBase64ToFloat32Array(state.initialProjectState.memorySnapshot); + state.compiler.allocatedMemorySize = state.compiler.memoryBuffer.byteLength; + log(state, 'Memory snapshot loaded and decoded successfully', 'Loader'); + } catch (err) { + state.compiler.memoryBuffer = new Int32Array(); + state.compiler.memoryBufferFloat = new Float32Array(); + state.compiler.allocatedMemorySize = 0; + console.error('[Loader] Failed to decode memory snapshot:', err); + error(state, 'Failed to decode memory snapshot', 'Loader'); + } + } }); + store.subscribe('graphicHelper.codeBlocks', onRecompile); store.subscribe('graphicHelper.selectedCodeBlock.code', () => { if ( state.graphicHelper.selectedCodeBlock?.blockType !== 'module' && From c5bc30a617ca6733b166455c8a617919f9a10690 Mon Sep 17 00:00:00 2001 From: andorthehood Date: Sat, 3 Jan 2026 19:32:19 +0000 Subject: [PATCH 7/9] fix(editor-state): fix failing tests --- .../src/effects/disableCompilation.test.ts | 51 +++++---------- .../src/effects/projectImport.test.ts | 15 ++--- .../src/effects/runtimeReadyProject.test.ts | 62 ++++++++----------- 3 files changed, 45 insertions(+), 83 deletions(-) diff --git a/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts b/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts index b85220c48..11d347a45 100644 --- a/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts +++ b/packages/editor/packages/editor-state/src/effects/disableCompilation.test.ts @@ -68,12 +68,8 @@ describe('disableAutoCompilation feature', () => { compiler(store, mockEvents); - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - expect(compileCall).toBeDefined(); - - const onRecompileCallback = compileCall![1]; - await onRecompileCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); expect(mockCompileCode).not.toHaveBeenCalled(); expect(mockState.compiler.isCompiling).toBe(false); @@ -92,11 +88,8 @@ describe('disableAutoCompilation feature', () => { compiler(store, mockEvents); - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - const onRecompileCallback = compileCall![1]; - - await onRecompileCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); expect(mockCompileCode).toHaveBeenCalled(); }); @@ -108,11 +101,8 @@ describe('disableAutoCompilation feature', () => { compiler(store, mockEvents); - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - const onRecompileCallback = compileCall![1]; - - await onRecompileCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); expect( mockState.console.logs.some(log => @@ -129,12 +119,8 @@ describe('disableAutoCompilation feature', () => { configEffect(store, mockEvents); - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const configCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - expect(configCall).toBeDefined(); - - const rebuildConfigCallback = configCall![1]; - await rebuildConfigCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); expect(mockCompileConfig).not.toHaveBeenCalled(); expect( @@ -151,11 +137,8 @@ describe('disableAutoCompilation feature', () => { configEffect(store, mockEvents); - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const configCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - const rebuildConfigCallback = configCall![1]; - - await rebuildConfigCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); expect(mockCompileConfig).toHaveBeenCalled(); }); @@ -215,11 +198,8 @@ describe('disableAutoCompilation feature', () => { configEffect(store, mockEvents); - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const configCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - const rebuildConfigCallback = configCall![1]; - - await rebuildConfigCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); expect(mockState.compiler.disableAutoCompilation).toBe(true); }); @@ -234,11 +214,8 @@ describe('disableAutoCompilation feature', () => { configEffect(store, mockEvents); - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const configCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - const rebuildConfigCallback = configCall![1]; - - await rebuildConfigCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); expect(mockState.compiler.disableAutoCompilation).toBe(false); }); diff --git a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts index fde5dff17..66c9f71bf 100644 --- a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts +++ b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts @@ -54,9 +54,7 @@ describe('projectImport', () => { // Give time for promises to resolve await new Promise(resolve => setTimeout(resolve, 10)); - const dispatchCalls = (mockEvents.dispatch as unknown as MockInstance).mock.calls; - const projectLoadedCall = dispatchCalls.find(call => call[0] === 'projectLoaded'); - expect(projectLoadedCall).toBeDefined(); + expect(mockState.initialProjectState).toEqual(EMPTY_DEFAULT_PROJECT); }); it('should load session from callback when persistentStorage is enabled', async () => { @@ -152,7 +150,7 @@ describe('projectImport', () => { expect(mockState.compiler.codeBuffer).toEqual(new Uint8Array()); }); - it('should dispatch projectLoaded event after loading', () => { + it('should store initial project state after loading', () => { projectImport(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; @@ -161,9 +159,7 @@ describe('projectImport', () => { loadProjectCallback({ project: EMPTY_DEFAULT_PROJECT }); - const dispatchCalls = (mockEvents.dispatch as unknown as MockInstance).mock.calls; - const projectLoadedCall = dispatchCalls.find(call => call[0] === 'projectLoaded'); - expect(projectLoadedCall).toBeDefined(); + expect(mockState.initialProjectState).toEqual(EMPTY_DEFAULT_PROJECT); }); it('should load runtime-ready project with pre-compiled WASM', () => { @@ -286,10 +282,7 @@ describe('projectImport', () => { expect(mockState.callbacks.getProject).toHaveBeenCalledWith('test-slug'); - // Verify project was loaded by checking if projectLoaded event was dispatched - const dispatchCalls = (mockEvents.dispatch as unknown as MockInstance).mock.calls; - const projectLoadedCall = dispatchCalls.find(call => call[0] === 'projectLoaded'); - expect(projectLoadedCall).toBeDefined(); + expect(mockState.initialProjectState).toEqual(mockProject); }); it('should warn when no getProject callback is provided', async () => { diff --git a/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts b/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts index a13271003..a589efb80 100644 --- a/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts +++ b/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts @@ -10,39 +10,43 @@ import encodeUint8ArrayToBase64 from '../pureHelpers/base64/base64Encoder'; import type { State } from '../types'; -const decodeBase64ToUint8ArrayMock = vi.fn((base64: string) => { - const binaryString = atob(base64); - return new Uint8Array(binaryString.split('').map(char => char.charCodeAt(0))); -}); +const { decodeBase64ToUint8ArrayMock, createTypedArrayMock } = vi.hoisted(() => { + const decodeBase64ToUint8ArrayMock = vi.fn((base64: string) => { + const binaryString = atob(base64); + return new Uint8Array(binaryString.split('').map(char => char.charCodeAt(0))); + }); + + const createTypedArrayMock = ( + ctor: new (buffer: ArrayBuffer, byteOffset: number, length: number) => T, + errorMessage: string + ) => { + return vi.fn((base64: string) => { + const uint8Array = decodeBase64ToUint8ArrayMock(base64); -const createTypedArrayMock = ( - ctor: new (buffer: ArrayBuffer, byteOffset: number, length: number) => T, - errorMessage: string -) => { - return vi.fn((base64: string) => { - const uint8Array = decodeBase64ToUint8ArrayMock(base64); + if (uint8Array.byteLength % 4 !== 0) { + throw new Error(errorMessage); + } - if (uint8Array.byteLength % 4 !== 0) { - throw new Error(errorMessage); - } + return new ctor(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength / 4); + }); + }; - return new ctor(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength / 4); - }); -}; + return { decodeBase64ToUint8ArrayMock, createTypedArrayMock }; +}); vi.mock('../pureHelpers/base64/decodeBase64ToUint8Array', () => ({ - decodeBase64ToUint8Array: decodeBase64ToUint8ArrayMock, + default: decodeBase64ToUint8ArrayMock, })); vi.mock('../pureHelpers/base64/decodeBase64ToInt32Array', () => ({ - decodeBase64ToInt32Array: createTypedArrayMock( + default: createTypedArrayMock( Int32Array, 'Invalid base64 data: byte length must be a multiple of 4 to decode as Int32Array' ), })); vi.mock('../pureHelpers/base64/decodeBase64ToFloat32Array', () => ({ - decodeBase64ToFloat32Array: createTypedArrayMock( + default: createTypedArrayMock( Float32Array, 'Invalid base64 data: byte length must be a multiple of 4 to decode as Float32Array' ), @@ -244,15 +248,8 @@ describe('Runtime-ready project functionality', () => { // Set up compiler functionality compiler(store, mockEvents); - // Get the onRecompile callback - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - expect(compileCall).toBeDefined(); - - const onRecompileCallback = compileCall![1]; - - // Trigger recompilation - await onRecompileCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); // Verify pre-compiled WASM was recognized via internal logger expect( @@ -285,13 +282,8 @@ describe('Runtime-ready project functionality', () => { // Set up compiler functionality compiler(store, mockEvents); - // Get the onRecompile callback - const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; - const compileCall = onCalls.find(call => call[0] === 'deleteCodeBlock'); - const onRecompileCallback = compileCall![1]; - - // Trigger recompilation - await onRecompileCallback(); + store.set('graphicHelper.codeBlocks', [...mockState.graphicHelper.codeBlocks]); + await new Promise(resolve => setTimeout(resolve, 0)); // Verify regular compilation was attempted expect(mockCompileCode).toHaveBeenCalled(); From c4c48020f8cf0b09347c4e9f5d5f4bd281a0cf8b Mon Sep 17 00:00:00 2001 From: andorthehood Date: Sat, 3 Jan 2026 19:59:33 +0000 Subject: [PATCH 8/9] fix(editor-state): fix failing tests --- .../src/effects/codeBlocks/creationIndex.test.ts | 7 +++++-- .../editor/packages/editor-state/src/effects/config.ts | 5 +++++ .../packages/editor-state/src/effects/loader.test.ts | 7 +++++-- .../editor-state/src/effects/projectImport.test.ts | 8 ++++++++ .../editor-state/src/effects/runtimeReadyProject.test.ts | 1 + 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/editor/packages/editor-state/src/effects/codeBlocks/creationIndex.test.ts b/packages/editor/packages/editor-state/src/effects/codeBlocks/creationIndex.test.ts index 08cd412bf..73f48abcf 100644 --- a/packages/editor/packages/editor-state/src/effects/codeBlocks/creationIndex.test.ts +++ b/packages/editor/packages/editor-state/src/effects/codeBlocks/creationIndex.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, type MockInstance } from 'vitest'; import createStateManager from '@8f4e/state-manager'; import codeBlockCreator from './codeBlockCreator'; +import graphicHelper from './graphicHelper'; import { flattenProjectForCompiler } from '../compiler'; import projectImport from '../projectImport'; @@ -100,7 +101,8 @@ describe('creationIndex', () => { describe('projectImport', () => { it('should assign creationIndex to code blocks when loading a project', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); + graphicHelper(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -127,7 +129,8 @@ describe('creationIndex', () => { }); it('should reset creationIndex counter when loading a new project', () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); + graphicHelper(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); diff --git a/packages/editor/packages/editor-state/src/effects/config.ts b/packages/editor/packages/editor-state/src/effects/config.ts index 1c28d34b6..96e188e85 100644 --- a/packages/editor/packages/editor-state/src/effects/config.ts +++ b/packages/editor/packages/editor-state/src/effects/config.ts @@ -104,6 +104,11 @@ export default function configEffect(store: StateManager, events: EventDi // Wire up event handlers // rebuildConfig runs BEFORE module compilation because blockTypeUpdater runs first events.on('compileConfig', forceCompileConfig); + store.subscribe('initialProjectState', () => { + if (state.initialProjectState?.compiledConfig) { + applyConfigToState(store, state.initialProjectState.compiledConfig); + } + }); store.subscribe('graphicHelper.codeBlocks', rebuildConfig); store.subscribe('graphicHelper.selectedCodeBlock.code', () => { if (state.graphicHelper.selectedCodeBlock?.blockType !== 'config') { diff --git a/packages/editor/packages/editor-state/src/effects/loader.test.ts b/packages/editor/packages/editor-state/src/effects/loader.test.ts index 931ac029b..c20e9866e 100644 --- a/packages/editor/packages/editor-state/src/effects/loader.test.ts +++ b/packages/editor/packages/editor-state/src/effects/loader.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach, type MockInstance } from 'vitest'; import createStateManager from '@8f4e/state-manager'; +import compiler from './compiler'; import projectImport from './projectImport'; import { createMockState } from '../pureHelpers/testingUtils/testUtils'; @@ -21,7 +22,8 @@ describe('Loader - Project-specific memory configuration', () => { }); it('should use default memory settings when loading project', async () => { - projectImport(store, mockEvents, mockState); + projectImport(store, mockEvents); + compiler(store, mockEvents); // Get the loadProject callback const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; @@ -50,7 +52,8 @@ describe('Loader - Project-specific memory configuration', () => { // Set custom memory first mockState.compiler.compilerOptions.memorySizeBytes = 500 * 65536; - projectImport(store, mockEvents, originalDefault); + projectImport(store, mockEvents); + compiler(store, mockEvents); // Get the loadProject callback const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; diff --git a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts index 66c9f71bf..c2eb5ee93 100644 --- a/packages/editor/packages/editor-state/src/effects/projectImport.test.ts +++ b/packages/editor/packages/editor-state/src/effects/projectImport.test.ts @@ -1,6 +1,8 @@ import { describe, it, expect, beforeEach, vi, type MockInstance } from 'vitest'; import createStateManager from '@8f4e/state-manager'; +import compiler from './compiler'; +import configEffect from './config'; import projectImport from './projectImport'; import { createMockState } from '../pureHelpers/testingUtils/testUtils'; @@ -93,6 +95,7 @@ describe('projectImport', () => { describe('loadProject', () => { it('should use default memory settings when loading project', () => { projectImport(store, mockEvents); + compiler(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -117,6 +120,7 @@ describe('projectImport', () => { mockState.compiler.compilerOptions.memorySizeBytes = 500 * 65536; projectImport(store, mockEvents); + compiler(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -134,6 +138,7 @@ describe('projectImport', () => { it('should reset compiler state when loading a project', () => { projectImport(store, mockEvents); + compiler(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -164,6 +169,7 @@ describe('projectImport', () => { it('should load runtime-ready project with pre-compiled WASM', () => { projectImport(store, mockEvents); + compiler(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -187,6 +193,7 @@ describe('projectImport', () => { it('should handle decoding errors gracefully', () => { projectImport(store, mockEvents); + compiler(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); @@ -317,6 +324,7 @@ describe('projectImport', () => { }; projectImport(store, mockEvents); + configEffect(store, mockEvents); const onCalls = (mockEvents.on as unknown as MockInstance).mock.calls; const loadProjectCall = onCalls.find(call => call[0] === 'loadProject'); diff --git a/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts b/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts index a589efb80..399ce9021 100644 --- a/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts +++ b/packages/editor/packages/editor-state/src/effects/runtimeReadyProject.test.ts @@ -276,6 +276,7 @@ describe('Runtime-ready project functionality', () => { compiledModules: {}, codeBuffer: new Uint8Array([100, 200]), allocatedMemorySize: 1024, + memoryAction: { action: 'reused' }, }); mockState.callbacks.compileCode = mockCompileCode; From 4385250ea4e9a06aa6a5a45af869d073fadb458d Mon Sep 17 00:00:00 2001 From: andorthehood Date: Sat, 3 Jan 2026 20:12:56 +0000 Subject: [PATCH 9/9] chore(editor-state): address pr comments --- .../packages/editor-state/src/effects/compiler.ts | 1 - .../packages/editor-state/src/effects/config.ts | 3 +-- .../src/impureHelpers/config/applyConfigToState.ts | 12 +----------- packages/editor/packages/editor-state/src/types.ts | 8 +++++++- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/editor/packages/editor-state/src/effects/compiler.ts b/packages/editor/packages/editor-state/src/effects/compiler.ts index a287f0406..9bf5871e1 100644 --- a/packages/editor/packages/editor-state/src/effects/compiler.ts +++ b/packages/editor/packages/editor-state/src/effects/compiler.ts @@ -79,7 +79,6 @@ export default async function compiler(store: StateManager, events: Event log(state, 'Compilation succeeded in ' + state.compiler.compilationTime.toFixed(2) + 'ms', 'Compiler'); } catch (error) { - console.log(error); store.set('compiler.isCompiling', false); const errorObject = error as Error & { line?: { lineNumber: number }; diff --git a/packages/editor/packages/editor-state/src/effects/config.ts b/packages/editor/packages/editor-state/src/effects/config.ts index 96e188e85..16e952dcb 100644 --- a/packages/editor/packages/editor-state/src/effects/config.ts +++ b/packages/editor/packages/editor-state/src/effects/config.ts @@ -7,8 +7,7 @@ import deepMergeConfig from '../pureHelpers/config/deepMergeConfig'; import { collectConfigBlocks, ConfigBlockSource } from '../pureHelpers/config/collectConfigBlocks'; import configSchema from '../configSchema'; -import type { ConfigObject } from '../impureHelpers/config/applyConfigToState'; -import type { CodeError, EventDispatcher, State } from '../types'; +import type { CodeError, EventDispatcher, State, ConfigObject } from '../types'; type CompileConfigFn = NonNullable; diff --git a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts index f57eda1af..3fad872c6 100644 --- a/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts +++ b/packages/editor/packages/editor-state/src/impureHelpers/config/applyConfigToState.ts @@ -2,17 +2,7 @@ import { StateManager } from '@8f4e/state-manager'; import isPlainObject from '../../pureHelpers/isPlainObject'; -import type { State, Runtimes } from '../../types'; - -/** - * Interface for the expected config object structure - */ -export interface ConfigObject { - memorySizeBytes?: number; - selectedRuntime?: number; - runtimeSettings?: Runtimes[]; - disableAutoCompilation?: boolean; -} +import type { State, Runtimes, ConfigObject } from '../../types'; /** * Applies the compiled config object to the editor state. diff --git a/packages/editor/packages/editor-state/src/types.ts b/packages/editor/packages/editor-state/src/types.ts index 587c93abf..73497454c 100644 --- a/packages/editor/packages/editor-state/src/types.ts +++ b/packages/editor/packages/editor-state/src/types.ts @@ -1,5 +1,4 @@ import { StateManager } from '@8f4e/state-manager'; -import { ConfigObject } from 'impureHelpers/config/applyConfigToState'; import type { Font, SpriteLookups, ColorScheme } from '@8f4e/sprite-generator'; import type { SpriteLookup, PostProcessEffect } from 'glugglug'; @@ -634,6 +633,13 @@ export interface CodeError { codeBlockId: string | number; } +export interface ConfigObject { + memorySizeBytes?: number; + selectedRuntime?: number; + runtimeSettings?: Runtimes[]; + disableAutoCompilation?: boolean; +} + export interface State { compiler: Compiler; midi: Midi;