From 005d612ef3ab3dfc92bfbcf2f324329825842928 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 25 May 2026 16:18:25 +0900 Subject: [PATCH] =?UTF-8?q?feat(autoScale):=20detectClothBounds=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1=20+=20usePinchScale=20hook=20(v0-plan=20Task?= =?UTF-8?q?=208=20=EB=A7=88=EB=AC=B4=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/features/autoScale/detectClothBounds.ts: - v0 폴백: detectClothWidthPx → 항상 0 반환 → calculateScale가 confidence=0으로 fallback, 사용자가 핀치로 직접 보정 - 정식 구현은 Task 12: iOS Vision Saliency (VNGenerateAttentionBasedSaliencyImageRequest) src/features/autoScale/__tests__/detectClothBounds.test.ts: - 회귀 테스트 2 케이스: 정상 입력 / 빈 문자열 → 둘 다 0 (Task 12 정식 구현 시 spec 변경 명시점) src/features/autoScale/usePinchScale.ts: - react-native-gesture-handler v2 Pinch + Pan Simultaneous - .runOnJS(true) 명시 → worklet→JS thread 트램폴린 없이 setState 직접 (reanimated 의존 회피) - [MIN_SCALE, MAX_SCALE] 재사용 (calculateScale 모듈에서 import) - reset(): initialScale로 scale 복원 + tx/ty 0 - 검증은 Mac 실기기 manual test (Windows에서 hook 동작 검증 불가) 검증: - npx tsc --noEmit: 무에러 - 전체 npm test: 6 suites / 17 tests passed (sanity 1 + pose 1 + photoValidation 3 + maskSplit 4 + calculateScale 6 + detectClothBounds 2) 남은 Task 8 부분: 없음 (Step 4-6 완료, Step 7 Mac 검증) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/detectClothBounds.test.ts | 13 ++++++ src/features/autoScale/detectClothBounds.ts | 12 ++++++ src/features/autoScale/usePinchScale.ts | 40 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 src/features/autoScale/__tests__/detectClothBounds.test.ts create mode 100644 src/features/autoScale/detectClothBounds.ts create mode 100644 src/features/autoScale/usePinchScale.ts 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 }; +}