fix: PDF finger ink, chrome UX, OneNote pens, and pen physics
Some checks failed
CI / Windows build (push) Has been cancelled

Wire finger drawing on PDF without breaking pinch; auto-hide page scrubber and fix bounce; share sticky tools with resize and per-page remember; side-button select; separate pen slots with colors; rnote pressure shapes plus tip-velocity width and lower stroke latency.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 16:01:01 +08:00
parent 85af037b7d
commit f31dd0fb52
17 changed files with 835 additions and 234 deletions

View File

@@ -19,7 +19,10 @@ enum PenButtonAction {
toggleTool,
pan,
/// Hold to temporarily enable PDF text selection (OneNote-style).
/// Rising-edge: switch to the universal stroke [select] tool (OneNote-like).
select,
/// Hold to temporarily enable PDF text selection.
selectText,
}
@@ -28,7 +31,7 @@ enum PenButtonAction {
/// Persisted under SharedPreferences key [PenConfigController.prefsKey].
class PenConfig {
const PenConfig({
this.sideButton = PenButtonAction.eraser,
this.sideButton = PenButtonAction.select,
this.eraserEnd = PenButtonAction.eraser,
this.pressureGamma = kNaturalPressureGamma,
this.palmRejectionMs = 150.0,

View File

@@ -8,6 +8,10 @@
// - [gamma]: the response exponent — γ<1 makes light touches register more
// width (more sensitive), γ>1 requires firmer pressure (less sensitive).
//
// Named [PressureCurveShape] presets mirror rnote-style curves (linear / soft /
// Pow2 / cubic / log / sqrt) via [PressureCurve.shaped]. Logarithmic uses
// ln(1+k·p)/ln(1+k); all others use p^gamma.
//
// Pure value type (widget-free, storage-free) so the full mapping is unit
// tested; PenConfig / the canvas wire it later (the wiring touches the live
// draw path and is validated on-device).
@@ -23,27 +27,86 @@ const double kNaturalPressureGamma = 0.7;
/// dynamic range so thin strokes have body instead of scratchy near-zero width.
const double kNaturalPressureFloor = 0.12;
/// Steepness for [PressureCurveShape.logarithmic]: `ln(1+k·p)/ln(1+k)`.
const double kLogarithmicPressureK = 9.0;
/// Named rnote-style pressure-response shapes.
enum PressureCurveShape {
/// Identity: gamma 1.
linear,
/// Light-touch sensitive: gamma ≈ 0.6.
soft,
/// rnote Pow2 / fountain: gamma 2.
quadratic,
/// gamma 3.
cubic,
/// Log curve: ln(1+k·p)/ln(1+k).
logarithmic,
/// Pencil: gamma 0.5.
sqrt,
}
/// Maps raw normalized pressure to a shaped response in `[floor, 1]`.
class PressureCurve {
const PressureCurve({this.floor = 0.0, this.gamma = 1.0})
: assert(floor >= 0.0 && floor < 1.0),
const PressureCurve({
this.floor = 0.0,
this.gamma = 1.0,
this.shape,
}) : assert(floor >= 0.0 && floor < 1.0),
assert(gamma > 0.0);
/// Named-shape factory. Sets [gamma] for power-law shapes; logarithmic
/// ignores gamma and uses [kLogarithmicPressureK] in [apply].
factory PressureCurve.shaped(
PressureCurveShape shape, {
double floor = 0.0,
}) {
switch (shape) {
case PressureCurveShape.linear:
return PressureCurve(floor: floor, gamma: 1.0, shape: shape);
case PressureCurveShape.soft:
return PressureCurve(floor: floor, gamma: 0.6, shape: shape);
case PressureCurveShape.quadratic:
return PressureCurve(floor: floor, gamma: 2.0, shape: shape);
case PressureCurveShape.cubic:
return PressureCurve(floor: floor, gamma: 3.0, shape: shape);
case PressureCurveShape.logarithmic:
return PressureCurve(floor: floor, gamma: 1.0, shape: shape);
case PressureCurveShape.sqrt:
return PressureCurve(floor: floor, gamma: 0.5, shape: shape);
}
}
/// Minimum output (>=0, <1). 0 = full dynamic range; raise toward 1 for a
/// fixed-pressure feel (marker).
final double floor;
/// Response exponent (>0). 1 = linear; <1 = more sensitive at light pressure;
/// >1 = firmer.
/// >1 = firmer. Unused when [shape] is [PressureCurveShape.logarithmic].
final double gamma;
/// Optional named shape. When [PressureCurveShape.logarithmic], [apply] uses
/// the log formula; otherwise (or when null) uses `p^gamma`.
final PressureCurveShape? shape;
/// Linear, full-range pen response (identity).
static const PressureCurve linear = PressureCurve();
/// Shape [pressure] (clamped to [0,1]) into `[floor, 1]`.
double apply(double pressure) {
final p = pressure.isNaN ? 0.0 : pressure.clamp(0.0, 1.0);
final shaped = gamma == 1.0 ? p : math.pow(p, gamma).toDouble();
final double shaped;
if (shape == PressureCurveShape.logarithmic) {
shaped = math.log(1.0 + kLogarithmicPressureK * p) /
math.log(1.0 + kLogarithmicPressureK);
} else {
shaped = gamma == 1.0 ? p : math.pow(p, gamma).toDouble();
}
return floor + (1.0 - floor) * shaped;
}
}