The HUD save key was derived from the active scene name, which now changes across the additive lobby/map loads (Main -> Lobby -> Test Map). Save and restore landed in different files, so settings appeared lost. Derive the key from the HUD's own (stable, persistent) scene instead. Brush settings also live on the avatar's painter, which is despawned and respawned on every scene switch, resetting them to prefab defaults. Mirror the brush values into an in-memory snapshot on the persistent HUD and seed the freshly bound painter from it, so in-session edits carry across the respawn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
272 lines
11 KiB
C#
272 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using UnityEngine;
|
|
|
|
public partial class PaintHud
|
|
{
|
|
// --- Persistence: panel positions + the widget-bound painter values ---
|
|
//
|
|
// Saved on quit / play-stop and restored on the next start, so the HUD opens where you left it
|
|
// with the brush settings you last used. This is small structured state (a handful of positions
|
|
// and scalars), so it goes to a human-readable JSON file - unlike the painter's bulk pixel maps
|
|
// (MeshPainter.Persistence) which use a raw byte dump. The two saves are independent: this one
|
|
// owns the HUD layout + brush values, the painter owns the canvas pixels.
|
|
//
|
|
// The brush values are also mirrored into an in-memory snapshot (_carry) on this persistent HUD, so
|
|
// they survive the avatar respawn that a scene switch (lobby -> map) triggers: the new painter gets
|
|
// the carried values instead of its prefab defaults. The save key is derived from the HUD's OWN
|
|
// scene (a stable persistent scene), not the active scene - the latter now changes across the
|
|
// additive lobby/map loads, which would split save and restore into different files.
|
|
|
|
[Header("Persistence")]
|
|
[Tooltip("Save the panel positions and their values on quit / play-stop and restore them on the " +
|
|
"next start. Off = the HUD opens at its authored layout / painter defaults every session.")]
|
|
public bool persistHud = true;
|
|
|
|
[Tooltip("Save file identifier. Empty = derived from the HUD's own scene name. Set explicitly to keep " +
|
|
"the layout stable across scene renames or to share one layout between scenes.")]
|
|
public string hudSaveId = "";
|
|
|
|
private const int HudFileVersion = 1;
|
|
private bool _hudSavedOnTeardown; // guards a double save across OnApplicationQuit + OnDestroy
|
|
|
|
// In-memory mirror of the brush values, kept on this persistent HUD so they survive the avatar
|
|
// respawn a scene switch (lobby <-> map) triggers: the freshly spawned painter is seeded with these
|
|
// instead of its prefab defaults. Single instance with mutable fields -> no per-frame allocation.
|
|
private readonly HudValues _carry = new HudValues();
|
|
private bool _hasCarry;
|
|
|
|
private static string HudSaveDir => Path.Combine(Application.persistentDataPath, "MeshPaint");
|
|
|
|
private string HudSavePath()
|
|
{
|
|
// Derive from the HUD's OWN scene (a stable persistent scene), not the active scene: the latter
|
|
// now changes across the additive lobby/map loads, so a key from it would split save and restore
|
|
// into different files and silently lose the saved layout.
|
|
string key = string.IsNullOrEmpty(hudSaveId)
|
|
? SanitizeHudKey(gameObject.scene.name + "_hud")
|
|
: SanitizeHudKey(hudSaveId);
|
|
return Path.Combine(HudSaveDir, key + ".hudlayout");
|
|
}
|
|
|
|
private static string SanitizeHudKey(string s)
|
|
{
|
|
foreach (char c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_');
|
|
return s.Replace(' ', '_');
|
|
}
|
|
|
|
[Serializable]
|
|
private class HudState
|
|
{
|
|
public int version = HudFileVersion;
|
|
public bool hasValues; // false = no painter was bound when saved -> restore positions only
|
|
public List<PanelPos> panels = new List<PanelPos>();
|
|
public HudValues values = new HudValues();
|
|
}
|
|
|
|
[Serializable]
|
|
private struct PanelPos
|
|
{
|
|
public string name;
|
|
public Vector2 pos; // anchoredPosition (point-anchored panels -> fully determines placement)
|
|
}
|
|
|
|
// Mirrors exactly the painter fields the HUD widgets bind to (PaintHud.WirePanel).
|
|
[Serializable]
|
|
private class HudValues
|
|
{
|
|
public bool paintAlbedo, paintMetallicSmoothness, paintOcclusion, paintEmission, spray;
|
|
public Color brushColor;
|
|
public float metallic, smoothness, metallicSmoothnessOpacity, occlusion, occlusionOpacity;
|
|
public Color emissionColor;
|
|
public float emissionIntensity;
|
|
public int brushSize;
|
|
public int activeBrushIndex;
|
|
public int sprayDots;
|
|
public float sprayDotSizeMin, sprayDotSizeMax, sprayDotAlphaMin, sprayDotAlphaMax;
|
|
}
|
|
|
|
private void OnApplicationQuit() => SaveHudOnTeardown();
|
|
|
|
/// <summary>Saves once during teardown (OnApplicationQuit and OnDestroy both call this; only the first writes).</summary>
|
|
private void SaveHudOnTeardown()
|
|
{
|
|
if (_hudSavedOnTeardown) return;
|
|
_hudSavedOnTeardown = true;
|
|
SaveHudState();
|
|
}
|
|
|
|
/// <summary>Writes the current panel positions and bound painter values to disk.</summary>
|
|
public void SaveHudState()
|
|
{
|
|
if (!persistHud || !_panel) return;
|
|
|
|
try
|
|
{
|
|
var state = new HudState();
|
|
var panel = _panel.transform;
|
|
|
|
foreach (var name in DraggablePanels)
|
|
{
|
|
if (FindDeep(panel, name) is RectTransform rt)
|
|
state.panels.Add(new PanelPos { name = name, pos = rt.anchoredPosition });
|
|
}
|
|
|
|
if (painter)
|
|
{
|
|
CopyValues(painter, state.values);
|
|
state.hasValues = true;
|
|
}
|
|
else if (_hasCarry)
|
|
{
|
|
// No painter bound at teardown (e.g. quit with no avatar) -> persist the last mirrored values.
|
|
state.values = _carry;
|
|
state.hasValues = true;
|
|
}
|
|
|
|
Directory.CreateDirectory(HudSaveDir);
|
|
File.WriteAllText(HudSavePath(), JsonUtility.ToJson(state, true));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[PaintHud] Saving HUD state failed: {e.Message}", this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores the saved panel positions and brush values. Values are written into the painter (the
|
|
/// source of truth the widgets read) and then pushed to every widget via <see cref="SyncFromPainter"/>,
|
|
/// reusing the exact same painter -> HUD path the pipette pick uses. Positions are applied last.
|
|
/// </summary>
|
|
public void RestoreHudState()
|
|
{
|
|
if (!persistHud || !_panel) return;
|
|
|
|
var state = LoadHudState();
|
|
if (state == null) return;
|
|
|
|
if (painter && state.hasValues && state.values != null)
|
|
{
|
|
ApplyValues(painter, state.values);
|
|
SyncFromPainter();
|
|
}
|
|
|
|
// Point-anchored panels: anchoredPosition is resolution-independent under the canvas'
|
|
// ScaleWithScreenSize scaler, so the raw saved value places them identically next session.
|
|
var root = _panel.transform;
|
|
if (state.panels != null)
|
|
foreach (var p in state.panels)
|
|
if (FindDeep(root, p.name) is RectTransform rt)
|
|
rt.anchoredPosition = p.pos;
|
|
}
|
|
|
|
// --- In-session carry: brush values survive the avatar respawn a scene switch triggers ---
|
|
|
|
private void LateUpdate()
|
|
{
|
|
// Mirror the live painter every frame; the next BindPainter (after a respawn) seeds the fresh
|
|
// painter from this snapshot instead of its prefab defaults. Captured while the painter lives, so
|
|
// it is immune to the destroy-pending timing of the despawn.
|
|
if (painter) CaptureCarry();
|
|
}
|
|
|
|
/// <summary>Snapshots the bound painter's brush values into the in-memory mirror.</summary>
|
|
public void CaptureCarry()
|
|
{
|
|
if (!painter) return;
|
|
CopyValues(painter, _carry);
|
|
_hasCarry = true;
|
|
}
|
|
|
|
/// <summary>Writes the mirrored brush values onto the (freshly bound) painter and pushes them to the widgets.</summary>
|
|
public void ApplyCarry()
|
|
{
|
|
if (!painter || !_hasCarry) return;
|
|
ApplyValues(painter, _carry);
|
|
SyncFromPainter();
|
|
}
|
|
|
|
private static void CopyValues(MeshPainter from, HudValues v)
|
|
{
|
|
v.paintAlbedo = from.paintAlbedo;
|
|
v.paintMetallicSmoothness = from.paintMetallicSmoothness;
|
|
v.paintOcclusion = from.paintOcclusion;
|
|
v.paintEmission = from.paintEmission;
|
|
v.spray = from.spray;
|
|
v.brushColor = from.brushColor;
|
|
v.metallic = from.metallic;
|
|
v.smoothness = from.smoothness;
|
|
v.metallicSmoothnessOpacity = from.metallicSmoothnessOpacity;
|
|
v.occlusion = from.occlusion;
|
|
v.occlusionOpacity = from.occlusionOpacity;
|
|
v.emissionColor = from.emissionColor;
|
|
v.emissionIntensity = from.emissionIntensity;
|
|
v.brushSize = from.brushSize;
|
|
v.activeBrushIndex = from.activeBrushIndex;
|
|
v.sprayDots = from.sprayDots;
|
|
v.sprayDotSizeMin = from.sprayDotSizeMin;
|
|
v.sprayDotSizeMax = from.sprayDotSizeMax;
|
|
v.sprayDotAlphaMin = from.sprayDotAlphaMin;
|
|
v.sprayDotAlphaMax = from.sprayDotAlphaMax;
|
|
}
|
|
|
|
private static void ApplyValues(MeshPainter to, HudValues v)
|
|
{
|
|
to.paintAlbedo = v.paintAlbedo;
|
|
to.paintMetallicSmoothness = v.paintMetallicSmoothness;
|
|
to.paintOcclusion = v.paintOcclusion;
|
|
to.paintEmission = v.paintEmission;
|
|
to.spray = v.spray;
|
|
to.brushColor = v.brushColor;
|
|
to.metallic = v.metallic;
|
|
to.smoothness = v.smoothness;
|
|
to.metallicSmoothnessOpacity = v.metallicSmoothnessOpacity;
|
|
to.occlusion = v.occlusion;
|
|
to.occlusionOpacity = v.occlusionOpacity;
|
|
to.emissionColor = v.emissionColor;
|
|
to.emissionIntensity = v.emissionIntensity;
|
|
to.brushSize = v.brushSize;
|
|
to.activeBrushIndex = v.activeBrushIndex;
|
|
to.sprayDots = v.sprayDots;
|
|
to.sprayDotSizeMin = v.sprayDotSizeMin;
|
|
to.sprayDotSizeMax = v.sprayDotSizeMax;
|
|
to.sprayDotAlphaMin = v.sprayDotAlphaMin;
|
|
to.sprayDotAlphaMax = v.sprayDotAlphaMax;
|
|
}
|
|
|
|
private HudState LoadHudState()
|
|
{
|
|
string path = HudSavePath();
|
|
if (!File.Exists(path)) return null;
|
|
|
|
try
|
|
{
|
|
var state = JsonUtility.FromJson<HudState>(File.ReadAllText(path));
|
|
if (state == null || state.version != HudFileVersion) return null;
|
|
return state;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[PaintHud] Loading HUD state failed: {e.Message}", this);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>Deletes the saved HUD layout (next start opens at the authored layout / defaults). Editor convenience.</summary>
|
|
[ContextMenu("Clear Saved HUD Layout")]
|
|
public void ClearSavedHudState()
|
|
{
|
|
try
|
|
{
|
|
string path = HudSavePath();
|
|
if (File.Exists(path)) File.Delete(path);
|
|
Debug.Log($"[PaintHud] Cleared saved HUD layout: {path}", this);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[PaintHud] Clearing saved HUD layout failed: {e.Message}", this);
|
|
}
|
|
}
|
|
}
|