teqo-backup/src/components/cards/useCardCutout.ts
Francisco Solla 48e08bf7f5 fix(cards): recorte no celular (WASM/WebGL) e volta do enquadramento S15
O recorte do card 'Time de você' falhava em navegadores endurecidos (IronFox/Tor
desligam WebAssembly e WebGL por padrão) com a copy errada de 'tente outra foto';
o motor MediaPipe exige ambos (o grafo pede kGpuService mesmo com delegate CPU).
Agora o adapter sonda WASM/WebGL antes do download e devolve 'unsupported' com
mensagem acionável; a entrada de inferência é capada em 1600 e a foto passa pelo
loadCardPhoto (foto de 61MP que falhava agora conclui).

O enquadramento automático volta ao S15 (bbox cobre a janela): o S18 mirava a
caixa do rosto em 103px e deixava o busto inteiro pequeno na janela. Saem o
detector de rosto (asset removido), frameCardPhotoOnFace, a referência 103px, o
piso 0.2 e a âncora S20 (inerte sob o cover). S16/S17 intactos.
2026-09-20 17:33:29 -03:00

104 lines
3.3 KiB
TypeScript

'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { removeCardPhotoBackground } from '@/components/cards/cardCutout'
import { harmonizeCardCutout } from '@/components/cards/cardPhotoHarmonyCanvas'
import type { CardRect } from '@/lib/cardModels'
import { frameCardPhotoOnBbox, type CardPhotoTransform } from '@/lib/cardPhotoTransform'
export type CardCutoutState =
| { status: 'idle' }
| { status: 'processing'; phase: 'download' | 'process'; ratio: number }
| {
status: 'ready'
canvas: HTMLCanvasElement
/**
* S17 — tone-harmonized variant of `canvas` (same size/alpha). Equals
* `canvas` when the photo is already on tone; `null` when the reference
* could not be read (the composer then hides the control).
*/
harmonized: HTMLCanvasElement | null
width: number
height: number
/** Initial framing: cutout top on the slot top, centered (S15). */
transform: CardPhotoTransform
}
| { status: 'error'; reason: 'engine' | 'empty' | 'unsupported' }
/**
* S15 — owns the cutout lifecycle for the team model: engine import, progress,
* degenerate framing (fail-closed), retry with the same file and run-id guards
* so a stale inference or an unmount never writes state. The engine itself is a
* page-lifetime singleton in the adapter (re-creating the wasm session per open
* would cost seconds); each result's masks/bitmap are released there.
*
* S17 — the ready state also carries the harmonized variant, computed once
* before the first ready paint (default ON must not flash the raw photo).
*/
export const useCardCutout = (photoWindow: CardRect | undefined, harmonyReferenceSrc?: string) => {
const [state, setState] = useState<CardCutoutState>({ status: 'idle' })
const fileRef = useRef<File | null>(null)
const runRef = useRef(0)
useEffect(
() => () => {
runRef.current += 1
},
[],
)
const start = useCallback(
async (file: File) => {
runRef.current += 1
const run = runRef.current
fileRef.current = file
setState({ status: 'processing', phase: 'download', ratio: 0 })
const result = await removeCardPhotoBackground(file, (progress) => {
if (runRef.current !== run) return
setState({ status: 'processing', phase: progress.phase, ratio: progress.ratio })
})
if (runRef.current !== run) return
if (!result.ok) {
setState({ status: 'error', reason: result.reason })
return
}
const transform = photoWindow
? frameCardPhotoOnBbox(
{ width: result.width, height: result.height },
photoWindow,
result.bbox,
)
: null
if (!transform) {
setState({ status: 'error', reason: 'empty' })
return
}
const harmonized = harmonyReferenceSrc
? await harmonizeCardCutout(result.canvas, harmonyReferenceSrc)
: null
if (runRef.current !== run) return
setState({
status: 'ready',
canvas: result.canvas,
harmonized,
width: result.width,
height: result.height,
transform,
})
},
[photoWindow, harmonyReferenceSrc],
)
const retry = useCallback(() => {
const file = fileRef.current
if (file) void start(file)
}, [start])
return { state, start, retry }
}