Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Optional UI-occlusion guard: when enabled, handle interactions do not start while the pointer
is over a uGUI element. Opt-in via `TransformHandleManager.BlockWhenPointerOverUI` (serialized,
**off by default** so it never silently changes existing input behavior); an interaction already
in progress is never interrupted, and touch is handled via the active finger's pointer id.
Requires the uGUI package and an `EventSystem` in the scene — projects without uGUI are
unaffected (no-op via the `TH_UGUI` version define). Override `IsPointerOverUI()` to integrate a
non-uGUI UI stack.
The Demo sample now spawns a uGUI panel and an EventSystem and exposes a HUD toggle so the
guard can be tried directly: with it on, clicking the panel does not disturb objects behind it.

## [3.1.0] - 2026-06-11

### Added
Expand Down
14 changes: 14 additions & 0 deletions Packages/com.orkunmanap.runtime-transform-handles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ type/space/axes, and highlight color, then assign it:
TransformHandleManager.Instance.Settings = mySettings;
```

### Blocking interaction over UI (opt-in)

Optionally, a handle interaction can be prevented from starting while the pointer is over a uGUI
element, so clicking a button or panel above the scene does not begin a drag (an interaction
already in progress is never interrupted; touch uses the active finger). It is **off by default**
so it never silently changes input behavior. Needs an `EventSystem` and the uGUI package; projects
without uGUI are unaffected. Enable it at runtime or in the Inspector:

```csharp
TransformHandleManager.Instance.BlockWhenPointerOverUI = true;
```

For a non-uGUI UI stack, subclass and override `IsPointerOverUI()`.

## Default keyboard shortcuts

| Key | Action |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public Camera MainCamera
[SerializeField] private string handleLayerName = "TransformHandle";
[SerializeField] private Color highlightColor = Color.white;

[Tooltip("Opt-in: when enabled, a handle interaction will not start while the pointer is " +
"over a uGUI element (requires an EventSystem in the scene). An interaction " +
"already in progress is never interrupted, even if the pointer moves over UI. " +
"Off by default so it never silently changes existing input behavior.")]
[SerializeField] private bool blockWhenPointerOverUI;

[Header("Shortcuts (used when no Settings asset is assigned)")]
[SerializeField] private KeyCode positionShortcut = KeyCode.W;
[SerializeField] private KeyCode rotationShortcut = KeyCode.E;
Expand All @@ -59,6 +65,18 @@ public TransformHandleSettings Settings
set => settings = value;
}

/// <summary>
/// Opt-in. When true, a handle interaction will not start while the pointer is over a uGUI
/// element (requires the uGUI package and an EventSystem in the scene). An in-progress
/// interaction is never interrupted. Defaults to false so enabling the package never
/// silently changes input behavior. Has no effect when uGUI is not installed.
/// </summary>
public bool BlockWhenPointerOverUI
{
get => blockWhenPointerOverUI;
set => blockWhenPointerOverUI = value;
}

// Properties that check settings first, then fall back to serialized fields
private bool ShortcutsEnabled => settings == null || settings.EnableShortcuts;
private KeyCode PositionKey => settings != null ? settings.PositionKey : positionShortcut;
Expand Down Expand Up @@ -407,7 +425,14 @@ protected virtual void Update()
_hoveredHandle = null;
_handleHitPoint = Vector3.zero;

GetHandle(ref _hoveredHandle, ref _handleHitPoint);
// Leaving the hovered handle null while the pointer is over UI both clears the
// highlight and prevents MouseInput from starting a drag (it requires a hovered
// handle). An interaction already in progress is unaffected — this branch only
// runs when not dragging.
if (!IsPointerOverUI())
{
GetHandle(ref _hoveredHandle, ref _handleHitPoint);
}

HandleOverEffect(_hoveredHandle);
}
Expand All @@ -416,6 +441,29 @@ protected virtual void Update()
KeyboardInput();
}

/// <summary>
/// Whether the pointer is currently over a uGUI element that should suppress starting a
/// handle interaction. Returns false when <see cref="BlockWhenPointerOverUI"/> is disabled,
/// no EventSystem is present, or the uGUI package is not installed. Override to plug in a
/// different UI stack.
/// </summary>
protected virtual bool IsPointerOverUI()
{
if (!blockWhenPointerOverUI) return false;
#if TH_UGUI
var eventSystem = UnityEngine.EventSystems.EventSystem.current;
if (eventSystem == null) return false;

// The no-arg overload only queries the mouse pointer. The package treats the first
// active touch as the primary pointer (see InputWrapper), so on touch devices also
// test that finger's pointer id — otherwise a tap over UI bypasses the guard.
if (eventSystem.IsPointerOverGameObject()) return true;
return HasActiveTouch && eventSystem.IsPointerOverGameObject(PrimaryTouchPointerId);
#else
return false;
#endif
Comment thread
cursor[bot] marked this conversation as resolved.
}

protected virtual void GetHandle(ref HandleBase handle, ref Vector3 hitPoint)
{
// Unity's overloaded == reports a destroyed camera as null, so this proactive check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ public static Vector2 TouchPosition
}
}

/// <summary>
/// Pointer id of the primary active touch, for passing to
/// <c>EventSystem.IsPointerOverGameObject(int)</c>. Returns -1 when no touch is active
/// (-1 is the mouse/default pointer id, so the result also covers the mouse case).
/// Only meaningful while <see cref="HasActiveTouch"/> is true.
/// </summary>
public static int PrimaryTouchPointerId
{
get
{
#if ENABLE_INPUT_SYSTEM && TH_INPUTSYSTEM
EnsureTouchInitialized();
if (Touch.activeTouches.Count > 0)
return Touch.activeTouches[0].touchId;
return -1;
#else
if (Input.touchCount > 0)
return Input.GetTouch(0).fingerId;
return -1;
#endif
}
}

/// <summary>
/// Gets the current pointer position (mouse or touch) in screen coordinates.
/// Prioritizes touch input on touch devices when a touch is active.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "com.orkunmanap.runtime-transform-handles",
"rootNamespace": "TransformHandles",
"references": [
"GUID:75469ad4d38634e559750d17036d5f7c"
"GUID:75469ad4d38634e559750d17036d5f7c",
"GUID:2bafac87e7f4b9b418d9448d219b01ab"
],
"includePlatforms": [],
"excludePlatforms": [],
Expand All @@ -16,6 +17,11 @@
"name": "com.unity.inputsystem",
"expression": "1.0.0",
"define": "TH_INPUTSYSTEM"
},
{
"name": "com.unity.ugui",
"expression": "1.0.0",
"define": "TH_UGUI"
}
],
"noEngineReferences": false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
using TransformHandles;
using TransformHandles.Utils;
using UnityEngine;
#if TH_UGUI
using UnityEngine.EventSystems;
using UnityEngine.UI;
#endif

/// <summary>
/// Self-contained showcase that exercises the whole public surface of the package:
Expand Down Expand Up @@ -105,6 +109,79 @@ private void Start()
l.type = LightType.Directional;
lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
}

BuildUiOcclusionOverlay();
}

// Builds a real uGUI panel (+ an EventSystem if the scene lacks one) so the
// TransformHandleManager.BlockWhenPointerOverUI guard is demonstrable: with the guard on,
// clicking the panel must not select targets or start a handle drag underneath it.
private void BuildUiOcclusionOverlay()
{
#if TH_UGUI
// The guard is opt-in (off by default on the manager); turn it on here so the sample
// demonstrates it out of the box. Flip it from the HUD to compare on/off behavior.
_manager.BlockWhenPointerOverUI = true;
#if UNITY_2023_1_OR_NEWER
var hasEventSystem = Object.FindAnyObjectByType<EventSystem>() != null;
#else
var hasEventSystem = Object.FindObjectOfType<EventSystem>() != null;
#endif
if (!hasEventSystem)
{
var esGo = new GameObject("EventSystem");
esGo.AddComponent<EventSystem>();
#if ENABLE_INPUT_SYSTEM && TH_INPUTSYSTEM
esGo.AddComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>().AssignDefaultActions();
#else
esGo.AddComponent<StandaloneInputModule>();
#endif
}

var canvasGo = new GameObject("Demo UI Canvas");
var canvas = canvasGo.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGo.AddComponent<CanvasScaler>();
canvasGo.AddComponent<GraphicRaycaster>();

var panelGo = new GameObject("UI Occlusion Test Panel");
panelGo.transform.SetParent(canvasGo.transform, false);
var panel = panelGo.AddComponent<Image>();
panel.color = new Color(0.15f, 0.45f, 0.85f, 0.85f); // raycast target by default
var rect = panel.rectTransform;
rect.anchorMin = rect.anchorMax = new Vector2(0.5f, 1f);
rect.pivot = new Vector2(0.5f, 1f);
rect.sizeDelta = new Vector2(420f, 64f);
rect.anchoredPosition = new Vector2(0f, -12f);

var labelGo = new GameObject("Label");
labelGo.transform.SetParent(panelGo.transform, false);
var label = labelGo.AddComponent<Text>();
label.text = "uGUI panel — clicks here must NOT move objects\n(toggle the guard in the HUD)";
label.alignment = TextAnchor.MiddleCenter;
label.color = Color.white;
label.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf")
?? Resources.GetBuiltinResource<Font>("Arial.ttf");
var labelRect = label.rectTransform;
labelRect.anchorMin = Vector2.zero;
labelRect.anchorMax = Vector2.one;
labelRect.offsetMin = labelRect.offsetMax = Vector2.zero;
#endif
}

// True while the pointer is over a uGUI element, so the demo's own target picking respects
// UI occlusion the same way the handle manager does. No-op without uGUI.
private bool PointerOverUi()
{
#if TH_UGUI
var es = EventSystem.current;
if (es == null) return false;
if (es.IsPointerOverGameObject()) return true; // mouse / default pointer
// Also test the active finger so the guard works under touch (mouse-only otherwise).
return InputWrapper.HasActiveTouch && es.IsPointerOverGameObject(InputWrapper.PrimaryTouchPointerId);
#else
return false;
#endif
}

private void Update()
Expand All @@ -118,6 +195,9 @@ private void Update()
private void HandleSelectionInput()
{
if (_interacting) return; // don't pick targets mid-drag
// Respect the same toggle the handle manager uses, so flipping it off in the HUD lets
// both picking and handle drags pass through the panel (matching the on-screen hint).
if (_manager.BlockWhenPointerOverUI && PointerOverUi()) return;

// Left click: select (Shift adds to the current handle's group).
if (InputWrapper.GetMouseButtonDown(0) && TryPickTarget(out var picked))
Expand Down Expand Up @@ -332,6 +412,10 @@ private void OnGUI()
var useSettings = GUILayout.Toggle(_useSettings, " Apply runtime Settings asset");
if (useSettings != _useSettings) { _useSettings = useSettings; if (_useSettings && _activeHandle != null) _activeHandle.ApplySettings(_settings); }

var blockUi = GUILayout.Toggle(_manager.BlockWhenPointerOverUI, " Block interaction over UI");
if (blockUi != _manager.BlockWhenPointerOverUI) _manager.BlockWhenPointerOverUI = blockUi;
GUILayout.Label("<size=11>Click the blue panel (top): blocked when on, drags through when off.</size>");

GUILayout.Space(6);
GUILayout.BeginHorizontal();
if (GUILayout.Button("Group all")) GroupAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"name": "TransformHandles.Samples.Demo",
"rootNamespace": "",
"references": [
"com.orkunmanap.runtime-transform-handles"
"com.orkunmanap.runtime-transform-handles",
"GUID:2bafac87e7f4b9b418d9448d219b01ab",
"GUID:75469ad4d38634e559750d17036d5f7c"
],
"includePlatforms": [],
"excludePlatforms": [],
Expand All @@ -11,6 +13,17 @@
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"versionDefines": [
{
"name": "com.unity.ugui",
"expression": "1.0.0",
"define": "TH_UGUI"
},
{
"name": "com.unity.inputsystem",
"expression": "1.0.0",
"define": "TH_INPUTSYSTEM"
}
],
"noEngineReferences": false
}
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ TransformHandleManager.Instance.Settings = mySettings;

If no settings asset is assigned, the manager uses its serialized field values.

### Blocking Interaction Over UI (opt-in)

Optionally, a handle interaction can be prevented from starting while the pointer is over a uGUI
element, so clicking a button or panel above the scene does not begin a drag (an in-progress
interaction is never interrupted; touch uses the active finger). It is **off by default** so it
never silently changes input behavior. Requires an `EventSystem` and the uGUI package; projects
without uGUI are unaffected. Enable in the Inspector or via code, and override `IsPointerOverUI()`
for a non-uGUI UI stack:

```csharp
TransformHandleManager.Instance.BlockWhenPointerOverUI = true;
```

## Default Keyboard Shortcuts

| Key | Action |
Expand Down
1 change: 1 addition & 0 deletions ci/floor-2021/Packages/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"dependencies": {
"com.orkunmanap.runtime-transform-handles": "file:../../../Packages/com.orkunmanap.runtime-transform-handles",
"com.unity.test-framework": "1.1.33",
"com.unity.ugui": "1.0.0",
"com.unity.modules.ai": "1.0.0",
"com.unity.modules.androidjni": "1.0.0",
"com.unity.modules.animation": "1.0.0",
Expand Down
Loading