From 600681c3e179053f1e3f045f982752860d6e30f3 Mon Sep 17 00:00:00 2001
From: Orkun Manap <31966136+manaporkun@users.noreply.github.com>
Date: Thu, 11 Jun 2026 15:19:35 +0200
Subject: [PATCH 1/5] feat: block handle interaction start when pointer is over
uGUI
A handle interaction no longer starts 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. Exposed as
TransformHandleManager.BlockWhenPointerOverUI (serialized, default on) and
the overridable IsPointerOverUI() seam.
uGUI is an optional dependency via the TH_UGUI version define (mirrors the
Input System integration): projects without com.unity.ugui compile cleanly
and the guard is a no-op. The 2021.3 floor CI project gains com.unity.ugui
so the guarded path is exercised on the floor leg too.
---
.../CHANGELOG.md | 7 ++++
.../README.md | 13 ++++++
.../Runtime/Scripts/TransformHandleManager.cs | 42 ++++++++++++++++++-
...rkunmanap.runtime-transform-handles.asmdef | 8 +++-
README.md | 12 ++++++
ci/floor-2021/Packages/manifest.json | 1 +
6 files changed, 81 insertions(+), 2 deletions(-)
diff --git a/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md b/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
index 6353f7b..8606b7b 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
+++ b/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- UI-occlusion guard: handle interactions no longer start while the pointer is over a uGUI
+ element. Controlled by `TransformHandleManager.BlockWhenPointerOverUI` (serialized, default on);
+ an interaction already in progress is never interrupted. Requires the uGUI package and an
+ `EventSystem` in the scene — projects without uGUI are unaffected (the guard is a no-op via the
+ `TH_UGUI` version define). Override `IsPointerOverUI()` to integrate a non-uGUI UI stack.
+
## [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..669b915 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/README.md
+++ b/Packages/com.orkunmanap.runtime-transform-handles/README.md
@@ -92,6 +92,19 @@ type/space/axes, and highlight color, then assign it:
TransformHandleManager.Instance.Settings = mySettings;
```
+### Blocking interaction over UI
+
+By default a handle interaction will not start while the pointer is over a uGUI element, so
+clicking a button or panel rendered above the scene does not begin a drag (an interaction already
+in progress is never interrupted). This needs an `EventSystem` in the scene and the uGUI package;
+projects without uGUI are unaffected. Toggle it at runtime or in the Inspector:
+
+```csharp
+TransformHandleManager.Instance.BlockWhenPointerOverUI = false;
+```
+
+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..bc30d10 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,11 @@ public Camera MainCamera
[SerializeField] private string handleLayerName = "TransformHandle";
[SerializeField] private Color highlightColor = Color.white;
+ [Tooltip("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.")]
+ [SerializeField] private bool blockWhenPointerOverUI = true;
+
[Header("Shortcuts (used when no Settings asset is assigned)")]
[SerializeField] private KeyCode positionShortcut = KeyCode.W;
[SerializeField] private KeyCode rotationShortcut = KeyCode.E;
@@ -59,6 +64,17 @@ public TransformHandleSettings Settings
set => settings = value;
}
+ ///
+ /// 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 true. 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 +423,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 +439,23 @@ 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;
+ return eventSystem != null && eventSystem.IsPointerOverGameObject();
+#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/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/README.md b/README.md
index 66c4ba0..c16f65b 100644
--- a/README.md
+++ b/README.md
@@ -163,6 +163,18 @@ TransformHandleManager.Instance.Settings = mySettings;
If no settings asset is assigned, the manager uses its serialized field values.
+### Blocking Interaction Over UI
+
+By default a handle interaction will not start 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). Requires an `EventSystem` and the uGUI package; projects without uGUI are
+unaffected. Toggle in the Inspector or via code, and override `IsPointerOverUI()` for a non-uGUI
+UI stack:
+
+```csharp
+TransformHandleManager.Instance.BlockWhenPointerOverUI = false;
+```
+
## Default Keyboard Shortcuts
| Key | Action |
diff --git a/ci/floor-2021/Packages/manifest.json b/ci/floor-2021/Packages/manifest.json
index b9a3808..24b9b23 100644
--- a/ci/floor-2021/Packages/manifest.json
+++ b/ci/floor-2021/Packages/manifest.json
@@ -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",
From cef0f8739ed38b7fce5717348815bde7ab47e9c9 Mon Sep 17 00:00:00 2001
From: Orkun Manap <31966136+manaporkun@users.noreply.github.com>
Date: Thu, 11 Jun 2026 15:27:48 +0200
Subject: [PATCH 2/5] feat: demonstrate UI-occlusion guard in the Demo sample
The Demo now spawns a uGUI Canvas + panel (and an EventSystem with the input
module matching the active backend) and adds a HUD toggle for
BlockWhenPointerOverUI, so the guard is directly testable: clicking the panel
must not move objects behind it while the guard is on. The sample's own target
picking now also respects UI occlusion. uGUI/Input System are referenced by
the sample asmdef and gated behind TH_UGUI/TH_INPUTSYSTEM.
---
.../CHANGELOG.md | 2 +
.../Samples~/Demo/HandleDemo.cs | 76 +++++++++++++++++++
.../Demo/TransformHandles.Samples.Demo.asmdef | 17 ++++-
3 files changed, 93 insertions(+), 2 deletions(-)
diff --git a/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md b/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
index 8606b7b..9999846 100644
--- a/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
+++ b/Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
an interaction already in progress is never interrupted. Requires the uGUI package and an
`EventSystem` in the scene — projects without uGUI are unaffected (the guard is a 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
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..c12d7e1 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,73 @@ 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
+#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