diff --git a/docs/TAURI_RUNTIME_TESTING.md b/docs/TAURI_RUNTIME_TESTING.md index a856177..921cbd9 100644 --- a/docs/TAURI_RUNTIME_TESTING.md +++ b/docs/TAURI_RUNTIME_TESTING.md @@ -133,6 +133,7 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before ` 4. Change scale via slider and verify: - runtime scale changes immediately - main overlay auto-fits without clipping +- persisted/runtime scale value reflects the effective fitted scale after monitor/window bounds - value persists after restart 5. Toggle `Visible` and verify: - main overlay hide/show behavior diff --git a/frontend/tauri-ui/src/main.tsx b/frontend/tauri-ui/src/main.tsx index d9ad372..46237d5 100644 --- a/frontend/tauri-ui/src/main.tsx +++ b/frontend/tauri-ui/src/main.tsx @@ -59,25 +59,16 @@ function fittedWindowSize( return { width, height }; } -function maxVisibleScale( - pack: UiSpritePack, - workArea: { width: number; height: number } -): number { - const availableWidth = Math.max( - workArea.width - WINDOW_PADDING - WINDOW_WORKAREA_MARGIN, - MIN_WINDOW_SIZE - ); - const availableHeight = Math.max( - workArea.height - WINDOW_PADDING - WINDOW_WORKAREA_MARGIN, - MIN_WINDOW_SIZE - ); - const maxByWidth = availableWidth / Math.max(pack.frame_width, 1); - const maxByHeight = availableHeight / Math.max(pack.frame_height, 1); - const cap = Math.min(maxByWidth, maxByHeight); - return Math.max(SCALE_MIN, Math.min(cap, SCALE_MAX)); +function effectiveScaleForWindowSize(pack: UiSpritePack, width: number, height: number): number { + const availableWidth = Math.max(width - WINDOW_PADDING, MIN_WINDOW_SIZE); + const availableHeight = Math.max(height - WINDOW_PADDING, MIN_WINDOW_SIZE); + const scaleByWidth = availableWidth / Math.max(pack.frame_width, 1); + const scaleByHeight = availableHeight / Math.max(pack.frame_height, 1); + const scale = Math.min(scaleByWidth, scaleByHeight); + return Math.max(SCALE_MIN, Math.min(scale, SCALE_MAX)); } -async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise { +async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise { const window = getCurrentWindow(); const [outerPosition, innerSize] = await Promise.all([window.outerPosition(), window.innerSize()]); const target = fittedWindowSize(pack.frame_width, pack.frame_height, scale); @@ -118,20 +109,24 @@ async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise SIZE_EPSILON; const heightChanged = Math.abs(targetHeight - innerSize.height) > SIZE_EPSILON; - if (!widthChanged && !heightChanged) { - return; + if (widthChanged || heightChanged) { + await window.setSize(new PhysicalSize(targetWidth, targetHeight)); + if (monitor !== null) { + const minX = Math.round(monitor.workArea.position.x); + const minY = Math.round(monitor.workArea.position.y); + const maxX = Math.round( + monitor.workArea.position.x + monitor.workArea.size.width - targetWidth + ); + const maxY = Math.round( + monitor.workArea.position.y + monitor.workArea.size.height - targetHeight + ); + targetX = maxX < minX ? minX : Math.min(Math.max(targetX, minX), maxX); + targetY = maxY < minY ? minY : Math.min(Math.max(targetY, minY), maxY); + } + await window.setPosition(new PhysicalPosition(Math.round(targetX), Math.round(targetY))); } - await window.setSize(new PhysicalSize(targetWidth, targetHeight)); - if (monitor !== null) { - const minX = Math.round(monitor.workArea.position.x); - const minY = Math.round(monitor.workArea.position.y); - const maxX = Math.round(monitor.workArea.position.x + monitor.workArea.size.width - targetWidth); - const maxY = Math.round(monitor.workArea.position.y + monitor.workArea.size.height - targetHeight); - targetX = maxX < minX ? minX : Math.min(Math.max(targetX, minX), maxX); - targetY = maxY < minY ? minY : Math.min(Math.max(targetY, minY), maxY); - } - await window.setPosition(new PhysicalPosition(Math.round(targetX), Math.round(targetY))); + return effectiveScaleForWindowSize(pack, targetWidth, targetHeight); } function MainOverlayWindow(): JSX.Element { @@ -142,7 +137,7 @@ function MainOverlayWindow(): JSX.Element { const rendererRef = React.useRef(null); const activePackRef = React.useRef(null); const loadedPackKeyRef = React.useRef(null); - const scaleFitRef = React.useRef(null); + const effectiveScaleSyncRef = React.useRef(null); const loadingPackRef = React.useRef(false); const mountedRef = React.useRef(false); @@ -166,16 +161,34 @@ function MainOverlayWindow(): JSX.Element { return true; }; - const tryFitWindow = async (pack: UiSpritePack, scale: number): Promise => { + const tryFitWindow = async (pack: UiSpritePack, scale: number): Promise => { try { - await fitWindowForScale(pack, scale); - scaleFitRef.current = scale; - return true; + return await fitWindowForScale(pack, scale); + } catch (err) { + if (mountedRef.current) { + setError(String(err)); + } + return null; + } + }; + + const syncEffectiveScale = async (snapshotScale: number, effectiveScale: number): Promise => { + if (Math.abs(snapshotScale - effectiveScale) < SCALE_EPSILON) { + return; + } + if ( + effectiveScaleSyncRef.current !== null && + Math.abs(effectiveScaleSyncRef.current - effectiveScale) < SCALE_EPSILON + ) { + return; + } + effectiveScaleSyncRef.current = effectiveScale; + try { + await invokeSetScale(effectiveScale); } catch (err) { if (mountedRef.current) { setError(String(err)); } - return false; } }; @@ -183,6 +196,12 @@ function MainOverlayWindow(): JSX.Element { if (!mountedRef.current) { return; } + if ( + effectiveScaleSyncRef.current !== null && + Math.abs(effectiveScaleSyncRef.current - value.scale) < SCALE_EPSILON + ) { + effectiveScaleSyncRef.current = null; + } setSnapshot(value); rendererRef.current?.applySnapshot(value); @@ -196,8 +215,11 @@ function MainOverlayWindow(): JSX.Element { const pack = await invoke("load_active_sprite_pack"); reloaded = await recreateRenderer(pack, value); if (reloaded) { - await tryFitWindow(pack, value.scale); - if (mountedRef.current) { + const effectiveScale = await tryFitWindow(pack, value.scale); + if (effectiveScale !== null) { + await syncEffectiveScale(value.scale, effectiveScale); + } + if (mountedRef.current && effectiveScale !== null) { setError(null); } } @@ -214,14 +236,11 @@ function MainOverlayWindow(): JSX.Element { if (activePackRef.current === null) { return; } - if ( - scaleFitRef.current !== null && - Math.abs(scaleFitRef.current - value.scale) < SCALE_EPSILON - ) { - return; + const effectiveScale = await tryFitWindow(activePackRef.current, value.scale); + if (effectiveScale !== null) { + await syncEffectiveScale(value.scale, effectiveScale); } - const fitApplied = await tryFitWindow(activePackRef.current, value.scale); - if (fitApplied && mountedRef.current) { + if (effectiveScale !== null && mountedRef.current) { setError(null); } }; @@ -443,30 +462,13 @@ function SettingsWindow(): JSX.Element { if (!Number.isFinite(value)) { return; } - let effectiveScale = value; - if (activePack !== null) { - try { - const monitor = await currentMonitor(); - if (monitor !== null) { - effectiveScale = Math.min( - value, - maxVisibleScale(activePack, { - width: monitor.workArea.size.width, - height: monitor.workArea.size.height - }) - ); - } - } catch { - // Keep requested value if monitor query fails. - } - } - const next = await withPending(() => invokeSetScale(effectiveScale)); + const next = await withPending(() => invokeSetScale(value)); if (next === null) { return; } setSettings((prev) => (prev === null ? prev : { ...prev, scale: next.scale })); }, - [activePack, withPending] + [withPending] ); const onVisibleChange = React.useCallback( diff --git a/frontend/tauri-ui/src/renderer/pixi_pet.ts b/frontend/tauri-ui/src/renderer/pixi_pet.ts index 6603e6e..0b9d6ea 100644 --- a/frontend/tauri-ui/src/renderer/pixi_pet.ts +++ b/frontend/tauri-ui/src/renderer/pixi_pet.ts @@ -51,6 +51,8 @@ const HALO_HUE_MIN = 245; const HALO_HUE_MAX = 355; const HALO_SAT_MIN = 0.15; const HALO_VAL_MIN = 0.04; +const RENDER_FIT_PADDING = 16; +const MIN_RENDER_SCALE = 0.01; export class PixiPetRenderer { private app: Application; @@ -426,7 +428,6 @@ export class PixiPetRenderer { this.frameElapsedMs = 0; this.applyFrameTexture(this.currentClip.frames[0] ?? 0); } - this.sprite.scale.set(snapshot.scale); this.layoutSprite(); } @@ -457,6 +458,12 @@ export class PixiPetRenderer { } private layoutSprite(): void { + const availableWidth = Math.max(this.app.renderer.width - RENDER_FIT_PADDING, 1); + const availableHeight = Math.max(this.app.renderer.height - RENDER_FIT_PADDING, 1); + const fitScaleX = availableWidth / Math.max(this.pack.frame_width, 1); + const fitScaleY = availableHeight / Math.max(this.pack.frame_height, 1); + const fitScale = Math.max(Math.min(fitScaleX, fitScaleY), MIN_RENDER_SCALE); + this.sprite.scale.set(fitScale); this.sprite.position.set(this.app.renderer.width / 2, this.app.renderer.height); } diff --git a/issues/issue4.md b/issues/issue4.md index 01c6b4d..e3354e0 100644 --- a/issues/issue4.md +++ b/issues/issue4.md @@ -74,6 +74,8 @@ and scaling becomes ineffective after the error. practical visible bounds and appear clipped. 12. Ferris/demogorgon packaged backgrounds are gradient-magenta (not exact `#FF00FF`), requiring a border-connected magenta-family mask instead of exact-key assumptions. +13. Clipping persisted because sprite rendering scale followed snapshot/requested scale directly + instead of fitting to the actual post-clamp window size. ## Fix Plan @@ -142,6 +144,12 @@ Implemented: 17. `frontend/tauri-ui/src/renderer/pixi_pet.ts` - added deterministic border-connected strong-magenta flood-fill cleanup pass so non-`#FF00FF` gradient backgrounds are removed consistently in packaged ferris/demogorgon atlases. +18. `frontend/tauri-ui/src/main.tsx` +- changed scale flow to window-driven fit semantics: scale request resizes/clamps main window and + then persists effective scale derived from applied window size. +19. `frontend/tauri-ui/src/renderer/pixi_pet.ts` +- renderer sprite scale is now derived from current canvas/window size each layout pass, removing + clipping caused by mismatch between requested scale and bounded window dimensions. ## Verification @@ -181,6 +189,8 @@ Implemented: - `2026-02-14 00:00` - codex - `Fix Implemented` - added integer-safe physical setPosition and HSV magenta cleanup strategy. - `2026-02-14 00:00` - reporter - `In Progress` - reported remaining default clipping and ferris magenta background persistence. - `2026-02-14 00:00` - codex - `Fix Implemented` - tightened work-area scale guard and added border-connected strong-magenta cleanup pass. +- `2026-02-14 00:00` - reporter - `In Progress` - reported clipping still present on `default` and `demogorgon` after prior fixes. +- `2026-02-14 00:00` - codex - `Fix Implemented` - moved tauri scale to window-driven effective-fit persistence and renderer fit-to-window scaling. ## Closure