From 50e0a78e1aa091d9b2e6d9c5fc04ff2ee15f305b Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 27 Apr 2026 12:54:42 +0900 Subject: [PATCH] =?UTF-8?q?fix(study):=20Apple=20Pencil=20hover=20(buttons?= =?UTF-8?q?=3D=3D=3D0)=20stroke=20=EC=97=B0=EC=9E=A5=20=EC=B0=A8=EB=8B=A8?= =?UTF-8?q?=20+=20=3Fdebug=3D1=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스크린샷 진단: 사용자 시나리오에서 stroke 자체는 들어가지만 글씨가 흩어지고 ㄱ→ㅏ 가 의도와 다르게 연결됨. 코드 재검토 결과 명백한 누락 — pointermove 가 e.buttons===0 케이스 (Apple Pencil hover, iPadOS 17+) 를 잡지 않아 hover 이동이 stroke 의 점으로 추가됨. ㄱ 그리고 → 펜 살짝 떼고 (hover 모드, pointerup 안 옴) → ㅏ 위치로 hover 이동 → hover pointermove 가 점 push → ㄱ 끝점에서 ㅏ 위치까지 직선/엉킴. Fix: - onPointerMove 에서 e.pointerType==='pen' && e.buttons===0 감지 시 stroke 즉시 finalize: capture release + isDrawing=false + inflight 보존 (pointerup 흐름). pointerup 안 와도 hover 모드 = 사실상 펜 떼짐. 다음 stroke 진입 보장. - onPointerDown 에서도 같은 가드 (hover-down reject) — hover 진입을 stroke 시작으로 오인 차단. Diagnostic: - DBG = import.meta.env.DEV || (?debug=1 query). prod 에서도 사용자 iPad 진단용으로 디버그 패널 토글 가능. URL 에 ?debug=1 추가 후 reload. - 디버그 패널 {#if DBG} 로 게이트. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/components/HandwriteCanvas.svelte | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/HandwriteCanvas.svelte b/frontend/src/lib/components/HandwriteCanvas.svelte index 9ecc1df..636c3d5 100644 --- a/frontend/src/lib/components/HandwriteCanvas.svelte +++ b/frontend/src/lib/components/HandwriteCanvas.svelte @@ -63,9 +63,12 @@ let isDrawing = $state(false); let activePointerId = $state(null); - // ── 디버그 카운터 — DEV 빌드 한정. prod 에선 mutation 자체가 DCE 되도록 모든 - // dbg = ... 호출을 if (DBG) 로 감쌌음. - const DBG = import.meta.env.DEV; + // ── 디버그 카운터 — DEV 빌드 또는 prod 에서 ?debug=1 query 시 활성화. + // prod 에서 mutation 자체를 DCE 하려면 import.meta.env.DEV 단독이지만, + // 사용자 iPad 진단을 위해 prod 에서도 query 로 토글 가능하게 함. const 라 + // 페이지 로드 시 한 번만 평가 (성능 영향 미미). + const DBG = import.meta.env.DEV || + (typeof window !== 'undefined' && /[?&]debug=1\b/.test(window.location.search)); let dbg = $state({ down: 0, move: 0, up: 0, cancel: 0, leave: 0, rejectedByType: 0, rejectedByPointerId: 0, @@ -297,6 +300,13 @@ if (DBG) dbg = { ...dbg, rejectedByType: dbg.rejectedByType + 1 }; return; } + // pen 의 hover-down (buttons===0) 은 무시 — 실제 접촉이 아닌 hover 진입. + // Pencil 닿는 순간 buttons===1. 이걸 stroke 시작으로 오인하면 hover 이동이 + // 점으로 추가됨. + if (e.pointerType === 'pen' && e.buttons === 0) { + if (DBG) dbg = { ...dbg, rejectedByType: dbg.rejectedByType + 1, lastType: 'hover-down' }; + return; + } // 이미 다른 pointer 가 그리는 중이면 무시 — palm rejection / multi-touch race 차단. // (Apple Pencil 진행 중에 손가락 닿거나 두 번째 pen 시도하면 둘 다 망가짐) if (isDrawing) return; @@ -343,6 +353,35 @@ return; } + // ── Apple Pencil hover 감지 (iPadOS 17+) ───────────────────────── + // 펜이 화면에서 떨어진 채로도 pointermove 가 발화 — pointerType==='pen' 이지만 + // buttons===0 (펜 안 닿음). pointerup 안 와도 hover 모드는 사실상 stroke 종료. + // 이 케이스를 잡지 않으면 hover 이동이 stroke 의 점으로 추가됨 → ㄱ 다음에 + // ㅏ 위치로 hover 이동 시 ㄱ 끝점에서 ㅏ 위치까지 직선이 그어지거나 한 stroke + // 가 의도치 않게 연장됨 (사용자 보고: ㄱ→ㅏ 가 안 써짐, 글씨 흩어짐). + if (e.pointerType === 'pen' && e.buttons === 0) { + try { canvas?.releasePointerCapture?.(e.pointerId); } catch {} + activePointerId = null; + isDrawing = false; + if (DBG) dbg = { ...dbg, up: dbg.up + 1, lastType: 'hover-end' }; + if (tool === 'eraser') { + inflight = null; + eraserLast = null; + return; + } + // pointerup 흐름: 1점 stroke (짧은 탭) 도 보존. + if (inflight && inflight.points.length >= 1) { + strokes = [...strokes, inflight]; + undoStack = []; + isDirty = true; + backup(); + scheduleSave(); + } + inflight = null; + scheduleRedraw(); + return; + } + const [x, y] = getLocalXY(e); if (tool === 'eraser') { @@ -653,8 +692,8 @@ style="touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; cursor: {tool === 'eraser' ? 'cell' : 'crosshair'};" > - {#if import.meta.env.DEV} - + {#if DBG} +
type:{dbg.lastType} p:{dbg.lastPressure.toFixed(2)}
down:{dbg.down} move:{dbg.move} up:{dbg.up} cancel:{dbg.cancel}