Replace auto paint persistence with a named painting library
Persistence rework: - Drop auto-load on setup and auto-save on teardown; the painter now always seeds fresh from the material. - Named slots under MeshPaint/Saved/<name>.paint via the existing CHMP machinery: SavePaintAs / LoadPaintByName / DeletePaint / ListSavedPaintings / PaintExists. Slot files carry the display name in the header. - Remove the obsolete persistence partials (PlayerCharacterController, PlayerPoseController, ThirdPersonOrbitInput) and their references. Paintings HUD panel: - Authored Panel_Paintings + Save/Load/Delete dialogs into PaintHudPanel.prefab (TMP throughout); PaintHud.Library.cs is now a pure binder that clones the entry template per slot. - Save dialog blocks empty/existing names; Load and Delete confirm first. - Fix dialog z-order: open dialogs SetAsLastSibling so a dragged panel can no longer cover them. - Lib_ListContent horizontal offsets corrected to 0 (was clipping entry labels out of the masked viewport). Multiplayer: - Loading a painting re-syncs to all peers via the snapshot broadcast path (BroadcastLoadedPaint), covering later late-joiners for free. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
56caa0edd8
commit
ee482905f9
16 files changed
+4586
-521
No files matched your search
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Binder for the "Paintings" library panel. The panel's visual layout (Panel_Paintings, the scrollable
|
||||
/// list, an entry template, and the Save / Confirm dialogs) is authored inside the PaintHudPanel layout
|
||||
/// prefab - this code only finds those objects by their stable names and wires the behaviour, exactly like
|
||||
/// <see cref="WirePanel"/> does for the brush widgets. The list is populated by cloning the authored
|
||||
/// Lib_EntryTemplate per saved slot. Save opens a name dialog (blocked if the name already exists, no
|
||||
/// overwrite); Load and Delete go through a confirmation dialog. Loading replaces the current painting and,
|
||||
/// when networked, re-broadcasts the loaded state to every other peer.
|
||||
/// </summary>
|
||||
public partial class PaintHud
|
||||
{
|
||||
// Stable widget names, matching the objects authored in the PaintHudPanel prefab.
|
||||
private const string LibListContent = "Lib_ListContent";
|
||||
private const string LibEntryTemplate = "Lib_EntryTemplate";
|
||||
private const string LibSaveButton = "Lib_Save";
|
||||
private const string LibLoadButton = "Lib_Load";
|
||||
private const string LibDeleteButton = "Lib_Delete";
|
||||
private const string LibSaveDialog = "Lib_SaveDialog";
|
||||
private const string LibNameInput = "Lib_NameInput";
|
||||
private const string LibSaveInfo = "Lib_SaveInfo";
|
||||
private const string LibSaveOk = "Lib_SaveOk";
|
||||
private const string LibSaveCancel = "Lib_SaveCancel";
|
||||
private const string LibConfirmDialog = "Lib_ConfirmDialog";
|
||||
private const string LibConfirmText = "Lib_ConfirmText";
|
||||
private const string LibConfirmOk = "Lib_ConfirmOk";
|
||||
private const string LibConfirmCancel = "Lib_ConfirmCancel";
|
||||
|
||||
private bool _libraryBound;
|
||||
private RectTransform _listContent;
|
||||
private GameObject _entryTemplate;
|
||||
private Button _saveButton, _loadButton, _deleteButton;
|
||||
private string _selectedPainting;
|
||||
|
||||
// Save dialog
|
||||
private GameObject _saveDialog;
|
||||
private TMP_InputField _nameInput;
|
||||
private TMP_Text _saveInfo;
|
||||
private Button _saveOkButton;
|
||||
|
||||
// Confirmation dialog (reused for Load + Delete)
|
||||
private GameObject _confirmDialog;
|
||||
private TMP_Text _confirmText;
|
||||
private Button _confirmButton;
|
||||
private Action _confirmAction;
|
||||
|
||||
private struct EntryRef { public string name; public Image bg; }
|
||||
private readonly List<EntryRef> _entries = new List<EntryRef>();
|
||||
|
||||
private static readonly Color EntryNormal = new Color(0.18f, 0.18f, 0.22f, 1f);
|
||||
private static readonly Color EntrySelected = new Color(0.20f, 0.45f, 0.80f, 1f);
|
||||
private static readonly Color OkAccent = new Color(0.20f, 0.45f, 0.80f, 1f);
|
||||
private static readonly Color OkDanger = new Color(0.62f, 0.26f, 0.26f, 1f);
|
||||
|
||||
// --- Bind (find authored objects, hook listeners) ---
|
||||
|
||||
/// <summary>
|
||||
/// Binds the authored library panel under <paramref name="root"/> (the instantiated layout prefab).
|
||||
/// Idempotent: binds exactly once. No-op if the prefab carries no library objects.
|
||||
/// </summary>
|
||||
private void BindLibrary(Transform root)
|
||||
{
|
||||
if (_libraryBound || !root) return;
|
||||
|
||||
_listContent = FindDeep(root, LibListContent) as RectTransform;
|
||||
var template = FindDeep(root, LibEntryTemplate);
|
||||
_entryTemplate = template ? template.gameObject : null;
|
||||
if (!_listContent || !_entryTemplate) return; // panel not present in this layout
|
||||
_entryTemplate.SetActive(false);
|
||||
|
||||
_saveButton = Find<Button>(root, LibSaveButton);
|
||||
_loadButton = Find<Button>(root, LibLoadButton);
|
||||
_deleteButton = Find<Button>(root, LibDeleteButton);
|
||||
Hook(_saveButton, OnSaveClicked);
|
||||
Hook(_loadButton, OnLoadClicked);
|
||||
Hook(_deleteButton, OnDeleteClicked);
|
||||
|
||||
var sd = FindDeep(root, LibSaveDialog);
|
||||
_saveDialog = sd ? sd.gameObject : null;
|
||||
_nameInput = Find<TMP_InputField>(root, LibNameInput);
|
||||
_saveInfo = Find<TMP_Text>(root, LibSaveInfo);
|
||||
_saveOkButton = Find<Button>(root, LibSaveOk);
|
||||
Hook(_saveOkButton, ConfirmSave);
|
||||
Hook(Find<Button>(root, LibSaveCancel), () => { if (_saveDialog) _saveDialog.SetActive(false); });
|
||||
if (_nameInput) _nameInput.onValueChanged.AddListener(_ => ValidateSaveName());
|
||||
|
||||
var cd = FindDeep(root, LibConfirmDialog);
|
||||
_confirmDialog = cd ? cd.gameObject : null;
|
||||
_confirmText = Find<TMP_Text>(root, LibConfirmText);
|
||||
_confirmButton = Find<Button>(root, LibConfirmOk);
|
||||
Hook(_confirmButton, OnConfirmClicked);
|
||||
Hook(Find<Button>(root, LibConfirmCancel), () => { if (_confirmDialog) _confirmDialog.SetActive(false); });
|
||||
|
||||
HideDialogs();
|
||||
_libraryBound = true;
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
private static void Hook(Button button, Action onClick)
|
||||
{
|
||||
if (!button || onClick == null) return;
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => onClick());
|
||||
}
|
||||
|
||||
// --- List ---
|
||||
|
||||
private void RefreshList()
|
||||
{
|
||||
if (!_listContent || !_entryTemplate) return;
|
||||
|
||||
for (int i = _listContent.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = _listContent.GetChild(i).gameObject;
|
||||
if (child != _entryTemplate) Destroy(child);
|
||||
}
|
||||
_entries.Clear();
|
||||
|
||||
var names = MeshPainter.ListSavedPaintings();
|
||||
if (!string.IsNullOrEmpty(_selectedPainting) && !names.Contains(_selectedPainting))
|
||||
_selectedPainting = null;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
var go = Instantiate(_entryTemplate, _listContent);
|
||||
go.name = "Lib_Entry";
|
||||
go.SetActive(true);
|
||||
|
||||
var bg = go.GetComponent<Image>();
|
||||
if (bg) bg.color = name == _selectedPainting ? EntrySelected : EntryNormal;
|
||||
|
||||
var label = go.GetComponentInChildren<TMP_Text>(true);
|
||||
if (label) label.text = name;
|
||||
|
||||
var btn = go.GetComponent<Button>();
|
||||
if (btn)
|
||||
{
|
||||
string captured = name;
|
||||
btn.onClick.RemoveAllListeners();
|
||||
btn.onClick.AddListener(() => SelectPainting(captured));
|
||||
}
|
||||
|
||||
_entries.Add(new EntryRef { name = name, bg = bg });
|
||||
}
|
||||
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
private void SelectPainting(string name)
|
||||
{
|
||||
_selectedPainting = name;
|
||||
RefreshHighlights();
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
private void RefreshHighlights()
|
||||
{
|
||||
foreach (var e in _entries)
|
||||
if (e.bg) e.bg.color = e.name == _selectedPainting ? EntrySelected : EntryNormal;
|
||||
}
|
||||
|
||||
private void UpdateButtonStates()
|
||||
{
|
||||
bool hasPainter = painter;
|
||||
bool hasSel = !string.IsNullOrEmpty(_selectedPainting);
|
||||
if (_saveButton) _saveButton.interactable = hasPainter;
|
||||
if (_loadButton) _loadButton.interactable = hasPainter && hasSel;
|
||||
if (_deleteButton) _deleteButton.interactable = hasSel;
|
||||
}
|
||||
|
||||
private void HideDialogs()
|
||||
{
|
||||
if (_saveDialog) _saveDialog.SetActive(false);
|
||||
if (_confirmDialog) _confirmDialog.SetActive(false);
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
private void OnSaveClicked()
|
||||
{
|
||||
if (!painter || !_saveDialog) return;
|
||||
if (_nameInput) _nameInput.text = "";
|
||||
if (_saveInfo) _saveInfo.text = "";
|
||||
_saveDialog.SetActive(true);
|
||||
_saveDialog.transform.SetAsLastSibling(); // a dragged panel may have moved itself in front; stay on top
|
||||
ValidateSaveName();
|
||||
if (_nameInput) _nameInput.ActivateInputField();
|
||||
}
|
||||
|
||||
private void ValidateSaveName()
|
||||
{
|
||||
string name = _nameInput ? _nameInput.text.Trim() : "";
|
||||
bool empty = string.IsNullOrEmpty(name);
|
||||
bool exists = !empty && MeshPainter.PaintExists(name);
|
||||
if (_saveOkButton) _saveOkButton.interactable = !empty && !exists && painter;
|
||||
if (_saveInfo) _saveInfo.text = exists ? "A painting with this name already exists." : "";
|
||||
}
|
||||
|
||||
private void ConfirmSave()
|
||||
{
|
||||
if (!painter || !_nameInput) return;
|
||||
string name = _nameInput.text.Trim();
|
||||
if (string.IsNullOrEmpty(name) || MeshPainter.PaintExists(name)) { ValidateSaveName(); return; }
|
||||
|
||||
if (painter.SavePaintAs(name))
|
||||
{
|
||||
_selectedPainting = name;
|
||||
if (_saveDialog) _saveDialog.SetActive(false);
|
||||
RefreshList();
|
||||
}
|
||||
else if (_saveInfo)
|
||||
{
|
||||
_saveInfo.text = "Save failed.";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoadClicked()
|
||||
{
|
||||
if (!painter || string.IsNullOrEmpty(_selectedPainting)) return;
|
||||
string name = _selectedPainting;
|
||||
ShowConfirm($"Load \"{name}\"?\nThe current painting will be lost.", OkAccent, () => DoLoad(name));
|
||||
}
|
||||
|
||||
private void OnDeleteClicked()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_selectedPainting)) return;
|
||||
string name = _selectedPainting;
|
||||
ShowConfirm($"Delete \"{name}\"?", OkDanger, () => DoDelete(name));
|
||||
}
|
||||
|
||||
private void DoLoad(string name)
|
||||
{
|
||||
if (!painter || !painter.LoadPaintByName(name)) return;
|
||||
|
||||
// Re-sync the loaded baseline to every other peer (no-op offline / single-player).
|
||||
var sync = painter.GetComponentInParent<PlayerPaintSync>();
|
||||
if (sync) sync.BroadcastLoadedPaint();
|
||||
}
|
||||
|
||||
private void DoDelete(string name)
|
||||
{
|
||||
MeshPainter.DeletePaint(name);
|
||||
if (_selectedPainting == name) _selectedPainting = null;
|
||||
RefreshList();
|
||||
}
|
||||
|
||||
private void ShowConfirm(string message, Color okColor, Action onConfirm)
|
||||
{
|
||||
if (!_confirmDialog) return;
|
||||
if (_confirmText) _confirmText.text = message;
|
||||
_confirmAction = onConfirm;
|
||||
if (_confirmButton && _confirmButton.targetGraphic is Image okImg) okImg.color = okColor;
|
||||
_confirmDialog.SetActive(true);
|
||||
_confirmDialog.transform.SetAsLastSibling(); // a dragged panel may have moved itself in front; stay on top
|
||||
}
|
||||
|
||||
private void OnConfirmClicked()
|
||||
{
|
||||
var act = _confirmAction;
|
||||
_confirmAction = null;
|
||||
if (_confirmDialog) _confirmDialog.SetActive(false);
|
||||
act?.Invoke();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user