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' }; }