Menu: stylus sticks out of the hand front and matches the touch point; haptic ticks for hover, release, sliders, scrolling
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2f2fcd0aa2
commit
d0b50fbc41
6 files changed
+95
-6
No files matched your search
+79
-1
@@ -22,7 +22,12 @@ using System;
|
||||
// sooner = a tap: press + release are sent together at the contact point. Thumbstick Y = wheel.
|
||||
public partial class MenuScreen : Node3D
|
||||
{
|
||||
public const float TipLength = 0.12f;
|
||||
// The touch point IS the drawn stylus tip (Locomotion draws the stylus from StylusBase to
|
||||
// TipLength): a short "finger" sticking out of the front of the 12 cm hand box. It used to
|
||||
// be a cone from the palm center ending 3 cm short of the real touch point (owner: "the
|
||||
// stylus is in the middle of the palm").
|
||||
public const float StylusBase = 0.06f;
|
||||
public const float TipLength = 0.11f;
|
||||
public const float HoverDepth = 0.06f;
|
||||
public const float ContactDepth = 0.006f;
|
||||
public const float ReleaseDepth = 0.018f;
|
||||
@@ -64,6 +69,14 @@ public partial class MenuScreen : Node3D
|
||||
|
||||
// Fired when a hand presses down on the UI (touch contact or trigger) — for haptics.
|
||||
public event Action<int, PointerMode> PointerPressed;
|
||||
// Light feedback ticks (hand, amplitude 0..1, seconds): hovering a new control, click
|
||||
// release, slider steps, scroll detents. Press itself stays on PointerPressed.
|
||||
public event Action<int, float, float> Haptic;
|
||||
public const float HoverTick = 0.12f;
|
||||
public const float ReleaseTick = 0.25f;
|
||||
public const float DetentTick = 0.08f;
|
||||
public const float SwipeDetentPixels = 48f;
|
||||
public const int SliderDetents = 20;
|
||||
|
||||
private readonly HandState[] _hands = new HandState[2];
|
||||
private readonly MeshInstance3D[] _dots = new MeshInstance3D[2];
|
||||
@@ -83,6 +96,10 @@ public partial class MenuScreen : Node3D
|
||||
private float _swipeVelocity;
|
||||
private ScrollContainer _flingTarget;
|
||||
private float _flingVelocity;
|
||||
private Control _hoverControl;
|
||||
private Godot.Range _dragRange;
|
||||
private double _dragRangeTicked;
|
||||
private float _swipeTickFrom;
|
||||
private StandardMaterial3D _screenMaterial;
|
||||
|
||||
public MenuScreen() : this(new Vector2(0.36f, 0.24f), new Vector2I(960, 640)) { }
|
||||
@@ -147,6 +164,8 @@ public partial class MenuScreen : Node3D
|
||||
_down = false;
|
||||
_pending = false;
|
||||
_flingTarget = null;
|
||||
_dragRange = null;
|
||||
_hoverControl = null;
|
||||
_owner = -1;
|
||||
if (_mouseInside)
|
||||
{
|
||||
@@ -295,6 +314,7 @@ public partial class MenuScreen : Node3D
|
||||
if (h.Pixel != _lastSent)
|
||||
SendMotion(h.Pixel);
|
||||
int current = _owner;
|
||||
UpdateHover(current);
|
||||
if (h.WantDown != _down)
|
||||
{
|
||||
_down = h.WantDown;
|
||||
@@ -304,16 +324,27 @@ public partial class MenuScreen : Node3D
|
||||
if (h.Mode == PointerMode.Touch && FindSwipeTarget() is ScrollContainer target)
|
||||
BeginPending(target, h.Pixel);
|
||||
else
|
||||
{
|
||||
_dragRange = Interactable(Viewport.GuiGetHoveredControl()) as Godot.Range;
|
||||
if (_dragRange != null)
|
||||
_dragRangeTicked = _dragRange.Value;
|
||||
SendButton(h.Pixel, true);
|
||||
}
|
||||
PointerPressed?.Invoke(current, h.Mode);
|
||||
}
|
||||
else if (_pending)
|
||||
EndPending();
|
||||
else
|
||||
{
|
||||
SendButton(h.Pixel, false);
|
||||
_dragRange = null;
|
||||
Haptic?.Invoke(current, ReleaseTick, 0.015f);
|
||||
}
|
||||
}
|
||||
else if (_down && _pending)
|
||||
UpdateSwipe(h.Pixel, (float)delta);
|
||||
else if (_down)
|
||||
RangeDetents(current);
|
||||
// A UI handler may have reset us (ReleaseAll) during the injected event.
|
||||
if (_owner == current && current < inputs.Length)
|
||||
Scroll(inputs[current].Stick, h.Pixel, (float)delta);
|
||||
@@ -323,6 +354,48 @@ public partial class MenuScreen : Node3D
|
||||
UpdateDots();
|
||||
}
|
||||
|
||||
// The nearest control (hovered or an ancestor) that reacts to clicks.
|
||||
private static Control Interactable(Control c)
|
||||
{
|
||||
for (Node n = c; n is Control control; n = n.GetParent())
|
||||
if (control is BaseButton or Godot.Range or TabBar)
|
||||
return control;
|
||||
return null;
|
||||
}
|
||||
|
||||
private void UpdateHover(int hand)
|
||||
{
|
||||
Control hovered = Interactable(Viewport.GuiGetHoveredControl());
|
||||
if (hovered == _hoverControl)
|
||||
return;
|
||||
_hoverControl = hovered;
|
||||
bool enabled = hovered is not BaseButton b || !b.Disabled;
|
||||
if (hovered != null && enabled && !(_down && _pending && _swiping))
|
||||
Haptic?.Invoke(hand, HoverTick, 0.01f);
|
||||
}
|
||||
|
||||
// A slider being dragged ticks every 1/SliderDetents of its range.
|
||||
private void RangeDetents(int hand)
|
||||
{
|
||||
if (_dragRange == null || !IsInstanceValid(_dragRange))
|
||||
return;
|
||||
double step = (_dragRange.MaxValue - _dragRange.MinValue) / SliderDetents;
|
||||
if (step > 0 && Math.Abs(_dragRange.Value - _dragRangeTicked) >= step)
|
||||
{
|
||||
_dragRangeTicked = _dragRange.Value;
|
||||
Haptic?.Invoke(hand, DetentTick, 0.008f);
|
||||
}
|
||||
}
|
||||
|
||||
private void Detent(float scroll)
|
||||
{
|
||||
if (Mathf.Abs(scroll - _swipeTickFrom) < SwipeDetentPixels)
|
||||
return;
|
||||
_swipeTickFrom = scroll;
|
||||
if (_owner >= 0)
|
||||
Haptic?.Invoke(_owner, DetentTick, 0.006f);
|
||||
}
|
||||
|
||||
// The scroll container a touch at the current mouse position would swipe: walk up from the
|
||||
// hovered control; a Range (slider, scrollbar) on the way keeps the normal immediate press.
|
||||
private ScrollContainer FindSwipeTarget()
|
||||
@@ -361,6 +434,7 @@ public partial class MenuScreen : Node3D
|
||||
_swiping = true;
|
||||
_swipeLastY = pixel.Y;
|
||||
_swipeScroll = _swipeTarget.ScrollVertical;
|
||||
_swipeTickFrom = _swipeScroll;
|
||||
return;
|
||||
}
|
||||
// Content follows the tip: dragging up reveals what is below.
|
||||
@@ -369,6 +443,7 @@ public partial class MenuScreen : Node3D
|
||||
_swipeScroll -= dy;
|
||||
if (!SetScroll(_swipeTarget))
|
||||
_swipeScroll = _swipeTarget.ScrollVertical; // at an end: no stored overscroll
|
||||
Detent(_swipeScroll);
|
||||
if (delta > 0f)
|
||||
_swipeVelocity = Mathf.Lerp(_swipeVelocity, -dy / delta, 1f - Mathf.Exp(-delta * 30f));
|
||||
}
|
||||
@@ -389,6 +464,7 @@ public partial class MenuScreen : Node3D
|
||||
SendMotion(_pressPixel);
|
||||
SendButton(_pressPixel, true);
|
||||
SendButton(_pressPixel, false);
|
||||
Haptic?.Invoke(_owner, ReleaseTick, 0.015f);
|
||||
if (back != _pressPixel)
|
||||
SendMotion(back);
|
||||
}
|
||||
@@ -516,6 +592,8 @@ public partial class MenuScreen : Node3D
|
||||
bool up = _scrollAccum > 0f;
|
||||
_scrollAccum -= up ? 1f : -1f;
|
||||
SendWheel(pixel, up ? MouseButton.WheelUp : MouseButton.WheelDown);
|
||||
if (_owner >= 0)
|
||||
Haptic?.Invoke(_owner, DetentTick * 0.75f, 0.006f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user