From f2954ad22b2230f10da2a7a43868edc6b5652902 Mon Sep 17 00:00:00 2001 From: DaZuo0122 <1085701449@qq.com> Date: Sat, 14 Feb 2026 17:31:55 +0800 Subject: [PATCH] Fix: attempt for clipping bug - not fixed yet --- docs/TAURI_RUNTIME_TESTING.md | 3 +- frontend/tauri-ui/src/main.tsx | 82 +++++++++++++++++--- frontend/tauri-ui/src/renderer/pixi_pet.ts | 89 ++++++++++++++++++++++ issues/issue4.md | 12 +++ 4 files changed, 176 insertions(+), 10 deletions(-) diff --git a/docs/TAURI_RUNTIME_TESTING.md b/docs/TAURI_RUNTIME_TESTING.md index 353b6bb..a856177 100644 --- a/docs/TAURI_RUNTIME_TESTING.md +++ b/docs/TAURI_RUNTIME_TESTING.md @@ -113,7 +113,8 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before ` - no frontend runtime exception (including `TypeError`) is allowed - scaling behavior remains responsive after each pack switch 8. Chroma-key quality check: -- verify no visible magenta (`#FF00FF`) fringe remains around sprite edges in normal runtime view +- verify no visible magenta background/fringe remains around sprite edges in normal runtime view, + including non-exact gradient magenta atlas backgrounds (for example `ferris`/`demogorgon`) 9. Scale anchor and bounds check: - repeated scale changes should keep window centered without directional drift - window must remain within current monitor bounds during scale adjustments diff --git a/frontend/tauri-ui/src/main.tsx b/frontend/tauri-ui/src/main.tsx index ca9b775..d9ad372 100644 --- a/frontend/tauri-ui/src/main.tsx +++ b/frontend/tauri-ui/src/main.tsx @@ -25,9 +25,12 @@ type UiSpritePackOption = { }; const WINDOW_PADDING = 16; +const WINDOW_WORKAREA_MARGIN = 80; const MIN_WINDOW_SIZE = 64; const SIZE_EPSILON = 0.5; const SCALE_EPSILON = 0.0001; +const SCALE_MIN = 0.5; +const SCALE_MAX = 3.0; async function invokeSetSpritePack(packIdOrPath: string): Promise { return invoke("set_sprite_pack", { packIdOrPath }); @@ -56,6 +59,24 @@ 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)); +} + async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise { const window = getCurrentWindow(); const [outerPosition, innerSize] = await Promise.all([window.outerPosition(), window.innerSize()]); @@ -81,8 +102,16 @@ async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise(null); const [packs, setPacks] = React.useState([]); + const [activePack, setActivePack] = React.useState(null); const [error, setError] = React.useState(null); const [pending, setPending] = React.useState(false); @@ -319,19 +349,32 @@ function SettingsWindow(): JSX.Element { let mounted = true; Promise.all([ invoke("settings_snapshot"), - invoke("list_sprite_packs") + invoke("list_sprite_packs"), + invoke("load_active_sprite_pack") ]) - .then(async ([snapshot, options]) => { + .then(async ([snapshot, options, pack]) => { if (!mounted) { return; } setSettings(snapshot); setPacks(options); + setActivePack(pack); unlisten = await listen("runtime:snapshot", (event) => { if (!mounted) { return; } const payload = event.payload; + if (payload.active_sprite_pack !== activePack?.id) { + void invoke("load_active_sprite_pack") + .then((nextPack) => { + if (mounted) { + setActivePack(nextPack); + } + }) + .catch(() => { + // Keep existing pack metadata if reload fails. + }); + } setSettings((prev) => { if (prev === null) { return prev; @@ -356,7 +399,7 @@ function SettingsWindow(): JSX.Element { unlisten(); } }; - }, []); + }, [activePack?.id]); const withPending = React.useCallback(async (fn: () => Promise): Promise => { setPending(true); @@ -378,6 +421,10 @@ function SettingsWindow(): JSX.Element { if (next === null) { return; } + const refreshedPack = await withPending(() => invoke("load_active_sprite_pack")); + if (refreshedPack !== null) { + setActivePack(refreshedPack); + } setSettings((prev) => prev === null ? prev @@ -396,13 +443,30 @@ function SettingsWindow(): JSX.Element { if (!Number.isFinite(value)) { return; } - const next = await withPending(() => invokeSetScale(value)); + 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)); if (next === null) { return; } setSettings((prev) => (prev === null ? prev : { ...prev, scale: next.scale })); }, - [withPending] + [activePack, withPending] ); const onVisibleChange = React.useCallback( @@ -457,8 +521,8 @@ function SettingsWindow(): JSX.Element { Scale: {settings.scale.toFixed(2)}x = CONNECTED_SAT_MIN && v >= CONNECTED_VAL_MIN + ) { + isKeyLike[idx] = 1; + continue; + } + if ( + PixiPetRenderer.isStrongMagentaFamily( + data[offset], + data[offset + 1], + data[offset + 2] + ) ) { isKeyLike[idx] = 1; } @@ -212,6 +224,11 @@ export class PixiPetRenderer { (PixiPetRenderer.isHueInRange(h, FALLBACK_HUE_MIN, FALLBACK_HUE_MAX) && s >= FALLBACK_SAT_MIN && v >= FALLBACK_VAL_MIN) || + PixiPetRenderer.isStrongMagentaFamily( + data[offset], + data[offset + 1], + data[offset + 2] + ) || maxDistanceFromHardKey <= 96 ) { data[offset + 3] = 0; @@ -219,6 +236,69 @@ export class PixiPetRenderer { } } + // Deterministic last pass: remove any border-connected magenta-family background. + head = 0; + tail = 0; + removedBg.fill(0); + const enqueueIfMagentaBorder = (x: number, y: number): void => { + const idx = indexFor(x, y); + if (removedBg[idx] === 1) { + return; + } + const offset = channelOffset(idx); + if (data[offset + 3] === 0) { + return; + } + if ( + !PixiPetRenderer.isStrongMagentaFamily( + data[offset], + data[offset + 1], + data[offset + 2] + ) + ) { + return; + } + removedBg[idx] = 1; + queue[tail] = idx; + tail += 1; + }; + + for (let x = 0; x < width; x += 1) { + enqueueIfMagentaBorder(x, 0); + enqueueIfMagentaBorder(x, height - 1); + } + for (let y = 1; y < height - 1; y += 1) { + enqueueIfMagentaBorder(0, y); + enqueueIfMagentaBorder(width - 1, y); + } + + while (head < tail) { + const idx = queue[head]; + head += 1; + const x = idx % width; + const y = Math.floor(idx / width); + if (x > 0) { + enqueueIfMagentaBorder(x - 1, y); + } + if (x + 1 < width) { + enqueueIfMagentaBorder(x + 1, y); + } + if (y > 0) { + enqueueIfMagentaBorder(x, y - 1); + } + if (y + 1 < height) { + enqueueIfMagentaBorder(x, y + 1); + } + } + + for (let idx = 0; idx < pixelCount; idx += 1) { + if (removedBg[idx] !== 1) { + continue; + } + const offset = channelOffset(idx); + data[offset + 3] = 0; + } + for (let y = 0; y < height; y += 1) { for (let x = 0; x < width; x += 1) { const idx = indexFor(x, y); @@ -314,6 +394,15 @@ export class PixiPetRenderer { return hue >= min || hue <= max; } + private static isStrongMagentaFamily(r: number, g: number, b: number): boolean { + const minRb = Math.min(r, b); + return ( + r >= STRONG_MAGENTA_RB_MIN && + b >= STRONG_MAGENTA_RB_MIN && + g + STRONG_MAGENTA_DOMINANCE <= minRb + ); + } + dispose(): void { if (this.disposed) { return; diff --git a/issues/issue4.md b/issues/issue4.md index 3580686..01c6b4d 100644 --- a/issues/issue4.md +++ b/issues/issue4.md @@ -70,6 +70,10 @@ and scaling becomes ineffective after the error. too brittle for packaged atlas variants. 10. Scale-path physical positioning used non-integer coordinates in `setPosition`, triggering runtime arg errors (`expected i32`) and bypassing window fit updates. +11. Monitor-fit cap remained too optimistic for large frame packs, so max scale could still exceed + 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. ## Fix Plan @@ -132,6 +136,12 @@ Implemented: 15. `frontend/tauri-ui/src/renderer/pixi_pet.ts` - switched ferris cleanup to hue/saturation/value magenta-band masking with connected background removal and stronger fallback cleanup. +16. `frontend/tauri-ui/src/main.tsx` +- added stricter monitor work-area guard (`WINDOW_WORKAREA_MARGIN`) in both scale-cap and resize + clamp paths to prevent large-pack clipping at high scales. +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. ## Verification @@ -169,6 +179,8 @@ Implemented: - `2026-02-14 00:00` - codex - `Fix Implemented` - switched to physical-unit resize math, adaptive key detection, and packaging UI refresh enforcement. - `2026-02-14 00:00` - reporter - `In Progress` - reported default clipping, ferris background still present, and set_position float arg error. - `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. ## Closure