Fix: tauri window scaling bug
This commit is contained in:
@@ -6,6 +6,10 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"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-listen",
|
||||||
"core:event:allow-unlisten"
|
"core:event:allow-unlisten"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"]}}
|
{"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"]}}
|
||||||
@@ -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 |
|
| 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` |
|
| 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 |
|
| 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 |
|
| Tauri runtime testing workflow | Implemented | `docs/TAURI_RUNTIME_TESTING.md` defines strict workspace testing; packaged mode pending packaging support |
|
||||||
|
|
||||||
## Next Major Gaps
|
## Next Major Gaps
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ Authoritative workflow: `docs/TAURI_RUNTIME_TESTING.md`.
|
|||||||
8. Run randomized backend API interaction:
|
8. Run randomized backend API interaction:
|
||||||
- `just random-backend-test`
|
- `just random-backend-test`
|
||||||
- verify command traffic remains stable and runtime stays alive.
|
- 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)
|
### Packaged Mode (Required Once Tauri Packaging Exists)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
|||||||
2. Verify sprite renders in the tauri window.
|
2. Verify sprite renders in the tauri window.
|
||||||
3. Verify animation advances over time.
|
3. Verify animation advances over time.
|
||||||
4. Send `PlayAnimation` command and verify clip switch is reflected.
|
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:
|
6. Verify missing animation fallback:
|
||||||
- unknown animation name falls back to `idle` or first available clip.
|
- unknown animation name falls back to `idle` or first available clip.
|
||||||
7. Verify sprite-pack loading:
|
7. Verify sprite-pack loading:
|
||||||
|
|||||||
@@ -2,19 +2,60 @@ import React from "react";
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
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 { PixiPetRenderer, type UiSpritePack, type UiSnapshot } from "./renderer/pixi_pet";
|
||||||
import "./styles.css";
|
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<void> {
|
||||||
|
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 {
|
function App(): JSX.Element {
|
||||||
const [snapshot, setSnapshot] = React.useState<UiSnapshot | null>(null);
|
const [snapshot, setSnapshot] = React.useState<UiSnapshot | null>(null);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const rendererRef = React.useRef<PixiPetRenderer | null>(null);
|
const rendererRef = React.useRef<PixiPetRenderer | null>(null);
|
||||||
|
const scaleFitRef = React.useRef<number | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let unlisten: null | (() => void) = null;
|
let unlisten: null | (() => void) = null;
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
let activePack: UiSpritePack | null = null;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
invoke<UiSpritePack>("load_active_sprite_pack"),
|
invoke<UiSpritePack>("load_active_sprite_pack"),
|
||||||
invoke<UiSnapshot>("current_state")
|
invoke<UiSnapshot>("current_state")
|
||||||
@@ -23,6 +64,7 @@ function App(): JSX.Element {
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
activePack = pack;
|
||||||
setSnapshot(initialSnapshot);
|
setSnapshot(initialSnapshot);
|
||||||
if (hostRef.current !== null) {
|
if (hostRef.current !== null) {
|
||||||
rendererRef.current = await PixiPetRenderer.create(
|
rendererRef.current = await PixiPetRenderer.create(
|
||||||
@@ -31,6 +73,9 @@ function App(): JSX.Element {
|
|||||||
initialSnapshot
|
initialSnapshot
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
scaleFitRef.current = initialSnapshot.scale;
|
||||||
|
await fitWindowForScale(pack, initialSnapshot.scale);
|
||||||
|
|
||||||
unlisten = await listen<UiSnapshot>("runtime:snapshot", (event) => {
|
unlisten = await listen<UiSnapshot>("runtime:snapshot", (event) => {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -38,6 +83,21 @@ function App(): JSX.Element {
|
|||||||
const value = event.payload;
|
const value = event.payload;
|
||||||
setSnapshot(value);
|
setSnapshot(value);
|
||||||
rendererRef.current?.applySnapshot(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) => {
|
.catch((err) => {
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ export class PixiPetRenderer {
|
|||||||
}
|
}
|
||||||
const sprite = new Sprite();
|
const sprite = new Sprite();
|
||||||
sprite.anchor.set(pack.anchor.x, pack.anchor.y);
|
sprite.anchor.set(pack.anchor.x, pack.anchor.y);
|
||||||
sprite.position.set(app.renderer.width / 2, app.renderer.height);
|
|
||||||
app.stage.addChild(sprite);
|
app.stage.addChild(sprite);
|
||||||
|
|
||||||
const renderer = new PixiPetRenderer(app, sprite, pack, baseTexture);
|
const renderer = new PixiPetRenderer(app, sprite, pack, baseTexture);
|
||||||
|
renderer.layoutSprite();
|
||||||
renderer.applySnapshot(snapshot);
|
renderer.applySnapshot(snapshot);
|
||||||
renderer.startTicker();
|
renderer.startTicker();
|
||||||
return renderer;
|
return renderer;
|
||||||
@@ -145,10 +145,12 @@ export class PixiPetRenderer {
|
|||||||
this.applyFrameTexture(this.currentClip.frames[0] ?? 0);
|
this.applyFrameTexture(this.currentClip.frames[0] ?? 0);
|
||||||
}
|
}
|
||||||
this.sprite.scale.set(snapshot.scale);
|
this.sprite.scale.set(snapshot.scale);
|
||||||
|
this.layoutSprite();
|
||||||
}
|
}
|
||||||
|
|
||||||
private startTicker(): void {
|
private startTicker(): void {
|
||||||
this.app.ticker.add((ticker) => {
|
this.app.ticker.add((ticker) => {
|
||||||
|
this.layoutSprite();
|
||||||
const frameMs = 1000 / Math.max(this.currentClip.fps, 1);
|
const frameMs = 1000 / Math.max(this.currentClip.fps, 1);
|
||||||
this.frameElapsedMs += ticker.deltaMS;
|
this.frameElapsedMs += ticker.deltaMS;
|
||||||
if (this.frameElapsedMs < frameMs) {
|
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 {
|
private resolveClip(name: string): UiAnimationClip {
|
||||||
return (
|
return (
|
||||||
this.animationMap.get(name) ??
|
this.animationMap.get(name) ??
|
||||||
|
|||||||
Reference in New Issue
Block a user