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(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvasGo.AddComponent(); + canvasGo.AddComponent(); + + var panelGo = new GameObject("UI Occlusion Test Panel"); + panelGo.transform.SetParent(canvasGo.transform, false); + var panel = panelGo.AddComponent(); + 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(); + 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("LegacyRuntime.ttf") + ?? Resources.GetBuiltinResource("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() @@ -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)) @@ -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("Click the blue panel (top): blocked when on, drags through when off."); + GUILayout.Space(6); GUILayout.BeginHorizontal(); if (GUILayout.Button("Group all")) GroupAll(); diff --git a/Packages/com.orkunmanap.runtime-transform-handles/Samples~/Demo/TransformHandles.Samples.Demo.asmdef b/Packages/com.orkunmanap.runtime-transform-handles/Samples~/Demo/TransformHandles.Samples.Demo.asmdef index fb086df..19a0586 100644 --- a/Packages/com.orkunmanap.runtime-transform-handles/Samples~/Demo/TransformHandles.Samples.Demo.asmdef +++ b/Packages/com.orkunmanap.runtime-transform-handles/Samples~/Demo/TransformHandles.Samples.Demo.asmdef @@ -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": [], @@ -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 } diff --git a/README.md b/README.md index 66c4ba0..688451e 100644 --- a/README.md +++ b/README.md @@ -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 | 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",