Skip to content

[feat/MAT-363] Zoom/Pan 인프라#305

Open
b0nsu wants to merge 1 commit intorefactor/mat-362-catmull-romfrom
refactor/mat-363-zoom-pan
Open

[feat/MAT-363] Zoom/Pan 인프라#305
b0nsu wants to merge 1 commit intorefactor/mat-362-catmull-romfrom
refactor/mat-363-zoom-pan

Conversation

@b0nsu
Copy link
Copy Markdown
Collaborator

@b0nsu b0nsu commented Apr 30, 2026

Summary

뷰포트 변환(Zoom/Pan) 인프라를 도입합니다.
useCanvasViewportController, useCanvasGestureComposer, transform.ts로 핀치/팬 제스처를 처리합니다.

Stacked PR 8/10 — base: refactor/mat-362-catmull-rom

Linear

Changes

  • canvas/useCanvasViewportController.ts — 뷰포트 상태 관리, 핀치/팬
  • canvas/useCanvasGestureComposer.ts — 제스처 합성 (드로잉 + 뷰포트 전환)
  • transform.ts — ViewTransform, IDENTITY_TRANSFORM, screenToCanvas, canvasToScreen
  • render/rendererTypes.ts — RendererViewport 타입
  • index.ts export 추가

Testing

  • pnpm typecheck 통과
  • pnpm lint 통과

Risk / Impact

  • 영향 범위: 캔버스 뷰포트/제스처 레이어 추가
  • 확인이 필요한 부분: enableZoomPan 플래그 연동, 좌표 변환 정확성
  • 배포 시 유의사항: 없음

- 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>
@linear
Copy link
Copy Markdown

linear Bot commented Apr 30, 2026

MAT-363 Zoom/Pan

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pointer-admin Ready Ready Preview, Comment Apr 30, 2026 11:53am

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)();
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants