Split the single HUD panel into four independent per-channel sub-panels plus a global brush-size panel, each a standalone RectTransform with its own header (drag-handle ready). PaintHud now binds to a baked layout prefab via recursive name lookup instead of building everything in code, with a code-gen fallback. HsvColorPicker gains Rebind() to re-apply the procedural gradient sprites and drag relays that a prefab cannot serialize. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
570 lines
23 KiB
C#
570 lines
23 KiB
C#
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.EventSystems;
|
|
#if ENABLE_INPUT_SYSTEM
|
|
using UnityEngine.InputSystem.UI;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// In-game paint UI. Shows its panels only while the active <see cref="GameMode"/> is
|
|
/// <see cref="PaintMode"/> (it follows the manager's ModeChanged event) and binds its
|
|
/// widgets directly to the live <see cref="MeshPainter"/> fields, so edits apply immediately.
|
|
///
|
|
/// The layout is a transparent root holding one independently positioned sub-panel per channel
|
|
/// (Albedo, Metallic+Smoothness, Occlusion, Emission) plus a global Brush card, each with a title
|
|
/// bar that doubles as a drag handle. The layout comes from one of two sources:
|
|
/// - an authored prefab assigned to <see cref="layoutPrefab"/> (instantiated, then bound), or
|
|
/// - generated in code (the fallback when no prefab is set).
|
|
/// Either way the *bindings* (listeners, picker gradient sprites, painter values) live in code
|
|
/// and are (re)applied at runtime by <see cref="WirePanel"/>. A prefab stores only the visual
|
|
/// layout, never the behaviour, so both paths run the exact same wiring against the same stable
|
|
/// widget names - there is no separate "dead" prefab path.
|
|
///
|
|
/// Widgets: 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.
|
|
/// </summary>
|
|
[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;
|
|
|
|
[Tooltip("Authored panel layout. If set it is instantiated and bound; empty = build the panel in code.")]
|
|
public GameObject layoutPrefab;
|
|
|
|
// Stable widget names shared by the code-gen builder, the baked prefab and the binder.
|
|
private const string RootName = "PaintHudPanel";
|
|
private const string PanelAlbedo = "Panel_Albedo";
|
|
private const string PanelMetallicSmoothness = "Panel_MetallicSmoothness";
|
|
private const string PanelOcclusion = "Panel_Occlusion";
|
|
private const string PanelEmission = "Panel_Emission";
|
|
private const string PanelBrushSize = "Panel_BrushSize";
|
|
private const string ToggleAlbedo = "Toggle_Albedo";
|
|
private const string ToggleMetallicSmoothness = "Toggle_MetallicSmoothness";
|
|
private const string ToggleOcclusion = "Toggle_Occlusion";
|
|
private const string ToggleEmission = "Toggle_Emission";
|
|
private const string PickerAlbedo = "Picker_Albedo";
|
|
private const string PickerEmission = "Picker_Emission";
|
|
private const string SliderMetallic = "Slider_Metallic";
|
|
private const string SliderSmoothness = "Slider_Smoothness";
|
|
private const string SliderOcclusion = "Slider_Occlusion";
|
|
private const string SliderEmissionIntensity = "Slider_EmissionIntensity";
|
|
private const string SliderBrushSize = "Slider_BrushSize";
|
|
|
|
private static readonly Color PanelColor = new Color(0.09f, 0.09f, 0.11f, 0.9f);
|
|
private static readonly Color HeaderColor = new Color(0.18f, 0.18f, 0.22f, 1f);
|
|
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<GameModeManager>();
|
|
}
|
|
|
|
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<MeshPainter>();
|
|
|
|
_font = LoadFont();
|
|
|
|
EnsureEventSystem();
|
|
EnsureCanvas();
|
|
|
|
if (layoutPrefab)
|
|
{
|
|
var go = Instantiate(layoutPrefab, transform);
|
|
go.name = layoutPrefab.name; // strip the "(Clone)" suffix
|
|
_panel = go;
|
|
}
|
|
else
|
|
{
|
|
_panel = BuildPanelStructure(transform);
|
|
}
|
|
|
|
WirePanel(_panel.transform);
|
|
|
|
// 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);
|
|
}
|
|
|
|
private static Font LoadFont()
|
|
{
|
|
var f = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
|
if (!f) f = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
|
return f;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// Editor-only: builds a fresh, unbound panel subtree (stable widget names, no listeners and
|
|
/// no runtime gradient sprites) for saving as a layout prefab. The caller reparents/saves the
|
|
/// returned object and destroys it. The wiring is applied at runtime by <see cref="WirePanel"/>.
|
|
/// </summary>
|
|
public GameObject CreatePanelForBake()
|
|
{
|
|
_font = LoadFont();
|
|
return BuildPanelStructure(transform);
|
|
}
|
|
#endif
|
|
|
|
// --- Canvas ---
|
|
|
|
private void EnsureCanvas()
|
|
{
|
|
var canvas = GetComponent<Canvas>();
|
|
if (!canvas) canvas = gameObject.AddComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
canvas.sortingOrder = 100;
|
|
|
|
var scaler = GetComponent<CanvasScaler>();
|
|
if (!scaler) scaler = gameObject.AddComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
|
scaler.matchWidthOrHeight = 0.5f;
|
|
|
|
if (!GetComponent<GraphicRaycaster>()) gameObject.AddComponent<GraphicRaycaster>();
|
|
}
|
|
|
|
private void EnsureEventSystem()
|
|
{
|
|
if (FindFirstObjectByType<EventSystem>()) return;
|
|
|
|
var es = new GameObject("EventSystem", typeof(EventSystem));
|
|
#if ENABLE_INPUT_SYSTEM
|
|
var module = es.AddComponent<InputSystemUIInputModule>();
|
|
if (!module.actionsAsset) module.AssignDefaultActions();
|
|
#else
|
|
es.AddComponent<StandaloneInputModule>();
|
|
#endif
|
|
}
|
|
|
|
// --- Structure (layout only; matches the baked prefab) ---
|
|
|
|
private GameObject BuildPanelStructure(Transform parent)
|
|
{
|
|
// Root: a transparent, full-canvas container (no image -> never blocks raycasts). Each channel
|
|
// lives in its own independently positioned sub-panel, so panels can be placed freely in the
|
|
// prefab now and dragged individually at runtime in a later step.
|
|
var root = NewRect(RootName, parent);
|
|
root.anchorMin = Vector2.zero;
|
|
root.anchorMax = Vector2.one;
|
|
root.offsetMin = Vector2.zero;
|
|
root.offsetMax = Vector2.zero;
|
|
|
|
const float w = 280f;
|
|
|
|
// Albedo channel.
|
|
var albedo = NewSubPanel(root, PanelAlbedo, new Vector2(16f, -16f), w, "Albedo");
|
|
AddToggleStructure(albedo, ToggleAlbedo, "Paint");
|
|
AddColorPickerStructure(albedo, PickerAlbedo);
|
|
|
|
// Metallic + Smoothness channel.
|
|
var ms = NewSubPanel(root, PanelMetallicSmoothness, new Vector2(16f, -262f), w, "Metallic + Smoothness");
|
|
AddToggleStructure(ms, ToggleMetallicSmoothness, "Paint");
|
|
AddSliderStructure(ms, SliderMetallic, "Metallic", 0f, 1f, false);
|
|
AddSliderStructure(ms, SliderSmoothness, "Smoothness", 0f, 1f, false);
|
|
|
|
// Occlusion channel.
|
|
var occ = NewSubPanel(root, PanelOcclusion, new Vector2(16f, -452f), w, "Occlusion");
|
|
AddToggleStructure(occ, ToggleOcclusion, "Paint");
|
|
AddSliderStructure(occ, SliderOcclusion, "Occlusion", 0f, 1f, false);
|
|
|
|
// Emission channel.
|
|
var emission = NewSubPanel(root, PanelEmission, new Vector2(312f, -16f), w, "Emission");
|
|
AddToggleStructure(emission, ToggleEmission, "Paint");
|
|
AddColorPickerStructure(emission, PickerEmission);
|
|
AddSliderStructure(emission, SliderEmissionIntensity, "Emission Intensity", 0f, 8f, false);
|
|
|
|
// Brush size (global, not a paint channel).
|
|
var brush = NewSubPanel(root, PanelBrushSize, new Vector2(312f, -286f), w, "Brush");
|
|
AddSliderStructure(brush, SliderBrushSize, "Brush Size", 1f, 256f, true);
|
|
|
|
return root.gameObject;
|
|
}
|
|
|
|
/// <summary>Builds one self-contained, independently positioned channel card with a title bar.</summary>
|
|
private RectTransform NewSubPanel(RectTransform root, string name, Vector2 pos, float width, string title)
|
|
{
|
|
var panel = NewRect(name, root);
|
|
panel.anchorMin = panel.anchorMax = new Vector2(0f, 1f);
|
|
panel.pivot = new Vector2(0f, 1f);
|
|
panel.anchoredPosition = pos;
|
|
panel.sizeDelta = new Vector2(width, 0f); // width fixed; height from the ContentSizeFitter
|
|
AddImage(panel.gameObject, PanelColor);
|
|
|
|
var vlg = panel.gameObject.AddComponent<VerticalLayoutGroup>();
|
|
vlg.padding = new RectOffset(10, 10, 10, 10);
|
|
vlg.spacing = 8f;
|
|
vlg.childAlignment = TextAnchor.UpperLeft;
|
|
vlg.childControlWidth = true;
|
|
vlg.childControlHeight = true;
|
|
vlg.childForceExpandWidth = true;
|
|
vlg.childForceExpandHeight = false;
|
|
|
|
var fitter = panel.gameObject.AddComponent<ContentSizeFitter>();
|
|
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
AddPanelHeader(panel, title);
|
|
return panel;
|
|
}
|
|
|
|
private void AddPanelHeader(RectTransform panel, string title)
|
|
{
|
|
// Title bar. Its image is a raycast target so it can double as the drag handle in a later step.
|
|
var rt = NewRect("Header", panel);
|
|
var le = rt.gameObject.AddComponent<LayoutElement>();
|
|
le.minHeight = le.preferredHeight = 24f;
|
|
AddImage(rt.gameObject, HeaderColor);
|
|
|
|
var lbl = NewRect("Label", rt);
|
|
lbl.anchorMin = Vector2.zero;
|
|
lbl.anchorMax = Vector2.one;
|
|
lbl.offsetMin = new Vector2(8f, 0f);
|
|
lbl.offsetMax = new Vector2(-8f, 0f);
|
|
var t = MakeText(lbl.gameObject, title, 13, FontStyle.Bold, new Color(0.9f, 0.9f, 0.95f, 1f), TextAnchor.MiddleLeft);
|
|
t.raycastTarget = false;
|
|
}
|
|
|
|
private void AddToggleStructure(RectTransform panel, string name, string label)
|
|
{
|
|
var rt = NewRect(name, panel);
|
|
var le = rt.gameObject.AddComponent<LayoutElement>();
|
|
le.minHeight = le.preferredHeight = 22f;
|
|
|
|
var toggle = rt.gameObject.AddComponent<Toggle>();
|
|
|
|
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;
|
|
}
|
|
|
|
private void AddSliderStructure(RectTransform panel, string name, string label, float min, float max, bool whole)
|
|
{
|
|
var rt = NewRect(name, panel);
|
|
var le = rt.gameObject.AddComponent<LayoutElement>();
|
|
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<Slider>();
|
|
|
|
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; // serialized: the binder reads it back for value formatting
|
|
}
|
|
|
|
private HsvColorPicker AddColorPickerStructure(RectTransform panel, string name)
|
|
{
|
|
const float square = 120f;
|
|
const float hueH = 14f;
|
|
|
|
var rt = NewRect(name, panel);
|
|
var le = rt.gameObject.AddComponent<LayoutElement>();
|
|
le.minHeight = le.preferredHeight = square + 6f + hueH;
|
|
|
|
var picker = rt.gameObject.AddComponent<HsvColorPicker>();
|
|
picker.Build(square, hueH); // structure only; sprites + hooks are applied by Rebind in WirePanel
|
|
return picker;
|
|
}
|
|
|
|
// --- Wiring (behaviour; shared by the code-gen and prefab paths) ---
|
|
|
|
private void WirePanel(Transform panel)
|
|
{
|
|
EnsureFonts(panel); // builtin font references may not survive prefab serialization
|
|
|
|
WireToggle(panel, ToggleAlbedo, painter && painter.paintAlbedo, v => { if (painter) painter.paintAlbedo = v; });
|
|
WireToggle(panel, ToggleMetallicSmoothness, painter && painter.paintMetallicSmoothness, v => { if (painter) painter.paintMetallicSmoothness = v; });
|
|
WireToggle(panel, ToggleOcclusion, painter && painter.paintOcclusion, v => { if (painter) painter.paintOcclusion = v; });
|
|
WireToggle(panel, ToggleEmission, painter && painter.paintEmission, v => { if (painter) painter.paintEmission = v; });
|
|
|
|
WirePicker(panel, PickerAlbedo, painter ? painter.brushColor : Color.red,
|
|
c => { if (painter) painter.brushColor = new Color(c.r, c.g, c.b, painter.brushColor.a); });
|
|
|
|
WireSlider(panel, SliderMetallic, painter ? painter.metallic : 0f, v => { if (painter) painter.metallic = v; });
|
|
WireSlider(panel, SliderSmoothness, painter ? painter.smoothness : 0.5f, v => { if (painter) painter.smoothness = v; });
|
|
WireSlider(panel, SliderOcclusion, painter ? painter.occlusion : 0.5f, v => { if (painter) painter.occlusion = v; });
|
|
|
|
WirePicker(panel, PickerEmission, painter ? painter.emissionColor : Color.black,
|
|
c => { if (painter) painter.emissionColor = new Color(c.r, c.g, c.b, painter.emissionColor.a); });
|
|
WireSlider(panel, SliderEmissionIntensity, painter ? painter.emissionIntensity : 1f, v => { if (painter) painter.emissionIntensity = v; });
|
|
|
|
WireSlider(panel, SliderBrushSize, painter ? painter.brushSize : 16f, v => { if (painter) painter.brushSize = Mathf.RoundToInt(v); });
|
|
}
|
|
|
|
private void EnsureFonts(Transform panel)
|
|
{
|
|
foreach (var t in panel.GetComponentsInChildren<Text>(true))
|
|
if (!t.font) t.font = _font;
|
|
}
|
|
|
|
private static void WireToggle(Transform panel, string name, bool value, Action<bool> onChanged)
|
|
{
|
|
var toggle = Find<Toggle>(panel, name);
|
|
if (!toggle) return;
|
|
toggle.isOn = value;
|
|
toggle.onValueChanged.AddListener(v => onChanged(v));
|
|
}
|
|
|
|
private static void WireSlider(Transform panel, string name, float value, Action<float> onChanged)
|
|
{
|
|
var cell = FindDeep(panel, name);
|
|
if (!cell) return;
|
|
var slider = cell.GetComponentInChildren<Slider>();
|
|
if (!slider) return;
|
|
var valueRt = cell.Find("Value");
|
|
var valueText = valueRt ? valueRt.GetComponent<Text>() : null;
|
|
|
|
bool whole = slider.wholeNumbers;
|
|
slider.value = value;
|
|
|
|
Action<float> setText = v =>
|
|
{
|
|
if (valueText) valueText.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 void WirePicker(Transform panel, string name, Color color, Action<Color> onChanged)
|
|
{
|
|
var picker = Find<HsvColorPicker>(panel, name);
|
|
if (!picker) return;
|
|
picker.Rebind(); // re-find children, re-assign runtime gradient sprites, re-hook drag relays
|
|
picker.SetColor(color);
|
|
picker.Changed += onChanged;
|
|
}
|
|
|
|
private static T Find<T>(Transform panel, string name) where T : Component
|
|
{
|
|
var child = FindDeep(panel, name);
|
|
return child ? child.GetComponent<T>() : null;
|
|
}
|
|
|
|
/// <summary>Depth-first search by name (widgets now live nested inside per-channel sub-panels).</summary>
|
|
private static Transform FindDeep(Transform root, string name)
|
|
{
|
|
if (root.name == name) return root;
|
|
for (int i = 0; i < root.childCount; i++)
|
|
{
|
|
var found = FindDeep(root.GetChild(i), name);
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private Text MakeText(GameObject go, string text, int size, FontStyle style, Color color, TextAnchor anchor)
|
|
{
|
|
var t = go.AddComponent<Text>();
|
|
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<Image>();
|
|
img.color = color;
|
|
return img;
|
|
}
|
|
|
|
/// <summary>Adds a full-rect, non-raycasting overlay image (sprite assigned by the caller).</summary>
|
|
public static Image AddOverlay(RectTransform parent, string name)
|
|
{
|
|
var rt = NewRect(name, parent);
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero;
|
|
rt.offsetMax = Vector2.zero;
|
|
var img = rt.gameObject.AddComponent<Image>();
|
|
img.type = Image.Type.Simple;
|
|
img.raycastTarget = false;
|
|
return img;
|
|
}
|
|
|
|
// Cached gradient sprites for the colour picker.
|
|
private static Sprite _satSprite, _valSprite, _hueSprite;
|
|
|
|
/// <summary>Horizontal white-out: opaque white (left) -> transparent (right) = saturation.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Vertical black-out: opaque black (bottom) -> transparent (top) = value.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Horizontal full-saturation hue ramp 0..1.</summary>
|
|
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;
|
|
}
|
|
}
|