MVP tauri frontend - Windows #1
@@ -133,6 +133,7 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
|
|||||||
4. Change scale via slider and verify:
|
4. Change scale via slider and verify:
|
||||||
- runtime scale changes immediately
|
- runtime scale changes immediately
|
||||||
- main overlay auto-fits without clipping
|
- main overlay auto-fits without clipping
|
||||||
|
- persisted/runtime scale value reflects the effective fitted scale after monitor/window bounds
|
||||||
- value persists after restart
|
- value persists after restart
|
||||||
5. Toggle `Visible` and verify:
|
5. Toggle `Visible` and verify:
|
||||||
- main overlay hide/show behavior
|
- main overlay hide/show behavior
|
||||||
|
|||||||
@@ -59,25 +59,16 @@ function fittedWindowSize(
|
|||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
function maxVisibleScale(
|
function effectiveScaleForWindowSize(pack: UiSpritePack, width: number, height: number): number {
|
||||||
pack: UiSpritePack,
|
const availableWidth = Math.max(width - WINDOW_PADDING, MIN_WINDOW_SIZE);
|
||||||
workArea: { width: number; height: number }
|
const availableHeight = Math.max(height - WINDOW_PADDING, MIN_WINDOW_SIZE);
|
||||||
): number {
|
const scaleByWidth = availableWidth / Math.max(pack.frame_width, 1);
|
||||||
const availableWidth = Math.max(
|
const scaleByHeight = availableHeight / Math.max(pack.frame_height, 1);
|
||||||
workArea.width - WINDOW_PADDING - WINDOW_WORKAREA_MARGIN,
|
const scale = Math.min(scaleByWidth, scaleByHeight);
|
||||||
MIN_WINDOW_SIZE
|
return Math.max(SCALE_MIN, Math.min(scale, SCALE_MAX));
|
||||||
);
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<void> {
|
async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<number> {
|
||||||
const window = getCurrentWindow();
|
const window = getCurrentWindow();
|
||||||
const [outerPosition, innerSize] = await Promise.all([window.outerPosition(), window.innerSize()]);
|
const [outerPosition, innerSize] = await Promise.all([window.outerPosition(), window.innerSize()]);
|
||||||
const target = fittedWindowSize(pack.frame_width, pack.frame_height, scale);
|
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 widthChanged = Math.abs(targetWidth - innerSize.width) > SIZE_EPSILON;
|
||||||
const heightChanged = Math.abs(targetHeight - innerSize.height) > SIZE_EPSILON;
|
const heightChanged = Math.abs(targetHeight - innerSize.height) > SIZE_EPSILON;
|
||||||
if (!widthChanged && !heightChanged) {
|
if (widthChanged || heightChanged) {
|
||||||
return;
|
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));
|
return effectiveScaleForWindowSize(pack, 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)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function MainOverlayWindow(): JSX.Element {
|
function MainOverlayWindow(): JSX.Element {
|
||||||
@@ -142,7 +137,7 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
const rendererRef = React.useRef<PixiPetRenderer | null>(null);
|
const rendererRef = React.useRef<PixiPetRenderer | null>(null);
|
||||||
const activePackRef = React.useRef<UiSpritePack | null>(null);
|
const activePackRef = React.useRef<UiSpritePack | null>(null);
|
||||||
const loadedPackKeyRef = React.useRef<string | 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 loadingPackRef = React.useRef(false);
|
||||||
const mountedRef = React.useRef(false);
|
const mountedRef = React.useRef(false);
|
||||||
|
|
||||||
@@ -166,16 +161,34 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryFitWindow = async (pack: UiSpritePack, scale: number): Promise<boolean> => {
|
const tryFitWindow = async (pack: UiSpritePack, scale: number): Promise<number | null> => {
|
||||||
try {
|
try {
|
||||||
await fitWindowForScale(pack, scale);
|
return await fitWindowForScale(pack, scale);
|
||||||
scaleFitRef.current = scale;
|
} catch (err) {
|
||||||
return true;
|
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) {
|
} catch (err) {
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
setError(String(err));
|
setError(String(err));
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,6 +196,12 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
if (!mountedRef.current) {
|
if (!mountedRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
effectiveScaleSyncRef.current !== null &&
|
||||||
|
Math.abs(effectiveScaleSyncRef.current - value.scale) < SCALE_EPSILON
|
||||||
|
) {
|
||||||
|
effectiveScaleSyncRef.current = null;
|
||||||
|
}
|
||||||
setSnapshot(value);
|
setSnapshot(value);
|
||||||
rendererRef.current?.applySnapshot(value);
|
rendererRef.current?.applySnapshot(value);
|
||||||
|
|
||||||
@@ -196,8 +215,11 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
const pack = await invoke<UiSpritePack>("load_active_sprite_pack");
|
const pack = await invoke<UiSpritePack>("load_active_sprite_pack");
|
||||||
reloaded = await recreateRenderer(pack, value);
|
reloaded = await recreateRenderer(pack, value);
|
||||||
if (reloaded) {
|
if (reloaded) {
|
||||||
await tryFitWindow(pack, value.scale);
|
const effectiveScale = await tryFitWindow(pack, value.scale);
|
||||||
if (mountedRef.current) {
|
if (effectiveScale !== null) {
|
||||||
|
await syncEffectiveScale(value.scale, effectiveScale);
|
||||||
|
}
|
||||||
|
if (mountedRef.current && effectiveScale !== null) {
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,14 +236,11 @@ function MainOverlayWindow(): JSX.Element {
|
|||||||
if (activePackRef.current === null) {
|
if (activePackRef.current === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
const effectiveScale = await tryFitWindow(activePackRef.current, value.scale);
|
||||||
scaleFitRef.current !== null &&
|
if (effectiveScale !== null) {
|
||||||
Math.abs(scaleFitRef.current - value.scale) < SCALE_EPSILON
|
await syncEffectiveScale(value.scale, effectiveScale);
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const fitApplied = await tryFitWindow(activePackRef.current, value.scale);
|
if (effectiveScale !== null && mountedRef.current) {
|
||||||
if (fitApplied && mountedRef.current) {
|
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -443,30 +462,13 @@ function SettingsWindow(): JSX.Element {
|
|||||||
if (!Number.isFinite(value)) {
|
if (!Number.isFinite(value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let effectiveScale = value;
|
const next = await withPending(() => invokeSetScale(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));
|
|
||||||
if (next === null) {
|
if (next === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSettings((prev) => (prev === null ? prev : { ...prev, scale: next.scale }));
|
setSettings((prev) => (prev === null ? prev : { ...prev, scale: next.scale }));
|
||||||
},
|
},
|
||||||
[activePack, withPending]
|
[withPending]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onVisibleChange = React.useCallback(
|
const onVisibleChange = React.useCallback(
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ const HALO_HUE_MIN = 245;
|
|||||||
const HALO_HUE_MAX = 355;
|
const HALO_HUE_MAX = 355;
|
||||||
const HALO_SAT_MIN = 0.15;
|
const HALO_SAT_MIN = 0.15;
|
||||||
const HALO_VAL_MIN = 0.04;
|
const HALO_VAL_MIN = 0.04;
|
||||||
|
const RENDER_FIT_PADDING = 16;
|
||||||
|
const MIN_RENDER_SCALE = 0.01;
|
||||||
|
|
||||||
export class PixiPetRenderer {
|
export class PixiPetRenderer {
|
||||||
private app: Application;
|
private app: Application;
|
||||||
@@ -426,7 +428,6 @@ export class PixiPetRenderer {
|
|||||||
this.frameElapsedMs = 0;
|
this.frameElapsedMs = 0;
|
||||||
this.applyFrameTexture(this.currentClip.frames[0] ?? 0);
|
this.applyFrameTexture(this.currentClip.frames[0] ?? 0);
|
||||||
}
|
}
|
||||||
this.sprite.scale.set(snapshot.scale);
|
|
||||||
this.layoutSprite();
|
this.layoutSprite();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +458,12 @@ export class PixiPetRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private layoutSprite(): void {
|
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);
|
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.
|
practical visible bounds and appear clipped.
|
||||||
12. Ferris/demogorgon packaged backgrounds are gradient-magenta (not exact `#FF00FF`), requiring a
|
12. Ferris/demogorgon packaged backgrounds are gradient-magenta (not exact `#FF00FF`), requiring a
|
||||||
border-connected magenta-family mask instead of exact-key assumptions.
|
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
|
## Fix Plan
|
||||||
|
|
||||||
@@ -142,6 +144,12 @@ Implemented:
|
|||||||
17. `frontend/tauri-ui/src/renderer/pixi_pet.ts`
|
17. `frontend/tauri-ui/src/renderer/pixi_pet.ts`
|
||||||
- added deterministic border-connected strong-magenta flood-fill cleanup pass so non-`#FF00FF`
|
- added deterministic border-connected strong-magenta flood-fill cleanup pass so non-`#FF00FF`
|
||||||
gradient backgrounds are removed consistently in packaged ferris/demogorgon atlases.
|
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
|
## 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` - 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` - 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` - 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
|
## Closure
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user