@@ -42,6 +42,36 @@ enum StickMode: String, Codable, CaseIterable {
4242 }
4343}
4444
45+ /// Which analog trigger, if any, scales cursor speed as a continuous precision control.
46+ enum AnalogPrecisionTriggerMode : String , Codable , CaseIterable {
47+ case off = " off "
48+ case left = " left "
49+ case right = " right "
50+ case either = " either "
51+
52+ var displayName : String {
53+ switch self {
54+ case . off: return " Off "
55+ case . left: return " L2 / LT "
56+ case . right: return " R2 / RT "
57+ case . either: return " Either "
58+ }
59+ }
60+
61+ func rawValue( leftTrigger: Double , rightTrigger: Double ) -> Double {
62+ switch self {
63+ case . off:
64+ return 0
65+ case . left:
66+ return leftTrigger
67+ case . right:
68+ return rightTrigger
69+ case . either:
70+ return max ( leftTrigger, rightTrigger)
71+ }
72+ }
73+ }
74+
4575/// Settings for joystick behavior.
4676///
4777/// Per-stick response (mode, sensitivity, acceleration, deadzone, invert, custom
@@ -114,6 +144,18 @@ struct JoystickSettings: Codable, Equatable {
114144 /// Modifier that triggers focus mode (slower mouse speed)
115145 var focusModeModifier : ModifierFlags = . command
116146
147+ /// Analog trigger that proportionally slows mouse/touchpad cursor movement.
148+ var analogPrecisionTriggerMode : AnalogPrecisionTriggerMode = . off
149+
150+ /// Cursor-speed floor at a full trigger pull (0.05 - 1.0).
151+ var analogPrecisionMinimumSpeed : Double = 0.15
152+
153+ /// Trigger value ignored before precision scaling begins (0.0 - 0.95).
154+ var analogPrecisionDeadzone : Double = 0.05
155+
156+ /// Response curve from trigger depth to speed reduction (0 = linear, 1 = cubic).
157+ var analogPrecisionCurve : Double = 0.35
158+
117159 /// Whether gyroscope aiming is enabled during focus mode.
118160 var gyroAimingEnabled : Bool = false
119161
@@ -198,6 +240,9 @@ struct JoystickSettings: Codable, Equatable {
198240 ( 0.5 ... 5.0 ) . contains ( touchpadZoomToPanRatio) &&
199241 ( 1.0 ... 4.0 ) . contains ( scrollBoostMultiplier) &&
200242 range. contains ( focusModeSensitivity) &&
243+ ( 0.05 ... 1.0 ) . contains ( analogPrecisionMinimumSpeed) &&
244+ ( 0.0 ... 0.95 ) . contains ( analogPrecisionDeadzone) &&
245+ range. contains ( analogPrecisionCurve) &&
201246 range. contains ( gyroAimingSensitivity) &&
202247 range. contains ( gyroAimingDeadzone) &&
203248 range. contains ( gestureSensitivity) &&
@@ -209,6 +254,25 @@ struct JoystickSettings: Codable, Equatable {
209254 return JoystickCurves . mouseMultiplier ( sensitivity: focusModeSensitivity)
210255 }
211256
257+ /// Multiplier applied to cursor motion based on analog trigger depth.
258+ /// Returns 1.0 when disabled or below deadzone, and approaches
259+ /// `analogPrecisionMinimumSpeed` at full trigger pull.
260+ func analogPrecisionMultiplier( leftTrigger: Double , rightTrigger: Double ) -> Double {
261+ guard analogPrecisionTriggerMode != . off else { return 1.0 }
262+ let rawTrigger = max ( 0.0 , min ( 1.0 , analogPrecisionTriggerMode. rawValue (
263+ leftTrigger: leftTrigger,
264+ rightTrigger: rightTrigger
265+ ) ) )
266+ let deadzone = max ( 0.0 , min ( 0.95 , analogPrecisionDeadzone) )
267+ guard rawTrigger > deadzone else { return 1.0 }
268+
269+ let minimumSpeed = max ( 0.05 , min ( 1.0 , analogPrecisionMinimumSpeed) )
270+ let normalized = ( rawTrigger - deadzone) / ( 1.0 - deadzone)
271+ let exponent = 1.0 + max( 0.0 , min ( 1.0 , analogPrecisionCurve) ) * 2.0
272+ let shaped = pow ( normalized, exponent)
273+ return 1.0 - shaped * ( 1.0 - minimumSpeed)
274+ }
275+
212276 func calibratedTouchpadValue( _ raw: Double , boost: Double ) -> Double {
213277 // Calibration curve: keeps 0 and 1 stable, but maps 0.5 -> 0.5 + boost * 0.25.
214278 return min ( 1.0 , max ( 0.0 , raw + boost * raw * ( 1.0 - raw) ) )
@@ -260,6 +324,10 @@ extension JoystickSettings {
260324 case scrollBoostMultiplier
261325 case focusModeSensitivity
262326 case focusModeModifier
327+ case analogPrecisionTriggerMode
328+ case analogPrecisionMinimumSpeed
329+ case analogPrecisionDeadzone
330+ case analogPrecisionCurve
263331 case gyroAimingEnabled
264332 case gyroAimingSensitivity
265333 case gyroAimingDeadzone
@@ -320,6 +388,17 @@ extension JoystickSettings {
320388 scrollBoostMultiplier = try container. decode ( . scrollBoostMultiplier, default: 2.0 , clampedTo: 1.0 ... 4.0 )
321389 focusModeSensitivity = try container. decode ( . focusModeSensitivity, default: 0.2 , clampedTo: unit)
322390 focusModeModifier = try container. decode ( . focusModeModifier, default: . command)
391+ analogPrecisionTriggerMode = try container. decodeLenient (
392+ . analogPrecisionTriggerMode,
393+ default: AnalogPrecisionTriggerMode . off
394+ )
395+ analogPrecisionMinimumSpeed = try container. decode (
396+ . analogPrecisionMinimumSpeed,
397+ default: 0.15 ,
398+ clampedTo: 0.05 ... 1.0
399+ )
400+ analogPrecisionDeadzone = try container. decode ( . analogPrecisionDeadzone, default: 0.05 , clampedTo: 0.0 ... 0.95 )
401+ analogPrecisionCurve = try container. decode ( . analogPrecisionCurve, default: 0.35 , clampedTo: unit)
323402 gyroAimingEnabled = try container. decode ( . gyroAimingEnabled, default: false )
324403 gyroAimingSensitivity = try container. decode ( . gyroAimingSensitivity, default: 0.3 , clampedTo: unit)
325404 gyroAimingDeadzone = try container. decode ( . gyroAimingDeadzone, default: 0.3 , clampedTo: unit)
@@ -361,6 +440,10 @@ extension JoystickSettings {
361440 try container. encode ( scrollBoostMultiplier, forKey: . scrollBoostMultiplier)
362441 try container. encode ( focusModeSensitivity, forKey: . focusModeSensitivity)
363442 try container. encode ( focusModeModifier, forKey: . focusModeModifier)
443+ try container. encode ( analogPrecisionTriggerMode, forKey: . analogPrecisionTriggerMode)
444+ try container. encode ( analogPrecisionMinimumSpeed, forKey: . analogPrecisionMinimumSpeed)
445+ try container. encode ( analogPrecisionDeadzone, forKey: . analogPrecisionDeadzone)
446+ try container. encode ( analogPrecisionCurve, forKey: . analogPrecisionCurve)
364447 try container. encode ( gyroAimingEnabled, forKey: . gyroAimingEnabled)
365448 try container. encode ( gyroAimingSensitivity, forKey: . gyroAimingSensitivity)
366449 try container. encode ( gyroAimingDeadzone, forKey: . gyroAimingDeadzone)
0 commit comments