From 8d8de1a82a9f814496b2e4cda78ba16810c4b8cf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 18:56:27 +0000 Subject: [PATCH] Persist window size and last camera preset (#69, #70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #69 — the controller body window now reopens at the size it was last left, with a clean 720×540 default for the single-controller mode so the user can frame it once for OBS and just hit "start recording" next time. Size is stored per mode (single/multi) in a small JSON file under userData, saved on resize (debounced) and on close, and sanity-checked on load so a corrupt or out-of-range value falls back to the default. Issue #70 — the selected camera preset (Front/Back/Left/Right/Player/Top) is persisted to localStorage and restored on launch, so reopening the overlay returns to the user's preferred view (e.g. Top) instead of always resetting to Player. The saved value is validated against the known preset list and re-applied to the 3D view once the overlay finishes initializing. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_017NiS2a4jZ877XgftkH1Dd1 --- apps/overlay/electron/main.js | 76 ++++++++++++++++++++++++++++++++++- apps/overlay/src/js/app.js | 25 ++++++++++-- 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/apps/overlay/electron/main.js b/apps/overlay/electron/main.js index 854f9eb..8822f67 100644 --- a/apps/overlay/electron/main.js +++ b/apps/overlay/electron/main.js @@ -16,6 +16,52 @@ if (require('./squirrel-startup')) { let mainWindow = null; let tray = null; let clickThrough = false; + +// ── Persisted window size (issue #69) ── +// The controller body window should reopen at the size it was last left, so +// the user can line it up once for OBS and just hit "start recording" next +// time. We store width/height in a small JSON file under userData, keyed per +// mode (single vs. multi) so the two layouts don't clobber each other. The +// default for the single-controller window is 720×540 — a clean capture size. +const DEFAULT_WINDOW_SIZE = { width: 720, height: 540 }; +const MULTI_WINDOW_SIZE = { width: 1100, height: 520 }; +let windowStatePath = null; // resolved once app is ready (needs userData path) +let saveWindowSizeTimer = null; + +function getWindowStatePath() { + if (!windowStatePath) { + windowStatePath = path.join(app.getPath('userData'), 'window-state.json'); + } + return windowStatePath; +} + +function readWindowState() { + try { + return JSON.parse(fs.readFileSync(getWindowStatePath(), 'utf8')) || {}; + } catch (e) { + return {}; + } +} + +function writeWindowState(state) { + try { + fs.writeFileSync(getWindowStatePath(), JSON.stringify(state, null, 2), 'utf8'); + } catch (e) { + // Non-fatal — size persistence is a convenience, not correctness. + console.log('[window-state] save failed:', e.message); + } +} + +// Sanity-check a saved size before trusting it (corrupt file, off-screen tiny +// values, etc.). Falls back to the mode's default for anything out of range. +function sanitizeSize(saved, fallback) { + const w = saved && Number(saved.width); + const h = saved && Number(saved.height); + return { + width: Number.isFinite(w) && w >= 200 && w <= 8000 ? Math.round(w) : fallback.width, + height: Number.isFinite(h) && h >= 200 && h <= 8000 ? Math.round(h) : fallback.height, + }; +} // Button HUD popout window — at most one open at a time. Tracked so the // main renderer can forward profile-change events to it via IPC without // re-opening or duplicating. @@ -92,9 +138,16 @@ function openInventoryWindow() { function createWindow() { const useMulti = process.env.OVERLAY_MULTI === '1' || process.argv.includes('--multi'); + + // Restore the last-used size for this mode (issue #69), falling back to the + // mode's default (single = 720×540). + const stateKey = useMulti ? 'multi' : 'main'; + const defaultSize = useMulti ? MULTI_WINDOW_SIZE : DEFAULT_WINDOW_SIZE; + const { width, height } = sanitizeSize(readWindowState()[stateKey], defaultSize); + mainWindow = new BrowserWindow({ - width: useMulti ? 1100 : 600, - height: useMulti ? 520 : 400, + width, + height, transparent: true, frame: false, alwaysOnTop: true, @@ -122,6 +175,25 @@ function createWindow() { // DevTools: Cmd+Shift+I to open manually (auto-open triggers Autofill errors) + // Persist the window size as the user resizes (issue #69). Debounced so a + // drag-resize doesn't hammer the disk; also saved on close as a backstop. + const persistSize = () => { + if (!mainWindow || mainWindow.isDestroyed()) return; + if (mainWindow.isMinimized() || mainWindow.isFullScreen()) return; + const [w, h] = mainWindow.getSize(); + const state = readWindowState(); + state[stateKey] = { width: w, height: h }; + writeWindowState(state); + }; + mainWindow.on('resize', () => { + if (saveWindowSizeTimer) clearTimeout(saveWindowSizeTimer); + saveWindowSizeTimer = setTimeout(persistSize, 400); + }); + mainWindow.on('close', () => { + if (saveWindowSizeTimer) { clearTimeout(saveWindowSizeTimer); saveWindowSizeTimer = null; } + persistSize(); + }); + mainWindow.on('closed', () => { mainWindow = null; }); diff --git a/apps/overlay/src/js/app.js b/apps/overlay/src/js/app.js index f1ee728..789ee5a 100644 --- a/apps/overlay/src/js/app.js +++ b/apps/overlay/src/js/app.js @@ -572,6 +572,11 @@ async function init() { overlay.setVisible(false); modelReady = false; } + // Re-apply the saved camera preset (issue #70) now that the 3D overlay + // exists — the module-level selectCameraPreset() ran before `overlay` was + // created, so it only updated the button highlight, not the actual view. + overlay.setCameraPreset(selectedCameraPreset); + // Apply HUD labels for whatever the current profile resolved to, even // when no gamepad was detected at startup — the user might enable the // HUD before plugging in, and the labels should match the model's @@ -2090,12 +2095,22 @@ function applyGreenScreen() { greenScreenToggle.addEventListener('change', applyGreenScreen); greenScreenColorInput.addEventListener('input', applyGreenScreen); -// Camera presets — one selected at a time, used as calibration view -let selectedCameraPreset = 'player'; +// Camera presets — one selected at a time, used as calibration view. +// The last-selected preset is persisted (issue #70) so reopening the overlay +// restores the user's preferred view (e.g. Top) instead of always resetting +// to Player. +const CAMERA_PRESETS = ['front', 'back', 'left', 'right', 'player', 'top']; +const CAMERA_PRESET_KEY = 'overlay:cameraPreset'; +function loadSavedCameraPreset() { + const saved = localStorage.getItem(CAMERA_PRESET_KEY); + return CAMERA_PRESETS.includes(saved) ? saved : 'player'; +} +let selectedCameraPreset = loadSavedCameraPreset(); const cameraPresetBtns = document.querySelectorAll('.camera-presets button'); function selectCameraPreset(preset) { selectedCameraPreset = preset; + try { localStorage.setItem(CAMERA_PRESET_KEY, preset); } catch (e) { /* ignore */ } if (overlay) overlay.setCameraPreset(preset); cameraPresetBtns.forEach(b => { b.classList.toggle('selected', b.dataset.preset === preset); @@ -2106,8 +2121,10 @@ cameraPresetBtns.forEach((btn) => { btn.addEventListener('click', () => selectCameraPreset(btn.dataset.preset)); }); -// Set default selection (overlay not ready yet, just highlights the button) -selectCameraPreset('player'); +// Apply the saved (or default) selection — overlay may not be ready yet, in +// which case this just highlights the button; init() re-applies it to the 3D +// view once the overlay exists. +selectCameraPreset(selectedCameraPreset); // ── Window display toggles (cosmetic only — never affects functionality) ── const showTitleCheck = document.getElementById('show-title');