feat(pen): apply pressure-response curve for natural feel
"手写笔就是一个带压感的手指,没有特殊适配" — correct. The new editor fed RAW LINEAR stylus pressure into perfect_freehand, and the pressureGamma config (a slider in pen-settings) was read ONLY by that slider's UI and NEVER applied to a stroke. Dead wiring, like the rest. Wire it for real: - PenCanvas applies PressureCurve(floor, gamma) at capture, so stored pressure carries the feel and live + PDF export replay identically. - Natural defaults: gamma 0.7 (light touches register more width, rnote/ OneNote-like) + floor 0.12 (thin strokes keep body, not scratchy). - pen_editor threads PenConfig.pressureGamma into the canvas — the slider now actually changes stroke width. - pen_config: natural default + one-time migration of the legacy inert gamma 1.0, guarded by a marker so a deliberate 1.0 still sticks. Tests: pen_config_gamma_migration (4) + pressure_curve (6) pass; pen widget regressions green. flutter analyze: 0 issues. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import '../engine/stroke_model.dart';
|
||||
import '../engine/stroke_store.dart';
|
||||
import '../input/input_arbiter.dart' as arbiter;
|
||||
import '../input/pen_config.dart';
|
||||
import '../input/pressure_curve.dart';
|
||||
import '../input/pen_input_service.dart';
|
||||
import '../render/ink_picture_cache.dart';
|
||||
import '../render/live_ink_painter.dart' as render;
|
||||
@@ -55,6 +56,8 @@ class PenCanvas extends StatefulWidget {
|
||||
this.maxScale = 8.0,
|
||||
this.onPenDebug,
|
||||
this.thinning = kDefaultPenThinning,
|
||||
this.pressureGamma = kNaturalPressureGamma,
|
||||
this.pressureFloor = kNaturalPressureFloor,
|
||||
this.sideButtonAction = PenButtonAction.eraser,
|
||||
this.eraserEndAction = PenButtonAction.eraser,
|
||||
this.onPenButtonAction,
|
||||
@@ -103,6 +106,15 @@ class PenCanvas extends StatefulWidget {
|
||||
/// perfect_freehand pressure→width response, from `PenConfig.pressureSensitivity`.
|
||||
final double thinning;
|
||||
|
||||
/// Pressure-response exponent applied to raw stylus pressure BEFORE it reaches
|
||||
/// perfect_freehand. <1 boosts light touches (responsive, rnote-like); 1 is
|
||||
/// raw linear (the old "pressure-finger" feel). From `PenConfig.pressureGamma`.
|
||||
final double pressureGamma;
|
||||
|
||||
/// Minimum shaped pressure, so a light stroke still has body instead of
|
||||
/// scratchy near-zero width. From `PenConfig.pressureFloor`.
|
||||
final double pressureFloor;
|
||||
|
||||
/// Configured action for the pen's side barrel button (W3 — resolved against
|
||||
/// the native pen plugin's flags on Windows).
|
||||
final PenButtonAction sideButtonAction;
|
||||
@@ -189,8 +201,21 @@ class _PenCanvasState extends State<PenCanvas> {
|
||||
|
||||
/// Normalize stylus pressure to [0,1], or null when the device reports no
|
||||
/// usable pressure range (then perfect_freehand simulates pressure).
|
||||
///
|
||||
/// The raw normalized force is then shaped by the pressure-response curve
|
||||
/// (floor + gamma) so the stored pressure already carries the rnote-like feel
|
||||
/// — and because the shaping happens at capture, the live stroke and the PDF
|
||||
/// export replay identical pressures (no divergence).
|
||||
double? _normalizedPressure(PointerEvent event) {
|
||||
if (!_isStylus(event.kind)) return null;
|
||||
final double? raw = _rawNormalizedPressure(event);
|
||||
if (raw == null) return null;
|
||||
return PressureCurve(floor: widget.pressureFloor, gamma: widget.pressureGamma)
|
||||
.apply(raw);
|
||||
}
|
||||
|
||||
/// Raw [0,1] stylus force before response shaping (see [_normalizedPressure]).
|
||||
double? _rawNormalizedPressure(PointerEvent event) {
|
||||
final range = event.pressureMax - event.pressureMin;
|
||||
if (range > 0.0001) {
|
||||
return ((event.pressure - event.pressureMin) / range).clamp(0.0, 1.0);
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../engine/undo_stack.dart';
|
||||
import '../input/diagnostic_logger.dart';
|
||||
import '../input/pen_config.dart';
|
||||
import '../input/pen_input_service.dart';
|
||||
import '../input/pressure_curve.dart' show kNaturalPressureGamma;
|
||||
import '../layout/viewport_fit.dart';
|
||||
import '../persistence/editor_repository.dart';
|
||||
import '../persistence/save_scheduler.dart';
|
||||
@@ -558,6 +559,11 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
: (_penConfig?.value.penWidth ?? _penWidthFraction),
|
||||
thinning:
|
||||
_penConfig?.value.pressureSensitivity ?? kDefaultPenThinning,
|
||||
// Pressure-response shaping (the rnote-like feel). The pen-settings
|
||||
// gamma slider now actually drives stroke width; fall back to the
|
||||
// natural default when no config is loaded yet.
|
||||
pressureGamma:
|
||||
_penConfig?.value.pressureGamma ?? kNaturalPressureGamma,
|
||||
sideButtonAction:
|
||||
_penConfig?.value.sideButton ?? PenButtonAction.eraser,
|
||||
eraserEndAction:
|
||||
|
||||
Reference in New Issue
Block a user