using System; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem.UI; #endif /// /// In-game paint UI. Shows its panels only while the active is /// (it follows the manager's ModeChanged event) and binds its /// widgets directly to the live 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 and draggable from any of its non-widget surface. The layout comes from one of two sources: /// - an authored prefab assigned to (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 . 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. /// [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 PanelSpray = "Panel_Spray"; 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 ToggleSpray = "Toggle_Spray"; 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 SliderMetallicSmoothnessAlpha = "Slider_MetallicSmoothnessAlpha"; private const string SliderOcclusion = "Slider_Occlusion"; private const string SliderOcclusionAlpha = "Slider_OcclusionAlpha"; private const string SliderEmissionIntensity = "Slider_EmissionIntensity"; private const string SliderBrushSize = "Slider_BrushSize"; private const string SliderSprayDots = "Slider_SprayDots"; private const string SliderSprayDotSizeMin = "Slider_SprayDotSizeMin"; private const string SliderSprayDotSizeMax = "Slider_SprayDotSizeMax"; private const string SliderSprayDotAlphaMin = "Slider_SprayDotAlphaMin"; private const string SliderSprayDotAlphaMax = "Slider_SprayDotAlphaMax"; // Panels made draggable at runtime - grabbable from any non-widget surface (both prefab and code-gen paths). private static readonly string[] DraggablePanels = { PanelAlbedo, PanelMetallicSmoothness, PanelOcclusion, PanelEmission, PanelBrushSize, PanelSpray }; 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 = LoadFont(); EnsureEventSystem(); EnsureCanvas(); if (layoutPrefab) { var go = Instantiate(layoutPrefab, transform); go.name = layoutPrefab.name; // strip the "(Clone)" suffix _panel = go; } 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("LegacyRuntime.ttf"); if (!f) f = Resources.GetBuiltinResource("Arial.ttf"); return f; } // --- Canvas --- private void EnsureCanvas() { 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.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand; if (!GetComponent()) gameObject.AddComponent(); } 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 } // --- 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; }); WireToggle(panel, ToggleSpray, painter && painter.spray, v => { if (painter) painter.spray = v; }); // Dim a channel panel to 0.5 alpha while its toggle is off, full alpha while on. WirePanelFade(panel, PanelAlbedo, ToggleAlbedo); WirePanelFade(panel, PanelMetallicSmoothness, ToggleMetallicSmoothness); WirePanelFade(panel, PanelOcclusion, ToggleOcclusion); WirePanelFade(panel, PanelEmission, ToggleEmission); WirePanelFade(panel, PanelSpray, ToggleSpray); WirePicker(panel, PickerAlbedo, painter ? painter.brushColor : Color.red, c => { if (painter) painter.brushColor = c; }); 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, SliderMetallicSmoothnessAlpha, painter ? painter.metallicSmoothnessOpacity : 1f, v => { if (painter) painter.metallicSmoothnessOpacity = v; }); WireSlider(panel, SliderOcclusion, painter ? painter.occlusion : 0.5f, v => { if (painter) painter.occlusion = v; }); WireSlider(panel, SliderOcclusionAlpha, painter ? painter.occlusionOpacity : 1f, v => { if (painter) painter.occlusionOpacity = v; }); WirePicker(panel, PickerEmission, painter ? painter.emissionColor : Color.black, c => { if (painter) painter.emissionColor = c; }); 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); }); WireSlider(panel, SliderSprayDots, painter ? painter.sprayDots : 24f, v => { if (painter) painter.sprayDots = Mathf.RoundToInt(v); }); WireSlider(panel, SliderSprayDotSizeMin, painter ? painter.sprayDotSizeMin : .05f, v => { if (painter) painter.sprayDotSizeMin = v; }); WireSlider(panel, SliderSprayDotSizeMax, painter ? painter.sprayDotSizeMax : .18f, v => { if (painter) painter.sprayDotSizeMax = v; }); WireSlider(panel, SliderSprayDotAlphaMin, painter ? painter.sprayDotAlphaMin : .25f, v => { if (painter) painter.sprayDotAlphaMin = v; }); WireSlider(panel, SliderSprayDotAlphaMax, painter ? painter.sprayDotAlphaMax : 1f, v => { if (painter) painter.sprayDotAlphaMax = v; }); foreach (var name in DraggablePanels) MakeDraggable(panel, name); } /// /// Attaches (idempotently) a to the whole panel so it can be dragged /// from any of its non-interactive surface (header, padding, labels, background); the component /// ignores presses that start on a widget (toggle, slider, colour picker). /// private static void MakeDraggable(Transform root, string panelName) { var panel = FindDeep(root, panelName) as RectTransform; if (!panel) return; var drag = panel.GetComponent(); if (!drag) drag = panel.gameObject.AddComponent(); drag.target = panel; } private void EnsureFonts(Transform panel) { foreach (var t in panel.GetComponentsInChildren(true)) if (!t.font) t.font = _font; } private static void WireToggle(Transform panel, string name, bool value, Action onChanged) { var toggle = Find(panel, name); if (!toggle) return; toggle.isOn = value; toggle.onValueChanged.AddListener(v => onChanged(v)); } /// /// Ensures a on the channel panel and drives its alpha from the toggle: /// 1 while the channel is enabled, 0.5 while disabled. Call after so the /// initial alpha reflects the toggle's painter-synced state. The group stays interactive (default /// interactable/blocksRaycasts), so a dimmed panel can still be clicked to re-enable its channel. /// private static void WirePanelFade(Transform root, string panelName, string toggleName) { var panel = FindDeep(root, panelName); if (!panel) return; var group = panel.GetComponent(); if (!group) group = panel.gameObject.AddComponent(); var toggle = Find(panel, toggleName); if (!toggle) { group.alpha = 1f; return; } group.alpha = toggle.isOn ? 1f : 0.5f; toggle.onValueChanged.AddListener(v => group.alpha = v ? 1f : 0.5f); } private static void WireSlider(Transform panel, string name, float value, Action onChanged) { var cell = FindDeep(panel, name); if (!cell) return; var slider = cell.GetComponentInChildren(); if (!slider) return; var valueRt = cell.Find("Value"); var valueText = valueRt ? valueRt.GetComponent() : null; bool whole = slider.wholeNumbers; slider.value = value; Action 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 onChanged) { var picker = Find(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(Transform panel, string name) where T : Component { var child = FindDeep(panel, name); return child ? child.GetComponent() : null; } /// Depth-first search by name (widgets now live nested inside per-channel sub-panels). 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; } // --- 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 (sprite assigned by the caller). 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(); img.type = Image.Type.Simple; img.raycastTarget = false; return img; } // Cached gradient sprites for the colour picker. private static Sprite _satSprite, _valSprite, _hueSprite, _alphaSprite; /// 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 white transparency ramp: transparent (left) -> opaque (right) = alpha. /// Tinted to the current colour by the picker so it reads as a colour-over-backdrop fade. public static Sprite AlphaSprite() { if (_alphaSprite) return _alphaSprite; const int n = 64; var tex = NewGradientTex(n, 1); for (int x = 0; x < n; x++) { byte a = (byte)Mathf.RoundToInt(255f * (x / (float)(n - 1))); tex.SetPixel(x, 0, new Color32(255, 255, 255, a)); } tex.Apply(false); _alphaSprite = ToSprite(tex); return _alphaSprite; } /// 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; } }