ops(study): pressure 파이프라인 진단 패널 — raw/mapped/final 3단계 + tilt/buttons

사용자 분석: 수치 튜닝 무관해 보이면 pressure 입력 자체가 안 들어오는 케이스. perfect-
freehand 옵션 변경 의미 없음. 먼저 PointerEvent.pressure 가 실제로 변동하는지 확인 필요.

진단 패널 (?debug=1) 에 추가:
- PRESSURE PIPELINE 섹션:
  · raw  = PointerEvent.pressure 원본
  · mapped = getStrokePressure 의 inputP (raw 매핑 또는 속도 fallback)
  · final = fixedPressure update 후 perfect-freehand 에 전달되는 값
  · raw min/max — 세션 내 raw pressure 범위 (사용자가 펜 강약 시도 후 확인)
- tiltX, tiltY, ptr width/height, buttons — Pencil 추가 입력 필드.

판별:
- raw 가 항상 0.5 또는 1.0 → 디바이스/브라우저에서 pressure 미전달.
  현재 환경에서는 속도 기반 fallback 이 유일.
- raw 가 변동 (0.1~1.0) 인데 mapped/final 이 일정 → 우리 코드가 무시 중.
- raw + mapped + final 모두 변동 → perfect-freehand 가 무시 (thinning, simulatePressure).
This commit is contained in:
Hyungi Ahn
2026-04-27 15:54:23 +09:00
parent 8b27eadf2e
commit f005da2e83
@@ -87,6 +87,19 @@
coalesced: 0,
lastType: '-',
lastPressure: 0,
// pressure 파이프라인 진단 — raw → mapped → final.
// raw = PointerEvent.pressure 원본
// mapped = getStrokePressure 의 inputP (raw 매핑 또는 속도 fallback)
// final = fixedPressure update 후 perfect-freehand 에 전달되는 값
rawP: 0,
mappedP: 0,
finalP: 0,
tiltX: 0, tiltY: 0,
ptrW: 0, ptrH: 0,
buttons: 0,
// raw pressure 의 세션 min/max — 사용자가 펜 강약 시도 후 확인.
rawMin: 1,
rawMax: 0,
});
// dimension 측정 — button click 시 어느 element 의 dimension 이 변하는지 진단.
let dimDbg = $state({ canW: 0, canH: 0, conW: 0, conH: 0 });
@@ -359,31 +372,39 @@
// 1. inputP 결정.
let inputP: number;
if (Number.isFinite(rawPressure) && rawPressure > 0.1 && rawPressure < 0.99) {
// raw pressure 0.1~0.99 → 0.6~1.0 매핑. floor 보장.
inputP = PRESSURE_FLOOR + ((rawPressure - 0.1) / 0.89) * (1 - PRESSURE_FLOOR);
} else {
const last = target.points[target.points.length - 1];
if (last) {
const dist = Math.hypot(x - last[0], y - last[1]);
// 속도 기반 0.6~1.0.
inputP = Math.max(PRESSURE_FLOOR, Math.min(1.0, 1.6 - dist / 25));
} else {
inputP = FIRST_POINT_PRESSURE;
}
}
// 2. fixed hybrid.
let finalP: number;
if (target.fixedPressure === undefined) {
target.fixedPressure = inputP;
return inputP;
finalP = inputP;
} else {
const dev = Math.abs(inputP - target.fixedPressure);
if (dev >= FIXED_LARGE) {
target.fixedPressure = inputP;
finalP = inputP;
} else {
const alpha = dev < FIXED_THRESHOLD ? FIXED_ALPHA_NOISE : FIXED_ALPHA_INTENT;
target.fixedPressure = target.fixedPressure + (inputP - target.fixedPressure) * alpha;
finalP = target.fixedPressure;
}
}
const dev = Math.abs(inputP - target.fixedPressure);
if (dev >= FIXED_LARGE) {
target.fixedPressure = inputP;
return inputP;
// DBG: raw → mapped → final 3 단계 + raw min/max 추적.
if (DBG) {
const rawMin = Math.min(dbg.rawMin, rawPressure);
const rawMax = Math.max(dbg.rawMax, rawPressure);
dbg = { ...dbg, rawP: rawPressure, mappedP: inputP, finalP, rawMin, rawMax };
}
const alpha = dev < FIXED_THRESHOLD ? FIXED_ALPHA_NOISE : FIXED_ALPHA_INTENT;
target.fixedPressure = target.fixedPressure + (inputP - target.fixedPressure) * alpha;
return target.fixedPressure;
return finalP;
}
function pushPointWithInterp(target: Stroke, x: number, y: number, p: number) {
const last = target.points[target.points.length - 1];
@@ -514,7 +535,19 @@
}
function onPointerMove(e: PointerEvent) {
if (DBG) dbg = { ...dbg, move: dbg.move + 1, lastType: e.pointerType, lastPressure: e.pressure };
if (DBG) {
dbg = {
...dbg,
move: dbg.move + 1,
lastType: e.pointerType,
lastPressure: e.pressure,
tiltX: e.tiltX ?? 0,
tiltY: e.tiltY ?? 0,
ptrW: e.width ?? 0,
ptrH: e.height ?? 0,
buttons: e.buttons,
};
}
// 지우개 인디케이터 — hover (펜 미접촉) 만으로도 cursor 위치 추적. isDrawing
// 가드 *전*이라 mouse hover / Pencil hover 모두 잡힘.
@@ -934,15 +967,17 @@
<!-- 라이브 디버그 패널 — 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">
tool:{tool} width:{widthMode}<br/>
btn pen:{btnDbg.pen} er:{btnDbg.eraser} w:{btnDbg.width}<br/>
css:{cssWidth}×{cssHeight}<br/>
canvas:{dimDbg.canW}×{dimDbg.canH}<br/>
container:{dimDbg.conW}×{dimDbg.conH}<br/>
type:{dbg.lastType} p:{dbg.lastPressure.toFixed(2)}<br/>
type:{dbg.lastType} buttons:{dbg.buttons}<br/>
<span class="text-accent">PRESSURE PIPELINE</span><br/>
raw:{dbg.rawP.toFixed(3)} (min:{dbg.rawMin.toFixed(2)} max:{dbg.rawMax.toFixed(2)})<br/>
mapped:{dbg.mappedP.toFixed(3)} final:{dbg.finalP.toFixed(3)}<br/>
tilt:{dbg.tiltX},{dbg.tiltY} ptr:{dbg.ptrW}×{dbg.ptrH}<br/>
<span class="text-accent">EVENT COUNTERS</span><br/>
down:{dbg.down} move:{dbg.move} up:{dbg.up} cancel:{dbg.cancel}<br/>
rejType:{dbg.rejectedByType} rejId:{dbg.rejectedByPointerId} coal:{dbg.coalesced}<br/>
drawing:{isDrawing ? 'Y' : 'N'} actId:{activePointerId ?? '-'} infPts:{inflight?.points.length ?? 0}<br/>
strokes:{strokes.length}
strokes:{strokes.length} btn pen:{btnDbg.pen} er:{btnDbg.eraser}<br/>
css:{cssWidth}×{cssHeight} canvas:{dimDbg.canW}×{dimDbg.canH}
</div>
{/if}
</div>