Bake paint HUD layout into a prefab with separate channel panels
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>
This commit is contained in:
1 parent
4340970691
commit
ace1f949dc
5 files changed
+7484
-115
No files matched your search
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46a54f2f3e4ec1f449e91a96c7dbb6b8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa9dc2ea9e2c9ac4f815f666014804c8
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -3,12 +3,16 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Compact runtime HSV colour picker: a saturation/value square, a hue bar and a live
|
||||
/// swatch. Builds its own UI as children when <see cref="Build"/> is called and raises
|
||||
/// <see cref="Changed"/> 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).
|
||||
/// Compact runtime HSV colour picker: a saturation/value square, a hue bar and a live swatch.
|
||||
/// 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).
|
||||
///
|
||||
/// <see cref="Build"/> creates the child structure (stable names, no sprites or hooks); it can
|
||||
/// be baked into a prefab. <see cref="Rebind"/> then (re)assigns the runtime gradient sprites
|
||||
/// (which a prefab cannot store) and (re)hooks the drag relays - it runs for both the code-built
|
||||
/// picker and one instantiated from a prefab, so the two are behaviourally identical.
|
||||
/// </summary>
|
||||
public class HsvColorPicker : MonoBehaviour
|
||||
{
|
||||
@@ -41,43 +45,86 @@ public class HsvColorPicker : MonoBehaviour
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Builds the picker (square + hue bar + swatch) under this object, top-left aligned.</summary>
|
||||
/// <summary>
|
||||
/// Builds the picker structure (square + hue bar + swatch) under this object, top-left aligned.
|
||||
/// Layout only - no gradient sprites and no event hooks, so the result is safe to bake into a
|
||||
/// prefab. Call <see cref="Rebind"/> afterwards (at runtime) to make it functional.
|
||||
/// </summary>
|
||||
public void Build(float squareSize, float hueHeight)
|
||||
{
|
||||
// Saturation / value square: base hue + two gradient overlays, top-left of this rect.
|
||||
// Saturation / value square: base hue + two named 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.AddImage(sv.gameObject, Color.red); // raycast target for the drag relay
|
||||
|
||||
PaintHud.AddOverlay(sv, PaintHud.SaturationSprite());
|
||||
PaintHud.AddOverlay(sv, PaintHud.ValueSprite());
|
||||
PaintHud.AddOverlay(sv, "SatOverlay");
|
||||
PaintHud.AddOverlay(sv, "ValOverlay");
|
||||
|
||||
sv.gameObject.AddComponent<UiPointerSurface>().Picked += OnSvPicked;
|
||||
sv.gameObject.AddComponent<UiPointerSurface>();
|
||||
|
||||
_svHandle = PaintHud.NewRect("Handle", sv);
|
||||
_svHandle.sizeDelta = new Vector2(12f, 12f);
|
||||
var svHandleImg = PaintHud.AddImage(_svHandle.gameObject, Color.white);
|
||||
svHandleImg.raycastTarget = false;
|
||||
var svHandle = PaintHud.NewRect("Handle", sv);
|
||||
svHandle.sizeDelta = new Vector2(12f, 12f);
|
||||
PaintHud.AddImage(svHandle.gameObject, Color.white).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<Image>();
|
||||
hueImg.sprite = PaintHud.HueSprite();
|
||||
hueImg.type = Image.Type.Simple;
|
||||
|
||||
hue.gameObject.AddComponent<UiPointerSurface>().Picked += OnHuePicked;
|
||||
hue.gameObject.AddComponent<UiPointerSurface>();
|
||||
|
||||
_hueHandle = PaintHud.NewRect("Handle", hue);
|
||||
_hueHandle.sizeDelta = new Vector2(4f, hueHeight + 4f);
|
||||
var hueHandleImg = PaintHud.AddImage(_hueHandle.gameObject, Color.white);
|
||||
hueHandleImg.raycastTarget = false;
|
||||
var hueHandle = PaintHud.NewRect("Handle", hue);
|
||||
hueHandle.sizeDelta = new Vector2(4f, hueHeight + 4f);
|
||||
PaintHud.AddImage(hueHandle.gameObject, Color.white).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;
|
||||
PaintHud.AddImage(sw.gameObject, Color.white).raycastTarget = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-finds the child widgets, (re)assigns the runtime gradient sprites and (re)hooks the drag
|
||||
/// relays. Idempotent (unsubscribe-then-subscribe), so it is safe whether the structure was just
|
||||
/// built in code or instantiated from a prefab. Call before <see cref="SetColor"/>.
|
||||
/// </summary>
|
||||
public void Rebind()
|
||||
{
|
||||
var sv = transform.Find("SV");
|
||||
if (sv)
|
||||
{
|
||||
_hueBase = sv.GetComponent<Image>();
|
||||
|
||||
var sat = sv.Find("SatOverlay");
|
||||
if (sat && sat.TryGetComponent(out Image satImg)) satImg.sprite = PaintHud.SaturationSprite();
|
||||
var val = sv.Find("ValOverlay");
|
||||
if (val && val.TryGetComponent(out Image valImg)) valImg.sprite = PaintHud.ValueSprite();
|
||||
|
||||
_svHandle = sv.Find("Handle") as RectTransform;
|
||||
|
||||
if (sv.TryGetComponent(out UiPointerSurface svPointer))
|
||||
{
|
||||
svPointer.Picked -= OnSvPicked;
|
||||
svPointer.Picked += OnSvPicked;
|
||||
}
|
||||
}
|
||||
|
||||
var hue = transform.Find("Hue");
|
||||
if (hue)
|
||||
{
|
||||
if (hue.TryGetComponent(out Image hueImg)) hueImg.sprite = PaintHud.HueSprite();
|
||||
_hueHandle = hue.Find("Handle") as RectTransform;
|
||||
|
||||
if (hue.TryGetComponent(out UiPointerSurface huePointer))
|
||||
{
|
||||
huePointer.Picked -= OnHuePicked;
|
||||
huePointer.Picked += OnHuePicked;
|
||||
}
|
||||
}
|
||||
|
||||
var swatch = transform.Find("Swatch");
|
||||
if (swatch) _swatch = swatch.GetComponent<Image>();
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
+253
-91
@@ -7,15 +7,22 @@ using UnityEngine.InputSystem.UI;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="MeshPainter"/> fields, so changes apply immediately. The panel is shown
|
||||
/// only while the active <see cref="GameMode"/> is <see cref="PaintMode"/> (it follows
|
||||
/// the manager's ModeChanged event).
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
/// 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
|
||||
@@ -26,7 +33,30 @@ public class PaintHud : MonoBehaviour
|
||||
[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);
|
||||
|
||||
@@ -53,11 +83,23 @@ public class PaintHud : MonoBehaviour
|
||||
if (!painter) painter = modeManager ? modeManager.Painter : null;
|
||||
if (!painter) painter = FindFirstObjectByType<MeshPainter>();
|
||||
|
||||
_font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
if (!_font) _font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
_font = LoadFont();
|
||||
|
||||
EnsureEventSystem();
|
||||
BuildUi();
|
||||
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);
|
||||
@@ -68,9 +110,29 @@ public class PaintHud : MonoBehaviour
|
||||
if (_panel) _panel.SetActive(mode is PaintMode);
|
||||
}
|
||||
|
||||
// --- UI construction ---
|
||||
private static Font LoadFont()
|
||||
{
|
||||
var f = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
if (!f) f = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
return f;
|
||||
}
|
||||
|
||||
private void BuildUi()
|
||||
#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>();
|
||||
@@ -84,57 +146,6 @@ public class PaintHud : MonoBehaviour
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
if (!GetComponent<GraphicRaycaster>()) gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 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<VerticalLayoutGroup>();
|
||||
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<ContentSizeFitter>();
|
||||
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()
|
||||
@@ -150,21 +161,96 @@ public class PaintHud : MonoBehaviour
|
||||
#endif
|
||||
}
|
||||
|
||||
// --- Widgets ---
|
||||
// --- Structure (layout only; matches the baked prefab) ---
|
||||
|
||||
private void AddSectionLabel(RectTransform panel, string text, int size, FontStyle style)
|
||||
private GameObject BuildPanelStructure(Transform parent)
|
||||
{
|
||||
var rt = NewRect("Label", panel);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.minHeight = le.preferredHeight = 18f;
|
||||
// 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;
|
||||
|
||||
var t = MakeText(rt.gameObject, text, size, style, new Color(0.85f, 0.85f, 0.9f, 1f), TextAnchor.MiddleLeft);
|
||||
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 AddToggle(RectTransform panel, string label, bool value, Action<bool> onChanged)
|
||||
private void AddToggleStructure(RectTransform panel, string name, string label)
|
||||
{
|
||||
var rt = NewRect("Toggle", panel);
|
||||
var rt = NewRect(name, panel);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.minHeight = le.preferredHeight = 22f;
|
||||
|
||||
@@ -195,13 +281,11 @@ public class PaintHud : MonoBehaviour
|
||||
|
||||
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<float> onChanged)
|
||||
private void AddSliderStructure(RectTransform panel, string name, string label, float min, float max, bool whole)
|
||||
{
|
||||
var rt = NewRect("Slider", panel);
|
||||
var rt = NewRect(name, panel);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.minHeight = le.preferredHeight = 34f;
|
||||
|
||||
@@ -270,30 +354,109 @@ public class PaintHud : MonoBehaviour
|
||||
slider.direction = Slider.Direction.LeftToRight;
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.wholeNumbers = whole;
|
||||
slider.value = value;
|
||||
|
||||
Action<float> 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); });
|
||||
slider.wholeNumbers = whole; // serialized: the binder reads it back for value formatting
|
||||
}
|
||||
|
||||
private HsvColorPicker AddColorPicker(RectTransform panel)
|
||||
private HsvColorPicker AddColorPickerStructure(RectTransform panel, string name)
|
||||
{
|
||||
const float square = 120f;
|
||||
const float hueH = 14f;
|
||||
|
||||
var rt = NewRect("ColorPicker", panel);
|
||||
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);
|
||||
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>();
|
||||
@@ -324,16 +487,15 @@ public class PaintHud : MonoBehaviour
|
||||
return img;
|
||||
}
|
||||
|
||||
/// <summary>Adds a full-rect, non-raycasting overlay image with the given sprite.</summary>
|
||||
public static Image AddOverlay(RectTransform parent, Sprite sprite)
|
||||
/// <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("Overlay", parent);
|
||||
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.sprite = sprite;
|
||||
img.type = Image.Type.Simple;
|
||||
img.raycastTarget = false;
|
||||
return img;
|
||||
|
||||
Reference in new issue
Block a user