Fix: Clipping bug

This commit is contained in:
DaZuo0122
2026-02-14 17:55:35 +08:00
parent f2954ad22b
commit f50243ab96
4 changed files with 84 additions and 64 deletions

View File

@@ -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

View File

@@ -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<void> {
async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<number> {
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<voi
const widthChanged = Math.abs(targetWidth - innerSize.width) > 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);
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<PixiPetRenderer | null>(null);
const activePackRef = React.useRef<UiSpritePack | null>(null);
const loadedPackKeyRef = React.useRef<string | null>(null);
const scaleFitRef = React.useRef<number | null>(null);
const effectiveScaleSyncRef = React.useRef<number | null>(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<boolean> => {
const tryFitWindow = async (pack: UiSpritePack, scale: number): Promise<number | null> => {
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<void> => {
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<UiSpritePack>("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(

View File

@@ -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);
}

View File

@@ -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