- Cursor_Normal/Paint/Pipette: generierte Grafiken jetzt in Vollauflösung (766-829 px, Max Size im Importer steuert Anzeigegröße), Paint-Ring-Inneres freigestellt - Neuer Cursor_Blank (transparent): wird während eines Strokes auf dem Mesh gesetzt statt Cursor.visible zu togglen (fixt Flicker-Kampf mit dem Orbit-Input) - CursorController: Hotspots normalisiert (0-1, unabhängig von Import Max Size); Zustandskette Pipette > (Stroke ? Blank : Paint) > Normal; off-mesh während Stroke -> Normal - MeshPainter: StrokeInProgress-Flag (Maustaste auf Paint-Pfad gehalten) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
114 lines
4.9 KiB
C#
114 lines
4.9 KiB
C#
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<GameModeManager>();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|