From d2c6e617bb836741752ac83dd9675d33045a310c Mon Sep 17 00:00:00 2001 From: agent Date: Sun, 27 Sep 2026 21:28:01 +0000 Subject: [PATCH] Test palm rejection for add-page button taps Move the draw-or-navigate choice into touchIntent() so the rule the canvas uses can be tested, and cover finger taps on the plus button: accepted after writing and while the pen hovers, rejected while the pen is down, for palm-sized contacts and right after pen activity. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GaoZtJQZwLXkEgWs8twCia --- src/lib/components/CanvasView.svelte | 4 ++-- src/lib/ink/palm.test.ts | 35 +++++++++++++++++++++++++++- src/lib/ink/palm.ts | 9 +++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/lib/components/CanvasView.svelte b/src/lib/components/CanvasView.svelte index bab3a8a..c322b27 100644 --- a/src/lib/components/CanvasView.svelte +++ b/src/lib/components/CanvasView.svelte @@ -16,7 +16,7 @@ import { bounds, intersects, rectBeside, type Rect } from '$lib/model/layout'; import { canInsert, freeSlots, freeSlotsOf, neighbor, type Dir, type Slot } from '$lib/model/tree'; import { A4, PAGE_PRESETS, type Stroke } from '$lib/model/types'; - import { PalmRejector } from '$lib/ink/palm'; + import { PalmRejector, touchIntent } from '$lib/ink/palm'; import { StrokeRecorder, hitStroke } from '$lib/ink/stroke'; import { pageCssVars } from '$lib/editor/pageStyle'; import { downloadCanvas } from './download'; @@ -301,7 +301,7 @@ // pen-down, palm-sized contacts and right after pen use, but not for the 5 s pen session. const plus = target.closest('.ghost'); if (e.pointerType === 'touch') { - const decision = palm.evaluate(e, fingerInks && !plus ? 'draw' : 'navigate'); + const decision = palm.evaluate(e, touchIntent(fingerInks, !!plus)); if (!decision.accept) return; // palm: ignore entirely touches.set(e.pointerId, p); if (touches.size >= 2) { diff --git a/src/lib/ink/palm.test.ts b/src/lib/ink/palm.test.ts index 473358c..add4644 100644 --- a/src/lib/ink/palm.test.ts +++ b/src/lib/ink/palm.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { PalmRejector } from './palm'; +import { PalmRejector, touchIntent } from './palm'; const ev = (type: string, pointerType: string, extra: Partial = {}) => ({ type, pointerType, pointerId: pointerType === 'pen' ? 1 : 2, width: 10, height: 10, ...extra }) as PointerEvent; @@ -53,4 +53,37 @@ describe('palm rejection', () => { it('allows finger ink again once the pen session ends', () => { expect(afterPen(5100).evaluate(touch(), 'draw').accept).toBe(true); }); + + describe('add-page button taps', () => { + // With a pen in use fingers don't ink, but finger ink may still be on (before any pen). + const tap = (palm: PalmRejector, extra: Partial = {}, fingerInks = false) => + palm.evaluate(touch(extra), touchIntent(fingerInks, true)); + + it('are handled as navigation even when fingers ink', () => { + expect(touchIntent(true, true)).toBe('navigate'); + expect(touchIntent(false, true)).toBe('navigate'); + expect(touchIntent(true, false)).toBe('draw'); + expect(touchIntent(false, false)).toBe('navigate'); + }); + + it('are accepted a couple of seconds after writing', () => { + expect(tap(afterPen(2000)).accept).toBe(true); + expect(tap(afterPen(2000), {}, true).accept).toBe(true); + }); + + it('are accepted while the pen hovers', () => { + expect(tap(afterPen(2000, { leave: false })).accept).toBe(true); + }); + + it('are rejected while the pen is down', () => { + const palm = new PalmRejector(); + palm.track(ev('pointerdown', 'pen')); + expect(tap(palm).accept).toBe(false); + }); + + it('are rejected for a palm or right after the pen', () => { + expect(tap(afterPen(2000), { width: 60, height: 40 }).accept).toBe(false); + expect(tap(afterPen(50)).accept).toBe(false); + }); + }); }); diff --git a/src/lib/ink/palm.ts b/src/lib/ink/palm.ts index 7030b2b..2c9a233 100644 --- a/src/lib/ink/palm.ts +++ b/src/lib/ink/palm.ts @@ -27,6 +27,15 @@ const PALM_CONTACT_PX = 35; /** What an accepted touch would do: ink (draw/erase) or pan/zoom. */ export type TouchIntent = 'draw' | 'navigate'; +/** + * Which policy a touch falls under. It only draws when finger ink is on and it + * didn't land on a button; taps on buttons (e.g. add page) navigate, so they keep + * working between pen strokes. + */ +export function touchIntent(fingerInks: boolean, onButton: boolean): TouchIntent { + return fingerInks && !onButton ? 'draw' : 'navigate'; +} + export interface Decision { accept: boolean; reason: string;