Iteration 4 over Assets/Scripts (no component/file splits, no Unity assets touched): Dedup: - Route inline cursor-release, material guards, Ctrl-held and undo/redo input through existing InputUtil / SetMatFloat helpers - Add ungated InputUtil.KeyPressedThisFrame; GameplayKeyPressed builds on it - Unify FindDeep/FindDeepChild into GeometryUtil.FindDeep Dead code / stale docs: - Remove unused code-gen bake path (HsvColorPicker.Build, PaintHud factory) - Drop unreferenced symbols (SubPhaseChanged event, LocalPhaseDuration, IsLocalSceneMain, MouseSensitivity.Changed, IsPaused, InSession, NetworkControlled, PreviousPose, JointCount, voxel-count accessors) - Fix incorrect PlayerRole / PaintHud / Pick / PlayerPose docs Perf / robustness (behaviour-neutral): - ConcealmentMeter.Evaluate: reuse vertex/topology buffers, two-pointer crossing count - BuildMeshCaches: alloc-free vertex copy, cached topology/UVs - Guard all PlayerPaintSync LogSync sites behind logSync - Cache LocalViewerIsHunter / controller / CanShoot / TypingGuard lookups; dedup LocalIsHunter with MatchHud - Change-guard RefreshEmissionColor and HeroCutout.Disarm - Cache scene/countdown string comparisons per frame - Grow NonAlloc buffers and log-once on saturation Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
308 lines
12 KiB
C#
308 lines
12 KiB
C#
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 (an existing name is overwritten, with a
|
|
/// warning shown while typing); Load applies the selected slot immediately; Delete goes 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 LibResetButton = "Lib_ResetHud";
|
|
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, _resetButton;
|
|
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 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 = GeometryUtil.FindDeep(root, LibListContent) as RectTransform;
|
|
var template = GeometryUtil.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);
|
|
_resetButton = Find<Button>(root, LibResetButton); // optional: present only if authored in the layout
|
|
Hook(_saveButton, OnSaveClicked);
|
|
Hook(_loadButton, OnLoadClicked);
|
|
Hook(_deleteButton, OnDeleteClicked);
|
|
Hook(_resetButton, OnResetClicked);
|
|
|
|
var sd = GeometryUtil.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 = GeometryUtil.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;
|
|
Hook(btn, () => 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;
|
|
// Pre-fill the currently selected painting's name so "Save" defaults to overwriting it (empty when
|
|
// nothing is selected) and select that text so the first keystroke replaces it for a fresh name.
|
|
if (_nameInput) _nameInput.text = _selectedPainting ?? "";
|
|
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();
|
|
StartCoroutine(SelectNameFieldNextFrame());
|
|
}
|
|
}
|
|
|
|
// ActivateInputField only takes focus on a later update and resets the caret, so selecting the text this
|
|
// frame would be cleared. Wait one frame, then select the whole pre-filled name - done explicitly here so
|
|
// it works regardless of the field's authored "On Focus Select All" toggle.
|
|
private System.Collections.IEnumerator SelectNameFieldNextFrame()
|
|
{
|
|
yield return null;
|
|
if (!_nameInput) yield break;
|
|
_nameInput.selectionStringAnchorPosition = _nameInput.text.Length;
|
|
_nameInput.selectionStringFocusPosition = 0;
|
|
_nameInput.ForceLabelUpdate();
|
|
}
|
|
|
|
private void ValidateSaveName()
|
|
{
|
|
string name = _nameInput ? _nameInput.text.Trim() : "";
|
|
bool empty = string.IsNullOrEmpty(name);
|
|
bool exists = !empty && MeshPainter.PaintExists(name);
|
|
// Overwrite is allowed -> the OK button stays enabled even when the name exists; the warning is
|
|
// purely informational so you know you are about to replace an existing slot.
|
|
if (_saveOkButton) _saveOkButton.interactable = !empty && painter;
|
|
if (_saveInfo) _saveInfo.text = exists ? "A painting with this name already exists and will be overwritten." : "";
|
|
}
|
|
|
|
private void ConfirmSave()
|
|
{
|
|
if (!painter || !_nameInput) return;
|
|
string name = _nameInput.text.Trim();
|
|
if (string.IsNullOrEmpty(name)) { ValidateSaveName(); return; }
|
|
|
|
if (painter.SavePaintAs(name, overwrite: true))
|
|
{
|
|
_selectedPainting = name;
|
|
if (_saveDialog) _saveDialog.SetActive(false);
|
|
RefreshList();
|
|
}
|
|
else if (_saveInfo)
|
|
{
|
|
_saveInfo.text = "Save failed.";
|
|
}
|
|
}
|
|
|
|
private void OnLoadClicked()
|
|
{
|
|
if (!painter || string.IsNullOrEmpty(_selectedPainting)) return;
|
|
DoLoad(_selectedPainting); // load immediately, no confirmation
|
|
}
|
|
|
|
private void OnDeleteClicked()
|
|
{
|
|
if (string.IsNullOrEmpty(_selectedPainting)) return;
|
|
string name = _selectedPainting;
|
|
ShowConfirm($"Delete \"{name}\"?", OkDanger, () => DoDelete(name));
|
|
}
|
|
|
|
/// <summary>Reset the panels + brush values to their authored defaults (confirmed; the dialog reuses the
|
|
/// library's confirm path). Falls back to an immediate reset if no confirm dialog is authored.</summary>
|
|
private void OnResetClicked()
|
|
{
|
|
if (_confirmDialog)
|
|
ShowConfirm("Reset the HUD panels and brush values to their defaults?", OkDanger, ResetHudToDefaults);
|
|
else
|
|
ResetHudToDefaults();
|
|
}
|
|
|
|
private void DoLoad(string name)
|
|
{
|
|
if (!painter || !painter.LoadPaintByName(name)) return;
|
|
|
|
// The load also restored the slot's material scalars onto the painter (emission + camo from v3,
|
|
// the painted-height knobs from v4) -> push them into the sliders so the HUD reflects the loaded
|
|
// look, exactly like a pipette pick does. SyncCamoSliders covers the camo + height sliders.
|
|
SyncFromPainter();
|
|
SyncCamoSliders();
|
|
|
|
// Re-sync the loaded baseline to every other peer (no-op offline / single-player). The restored
|
|
// emission/camo replicate on their own via PlayerPaintSync's owner-authoritative NetworkVariables.
|
|
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();
|
|
}
|
|
}
|