diff --git a/Assets/Scripts/GameModeManager.cs b/Assets/Scripts/GameModeManager.cs
index 7fd93a8..85d7aa8 100644
--- a/Assets/Scripts/GameModeManager.cs
+++ b/Assets/Scripts/GameModeManager.cs
@@ -41,6 +41,10 @@ public class GameModeManager : MonoBehaviour
public MeshPainter Painter => painter;
public InsectController Insect => insect;
+ /// Raised after a mode switch finishes (the new mode's Enter has already run).
+ /// Argument = the now-active mode. Used by UI (e.g. the paint HUD) to follow the mode.
+ public event System.Action ModeChanged;
+
private readonly List _modes = new List();
private int _activeIndex = -1;
@@ -105,6 +109,7 @@ public class GameModeManager : MonoBehaviour
ActiveMode.Enter();
Debug.Log($"[GameModeManager] Mode -> {ActiveMode.DisplayName}", this);
+ ModeChanged?.Invoke(ActiveMode);
}
private bool WasSwitchPressed()
diff --git a/Assets/Scripts/HsvColorPicker.cs b/Assets/Scripts/HsvColorPicker.cs
new file mode 100644
index 0000000..bac6c6b
--- /dev/null
+++ b/Assets/Scripts/HsvColorPicker.cs
@@ -0,0 +1,124 @@
+using System;
+using UnityEngine;
+using UnityEngine.UI;
+
+///
+/// Compact runtime HSV colour picker: a saturation/value square, a hue bar and a live
+/// swatch. Builds its own UI as children when is called and raises
+/// on user interaction. The square is a pure-hue base with two
+/// static gradient overlays on top (white-out -> saturation, black-out -> value), so
+/// only the base tint changes per hue - no per-frame texture rebuilds. Hue is never
+/// lost while dragging the square (the square only edits S/V, the bar only edits H).
+///
+public class HsvColorPicker : MonoBehaviour
+{
+ /// Raised with the new colour when the user moves the square or hue bar.
+ public event Action Changed;
+
+ private float _h, _s, _v, _a = 1f;
+
+ private Image _hueBase; // pure-hue background of the SV square
+ private RectTransform _svHandle;
+ private RectTransform _hueHandle;
+ private Image _swatch;
+
+ /// Current colour (HSV -> RGB, with the preserved alpha).
+ public Color Value
+ {
+ get
+ {
+ Color c = Color.HSVToRGB(_h, _s, _v);
+ c.a = _a;
+ return c;
+ }
+ }
+
+ /// Shows without raising (external set).
+ public void SetColor(Color c)
+ {
+ Color.RGBToHSV(c, out _h, out _s, out _v);
+ _a = c.a;
+ Refresh();
+ }
+
+ /// Builds the picker (square + hue bar + swatch) under this object, top-left aligned.
+ public void Build(float squareSize, float hueHeight)
+ {
+ // Saturation / value square: base hue + two gradient overlays, top-left of this rect.
+ var sv = PaintHud.NewRect("SV", transform);
+ TopLeft(sv, new Vector2(0f, 0f), new Vector2(squareSize, squareSize));
+ _hueBase = PaintHud.AddImage(sv.gameObject, Color.red); // raycast target for the drag relay
+
+ PaintHud.AddOverlay(sv, PaintHud.SaturationSprite());
+ PaintHud.AddOverlay(sv, PaintHud.ValueSprite());
+
+ sv.gameObject.AddComponent().Picked += OnSvPicked;
+
+ _svHandle = PaintHud.NewRect("Handle", sv);
+ _svHandle.sizeDelta = new Vector2(12f, 12f);
+ var svHandleImg = PaintHud.AddImage(_svHandle.gameObject, Color.white);
+ svHandleImg.raycastTarget = false;
+
+ // Hue bar below the square.
+ var hue = PaintHud.NewRect("Hue", transform);
+ TopLeft(hue, new Vector2(0f, -(squareSize + 6f)), new Vector2(squareSize, hueHeight));
+ var hueImg = hue.gameObject.AddComponent();
+ hueImg.sprite = PaintHud.HueSprite();
+ hueImg.type = Image.Type.Simple;
+
+ hue.gameObject.AddComponent().Picked += OnHuePicked;
+
+ _hueHandle = PaintHud.NewRect("Handle", hue);
+ _hueHandle.sizeDelta = new Vector2(4f, hueHeight + 4f);
+ var hueHandleImg = PaintHud.AddImage(_hueHandle.gameObject, Color.white);
+ hueHandleImg.raycastTarget = false;
+
+ // Swatch, to the right of the square.
+ var sw = PaintHud.NewRect("Swatch", transform);
+ TopLeft(sw, new Vector2(squareSize + 10f, 0f), new Vector2(34f, 34f));
+ _swatch = PaintHud.AddImage(sw.gameObject, Color.white);
+ _swatch.raycastTarget = false;
+
+ Refresh();
+ }
+
+ private void OnSvPicked(Vector2 p)
+ {
+ _s = p.x;
+ _v = p.y;
+ Refresh();
+ Changed?.Invoke(Value);
+ }
+
+ private void OnHuePicked(Vector2 p)
+ {
+ _h = p.x;
+ Refresh();
+ Changed?.Invoke(Value);
+ }
+
+ private void Refresh()
+ {
+ if (_hueBase) _hueBase.color = Color.HSVToRGB(_h, 1f, 1f);
+ if (_svHandle)
+ {
+ _svHandle.anchorMin = _svHandle.anchorMax = new Vector2(_s, _v);
+ _svHandle.anchoredPosition = Vector2.zero;
+ }
+ if (_hueHandle)
+ {
+ _hueHandle.anchorMin = _hueHandle.anchorMax = new Vector2(_h, 0.5f);
+ _hueHandle.anchoredPosition = Vector2.zero;
+ }
+ if (_swatch) _swatch.color = Color.HSVToRGB(_h, _s, _v);
+ }
+
+ /// Anchors a child to this rect's top-left corner with the given offset and size.
+ private static void TopLeft(RectTransform rt, Vector2 offset, Vector2 size)
+ {
+ rt.anchorMin = rt.anchorMax = new Vector2(0f, 1f);
+ rt.pivot = new Vector2(0f, 1f);
+ rt.anchoredPosition = offset;
+ rt.sizeDelta = size;
+ }
+}
diff --git a/Assets/Scripts/HsvColorPicker.cs.meta b/Assets/Scripts/HsvColorPicker.cs.meta
new file mode 100644
index 0000000..eab3408
--- /dev/null
+++ b/Assets/Scripts/HsvColorPicker.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2a35dda49401e4045ba2165e15116a5f
\ No newline at end of file
diff --git a/Assets/Scripts/MeshPainter.cs b/Assets/Scripts/MeshPainter.cs
index 126ae32..a330c2c 100644
--- a/Assets/Scripts/MeshPainter.cs
+++ b/Assets/Scripts/MeshPainter.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using UnityEngine;
+using UnityEngine.EventSystems;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
@@ -578,6 +579,11 @@ public partial class MeshPainter : MonoBehaviour
if (!TryGetMouse(out bool pressed, out Vector2 screenPos))
return;
+ // Don't paint through the in-game UI: clicking a HUD slider/toggle must not
+ // also stamp the mesh behind it.
+ if (pressed && IsPointerOverUi())
+ pressed = false;
+
// Brush preview (size + mask) at the hover point - independent of painting,
// so you see the brush before pressing the button.
if (previewMode != BrushPreviewMode.Off && _previewMat && paintCamera)
@@ -741,6 +747,13 @@ public partial class MeshPainter : MonoBehaviour
#endif
}
+ /// True if the pointer is currently over an interactive UI element (raycast target).
+ private static bool IsPointerOverUi()
+ {
+ var es = EventSystem.current;
+ return es && es.IsPointerOverGameObject();
+ }
+
private void OnDestroy()
{
if (_bakedMesh) Destroy(_bakedMesh);
diff --git a/Assets/Scripts/PaintHud.cs b/Assets/Scripts/PaintHud.cs
new file mode 100644
index 0000000..c12165b
--- /dev/null
+++ b/Assets/Scripts/PaintHud.cs
@@ -0,0 +1,407 @@
+using System;
+using UnityEngine;
+using UnityEngine.UI;
+using UnityEngine.EventSystems;
+#if ENABLE_INPUT_SYSTEM
+using UnityEngine.InputSystem.UI;
+#endif
+
+///
+/// In-game paint UI. Builds its own Screen Space - Overlay canvas (and an EventSystem
+/// if the scene has none) entirely in code and binds the widgets directly to the live
+/// fields, so changes apply immediately. The panel is shown
+/// only while the active is (it follows
+/// the manager's ModeChanged event).
+///
+/// First, simple pass: channel toggles, an HSV colour picker for albedo, metallic /
+/// smoothness / occlusion sliders, an HSV picker + intensity for emission, and brush
+/// size. Attach to an empty GameObject; everything else is generated at runtime.
+///
+[DisallowMultipleComponent]
+public class PaintHud : MonoBehaviour
+{
+ [Tooltip("Mode manager driving visibility. Empty = found automatically.")]
+ public GameModeManager modeManager;
+
+ [Tooltip("Painter the HUD edits. Empty = the manager's painter, else found automatically.")]
+ public MeshPainter painter;
+
+ private static readonly Color PanelColor = new Color(0.09f, 0.09f, 0.11f, 0.9f);
+ private static readonly Color Accent = new Color(0.40f, 0.75f, 1f, 1f);
+ private static readonly Color BoxColor = new Color(0.20f, 0.20f, 0.24f, 1f);
+
+ private GameObject _panel;
+ private Font _font;
+
+ private void Awake()
+ {
+ if (!modeManager) modeManager = FindFirstObjectByType();
+ }
+
+ private void OnEnable()
+ {
+ if (modeManager) modeManager.ModeChanged += OnModeChanged;
+ }
+
+ private void OnDisable()
+ {
+ if (modeManager) modeManager.ModeChanged -= OnModeChanged;
+ }
+
+ private void Start()
+ {
+ if (!painter) painter = modeManager ? modeManager.Painter : null;
+ if (!painter) painter = FindFirstObjectByType();
+
+ _font = Resources.GetBuiltinResource("LegacyRuntime.ttf");
+ if (!_font) _font = Resources.GetBuiltinResource("Arial.ttf");
+
+ EnsureEventSystem();
+ BuildUi();
+
+ // Set initial visibility (the manager's first SwitchTo may have fired before this Start).
+ OnModeChanged(modeManager ? modeManager.ActiveMode : null);
+ }
+
+ private void OnModeChanged(GameMode mode)
+ {
+ if (_panel) _panel.SetActive(mode is PaintMode);
+ }
+
+ // --- UI construction ---
+
+ private void BuildUi()
+ {
+ var canvas = GetComponent