using UnityEngine; /// /// Swaps the hardware mouse cursor with the paint state of the local painter: normal outside /// paint mode, brush while painting, eyedropper while the pipette is active. Lives once in the /// Main scene; remote painters never have PaintingEnabled, so polling the manager's local /// painter is unambiguous. While a camera rig locks/hides the cursor this is simply invisible. /// public class CursorController : MonoBehaviour { [Tooltip("Source of the local painter. Found automatically when left empty.")] [SerializeField] private GameModeManager modeManager; [Header("Cursors (import type: Cursor)")] [SerializeField] private Texture2D normalCursor; [Tooltip("Click point inside the texture, normalized 0-1 from the top-left. Independent of the importer's Max Size.")] [SerializeField] private Vector2 normalHotspot = Vector2.zero; [SerializeField] private Texture2D paintCursor; [SerializeField] private Vector2 paintHotspot = new(0.5f, 0.5f); [SerializeField] private Texture2D pipetteCursor; [SerializeField] private Vector2 pipetteHotspot = new(0f, 1f); [Tooltip("Fully transparent texture shown while a stroke is running - swapping the cursor " + "image avoids fighting the camera rigs over Cursor.visible (which caused flicker).")] [SerializeField] private Texture2D blankCursor; private enum State { None, Normal, Paint, Pipette, Blank } private State _applied = State.None; private CursorMode _appliedMode = CursorMode.Auto; #if UNITY_EDITOR // Hardware cursors are composited by the OS and never end up in Unity Recorder captures. // While a recording runs we force the software cursor (rendered by Unity -> captured). // RecorderWindow lives in the editor-only Unity.Recorder.Editor assembly, hence reflection. private static readonly System.Type RecorderWindowType = System.Type.GetType("UnityEditor.Recorder.RecorderWindow, Unity.Recorder.Editor"); private static readonly System.Reflection.MethodInfo RecorderIsRecording = RecorderWindowType?.GetMethod("IsRecording"); private Object _recorderWindow; private float _nextRecorderPoll; private bool _recorderActive; private bool RecorderActive() { if (RecorderIsRecording == null) return false; if (Time.unscaledTime < _nextRecorderPoll) return _recorderActive; _nextRecorderPoll = Time.unscaledTime + 0.5f; if (!_recorderWindow) { var windows = Resources.FindObjectsOfTypeAll(RecorderWindowType); _recorderWindow = windows.Length > 0 ? windows[0] : null; } _recorderActive = _recorderWindow && (bool)RecorderIsRecording.Invoke(_recorderWindow, null); return _recorderActive; } #endif private void Awake() { if (!modeManager) modeManager = FindFirstObjectByType(); } // LateUpdate: MeshPainter.Update settles this frame's pipette state (Shift/Ctrl momentary // pipette) first, so the cursor never lags a frame behind the modifier. private void LateUpdate() { var painter = modeManager ? modeManager.Painter : null; State want = State.Normal; // Over UI neither the pipette nor the brush can act -> normal arrow. if (painter && painter.PaintingEnabled && !InputUtil.PointerOverUi()) { if (painter.pipette) want = State.Pipette; // Paint cursor only while painting is actually possible, i.e. the brush preview // decal is drawn at the hover point this frame (hovering off the mesh -> normal, // also mid-stroke). While the stroke touches the mesh the decal alone marks the // brush position -> blank cursor. else if (painter.BrushPreviewVisible) want = painter.StrokeInProgress ? State.Blank : State.Paint; } var mode = CursorMode.Auto; #if UNITY_EDITOR if (RecorderActive()) mode = CursorMode.ForceSoftware; #endif if (want == _applied && mode == _appliedMode) return; _applied = want; _appliedMode = mode; switch (want) { case State.Blank: Apply(blankCursor, Vector2.zero, mode); break; case State.Paint: Apply(paintCursor, paintHotspot, mode); break; case State.Pipette: Apply(pipetteCursor, pipetteHotspot, mode); break; default: Apply(normalCursor, normalHotspot, mode); break; } } private static void Apply(Texture2D cursor, Vector2 normalizedHotspot, CursorMode mode) { var hotspot = cursor ? Vector2.Scale(normalizedHotspot, new Vector2(cursor.width, cursor.height)) : Vector2.zero; Cursor.SetCursor(cursor, hotspot, mode); } private void OnDisable() { Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto); // back to the OS cursor _applied = State.None; _appliedMode = CursorMode.Auto; } }