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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GaoZtJQZwLXkEgWs8twCia
This commit is contained in:
agent 2026-09-27 21:28:01 +00:00
parent 2760819466
commit d2c6e617bb
3 changed files with 45 additions and 3 deletions

View file

@ -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<HTMLElement>('.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) {

View file

@ -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<PointerEvent> = {}) =>
({ 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<PointerEvent> = {}, 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);
});
});
});

View file

@ -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;