From 04c22f0ac187d5fc829b579d73fbed2eb40db85b Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Wed, 24 Jun 2026 08:26:56 +0200 Subject: [PATCH] Add in-game paint HUD bound to the mesh painter Add a code-driven HUD that appears in paint mode and binds live to the active MeshPainter: four channel toggles, HSV pickers for albedo and emission, sliders for metallic/smoothness/occlusion, emission intensity and brush size. The HUD self-assembles its own overlay canvas and event system at runtime. Add a ModeChanged event to GameModeManager so the HUD can follow the active mode, and guard MeshPainter against painting through the UI. Co-Authored-By: Claude Opus 4.8 --- Assets/Scripts/GameModeManager.cs | 5 + Assets/Scripts/HsvColorPicker.cs | 124 ++++++++ Assets/Scripts/HsvColorPicker.cs.meta | 2 + Assets/Scripts/MeshPainter.cs | 13 + Assets/Scripts/PaintHud.cs | 407 ++++++++++++++++++++++++ Assets/Scripts/PaintHud.cs.meta | 2 + Assets/Scripts/UiPointerSurface.cs | 39 +++ Assets/Scripts/UiPointerSurface.cs.meta | 2 + 8 files changed, 594 insertions(+) create mode 100644 Assets/Scripts/HsvColorPicker.cs create mode 100644 Assets/Scripts/HsvColorPicker.cs.meta create mode 100644 Assets/Scripts/PaintHud.cs create mode 100644 Assets/Scripts/PaintHud.cs.meta create mode 100644 Assets/Scripts/UiPointerSurface.cs create mode 100644 Assets/Scripts/UiPointerSurface.cs.meta 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(); + if (!canvas) canvas = gameObject.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvas.sortingOrder = 100; + + var scaler = GetComponent(); + if (!scaler) scaler = gameObject.AddComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920f, 1080f); + scaler.matchWidthOrHeight = 0.5f; + + if (!GetComponent()) gameObject.AddComponent(); + + // Panel (this is what gets shown/hidden per mode). + var panel = NewRect("Panel", transform); + _panel = panel.gameObject; + panel.anchorMin = panel.anchorMax = new Vector2(0f, 1f); + panel.pivot = new Vector2(0f, 1f); + panel.anchoredPosition = new Vector2(16f, -16f); + panel.sizeDelta = new Vector2(296f, 0f); // width fixed; height from the ContentSizeFitter + AddImage(panel.gameObject, PanelColor); + + var vlg = panel.gameObject.AddComponent(); + vlg.padding = new RectOffset(12, 12, 12, 12); + vlg.spacing = 8f; + vlg.childAlignment = TextAnchor.UpperLeft; + vlg.childControlWidth = true; + vlg.childControlHeight = true; + vlg.childForceExpandWidth = true; + vlg.childForceExpandHeight = false; + + var fitter = panel.gameObject.AddComponent(); + fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + AddSectionLabel(panel, "PAINT", 15, FontStyle.Bold); + + // Channels. + AddSectionLabel(panel, "Channels", 13, FontStyle.Bold); + AddToggle(panel, "Albedo", painter && painter.paintAlbedo, v => { if (painter) painter.paintAlbedo = v; }); + AddToggle(panel, "Metallic + Smoothness", painter && painter.paintMetallicSmoothness, v => { if (painter) painter.paintMetallicSmoothness = v; }); + AddToggle(panel, "Occlusion", painter && painter.paintOcclusion, v => { if (painter) painter.paintOcclusion = v; }); + AddToggle(panel, "Emission", painter && painter.paintEmission, v => { if (painter) painter.paintEmission = v; }); + + // Albedo colour. + AddSectionLabel(panel, "Albedo Color", 13, FontStyle.Bold); + var albedo = AddColorPicker(panel); + albedo.SetColor(painter ? painter.brushColor : Color.red); + albedo.Changed += c => { if (painter) painter.brushColor = new Color(c.r, c.g, c.b, painter.brushColor.a); }; + + // Metallic / smoothness / occlusion. + AddSlider(panel, "Metallic", 0f, 1f, false, painter ? painter.metallic : 0f, v => { if (painter) painter.metallic = v; }); + AddSlider(panel, "Smoothness", 0f, 1f, false, painter ? painter.smoothness : 0.5f, v => { if (painter) painter.smoothness = v; }); + AddSlider(panel, "Occlusion", 0f, 1f, false, painter ? painter.occlusion : 0.5f, v => { if (painter) painter.occlusion = v; }); + + // Emission. + AddSectionLabel(panel, "Emission Color", 13, FontStyle.Bold); + var emission = AddColorPicker(panel); + emission.SetColor(painter ? painter.emissionColor : Color.black); + emission.Changed += c => { if (painter) painter.emissionColor = new Color(c.r, c.g, c.b, painter.emissionColor.a); }; + AddSlider(panel, "Emission Intensity", 0f, 8f, false, painter ? painter.emissionIntensity : 1f, v => { if (painter) painter.emissionIntensity = v; }); + + // Brush size. + AddSlider(panel, "Brush Size", 1f, 256f, true, painter ? painter.brushSize : 16f, v => { if (painter) painter.brushSize = Mathf.RoundToInt(v); }); + } + + private void EnsureEventSystem() + { + if (FindFirstObjectByType()) return; + + var es = new GameObject("EventSystem", typeof(EventSystem)); +#if ENABLE_INPUT_SYSTEM + var module = es.AddComponent(); + if (!module.actionsAsset) module.AssignDefaultActions(); +#else + es.AddComponent(); +#endif + } + + // --- Widgets --- + + private void AddSectionLabel(RectTransform panel, string text, int size, FontStyle style) + { + var rt = NewRect("Label", panel); + var le = rt.gameObject.AddComponent(); + le.minHeight = le.preferredHeight = 18f; + + var t = MakeText(rt.gameObject, text, size, style, new Color(0.85f, 0.85f, 0.9f, 1f), TextAnchor.MiddleLeft); + t.raycastTarget = false; + } + + private void AddToggle(RectTransform panel, string label, bool value, Action onChanged) + { + var rt = NewRect("Toggle", panel); + var le = rt.gameObject.AddComponent(); + le.minHeight = le.preferredHeight = 22f; + + var toggle = rt.gameObject.AddComponent(); + + var box = NewRect("Box", rt); + box.anchorMin = box.anchorMax = new Vector2(0f, 0.5f); + box.pivot = new Vector2(0f, 0.5f); + box.anchoredPosition = Vector2.zero; + box.sizeDelta = new Vector2(18f, 18f); + var boxImg = AddImage(box.gameObject, BoxColor); + + var check = NewRect("Check", box); + check.anchorMin = Vector2.zero; + check.anchorMax = Vector2.one; + check.offsetMin = new Vector2(3f, 3f); + check.offsetMax = new Vector2(-3f, -3f); + var checkImg = AddImage(check.gameObject, Accent); + checkImg.raycastTarget = false; + + var lbl = NewRect("Label", rt); + lbl.anchorMin = Vector2.zero; + lbl.anchorMax = Vector2.one; + lbl.offsetMin = new Vector2(24f, 0f); + lbl.offsetMax = Vector2.zero; + var t = MakeText(lbl.gameObject, label, 14, FontStyle.Normal, Color.white, TextAnchor.MiddleLeft); + t.raycastTarget = false; + + toggle.targetGraphic = boxImg; + toggle.graphic = checkImg; + toggle.isOn = value; + toggle.onValueChanged.AddListener(v => onChanged(v)); + } + + private void AddSlider(RectTransform panel, string label, float min, float max, bool whole, float value, Action onChanged) + { + var rt = NewRect("Slider", panel); + var le = rt.gameObject.AddComponent(); + le.minHeight = le.preferredHeight = 34f; + + // Caption row (label left, value right), pinned to the top of the cell. + var lbl = NewRect("Label", rt); + lbl.anchorMin = new Vector2(0f, 1f); + lbl.anchorMax = new Vector2(1f, 1f); + lbl.pivot = new Vector2(0f, 1f); + lbl.offsetMin = new Vector2(0f, -16f); + lbl.offsetMax = new Vector2(-48f, 0f); + var lt = MakeText(lbl.gameObject, label, 13, FontStyle.Normal, new Color(0.8f, 0.8f, 0.85f, 1f), TextAnchor.MiddleLeft); + lt.raycastTarget = false; + + var valRt = NewRect("Value", rt); + valRt.anchorMin = new Vector2(1f, 1f); + valRt.anchorMax = new Vector2(1f, 1f); + valRt.pivot = new Vector2(1f, 1f); + valRt.anchoredPosition = Vector2.zero; + valRt.sizeDelta = new Vector2(56f, 16f); + var vt = MakeText(valRt.gameObject, "", 13, FontStyle.Normal, Color.white, TextAnchor.MiddleRight); + vt.raycastTarget = false; + + // Track, pinned to the bottom of the cell. + var track = NewRect("Track", rt); + track.anchorMin = new Vector2(0f, 0f); + track.anchorMax = new Vector2(1f, 0f); + track.pivot = new Vector2(0.5f, 0f); + track.anchoredPosition = Vector2.zero; + track.sizeDelta = new Vector2(0f, 16f); + var slider = track.gameObject.AddComponent(); + + var bg = NewRect("Background", track); + bg.anchorMin = new Vector2(0f, 0.5f); + bg.anchorMax = new Vector2(1f, 0.5f); + bg.pivot = new Vector2(0.5f, 0.5f); + bg.anchoredPosition = Vector2.zero; + bg.sizeDelta = new Vector2(0f, 5f); + AddImage(bg.gameObject, BoxColor); // raycast target so clicking the bar jumps the value + + var fillArea = NewRect("Fill Area", track); + fillArea.anchorMin = new Vector2(0f, 0.5f); + fillArea.anchorMax = new Vector2(1f, 0.5f); + fillArea.pivot = new Vector2(0.5f, 0.5f); + fillArea.anchoredPosition = Vector2.zero; + fillArea.sizeDelta = new Vector2(-16f, 5f); + var fill = NewRect("Fill", fillArea); + fill.anchorMin = Vector2.zero; + fill.anchorMax = Vector2.one; + fill.offsetMin = Vector2.zero; + fill.offsetMax = Vector2.zero; + var fillImg = AddImage(fill.gameObject, Accent); + fillImg.raycastTarget = false; + + var handleArea = NewRect("Handle Slide Area", track); + handleArea.anchorMin = Vector2.zero; + handleArea.anchorMax = Vector2.one; + handleArea.offsetMin = new Vector2(8f, 0f); + handleArea.offsetMax = new Vector2(-8f, 0f); + var handle = NewRect("Handle", handleArea); + handle.sizeDelta = new Vector2(14f, 0f); + var handleImg = AddImage(handle.gameObject, Color.white); + + slider.fillRect = fill; + slider.handleRect = handle; + slider.targetGraphic = handleImg; + slider.direction = Slider.Direction.LeftToRight; + slider.minValue = min; + slider.maxValue = max; + slider.wholeNumbers = whole; + slider.value = value; + + Action setText = v => vt.text = whole + ? Mathf.RoundToInt(v).ToString() + : v.ToString("0.00"); + setText(slider.value); // show the actual (clamped) value, not the unclamped input + slider.onValueChanged.AddListener(v => { setText(v); onChanged(v); }); + } + + private HsvColorPicker AddColorPicker(RectTransform panel) + { + const float square = 120f; + const float hueH = 14f; + + var rt = NewRect("ColorPicker", panel); + var le = rt.gameObject.AddComponent(); + le.minHeight = le.preferredHeight = square + 6f + hueH; + + var picker = rt.gameObject.AddComponent(); + picker.Build(square, hueH); + return picker; + } + + private Text MakeText(GameObject go, string text, int size, FontStyle style, Color color, TextAnchor anchor) + { + var t = go.AddComponent(); + t.font = _font; + t.fontSize = size; + t.fontStyle = style; + t.color = color; + t.alignment = anchor; + t.text = text; + t.horizontalOverflow = HorizontalWrapMode.Overflow; + t.verticalOverflow = VerticalWrapMode.Truncate; + return t; + } + + // --- Static UI factory (shared with HsvColorPicker) --- + + public static RectTransform NewRect(string name, Transform parent) + { + var go = new GameObject(name, typeof(RectTransform)); + go.transform.SetParent(parent, false); + return (RectTransform)go.transform; + } + + public static Image AddImage(GameObject go, Color color) + { + var img = go.AddComponent(); + img.color = color; + return img; + } + + /// Adds a full-rect, non-raycasting overlay image with the given sprite. + public static Image AddOverlay(RectTransform parent, Sprite sprite) + { + var rt = NewRect("Overlay", parent); + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + var img = rt.gameObject.AddComponent(); + img.sprite = sprite; + img.type = Image.Type.Simple; + img.raycastTarget = false; + return img; + } + + // Cached gradient sprites for the colour picker. + private static Sprite _satSprite, _valSprite, _hueSprite; + + /// Horizontal white-out: opaque white (left) -> transparent (right) = saturation. + public static Sprite SaturationSprite() + { + if (_satSprite) return _satSprite; + const int n = 64; + var tex = NewGradientTex(n, 1); + for (int x = 0; x < n; x++) + { + byte a = (byte)Mathf.RoundToInt(255f * (1f - x / (float)(n - 1))); + tex.SetPixel(x, 0, new Color32(255, 255, 255, a)); + } + tex.Apply(false); + _satSprite = ToSprite(tex); + return _satSprite; + } + + /// Vertical black-out: opaque black (bottom) -> transparent (top) = value. + public static Sprite ValueSprite() + { + if (_valSprite) return _valSprite; + const int n = 64; + var tex = NewGradientTex(1, n); + for (int y = 0; y < n; y++) + { + byte a = (byte)Mathf.RoundToInt(255f * (1f - y / (float)(n - 1))); + tex.SetPixel(0, y, new Color32(0, 0, 0, a)); + } + tex.Apply(false); + _valSprite = ToSprite(tex); + return _valSprite; + } + + /// Horizontal full-saturation hue ramp 0..1. + public static Sprite HueSprite() + { + if (_hueSprite) return _hueSprite; + const int n = 64; + var tex = NewGradientTex(n, 1); + for (int x = 0; x < n; x++) + tex.SetPixel(x, 0, Color.HSVToRGB(x / (float)(n - 1), 1f, 1f)); + tex.Apply(false); + _hueSprite = ToSprite(tex); + return _hueSprite; + } + + private static Texture2D NewGradientTex(int w, int h) + { + return new Texture2D(w, h, TextureFormat.RGBA32, false, false) // sRGB sampling for correct colours + { + name = "PaintHud_Gradient", + wrapMode = TextureWrapMode.Clamp, + filterMode = FilterMode.Bilinear, + hideFlags = HideFlags.HideAndDontSave + }; + } + + private static Sprite ToSprite(Texture2D tex) + { + var s = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f); + s.hideFlags = HideFlags.HideAndDontSave; + return s; + } +} diff --git a/Assets/Scripts/PaintHud.cs.meta b/Assets/Scripts/PaintHud.cs.meta new file mode 100644 index 0000000..0c87bff --- /dev/null +++ b/Assets/Scripts/PaintHud.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d0635b02a5454ec408df3a69b04f85a7 \ No newline at end of file diff --git a/Assets/Scripts/UiPointerSurface.cs b/Assets/Scripts/UiPointerSurface.cs new file mode 100644 index 0000000..c3bb9d5 --- /dev/null +++ b/Assets/Scripts/UiPointerSurface.cs @@ -0,0 +1,39 @@ +using System; +using UnityEngine; +using UnityEngine.EventSystems; + +/// +/// Turns a pointer press/drag on a UI graphic into a normalized (x, y) position in +/// [0,1] within its RectTransform (origin bottom-left). Added in code to the colour +/// picker's saturation/value field and hue bar so they react to both clicking and +/// dragging. The graphic on the same object must be a raycast target. +/// +[RequireComponent(typeof(RectTransform))] +public class UiPointerSurface : MonoBehaviour, IPointerDownHandler, IDragHandler +{ + /// Fired on press and while dragging with the normalized (x, y) in [0,1]. + public event Action Picked; + + private RectTransform _rect; + + private void Awake() => _rect = (RectTransform)transform; + + public void OnPointerDown(PointerEventData eventData) => Report(eventData); + public void OnDrag(PointerEventData eventData) => Report(eventData); + + private void Report(PointerEventData eventData) + { + // pressEventCamera is null for a Screen Space - Overlay canvas, which is the + // correct camera argument there. + if (!RectTransformUtility.ScreenPointToLocalPointInRectangle( + _rect, eventData.position, eventData.pressEventCamera, out Vector2 local)) + return; + + Rect r = _rect.rect; + if (r.width <= 0f || r.height <= 0f) return; + + float x = Mathf.Clamp01((local.x - r.xMin) / r.width); + float y = Mathf.Clamp01((local.y - r.yMin) / r.height); + Picked?.Invoke(new Vector2(x, y)); + } +} diff --git a/Assets/Scripts/UiPointerSurface.cs.meta b/Assets/Scripts/UiPointerSurface.cs.meta new file mode 100644 index 0000000..3d2ba0d --- /dev/null +++ b/Assets/Scripts/UiPointerSurface.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3aefb85fd241856408808aa5401c9d64 \ No newline at end of file