Fix: attempt for clipping bug - not fixed yet

This commit is contained in:
DaZuo0122
2026-02-14 17:31:55 +08:00
parent 1fa7080210
commit f2954ad22b
4 changed files with 176 additions and 10 deletions

View File

@@ -113,7 +113,8 @@ An issue touching Tauri runtime behaviors must satisfy all requirements before `
- no frontend runtime exception (including `TypeError`) is allowed - no frontend runtime exception (including `TypeError`) is allowed
- scaling behavior remains responsive after each pack switch - scaling behavior remains responsive after each pack switch
8. Chroma-key quality check: 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: 9. Scale anchor and bounds check:
- repeated scale changes should keep window centered without directional drift - repeated scale changes should keep window centered without directional drift
- window must remain within current monitor bounds during scale adjustments - window must remain within current monitor bounds during scale adjustments

View File

@@ -25,9 +25,12 @@ type UiSpritePackOption = {
}; };
const WINDOW_PADDING = 16; const WINDOW_PADDING = 16;
const WINDOW_WORKAREA_MARGIN = 80;
const MIN_WINDOW_SIZE = 64; const MIN_WINDOW_SIZE = 64;
const SIZE_EPSILON = 0.5; const SIZE_EPSILON = 0.5;
const SCALE_EPSILON = 0.0001; const SCALE_EPSILON = 0.0001;
const SCALE_MIN = 0.5;
const SCALE_MAX = 3.0;
async function invokeSetSpritePack(packIdOrPath: string): Promise<UiSnapshot> { async function invokeSetSpritePack(packIdOrPath: string): Promise<UiSnapshot> {
return invoke<UiSnapshot>("set_sprite_pack", { packIdOrPath }); return invoke<UiSnapshot>("set_sprite_pack", { packIdOrPath });
@@ -56,6 +59,24 @@ function fittedWindowSize(
return { width, height }; 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<void> { async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<void> {
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()]);
@@ -81,8 +102,16 @@ async function fitWindowForScale(pack: UiSpritePack, scale: number): Promise<voi
} }
if (monitor !== null) { if (monitor !== null) {
targetWidth = Math.min(targetWidth, monitor.workArea.size.width); const widthCap = Math.max(
targetHeight = Math.min(targetHeight, monitor.workArea.size.height); monitor.workArea.size.width - WINDOW_WORKAREA_MARGIN,
MIN_WINDOW_SIZE
);
const heightCap = Math.max(
monitor.workArea.size.height - WINDOW_WORKAREA_MARGIN,
MIN_WINDOW_SIZE
);
targetWidth = Math.min(targetWidth, widthCap);
targetHeight = Math.min(targetHeight, heightCap);
targetX = centerX - targetWidth / 2; targetX = centerX - targetWidth / 2;
targetY = centerY - targetHeight / 2; targetY = centerY - targetHeight / 2;
} }
@@ -311,6 +340,7 @@ function MainOverlayWindow(): JSX.Element {
function SettingsWindow(): JSX.Element { function SettingsWindow(): JSX.Element {
const [settings, setSettings] = React.useState<UiSettingsSnapshot | null>(null); const [settings, setSettings] = React.useState<UiSettingsSnapshot | null>(null);
const [packs, setPacks] = React.useState<UiSpritePackOption[]>([]); const [packs, setPacks] = React.useState<UiSpritePackOption[]>([]);
const [activePack, setActivePack] = React.useState<UiSpritePack | null>(null);
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
const [pending, setPending] = React.useState(false); const [pending, setPending] = React.useState(false);
@@ -319,19 +349,32 @@ function SettingsWindow(): JSX.Element {
let mounted = true; let mounted = true;
Promise.all([ Promise.all([
invoke<UiSettingsSnapshot>("settings_snapshot"), invoke<UiSettingsSnapshot>("settings_snapshot"),
invoke<UiSpritePackOption[]>("list_sprite_packs") invoke<UiSpritePackOption[]>("list_sprite_packs"),
invoke<UiSpritePack>("load_active_sprite_pack")
]) ])
.then(async ([snapshot, options]) => { .then(async ([snapshot, options, pack]) => {
if (!mounted) { if (!mounted) {
return; return;
} }
setSettings(snapshot); setSettings(snapshot);
setPacks(options); setPacks(options);
setActivePack(pack);
unlisten = await listen<UiSnapshot>("runtime:snapshot", (event) => { unlisten = await listen<UiSnapshot>("runtime:snapshot", (event) => {
if (!mounted) { if (!mounted) {
return; return;
} }
const payload = event.payload; const payload = event.payload;
if (payload.active_sprite_pack !== activePack?.id) {
void invoke<UiSpritePack>("load_active_sprite_pack")
.then((nextPack) => {
if (mounted) {
setActivePack(nextPack);
}
})
.catch(() => {
// Keep existing pack metadata if reload fails.
});
}
setSettings((prev) => { setSettings((prev) => {
if (prev === null) { if (prev === null) {
return prev; return prev;
@@ -356,7 +399,7 @@ function SettingsWindow(): JSX.Element {
unlisten(); unlisten();
} }
}; };
}, []); }, [activePack?.id]);
const withPending = React.useCallback(async <T,>(fn: () => Promise<T>): Promise<T | null> => { const withPending = React.useCallback(async <T,>(fn: () => Promise<T>): Promise<T | null> => {
setPending(true); setPending(true);
@@ -378,6 +421,10 @@ function SettingsWindow(): JSX.Element {
if (next === null) { if (next === null) {
return; return;
} }
const refreshedPack = await withPending(() => invoke<UiSpritePack>("load_active_sprite_pack"));
if (refreshedPack !== null) {
setActivePack(refreshedPack);
}
setSettings((prev) => setSettings((prev) =>
prev === null prev === null
? prev ? prev
@@ -396,13 +443,30 @@ function SettingsWindow(): JSX.Element {
if (!Number.isFinite(value)) { if (!Number.isFinite(value)) {
return; 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) { if (next === null) {
return; return;
} }
setSettings((prev) => (prev === null ? prev : { ...prev, scale: next.scale })); setSettings((prev) => (prev === null ? prev : { ...prev, scale: next.scale }));
}, },
[withPending] [activePack, withPending]
); );
const onVisibleChange = React.useCallback( const onVisibleChange = React.useCallback(
@@ -457,8 +521,8 @@ function SettingsWindow(): JSX.Element {
<span>Scale: {settings.scale.toFixed(2)}x</span> <span>Scale: {settings.scale.toFixed(2)}x</span>
<input <input
type="range" type="range"
min={0.5} min={SCALE_MIN}
max={3.0} max={SCALE_MAX}
step={0.05} step={0.05}
value={settings.scale} value={settings.scale}
disabled={pending} disabled={pending}

View File

@@ -45,6 +45,8 @@ const FALLBACK_HUE_MIN = 255;
const FALLBACK_HUE_MAX = 355; const FALLBACK_HUE_MAX = 355;
const FALLBACK_SAT_MIN = 0.15; const FALLBACK_SAT_MIN = 0.15;
const FALLBACK_VAL_MIN = 0.04; const FALLBACK_VAL_MIN = 0.04;
const STRONG_MAGENTA_RB_MIN = 72;
const STRONG_MAGENTA_DOMINANCE = 24;
const HALO_HUE_MIN = 245; 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;
@@ -148,6 +150,16 @@ export class PixiPetRenderer {
PixiPetRenderer.isHueInRange(h, CONNECTED_HUE_MIN, CONNECTED_HUE_MAX) && PixiPetRenderer.isHueInRange(h, CONNECTED_HUE_MIN, CONNECTED_HUE_MAX) &&
s >= CONNECTED_SAT_MIN && s >= CONNECTED_SAT_MIN &&
v >= CONNECTED_VAL_MIN v >= CONNECTED_VAL_MIN
) {
isKeyLike[idx] = 1;
continue;
}
if (
PixiPetRenderer.isStrongMagentaFamily(
data[offset],
data[offset + 1],
data[offset + 2]
)
) { ) {
isKeyLike[idx] = 1; isKeyLike[idx] = 1;
} }
@@ -212,6 +224,11 @@ export class PixiPetRenderer {
(PixiPetRenderer.isHueInRange(h, FALLBACK_HUE_MIN, FALLBACK_HUE_MAX) && (PixiPetRenderer.isHueInRange(h, FALLBACK_HUE_MIN, FALLBACK_HUE_MAX) &&
s >= FALLBACK_SAT_MIN && s >= FALLBACK_SAT_MIN &&
v >= FALLBACK_VAL_MIN) || v >= FALLBACK_VAL_MIN) ||
PixiPetRenderer.isStrongMagentaFamily(
data[offset],
data[offset + 1],
data[offset + 2]
) ||
maxDistanceFromHardKey <= 96 maxDistanceFromHardKey <= 96
) { ) {
data[offset + 3] = 0; 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 y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) { for (let x = 0; x < width; x += 1) {
const idx = indexFor(x, y); const idx = indexFor(x, y);
@@ -314,6 +394,15 @@ export class PixiPetRenderer {
return hue >= min || hue <= max; 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 { dispose(): void {
if (this.disposed) { if (this.disposed) {
return; return;

View File

@@ -70,6 +70,10 @@ and scaling becomes ineffective after the error.
too brittle for packaged atlas variants. too brittle for packaged atlas variants.
10. Scale-path physical positioning used non-integer coordinates in `setPosition`, triggering 10. Scale-path physical positioning used non-integer coordinates in `setPosition`, triggering
runtime arg errors (`expected i32`) and bypassing window fit updates. 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 ## Fix Plan
@@ -132,6 +136,12 @@ Implemented:
15. `frontend/tauri-ui/src/renderer/pixi_pet.ts` 15. `frontend/tauri-ui/src/renderer/pixi_pet.ts`
- switched ferris cleanup to hue/saturation/value magenta-band masking with connected background - switched ferris cleanup to hue/saturation/value magenta-band masking with connected background
removal and stronger fallback cleanup. 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 ## 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` - 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` - 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` - 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 ## Closure