From 8e79bd98e50de9b4cf9e573bf5b02082f30bc8b6 Mon Sep 17 00:00:00 2001 From: DaZuo0122 <1085701449@qq.com> Date: Fri, 13 Feb 2026 17:25:28 +0800 Subject: [PATCH] Fix: tauri window scaling bug --- crates/sprimo-tauri/capabilities/default.json | 4 ++ .../gen/schemas/capabilities.json | 2 +- docs/IMPLEMENTATION_STATUS.md | 2 +- docs/RELEASE_TESTING.md | 3 + docs/TAURI_RUNTIME_TESTING.md | 4 +- frontend/tauri-ui/src/main.tsx | 62 ++++++++++++++++++- frontend/tauri-ui/src/renderer/pixi_pet.ts | 8 ++- 7 files changed, 80 insertions(+), 5 deletions(-) diff --git a/crates/sprimo-tauri/capabilities/default.json b/crates/sprimo-tauri/capabilities/default.json index d4d74c2..58d1063 100644 --- a/crates/sprimo-tauri/capabilities/default.json +++ b/crates/sprimo-tauri/capabilities/default.json @@ -6,6 +6,10 @@ "permissions": [ "core:default", "core:window:allow-start-dragging", + "core:window:allow-set-size", + "core:window:allow-set-position", + "core:window:allow-inner-size", + "core:window:allow-outer-position", "core:event:allow-listen", "core:event:allow-unlisten" ] diff --git a/crates/sprimo-tauri/gen/schemas/capabilities.json b/crates/sprimo-tauri/gen/schemas/capabilities.json index 04b215a..9f6178b 100644 --- a/crates/sprimo-tauri/gen/schemas/capabilities.json +++ b/crates/sprimo-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:window:allow-start-dragging","core:event:allow-listen","core:event:allow-unlisten"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Default capability for sprimo-tauri main window runtime APIs.","local":true,"windows":["*"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-set-size","core:window:allow-set-position","core:window:allow-inner-size","core:window:allow-outer-position","core:event:allow-listen","core:event:allow-unlisten"]}} \ No newline at end of file diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 2a6383a..94aebc2 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -19,7 +19,7 @@ Date: 2026-02-12 | Random backend API tester | Implemented | `scripts/random_backend_tester.py` with `just random-backend-test` and strict variant | | QA/documentation workflow | Implemented | `docs/QA_WORKFLOW.md`, issue/evidence templates, and `scripts/qa_validate.py` with `just qa-validate` | | Shared runtime core | In progress | `sprimo-runtime-core` extracted with shared config/snapshot/API startup and command application | -| Tauri alternative frontend | In progress | `sprimo-tauri` now runs runtime-core/API + PixiJS sprite rendering shell, parity work remains | +| Tauri alternative frontend | In progress | `sprimo-tauri` now runs runtime-core/API + PixiJS sprite rendering shell; scale updates now auto-fit window to avoid top clipping | | Tauri runtime testing workflow | Implemented | `docs/TAURI_RUNTIME_TESTING.md` defines strict workspace testing; packaged mode pending packaging support | ## Next Major Gaps diff --git a/docs/RELEASE_TESTING.md b/docs/RELEASE_TESTING.md index 3dd3a67..b882659 100644 --- a/docs/RELEASE_TESTING.md +++ b/docs/RELEASE_TESTING.md @@ -121,6 +121,9 @@ Authoritative workflow: `docs/TAURI_RUNTIME_TESTING.md`. 8. Run randomized backend API interaction: - `just random-backend-test` - verify command traffic remains stable and runtime stays alive. +9. Verify scale-fit behavior in tauri runtime: +- send `SetTransform.scale` values above `1.0` +- confirm full sprite remains visible and window auto-resizes without top clipping ### Packaged Mode (Required Once Tauri Packaging Exists) diff --git a/docs/TAURI_RUNTIME_TESTING.md b/docs/TAURI_RUNTIME_TESTING.md index f06b000..fb8b155 100644 --- a/docs/TAURI_RUNTIME_TESTING.md +++ b/docs/TAURI_RUNTIME_TESTING.md @@ -62,7 +62,9 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before ` 2. Verify sprite renders in the tauri window. 3. Verify animation advances over time. 4. Send `PlayAnimation` command and verify clip switch is reflected. -5. Send `SetTransform.scale` and verify rendered sprite scale changes. +5. Send `SetTransform.scale` and verify rendered sprite scale changes without clipping: +- at `scale >= 1.0`, full sprite remains visible (no missing upper region) +- runtime auto-fits window size to sprite frame size and keeps bottom-center visually stable 6. Verify missing animation fallback: - unknown animation name falls back to `idle` or first available clip. 7. Verify sprite-pack loading: diff --git a/frontend/tauri-ui/src/main.tsx b/frontend/tauri-ui/src/main.tsx index cae4281..ff79fba 100644 --- a/frontend/tauri-ui/src/main.tsx +++ b/frontend/tauri-ui/src/main.tsx @@ -2,19 +2,60 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { LogicalPosition, LogicalSize, getCurrentWindow } from "@tauri-apps/api/window"; import { PixiPetRenderer, type UiSpritePack, type UiSnapshot } from "./renderer/pixi_pet"; import "./styles.css"; +const WINDOW_PADDING = 16; +const MIN_WINDOW_SIZE = 64; +const SIZE_EPSILON = 0.5; +const SCALE_EPSILON = 0.0001; + +function fittedWindowSize( + frameWidth: number, + frameHeight: number, + scale: number +): { width: number; height: number } { + const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 1; + const width = Math.max(frameWidth * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE); + const height = Math.max(frameHeight * safeScale + WINDOW_PADDING, MIN_WINDOW_SIZE); + return { width, height }; +} + +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); + const widthChanged = Math.abs(target.width - innerSize.width) > SIZE_EPSILON; + const heightChanged = Math.abs(target.height - innerSize.height) > SIZE_EPSILON; + if (!widthChanged && !heightChanged) { + return; + } + + const deltaWidth = target.width - innerSize.width; + const deltaHeight = target.height - innerSize.height; + const targetX = outerPosition.x - deltaWidth / 2; + const targetY = outerPosition.y - deltaHeight; + + await window.setSize(new LogicalSize(target.width, target.height)); + await window.setPosition(new LogicalPosition(targetX, targetY)); +} + function App(): JSX.Element { const [snapshot, setSnapshot] = React.useState(null); const [error, setError] = React.useState(null); const hostRef = React.useRef(null); const rendererRef = React.useRef(null); + const scaleFitRef = React.useRef(null); React.useEffect(() => { let unlisten: null | (() => void) = null; let mounted = true; + let activePack: UiSpritePack | null = null; Promise.all([ invoke("load_active_sprite_pack"), invoke("current_state") @@ -23,6 +64,7 @@ function App(): JSX.Element { if (!mounted) { return; } + activePack = pack; setSnapshot(initialSnapshot); if (hostRef.current !== null) { rendererRef.current = await PixiPetRenderer.create( @@ -31,6 +73,9 @@ function App(): JSX.Element { initialSnapshot ); } + scaleFitRef.current = initialSnapshot.scale; + await fitWindowForScale(pack, initialSnapshot.scale); + unlisten = await listen("runtime:snapshot", (event) => { if (!mounted) { return; @@ -38,6 +83,21 @@ function App(): JSX.Element { const value = event.payload; setSnapshot(value); rendererRef.current?.applySnapshot(value); + if (activePack === null) { + return; + } + if ( + scaleFitRef.current !== null && + Math.abs(scaleFitRef.current - value.scale) < SCALE_EPSILON + ) { + return; + } + scaleFitRef.current = value.scale; + void fitWindowForScale(activePack, value.scale).catch((err) => { + if (mounted) { + setError(String(err)); + } + }); }); }) .catch((err) => { diff --git a/frontend/tauri-ui/src/renderer/pixi_pet.ts b/frontend/tauri-ui/src/renderer/pixi_pet.ts index d7a1cab..c8c7fc0 100644 --- a/frontend/tauri-ui/src/renderer/pixi_pet.ts +++ b/frontend/tauri-ui/src/renderer/pixi_pet.ts @@ -75,10 +75,10 @@ export class PixiPetRenderer { } const sprite = new Sprite(); sprite.anchor.set(pack.anchor.x, pack.anchor.y); - sprite.position.set(app.renderer.width / 2, app.renderer.height); app.stage.addChild(sprite); const renderer = new PixiPetRenderer(app, sprite, pack, baseTexture); + renderer.layoutSprite(); renderer.applySnapshot(snapshot); renderer.startTicker(); return renderer; @@ -145,10 +145,12 @@ export class PixiPetRenderer { this.applyFrameTexture(this.currentClip.frames[0] ?? 0); } this.sprite.scale.set(snapshot.scale); + this.layoutSprite(); } private startTicker(): void { this.app.ticker.add((ticker) => { + this.layoutSprite(); const frameMs = 1000 / Math.max(this.currentClip.fps, 1); this.frameElapsedMs += ticker.deltaMS; if (this.frameElapsedMs < frameMs) { @@ -169,6 +171,10 @@ export class PixiPetRenderer { }); } + private layoutSprite(): void { + this.sprite.position.set(this.app.renderer.width / 2, this.app.renderer.height); + } + private resolveClip(name: string): UiAnimationClip { return ( this.animationMap.get(name) ??