feat(heal): add Hybrid Option 2 with Fast Engine and AI Server toggle

This commit is contained in:
2026-10-16 20:23:29 +07:00
parent 8aa59bcfb2
commit 9a5243030e
2 changed files with 104 additions and 12 deletions
+19 -4
View File
@@ -29,9 +29,9 @@ import { offerInstall } from './pwa/install';
import { cameraSupported } from './engine/camera';
import { clearSession, forgetPhoto, loadPhoto, loadState, savePhoto, saveState, type StudioState } from './engine/session';
import { analyzeCustomFrame, clearCustomFrame, loadCustomFrame, saveCustomFrame, type CustomFrameData } from './engine/customFrame';
import { keep } from './remember';
import { keep, recall } from './remember';
import { preloadSuperRes, upscaleJpeg } from './engine/superRes';
import { preloadInpaint, clearInpaintCache } from './engine/inpaint';
import { preloadInpaint, clearInpaintCache, setAiInpaintMode } from './engine/inpaint';
import { api, type Photo, type SavedRecipe } from './api';
import { refreshAuth, useAuth } from './auth';
import { TIERS } from './config/tiers';
@@ -519,6 +519,11 @@ export function Workspace() {
const [brushTool, setBrushTool] = useState<'heal' | 'mosaic' | null>(null);
const [healR, setHealR] = useState(HEAL_DEFAULT_R);
const [mosaicR, setMosaicR] = useState(MOSAIC_DEFAULT_R);
const [aiInpaint, setAiInpaint] = useState<boolean>(() => recall('recipescam.heal.ai') === 'true');
useEffect(() => {
setAiInpaintMode(aiInpaint);
keep('recipescam.heal.ai', String(aiInpaint));
}, [aiInpaint]);
// FX's gradient masks: which shape the next drag on the photo lays down (null
// for neither), the masks the recipe holds, and which of them is chosen — the
// one whose shape is on the photo and whose knobs are in the panel beside it.
@@ -1163,7 +1168,7 @@ export function Workspace() {
}, PREVIEW_DEBOUNCE_MS);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [engine, previewBytes, recipe, preview, frameId, cropEditing, cropLive, cropRect, rotation, straighten, flipH, flipV, useGeotag, gpsInfo, gpsStyle, markOn, markText, markStyle, comparing, shooting]);
}, [engine, previewBytes, recipe, preview, frameId, cropEditing, cropLive, cropRect, rotation, straighten, flipH, flipV, useGeotag, gpsInfo, gpsStyle, markOn, markText, markStyle, comparing, shooting, aiInpaint]);
// The source copy behind previewBytes, re-made only when previewMax moves:
// applying a crop upgrades it, CANCEL/UNDO brings the 1600px one back.
@@ -2785,7 +2790,7 @@ export function Workspace() {
if (!brushTool) return;
const dismiss = (e: PointerEvent) => {
const el = e.target as HTMLElement | null;
if (!el?.closest('.rail, .chip') || el.closest('.chip[data-key="heal"], .chip[data-key="mosaic"]')) return;
if (!el?.closest('.rail, .chip') || el.closest('.chip[data-key^="heal"], .chip[data-key^="mosaic"]')) return;
setBrushTool(null);
};
window.addEventListener('pointerdown', dismiss, true);
@@ -3719,6 +3724,16 @@ export function Workspace() {
}
},
},
...(brushTool === 'heal'
? [
{
key: 'heal-ai',
label: aiInpaint ? '✨ AI SERVER' : '⚡ FAST ENGINE',
active: aiInpaint,
onClick: () => setAiInpaint((v: boolean) => !v),
},
]
: []),
...(healSpots.length ? [{ key: 'heal-clear', label: 'CLEAR', onClick: clearHealSpots }] : []),
{
key: 'mosaic',