diff --git a/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md b/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
index 6353f7b..9f98cbe 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
+++ b/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
@@ -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
diff --git a/Packages/com.orkunmanap.runtime-transform-handles/README.md b/Packages/com.orkunmanap.runtime-transform-handles/README.md
index c1fdcef..6dd23d4 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/README.md
+++ b/Packages/com.orkunmanap.runtime-transform-handles/README.md
@@ -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 |
diff --git a/Packages/com.orkunmanap.runtime-transform-handles/Runtime/Scripts/TransformHandleManager.cs b/Packages/com.orkunmanap.runtime-transform-handles/Runtime/Scripts/TransformHandleManager.cs
index d3fffb8..5312c51 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/Runtime/Scripts/TransformHandleManager.cs
+++ b/Packages/com.orkunmanap.runtime-transform-handles/Runtime/Scripts/TransformHandleManager.cs
@@ -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;
@@ -59,6 +65,18 @@ public TransformHandleSettings Settings
set => settings = value;
}
+ ///
+ /// 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.
+ ///
+ 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;
@@ -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);
}
@@ -416,6 +441,29 @@ protected virtual void Update()
KeyboardInput();
}
+ ///
+ /// Whether the pointer is currently over a uGUI element that should suppress starting a
+ /// handle interaction. Returns false when is disabled,
+ /// no EventSystem is present, or the uGUI package is not installed. Override to plug in a
+ /// different UI stack.
+ ///
+ 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
+ }
+
protected virtual void GetHandle(ref HandleBase handle, ref Vector3 hitPoint)
{
// Unity's overloaded == reports a destroyed camera as null, so this proactive check
diff --git a/Packages/com.orkunmanap.runtime-transform-handles/Runtime/Scripts/Utils/InputWrapper.cs b/Packages/com.orkunmanap.runtime-transform-handles/Runtime/Scripts/Utils/InputWrapper.cs
index a418332..519ccf9 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/Runtime/Scripts/Utils/InputWrapper.cs
+++ b/Packages/com.orkunmanap.runtime-transform-handles/Runtime/Scripts/Utils/InputWrapper.cs
@@ -73,6 +73,29 @@ public static Vector2 TouchPosition
}
}
+ ///
+ /// Pointer id of the primary active touch, for passing to
+ /// EventSystem.IsPointerOverGameObject(int). 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 is true.
+ ///
+ 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
+ }
+ }
+
///
/// Gets the current pointer position (mouse or touch) in screen coordinates.
/// Prioritizes touch input on touch devices when a touch is active.
diff --git a/Packages/com.orkunmanap.runtime-transform-handles/Runtime/com.orkunmanap.runtime-transform-handles.asmdef b/Packages/com.orkunmanap.runtime-transform-handles/Runtime/com.orkunmanap.runtime-transform-handles.asmdef
index cb57178..f689afa 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/Runtime/com.orkunmanap.runtime-transform-handles.asmdef
+++ b/Packages/com.orkunmanap.runtime-transform-handles/Runtime/com.orkunmanap.runtime-transform-handles.asmdef
@@ -2,7 +2,8 @@
"name": "com.orkunmanap.runtime-transform-handles",
"rootNamespace": "TransformHandles",
"references": [
- "GUID:75469ad4d38634e559750d17036d5f7c"
+ "GUID:75469ad4d38634e559750d17036d5f7c",
+ "GUID:2bafac87e7f4b9b418d9448d219b01ab"
],
"includePlatforms": [],
"excludePlatforms": [],
@@ -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
diff --git a/Packages/com.orkunmanap.runtime-transform-handles/Samples~/Demo/HandleDemo.cs b/Packages/com.orkunmanap.runtime-transform-handles/Samples~/Demo/HandleDemo.cs
index b93b5ab..7df7b10 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/Samples~/Demo/HandleDemo.cs
+++ b/Packages/com.orkunmanap.runtime-transform-handles/Samples~/Demo/HandleDemo.cs
@@ -3,6 +3,10 @@
using TransformHandles;
using TransformHandles.Utils;
using UnityEngine;
+#if TH_UGUI
+using UnityEngine.EventSystems;
+using UnityEngine.UI;
+#endif
///
/// Self-contained showcase that exercises the whole public surface of the package:
@@ -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() != null;
+#else
+ var hasEventSystem = Object.FindObjectOfType() != null;
+#endif
+ if (!hasEventSystem)
+ {
+ var esGo = new GameObject("EventSystem");
+ esGo.AddComponent();
+#if ENABLE_INPUT_SYSTEM && TH_INPUTSYSTEM
+ esGo.AddComponent().AssignDefaultActions();
+#else
+ esGo.AddComponent();
+#endif
+ }
+
+ var canvasGo = new GameObject("Demo UI Canvas");
+ var canvas = canvasGo.AddComponent