diff --git a/src/features/autoScale/__tests__/detectClothBounds.test.ts b/src/features/autoScale/__tests__/detectClothBounds.test.ts new file mode 100644 index 0000000..3d1d960 --- /dev/null +++ b/src/features/autoScale/__tests__/detectClothBounds.test.ts @@ -0,0 +1,13 @@ +import { detectClothWidthPx } from '../detectClothBounds'; + +describe('detectClothWidthPx', () => { + it('v0 폴백: 자동 검출 미지원이므로 항상 0을 반환한다 (정식 구현은 Task 12)', async () => { + const result = await detectClothWidthPx('file://anyframe.jpg'); + expect(result).toBe(0); + }); + + it('빈 문자열 입력도 동일하게 0 폴백', async () => { + const result = await detectClothWidthPx(''); + expect(result).toBe(0); + }); +}); diff --git a/src/features/autoScale/detectClothBounds.ts b/src/features/autoScale/detectClothBounds.ts new file mode 100644 index 0000000..7e776bf --- /dev/null +++ b/src/features/autoScale/detectClothBounds.ts @@ -0,0 +1,12 @@ +/** + * v0 폴백: 카메라 프레임에서 사용자가 든 옷의 가로 픽셀 검출. + * + * v0에서는 항상 0을 반환 → calculateScale가 confidence=0으로 fallback, + * 사용자는 핀치 줌(usePinchScale)으로 직접 보정. + * + * 정식 구현은 Task 12: iOS Vision Saliency(VNGenerateAttentionBasedSaliencyImageRequest + * 또는 VNGenerateForegroundInstanceMaskRequest)로 손에 든 옷 mask → bbox 가로 너비. + */ +export async function detectClothWidthPx(_cameraFrameUri: string): Promise { + return 0; +} diff --git a/src/features/autoScale/usePinchScale.ts b/src/features/autoScale/usePinchScale.ts new file mode 100644 index 0000000..56cb947 --- /dev/null +++ b/src/features/autoScale/usePinchScale.ts @@ -0,0 +1,40 @@ +import { useCallback, useState } from 'react'; +import { Gesture } from 'react-native-gesture-handler'; +import { MIN_SCALE, MAX_SCALE } from './calculateScale'; + +/** + * 핀치 줌 + 팬 hook. 자동 스케일 추정값을 초기값으로 받고 사용자가 직접 보정. + * + * Mac 실기기 검증 항목: + * - .runOnJS(true)로 worklet→JS thread 트램폴린 없이 setState 직접 호출 (reanimated 의존 회피) + * - 핀치 줌 [MIN_SCALE, MAX_SCALE] 클램핑이 calculateScale과 동일 범위인지 + * - 팬은 누적 translation, reset은 initialScale로 복원 + */ +export function usePinchScale(initialScale: number = 1) { + const [scale, setScale] = useState(initialScale); + const [tx, setTx] = useState(0); + const [ty, setTy] = useState(0); + + const pinch = Gesture.Pinch() + .runOnJS(true) + .onUpdate((e) => { + setScale((s) => Math.max(MIN_SCALE, Math.min(MAX_SCALE, s * e.scale))); + }); + + const pan = Gesture.Pan() + .runOnJS(true) + .onUpdate((e) => { + setTx((x) => x + e.translationX); + setTy((y) => y + e.translationY); + }); + + const gesture = Gesture.Simultaneous(pinch, pan); + + const reset = useCallback(() => { + setScale(initialScale); + setTx(0); + setTy(0); + }, [initialScale]); + + return { scale, tx, ty, gesture, reset }; +}