Fix: Clipping bug
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
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<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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user