fix(study): Apple Pencil hover (buttons===0) stroke 연장 차단 + ?debug=1 toggle
스크린샷 진단: 사용자 시나리오에서 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) <noreply@anthropic.com>
This commit is contained in:
@@ -63,9 +63,12 @@
|
||||
let isDrawing = $state(false);
|
||||
let activePointerId = $state<number | null>(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'};"
|
||||
></canvas>
|
||||
|
||||
{#if import.meta.env.DEV}
|
||||
<!-- 라이브 디버그 패널 — DEV 빌드 한정. prod 에선 Vite 가 dead-code-eliminate. -->
|
||||
{#if DBG}
|
||||
<!-- 라이브 디버그 패널 — DEV 빌드 또는 prod 에서 ?debug=1 query 시 활성. -->
|
||||
<div class="absolute top-1 left-1 px-2 py-1 rounded bg-bg/90 text-[10px] text-dim font-mono pointer-events-none leading-tight">
|
||||
type:{dbg.lastType} p:{dbg.lastPressure.toFixed(2)}<br/>
|
||||
down:{dbg.down} move:{dbg.move} up:{dbg.up} cancel:{dbg.cancel}<br/>
|
||||
|
||||
Reference in New Issue
Block a user