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
/// (follows the manager's ModeChanged) and binds its widgets directly to the
/// live fields, so edits apply immediately.
///
/// The layout (independently draggable sub-panels per channel + a global brush card) comes from an
/// authored prefab () or is generated in code as the fallback. Either way
/// the BINDINGS live in code and are (re)applied at runtime by against the
/// same stable widget names - a prefab stores only the visual layout, never behaviour.
///
[DisallowMultipleComponent]
public partial 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 PanelGeneral = "Panel_General";
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 PanelPaintings = "Panel_Paintings";
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 TogglePipette = "Toggle_Pipette";
private const string PickerAlbedo = "Picker_Albedo";
private const string PickerEmission = "Picker_Emission";
private const string SliderAlbedoAlpha = "Slider_AlbedoAlpha";
private const string SliderEmissionAlpha = "Slider_EmissionAlpha";
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";
private const string SliderHalfLambert = "Slider_HalfLambert";
private const string SliderHalfLambertBrightness = "Slider_HalfLambertBrightness";
private const string SliderSpecularDamp = "Slider_SpecularDamp";
private const string SliderOcclusionThreshold = "Slider_OcclusionThreshold";
private const string SliderNormalIntensity = "Slider_NormalIntensity";
private const string SliderParallaxIntensity = "Slider_ParallaxIntensity";
private const string MaskSelector = "MaskSelector";
private const string MaskSwatchPrefix = "MaskSwatch_";
// 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,
PanelGeneral,
PanelPaintings
};
private GameObject _panel;
private Font _font;
private bool _camoSynced; // camo sliders are material-backed -> seeded once the painter's material instance exists
private void Awake()
{
if (!modeManager) modeManager = FindFirstObjectByType();
}
private void OnEnable()
{
if (modeManager)
{
modeManager.ModeChanged += OnModeChanged;
modeManager.PainterChanged += OnPainterChanged;
}
}
private void OnDisable()
{
if (modeManager)
{
modeManager.ModeChanged -= OnModeChanged;
modeManager.PainterChanged -= OnPainterChanged;
}
}
private void Start()
{
_font = LoadFont();
EnsureEventSystem();
EnsureCanvas();
if (layoutPrefab)
{
var go = Instantiate(layoutPrefab, transform);
go.name = layoutPrefab.name; // strip the "(Clone)" suffix
_panel = go;
}
// Snapshot the authored panel positions now, while the layout is untouched - before RestoreHudState
// (in BindPainter) overwrites them. The reset button restores exactly these.
CaptureAuthoredLayout();
// The paint library now lives inside the authored layout prefab; bind its widgets like the brush panel.
BindLibrary(_panel ? _panel.transform : null);
// Bind as soon as a painter exists. With networked spawn-on-connect there is none at scene
// start; PainterChanged then wires the widgets once the local avatar spawns.
var p = painter;
if (!p) p = modeManager ? modeManager.Painter : null;
if (!p) p = FindFirstObjectByType();
if (p) BindPainter(p);
// Set initial visibility (the manager's first SwitchTo may have fired before this Start).
OnModeChanged(modeManager ? modeManager.ActiveMode : null);
}
private void OnPainterChanged(MeshPainter newPainter)
{
if (newPainter) BindPainter(newPainter);
}
///
/// (Re)binds the HUD's widgets to a live painter. Called from Start when one already exists
/// (non-networked scene) or from the manager's PainterChanged event when the local networked
/// avatar spawns. Binds exactly once per painter: re-binding the same painter is a no-op, so
/// the deferred network path and the immediate scene path never double-add widget listeners.
///
private void BindPainter(MeshPainter newPainter)
{
if (!newPainter || newPainter == painter || !_panel) return;
UnhookPainterEvents();
painter = newPainter;
_camoSynced = false; // re-seed the camo sliders once this painter's material instance is ready
WirePanel(_panel.transform);
// Re-sync every widget when the pipette adopts a sampled material (painter -> HUD).
painter.Picked += SyncFromPainter;
// Re-sync the brush-size slider when Shift+wheel resizes the brush in-world (painter -> HUD).
painter.BrushSizeChanged += SyncBrushSize;
// Snapshot the prefab-default brush values before carry/restore mutate them (the reset button
// returns to exactly these). Once per HUD.
CaptureDefaults();
// First bind this session: restore from disk. A later bind is an avatar respawn -> carry the
// in-session values onto the fresh painter instead.
if (_hasCarry) ApplyCarry();
else RestoreHudState();
CaptureCarry(); // seed/refresh the mirror from the now-current painter
SnapshotSavedBaseline(); // values now match disk -> auto-save only fires on a real edit
UpdateButtonStates(); // enable Save now that a painter is bound
}
private void OnDestroy() => UnhookPainterEvents();
private void UnhookPainterEvents()
{
if (!painter) return;
painter.Picked -= SyncFromPainter;
painter.BrushSizeChanged -= SyncBrushSize;
}
private void OnModeChanged(GameMode mode)
{
// Require a bound painter too: pre-spawn the manager sits in a player-less Paint mode, but there
// is nothing to paint yet - keep the HUD hidden until a painter is bound.
bool paint = mode is PaintMode && painter;
if (_panel) _panel.SetActive(paint); // toggles the brush panels and the library panel together
if (paint)
{
if (_libraryBound) RefreshList(); // pick up slots saved/changed since last shown
}
else HideDialogs();
}
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