[feat/MAT-363] Zoom/Pan 인프라#305
Open
b0nsu wants to merge 1 commit intorefactor/mat-362-catmull-romfrom
Open
Conversation
- transform.ts: ViewTransform, screenToCanvas, canvasToScreen, clampTransform, transformToMatrix3 - render/rendererTypes.ts: RendererViewport 타입 - canvas/useCanvasViewportController.ts: 뷰포트 관리 (스크롤/줌 모드, 캔버스 높이 자동 확장) - canvas/useCanvasGestureComposer.ts: 제스처 조합 (1-finger draw + 2-finger pinch/pan) - DrawingCanvas 통합은 MAT-364에서 진행 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Pull request overview
Zoom/Pan(뷰포트 변환) 지원을 위한 캔버스 인프라 레이어를 pointer-native-drawing 패키지에 추가하는 PR입니다. 제스처(핀치/2-finger 팬)로 ViewTransform을 갱신하고, 좌표 변환 유틸과 렌더러 뷰포트 타입을 도입해 이후 Drawing/Viewport 제스처 합성을 가능하게 합니다.
Changes:
ViewTransform기반 좌표 변환/클램프 유틸(transform.ts) 추가- 뷰포트 상태/스크롤/캔버스 높이/트랜스폼 제어 훅(
useCanvasViewportController) 추가 - 드로잉 제스처와 Zoom/Pan 제스처를 합성하는 훅(
useCanvasGestureComposer) 추가
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/pointer-native-drawing/src/transform.ts | Zoom/Pan을 위한 transform 타입/좌표 변환/클램프 및 Skia 매트릭스 변환 유틸 추가 |
| packages/pointer-native-drawing/src/render/rendererTypes.ts | 렌더러에 전달할 최소 뷰포트 정보 타입 추가 |
| packages/pointer-native-drawing/src/index.ts | transform 유틸 및 RendererViewport 타입 export 추가 |
| packages/pointer-native-drawing/src/canvas/useCanvasViewportController.ts | 캔버스 높이/레이아웃/스크롤/transform 적용 및 클램프 로직 훅 추가 |
| packages/pointer-native-drawing/src/canvas/useCanvasGestureComposer.ts | 핀치/2-finger 팬/탭 제스처를 상황별로 합성하는 훅 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+19
to
+28
| export function screenToCanvas( | ||
| sx: number, | ||
| sy: number, | ||
| transform: ViewTransform | ||
| ): { x: number; y: number } { | ||
| const s = transform.scale || 1; | ||
| return { | ||
| x: (sx - transform.translateX) / s, | ||
| y: (sy - transform.translateY) / s, | ||
| }; |
Comment on lines
+37
to
+40
| const scrollViewRef = useRef<ScrollView>(null); | ||
| const minimumCanvasHeightRef = useRef<number>(Math.max(400, minCanvasHeight)); | ||
| const [canvasHeight, setCanvasHeight] = useState<number>(minimumCanvasHeightRef.current); | ||
|
|
Comment on lines
+48
to
+54
| const setCanvasHeightValue = useCallback((nextHeight: number) => { | ||
| const normalized = Math.max(minimumCanvasHeightRef.current, nextHeight); | ||
| setCanvasHeight((prev) => { | ||
| if (prev === normalized) return prev; | ||
| onCanvasHeightChangeRef.current?.(normalized); | ||
| return normalized; | ||
| }); |
Comment on lines
+93
to
+106
| const applyTransform = useCallback( | ||
| (next: ViewTransform) => { | ||
| const canvasW = viewportSize.width; | ||
| const canvasH = enableZoomPan | ||
| ? Math.max( | ||
| effectiveCanvasHeight, | ||
| maxYRef.current > 0 ? maxYRef.current + viewportSize.height : 0 | ||
| ) | ||
| : effectiveCanvasHeight; | ||
| const clamped = clampTransform(next, canvasW, canvasH, viewportSize.width, viewportSize.height, maxZoomScale); | ||
| viewTransformRef.current = clamped; | ||
| setViewTransform(clamped); | ||
| onTransformChangeRef.current?.(clamped); | ||
| }, |
Comment on lines
+38
to
+86
| const minimumCanvasHeightRef = useRef<number>(Math.max(400, minCanvasHeight)); | ||
| const [canvasHeight, setCanvasHeight] = useState<number>(minimumCanvasHeightRef.current); | ||
|
|
||
| const onCanvasHeightChangeRef = useRef(onCanvasHeightChange); | ||
| const onScrollOffsetChangeRef = useRef(onScrollOffsetChange); | ||
| const onTransformChangeRef = useRef(onTransformChange); | ||
| onCanvasHeightChangeRef.current = onCanvasHeightChange; | ||
| onScrollOffsetChangeRef.current = onScrollOffsetChange; | ||
| onTransformChangeRef.current = onTransformChange; | ||
|
|
||
| const setCanvasHeightValue = useCallback((nextHeight: number) => { | ||
| const normalized = Math.max(minimumCanvasHeightRef.current, nextHeight); | ||
| setCanvasHeight((prev) => { | ||
| if (prev === normalized) return prev; | ||
| onCanvasHeightChangeRef.current?.(normalized); | ||
| return normalized; | ||
| }); | ||
| }, []); | ||
|
|
||
| const resetCanvasHeight = useCallback(() => { | ||
| setCanvasHeightValue(minimumCanvasHeightRef.current); | ||
| }, [setCanvasHeightValue]); | ||
|
|
||
| const maybeGrowCanvasHeight = useCallback( | ||
| (nextMaxY: number) => { | ||
| if (nextMaxY > maxYRef.current) { | ||
| maxYRef.current = nextMaxY; | ||
| setCanvasHeightValue(nextMaxY + Math.max(200, viewportSize.height)); | ||
| } | ||
| }, | ||
| [setCanvasHeightValue, viewportSize.height, maxYRef] | ||
| ); | ||
|
|
||
| const syncCanvasHeightFromMaxY = useCallback( | ||
| (nextMaxY: number) => { | ||
| if (nextMaxY <= 0) { | ||
| maxYRef.current = 0; | ||
| resetCanvasHeight(); | ||
| return; | ||
| } | ||
| maxYRef.current = nextMaxY; | ||
| setCanvasHeightValue(nextMaxY + Math.max(200, viewportSize.height)); | ||
| }, | ||
| [resetCanvasHeight, setCanvasHeightValue, viewportSize.height, maxYRef] | ||
| ); | ||
|
|
||
| const effectiveCanvasHeight = enableZoomPan | ||
| ? Math.max(canvasHeight, viewportSize.height * 2) | ||
| : canvasHeight; |
Comment on lines
+93
to
+116
| Gesture.Pinch() | ||
| .onStart((e) => { | ||
| 'worklet'; | ||
| pinchDeadShared.value = false; | ||
| pinchActiveShared.value = true; | ||
| runOnJS(handlePinchStart)(e.focalX, e.focalY); | ||
| }) | ||
| .onUpdate((e) => { | ||
| 'worklet'; | ||
| if (e.numberOfPointers < 2) { | ||
| if (!pinchDeadShared.value) { | ||
| pinchDeadShared.value = true; | ||
| runOnJS(handlePinchEnd)(); | ||
| } | ||
| return; | ||
| } | ||
| if (pinchDeadShared.value) return; | ||
| runOnJS(handlePinchUpdate)(e.scale, e.focalX, e.focalY); | ||
| }) | ||
| .onEnd(() => { | ||
| 'worklet'; | ||
| pinchActiveShared.value = false; | ||
| runOnJS(handlePinchEnd)(); | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
뷰포트 변환(Zoom/Pan) 인프라를 도입합니다.
useCanvasViewportController, useCanvasGestureComposer, transform.ts로 핀치/팬 제스처를 처리합니다.
Linear
Changes
canvas/useCanvasViewportController.ts— 뷰포트 상태 관리, 핀치/팬canvas/useCanvasGestureComposer.ts— 제스처 합성 (드로잉 + 뷰포트 전환)transform.ts— ViewTransform, IDENTITY_TRANSFORM, screenToCanvas, canvasToScreenrender/rendererTypes.ts— RendererViewport 타입Testing
pnpm typecheck통과pnpm lint통과Risk / Impact