import React from "react"; import type { FaceBlurMethod, Layer, LayerEffect } from "@pien-studio/types"; import type { FaceDetectionOverlay } from "./use-face-detection"; type FaceBlurPreview = { layerId: string; effects: LayerEffect[]; }; type UseFaceBlurWorkflowOptions = { selectedLayer: Layer | null; faceDetectionsLayerId: string | null; faceDetections: FaceDetectionOverlay[]; setLayerEffect: (layerId: string, effect: LayerEffect) => void; removeLayerEffect: (layerId: string, kind: LayerEffect["kind"]) => void; }; export function useFaceBlurWorkflow(options: UseFaceBlurWorkflowOptions) { const { selectedLayer, faceDetectionsLayerId, faceDetections, setLayerEffect, removeLayerEffect } = options; const [blurMethod, setBlurMethod] = React.useState("gaussian"); const [blurAmount, setBlurAmount] = React.useState(14); const [censorColor, setCensorColor] = React.useState("#111111"); const [selectedFaceIndices, setSelectedFaceIndices] = React.useState([]); const [faceBlurPreview, setFaceBlurPreview] = React.useState(null); const hasDetectableSelection = Boolean(selectedLayer && selectedLayer.type === "raster" && faceDetectionsLayerId === selectedLayer.id); const faceBlurEffect = React.useMemo( () => selectedLayer?.effects.find((e): e is Extract => e.kind === "face-blur") ?? null, [selectedLayer?.effects], ); const buildBlurRegions = React.useCallback( (indices: number[]) => { if (!selectedLayer || selectedLayer.type !== "raster") return []; if (faceDetectionsLayerId !== selectedLayer.id || faceDetections.length === 0) return []; const indexSet = new Set(indices); return faceDetections .filter((_, index) => indexSet.has(index)) .map((face) => { const baseWidth = Math.max(1, selectedLayer.width ?? face.sourceWidth); const baseHeight = Math.max(1, selectedLayer.height ?? face.sourceHeight); const scaleX = face.sourceWidth / baseWidth; const scaleY = face.sourceHeight / baseHeight; return { x: Math.max(0, Math.floor(face.x * scaleX)), y: Math.max(0, Math.floor(face.y * scaleY)), width: Math.max(1, Math.floor(face.width * scaleX)), height: Math.max(1, Math.floor(face.height * scaleY)), sourceWidth: face.sourceWidth, sourceHeight: face.sourceHeight, censorColor, }; }); }, [censorColor, faceDetections, faceDetectionsLayerId, selectedLayer], ); const toggleFaceIndex = React.useCallback((index: number) => { setSelectedFaceIndices((prev) => (prev.includes(index) ? prev.filter((item) => item !== index) : [...prev, index])); }, []); const clearBlur = React.useCallback(() => { if (!selectedLayer) return; removeLayerEffect(selectedLayer.id, "face-blur"); setFaceBlurPreview(null); }, [selectedLayer, removeLayerEffect]); const blurFaces = React.useCallback( (indices: number[]) => { if (!selectedLayer || selectedLayer.type !== "raster" || !selectedLayer.sourceUri) return; if (faceDetectionsLayerId !== selectedLayer.id || faceDetections.length === 0) return; const regions = buildBlurRegions(indices); setLayerEffect(selectedLayer.id, { kind: "face-blur", enabled: true, method: blurMethod, amount: blurAmount, regions, censorColor, }); setSelectedFaceIndices([]); setFaceBlurPreview(null); }, [blurAmount, blurMethod, buildBlurRegions, censorColor, faceDetections.length, faceDetectionsLayerId, selectedLayer, setLayerEffect], ); React.useEffect(() => { if (!hasDetectableSelection) { setSelectedFaceIndices((prev) => (prev.length === 0 ? prev : [])); setFaceBlurPreview(null); return; } if (faceBlurEffect) { setSelectedFaceIndices((prev) => (prev.length === 0 ? prev : [])); return; } setSelectedFaceIndices((prev) => { const next = faceDetections.map((_, i) => i); if (prev.length === next.length && prev.every((value, index) => value === next[index])) return prev; return next; }); }, [faceDetections, hasDetectableSelection, faceBlurEffect]); React.useEffect(() => { if (!hasDetectableSelection || !selectedLayer || selectedLayer.type !== "raster") { setFaceBlurPreview(null); return; } if (selectedFaceIndices.length === 0) { setFaceBlurPreview(null); return; } setFaceBlurPreview({ layerId: selectedLayer.id, effects: [{ kind: "face-blur", enabled: true, method: blurMethod, amount: blurAmount, regions: buildBlurRegions(selectedFaceIndices), censorColor, }], }); }, [blurAmount, blurMethod, buildBlurRegions, censorColor, hasDetectableSelection, selectedFaceIndices, selectedLayer]); return { blurMethod, setBlurMethod, blurAmount, setBlurAmount, censorColor, setCensorColor, selectedFaceIndices, faceBlurPreview, toggleFaceIndex, clearBlur, blurFaces, }; }