diff --git a/osu.Framework/Graphics/Transforms/Spring.cs b/osu.Framework/Graphics/Transforms/Spring.cs index 8fe123a5f..9759d7238 100644 --- a/osu.Framework/Graphics/Transforms/Spring.cs +++ b/osu.Framework/Graphics/Transforms/Spring.cs @@ -117,10 +117,18 @@ namespace osu.Framework.Graphics.Transforms protected void ComputeSingleValue(float dt, ref float current, ref float velocity, float target, float targetVelocity) { - float k2Stable = MathF.Max(MathF.Max(k2, dt * dt / 2 + dt * k1 / 2), dt * k1); + float k2Stable = MathF.Max(MathF.Max(k2, (dt * dt) / 2 + (dt * k1) / 2), dt * k1); current += dt * velocity; - velocity += (dt * (target + k3 * targetVelocity - current - k1 * velocity)) / k2Stable; + velocity += (dt * (target + (k3 * targetVelocity) - current - (k1 * velocity))) / k2Stable; + } + + protected void ComputeSingleValue(float dt, ref double current, ref double velocity, double target, double targetVelocity) + { + float k2Stable = MathF.Max(MathF.Max(k2, (dt * dt) / 2 + (dt * k1) / 2), dt * k1); + + current += dt * velocity; + velocity += (dt * (target + (k3 * targetVelocity) - current - (k1 * velocity))) / k2Stable; } } @@ -136,6 +144,18 @@ namespace osu.Framework.Graphics.Transforms } } + public class DoubleSpring : Spring + { + protected override double GetTargetVelocity(double target, double previousTarget, float dt) => (target - previousTarget) / dt; + + protected override double ComputeNextValue(float dt, double target, double targetVelocity) + { + ComputeSingleValue(dt, ref Current, ref Velocity, target, targetVelocity); + + return Current; + } + } + public class Vector2Spring : Spring { protected override Vector2 GetTargetVelocity(Vector2 target, Vector2 previousTarget, float dt) => (target - previousTarget) / dt; diff --git a/osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs b/osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs index 99a983786..dcbe4c045 100644 --- a/osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs +++ b/osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -44,7 +45,7 @@ namespace osu.Framework.Input.Handlers.Tablet public Bindable Rotation { get; } = new Bindable(); - public BindableFloat PressureThreshold { get; } = new BindableFloat(0.0f) + public BindableFloat PressureThreshold { get; } = new BindableFloat { MinValue = 0f, MaxValue = 1f, @@ -119,7 +120,34 @@ namespace osu.Framework.Input.Handlers.Tablet enqueueInput(new MousePositionRelativeInputFromPen { Delta = new Vector2(delta.X, delta.Y), DeviceType = lastTabletDeviceType }); } - void IPressureHandler.SetPressure(float percentage) => enqueueInput(new MouseButtonInputFromPen(percentage > PressureThreshold.Value) { DeviceType = lastTabletDeviceType }); + private bool penPressed; + + void IPressureHandler.SetPressure(float pressure) + { + // Most important for edge cases where users have pressure set to 0 or 1 and tablets can report fuzzy data. + const float hysteresis_half = 0.02f; + + pressure = Math.Clamp(pressure, 0f, 1f); + + float releaseThreshold = PressureThreshold.Value - hysteresis_half; + float pressThreshold = PressureThreshold.Value + hysteresis_half; + + // keep press..release threshold range constant for edge cases. + if (releaseThreshold < 0f) + { + pressThreshold = hysteresis_half * 2; + releaseThreshold = 0f; + } + else if (pressThreshold > 1f) + { + releaseThreshold = 1 - (hysteresis_half * 2); + pressThreshold = 1f; + } + + setPressed(penPressed + ? pressure > releaseThreshold + : pressure > pressThreshold); + } private void handleTabletsChanged(object? sender, IEnumerable tablets) { @@ -135,7 +163,22 @@ namespace osu.Framework.Input.Handlers.Tablet updateOutputArea(host.Window); } else + { + // Ensure we don't leave the simulated mouse button pressed if the tablet disappears. + setPressed(false); tablet.Value = null; + } + } + + private void setPressed(bool pressed) + { + // Importantly, only fire input when the state changes. + // If we fire more often, this may intefere with users that click with mouse but use tablet for positional input (hovering). + if (pressed == penPressed) + return; + + enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = lastTabletDeviceType }); + penPressed = pressed; } private void handleDeviceReported(object? sender, IDeviceReport report) diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs index b332546ee..6c164e28e 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs @@ -275,8 +275,7 @@ namespace osu.Framework.Platform.SDL2 break; case SDL_EventType.SDL_CONTROLLERDEVICEREMOVED: - SDL_GameControllerClose(controllers[evtCdevice.which].ControllerHandle); - controllers.Remove(evtCdevice.which); + removeJoystick(evtCdevice.which); break; case SDL_EventType.SDL_CONTROLLERDEVICEREMAPPED: @@ -323,6 +322,20 @@ namespace osu.Framework.Platform.SDL2 controllers[instanceID] = new SDL2ControllerBindings(joystick, controller); } + private void removeJoystick(int which) + { + int instanceID = SDL_JoystickGetDeviceInstanceID(which); + + if (controllers.Remove(instanceID, out var controller)) + { + if (controller.ControllerHandle != IntPtr.Zero) + SDL_GameControllerClose(controller.ControllerHandle); + + if (controller.JoystickHandle != IntPtr.Zero) + SDL_JoystickClose(controller.JoystickHandle); + } + } + /// /// Populates with joysticks that are already connected. /// @@ -343,12 +356,7 @@ namespace osu.Framework.Platform.SDL2 break; case SDL_EventType.SDL_JOYDEVICEREMOVED: - // if the joystick is already closed, ignore it - if (!controllers.ContainsKey(evtJdevice.which)) - break; - - SDL_JoystickClose(controllers[evtJdevice.which].JoystickHandle); - controllers.Remove(evtJdevice.which); + removeJoystick(evtJdevice.which); break; } } diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs index 4f10522e6..e06f6efb0 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs @@ -324,8 +324,7 @@ namespace osu.Framework.Platform.SDL3 break; case SDL_EventType.SDL_EVENT_GAMEPAD_REMOVED: - SDL_CloseGamepad(controllers[evtCdevice.which].GamepadHandle); - controllers.Remove(evtCdevice.which); + removeJoystick(evtCdevice.which); break; case SDL_EventType.SDL_EVENT_GAMEPAD_REMAPPED: @@ -370,6 +369,18 @@ namespace osu.Framework.Platform.SDL3 controllers[instanceID] = new SDL3ControllerBindings(joystick, controller); } + private void removeJoystick(SDL_JoystickID instanceID) + { + if (controllers.Remove(instanceID, out var controller)) + { + if (controller.GamepadHandle != null) + SDL_CloseGamepad(controller.GamepadHandle); + + if (controller.JoystickHandle != null) + SDL_CloseJoystick(controller.JoystickHandle); + } + } + /// /// Populates with joysticks that are already connected. /// @@ -395,12 +406,7 @@ namespace osu.Framework.Platform.SDL3 break; case SDL_EventType.SDL_EVENT_JOYSTICK_REMOVED: - // if the joystick is already closed, ignore it - if (!controllers.ContainsKey(evtJdevice.which)) - break; - - SDL_CloseJoystick(controllers[evtJdevice.which].JoystickHandle); - controllers.Remove(evtJdevice.which); + removeJoystick(evtJdevice.which); break; } }