diff --git a/src/lib/components/CanvasView.svelte b/src/lib/components/CanvasView.svelte
index 4f05d39..ad72c68 100644
--- a/src/lib/components/CanvasView.svelte
+++ b/src/lib/components/CanvasView.svelte
@@ -271,10 +271,8 @@
if (target.closest('.no-stage')) return;
const p = local(e);
- // Fingers ink only when enabled and no pen has been used; otherwise they pan/zoom.
- const fingerInks = tools.inking && settings.data.fingerDraw && !palm.penSeen;
if (e.pointerType === 'touch') {
- const decision = palm.evaluate(e, fingerInks ? 'draw' : 'navigate');
+ const decision = palm.evaluate(e);
if (!decision.accept) return; // palm: ignore entirely
touches.set(e.pointerId, p);
if (touches.size >= 2) {
@@ -295,7 +293,7 @@
const wantsPan =
e.button === 1 ||
spaceDown ||
- (e.pointerType === 'touch' && (!fingerInks || !onPage)) ||
+ (e.pointerType === 'touch' && (!tools.inking || !settings.data.fingerDraw)) ||
(e.pointerType === 'mouse' && e.button === 0 && !onPage && !target.closest('.ghost, .slot'));
if (wantsPan) {
if (onPage && !inEditor) doc.activeId = onPage;
diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte
index c296b04..42a4e25 100644
--- a/src/lib/components/SettingsModal.svelte
+++ b/src/lib/components/SettingsModal.svelte
@@ -82,12 +82,12 @@
{:else if tab === 'input'}
Pen & touch
-
Draw with finger
When off, a single finger always pans; only pen and mouse draw. Once you use a pen, fingers only pan and zoom.
+
Draw with finger
When off, a single finger always pans; only pen and mouse draw.
Palm rejection
-
Pen session lockout
Ignore touches while the pen is writing, and finger ink for 5 seconds after it was used.
+
Pen session lockout
Ignore touches while the pen is writing or was used in the last 5 seconds.
@@ -99,7 +99,7 @@
-
Lock out on pen hover
Ignore finger ink while the pen hovers above the screen.
+
Lock out on pen hover
Ignore touches while the pen hovers above the screen.
{:else}
diff --git a/src/lib/ink/palm.test.ts b/src/lib/ink/palm.test.ts
deleted file mode 100644
index 473358c..0000000
--- a/src/lib/ink/palm.test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { PalmRejector } from './palm';
-
-const ev = (type: string, pointerType: string, extra: Partial = {}) =>
- ({ type, pointerType, pointerId: pointerType === 'pen' ? 1 : 2, width: 10, height: 10, ...extra }) as PointerEvent;
-const touch = (extra: Partial = {}) => ev('pointerdown', 'touch', extra);
-
-describe('palm rejection', () => {
- beforeEach(() => vi.useFakeTimers());
- afterEach(() => vi.useRealTimers());
-
- /** Pen writes a stroke, hovers, then leaves range; `ms` later a finger lands. */
- function afterPen(ms: number, { leave = true } = {}) {
- const palm = new PalmRejector();
- for (const t of ['pointermove', 'pointerdown', 'pointermove', 'pointerup', 'pointermove']) palm.track(ev(t, 'pen'));
- if (leave) palm.track(ev('pointerleave', 'pen'));
- vi.advanceTimersByTime(ms);
- return palm;
- }
-
- it('accepts fingers before any pen is used', () => {
- const palm = new PalmRejector();
- expect(palm.penSeen).toBe(false);
- expect(palm.evaluate(touch(), 'draw').accept).toBe(true);
- });
-
- it('rejects every touch while the pen is down', () => {
- const palm = new PalmRejector();
- palm.track(ev('pointerdown', 'pen'));
- vi.advanceTimersByTime(1000);
- expect(palm.evaluate(touch(), 'navigate').accept).toBe(false);
- expect(palm.evaluate(touch(), 'draw').accept).toBe(false);
- });
-
- it('lets fingers navigate between pen strokes but not draw', () => {
- const palm = afterPen(300);
- expect(palm.penSeen).toBe(true);
- expect(palm.evaluate(touch(), 'navigate').accept).toBe(true);
- expect(palm.evaluate(touch(), 'draw')).toEqual({ accept: false, reason: 'pen session active' });
- });
-
- it('lets fingers navigate while the pen hovers, but not draw', () => {
- const palm = afterPen(6000, { leave: false });
- expect(palm.evaluate(touch(), 'navigate').accept).toBe(true);
- expect(palm.evaluate(touch(), 'draw')).toEqual({ accept: false, reason: 'pen hovering' });
- });
-
- it('still rejects palms and touches right after the pen', () => {
- expect(afterPen(50).evaluate(touch(), 'navigate').accept).toBe(false);
- expect(afterPen(300).evaluate(touch({ width: 60, height: 60 }), 'navigate').accept).toBe(false);
- });
-
- it('allows finger ink again once the pen session ends', () => {
- expect(afterPen(5100).evaluate(touch(), 'draw').accept).toBe(true);
- });
-});
diff --git a/src/lib/ink/palm.ts b/src/lib/ink/palm.ts
index 7030b2b..341247d 100644
--- a/src/lib/ink/palm.ts
+++ b/src/lib/ink/palm.ts
@@ -1,20 +1,19 @@
// Palm rejection — ported from palm-rejection-test.html.
//
-// Pens are always accepted. A touch is rejected while the pen is down, when its
-// contact ellipse is palm-sized, or when it lands within 150 ms of pen activity.
-// Touches that would draw are also rejected for 5 s after pen activity and while
-// a pen hovers; touches that only pan/zoom are not, so navigating by hand keeps
-// working between pen strokes. A touch already drawing is cut off as soon as
+// Pens are always accepted. A touch is rejected when a pen session is active
+// (stroke in progress or pen activity within the last 5 s), when its contact
+// ellipse is palm-sized, when it lands within 150 ms of pen activity, or while
+// a pen hovers over the screen. A touch already drawing is cut off as soon as
// pen activity appears (callers re-run `evaluate` on every move).
export interface PalmOptions {
- /** Block touches while the pen is down, and finger ink for 5 s after pen activity. */
+ /** Block all touches while a pen session is active (5 s window). */
penSession: boolean;
/** Reject touches with a wide contact ellipse. */
geometry: boolean;
/** Reject touches within 150 ms of pen activity. */
timing: boolean;
- /** Block finger ink while the pen hovers. */
+ /** Arm the lockout as soon as the pen hovers. */
hover: boolean;
}
@@ -24,9 +23,6 @@ const PEN_SESSION_MS = 5000;
const PEN_TIMING_MS = 150;
const PALM_CONTACT_PX = 35;
-/** What an accepted touch would do: ink (draw/erase) or pan/zoom. */
-export type TouchIntent = 'draw' | 'navigate';
-
export interface Decision {
accept: boolean;
reason: string;
@@ -46,11 +42,6 @@ export class PalmRejector {
return this.activePens.size > 0;
}
- /** A pen has been used since the app loaded; fingers then only navigate. */
- get penSeen() {
- return this.lastPenActivityTime > 0;
- }
-
/** Feed every pointerdown/move/up/leave so pen state stays current. */
track(e: PointerEvent) {
if (e.pointerType !== 'pen') return;
@@ -80,15 +71,15 @@ export class PalmRejector {
}
}
- evaluate(e: PointerEvent, intent: TouchIntent = 'draw'): Decision {
+ evaluate(e: PointerEvent): Decision {
if (e.pointerType === 'pen') return { accept: true, reason: 'pen input' };
if (e.pointerType !== 'touch') return { accept: true, reason: 'non-touch pointer' };
const o = this.options;
const since = Date.now() - this.lastPenActivityTime;
- if (o.penSession && this.penIsDown) {
- return { accept: false, reason: 'pen is down' };
+ if (o.penSession && (since < PEN_SESSION_MS || this.penIsDown)) {
+ return { accept: false, reason: 'pen session active' };
}
if (o.geometry) {
const contact = Math.max(e.width || 0, e.height || 0);
@@ -99,13 +90,8 @@ export class PalmRejector {
if (o.timing && since < PEN_TIMING_MS) {
return { accept: false, reason: 'within 150ms of pen activity' };
}
- if (intent === 'draw') {
- if (o.penSession && since < PEN_SESSION_MS) {
- return { accept: false, reason: 'pen session active' };
- }
- if (o.hover && this.penHovering) {
- return { accept: false, reason: 'pen hovering' };
- }
+ if (o.hover && this.penHovering) {
+ return { accept: false, reason: 'pen hovering' };
}
return { accept: true, reason: 'touch accepted' };
}