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, in pixels from the top-left.")]
[SerializeField] private Vector2 normalHotspot = Vector2.zero;
[SerializeField] private Texture2D paintCursor;
[SerializeField] private Vector2 paintHotspot = new(16f, 16f);
[SerializeField] private Texture2D pipetteCursor;
[SerializeField] private Vector2 pipetteHotspot = new(1f, 30f);
private enum State { None, Normal, Paint, Pipette }
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.
else if (painter.BrushPreviewVisible) want = 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.Paint: Cursor.SetCursor(paintCursor, paintHotspot, mode); break;
case State.Pipette: Cursor.SetCursor(pipetteCursor, pipetteHotspot, mode); break;
default: Cursor.SetCursor(normalCursor, normalHotspot, mode); break;
}
}
private void OnDisable()
{
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto); // back to the OS cursor
_applied = State.None;
_appliedMode = CursorMode.Auto;
}
}