Persist HUD layout on drag-end instead of only on teardown
HUD panel positions and brush values were written only from OnApplicationQuit/OnDestroy, which is not guaranteed in a build (Alt-F4, process kill, crash), so layout work was lost. DraggablePanel now exposes an IEndDragHandler callback that saves the layout the moment a panel is dropped. Brush-value changes are detected against a disk baseline in LateUpdate and flushed after a 0.5s debounce so per-frame slider drags do not write every frame. The teardown save path is removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a6fc163b1
commit
f22ca4765f
3 files changed
+93
-23
No files matched your search
@@ -14,7 +14,7 @@ using UnityEngine.EventSystems;
|
|||||||
/// corners in its parent, not from a fixed top-left assumption).
|
/// corners in its parent, not from a fixed top-left assumption).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequireComponent(typeof(RectTransform))]
|
[RequireComponent(typeof(RectTransform))]
|
||||||
public class DraggablePanel : MonoBehaviour, IPointerDownHandler, IBeginDragHandler, IDragHandler
|
public class DraggablePanel : MonoBehaviour, IPointerDownHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||||
{
|
{
|
||||||
[Tooltip("Panel moved by the drag. Empty = this object's RectTransform.")]
|
[Tooltip("Panel moved by the drag. Empty = this object's RectTransform.")]
|
||||||
public RectTransform target;
|
public RectTransform target;
|
||||||
@@ -22,6 +22,10 @@ public class DraggablePanel : MonoBehaviour, IPointerDownHandler, IBeginDragHand
|
|||||||
[Tooltip("Panel pixels (canvas units) kept on screen on every edge so it can never be lost.")]
|
[Tooltip("Panel pixels (canvas units) kept on screen on every edge so it can never be lost.")]
|
||||||
public float keepVisible = 48f;
|
public float keepVisible = 48f;
|
||||||
|
|
||||||
|
// Raised once when a real panel drag ends (not for gestures that began on a widget). The HUD uses
|
||||||
|
// this to persist the layout the moment a panel is dropped. Optional - left null by other users.
|
||||||
|
public System.Action Moved;
|
||||||
|
|
||||||
private RectTransform _area; // the panel's parent; pointer positions are tracked in its local space
|
private RectTransform _area; // the panel's parent; pointer positions are tracked in its local space
|
||||||
private Vector2 _grabOffset;
|
private Vector2 _grabOffset;
|
||||||
private bool _dragging;
|
private bool _dragging;
|
||||||
@@ -66,6 +70,13 @@ public class DraggablePanel : MonoBehaviour, IPointerDownHandler, IBeginDragHand
|
|||||||
target.anchoredPosition = Clamp(local + _grabOffset);
|
target.anchoredPosition = Clamp(local + _grabOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnEndDrag(PointerEventData eventData)
|
||||||
|
{
|
||||||
|
if (!_dragging) return; // gesture began on a widget -> not our drag, nothing moved
|
||||||
|
_dragging = false;
|
||||||
|
Moved?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
// True when the press started on an interactive widget inside the panel (which then owns the gesture).
|
// True when the press started on an interactive widget inside the panel (which then owns the gesture).
|
||||||
private bool StartedOnWidget(PointerEventData eventData)
|
private bool StartedOnWidget(PointerEventData eventData)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ public partial class PaintHud
|
|||||||
{
|
{
|
||||||
// --- Persistence: panel positions + the widget-bound painter values ---
|
// --- 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
|
// Saved when state actually changes - a panel drop writes immediately (OnPanelMoved), brush-value
|
||||||
// with the brush settings you last used. This is small structured state (a handful of positions
|
// edits are coalesced and flushed shortly after they settle (LateUpdate debounce) - and restored on
|
||||||
// and scalars), so it goes to a human-readable JSON file - unlike the painter's bulk pixel maps
|
// the next start, so the HUD opens where you left it with the brush settings you last used. There is
|
||||||
// (MeshPainter.Persistence) which use a raw byte dump. The two saves are independent: this one
|
// deliberately NO save on quit/teardown: standalone builds do not guarantee a clean shutdown
|
||||||
|
// (OnApplicationQuit is skipped on force-close / kill / crash), so a teardown-only save silently
|
||||||
|
// lost the whole session's layout in the build. 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.
|
// 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
|
// The brush values are also mirrored into an in-memory snapshot (_carry) on this persistent HUD, so
|
||||||
@@ -20,8 +24,9 @@ public partial class PaintHud
|
|||||||
// additive lobby/map loads, which would split save and restore into different files.
|
// additive lobby/map loads, which would split save and restore into different files.
|
||||||
|
|
||||||
[Header("Persistence")]
|
[Header("Persistence")]
|
||||||
[Tooltip("Save the panel positions and their values on quit / play-stop and restore them on the " +
|
[Tooltip("Save the panel positions and their values when you move a panel or change a brush value, " +
|
||||||
"next start. Off = the HUD opens at its authored layout / painter defaults every session.")]
|
"and restore them on the next start. Off = the HUD opens at its authored layout / painter " +
|
||||||
|
"defaults every session.")]
|
||||||
public bool persistHud = true;
|
public bool persistHud = true;
|
||||||
|
|
||||||
[Tooltip("Save file identifier. Empty = derived from the HUD's own scene name. Set explicitly to keep " +
|
[Tooltip("Save file identifier. Empty = derived from the HUD's own scene name. Set explicitly to keep " +
|
||||||
@@ -29,7 +34,15 @@ public partial class PaintHud
|
|||||||
public string hudSaveId = "";
|
public string hudSaveId = "";
|
||||||
|
|
||||||
private const int HudFileVersion = 1;
|
private const int HudFileVersion = 1;
|
||||||
private bool _hudSavedOnTeardown; // guards a double save across OnApplicationQuit + OnDestroy
|
|
||||||
|
// Auto-save bookkeeping. Panel drops save instantly; brush-value edits are debounced because sliders
|
||||||
|
// fire every frame while dragged - we push the due time forward and write once they settle. No
|
||||||
|
// teardown save, so persistence never depends on a clean quit.
|
||||||
|
private const float HudSaveDebounce = 0.5f;
|
||||||
|
private readonly HudValues _savedValues = new HudValues(); // mirror of what is currently on disk
|
||||||
|
private bool _hasSavedBaseline;
|
||||||
|
private bool _hudSavePending;
|
||||||
|
private float _hudSaveDueTime;
|
||||||
|
|
||||||
// In-memory mirror of the brush values, kept on this persistent HUD so they survive the avatar
|
// 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
|
// respawn a scene switch (lobby <-> map) triggers: the freshly spawned painter is seeded with these
|
||||||
@@ -87,15 +100,8 @@ public partial class PaintHud
|
|||||||
public float sprayDotSizeMin, sprayDotSizeMax, sprayDotAlphaMin, sprayDotAlphaMax;
|
public float sprayDotSizeMin, sprayDotSizeMax, sprayDotAlphaMin, sprayDotAlphaMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnApplicationQuit() => SaveHudOnTeardown();
|
/// <summary>A draggable panel was dropped -> persist the layout immediately.</summary>
|
||||||
|
private void OnPanelMoved() => SaveHudState();
|
||||||
/// <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>
|
/// <summary>Writes the current panel positions and bound painter values to disk.</summary>
|
||||||
public void SaveHudState()
|
public void SaveHudState()
|
||||||
@@ -127,6 +133,11 @@ public partial class PaintHud
|
|||||||
|
|
||||||
Directory.CreateDirectory(HudSaveDir);
|
Directory.CreateDirectory(HudSaveDir);
|
||||||
File.WriteAllText(HudSavePath(), JsonUtility.ToJson(state, true));
|
File.WriteAllText(HudSavePath(), JsonUtility.ToJson(state, true));
|
||||||
|
|
||||||
|
// Refresh the on-disk baseline so the debounce does not immediately re-save unchanged values.
|
||||||
|
if (painter) CopyValues(painter, _savedValues);
|
||||||
|
_hasSavedBaseline = painter;
|
||||||
|
_hudSavePending = false;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -165,10 +176,57 @@ public partial class PaintHud
|
|||||||
|
|
||||||
private void LateUpdate()
|
private void LateUpdate()
|
||||||
{
|
{
|
||||||
// Mirror the live painter every frame; the next BindPainter (after a respawn) seeds the fresh
|
if (painter)
|
||||||
// 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.
|
// Mirror the live painter every frame; the next BindPainter (after a respawn) seeds the fresh
|
||||||
if (painter) CaptureCarry();
|
// 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.
|
||||||
|
CaptureCarry();
|
||||||
|
|
||||||
|
// A brush-value edit differs from what is on disk -> schedule a coalesced save. Sliders fire
|
||||||
|
// every frame while dragged, so we keep pushing the due time forward and write once they settle.
|
||||||
|
if (persistHud && _hasSavedBaseline && !ValuesEqual(painter, _savedValues))
|
||||||
|
{
|
||||||
|
_hudSavePending = true;
|
||||||
|
_hudSaveDueTime = Time.unscaledTime + HudSaveDebounce;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hudSavePending && Time.unscaledTime >= _hudSaveDueTime)
|
||||||
|
SaveHudState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Marks the current brush values as the on-disk baseline so auto-save only fires on real edits.</summary>
|
||||||
|
private void SnapshotSavedBaseline()
|
||||||
|
{
|
||||||
|
if (painter) CopyValues(painter, _savedValues);
|
||||||
|
_hasSavedBaseline = painter;
|
||||||
|
_hudSavePending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>True when every brush value on the painter still matches the snapshot last written to disk.</summary>
|
||||||
|
private static bool ValuesEqual(MeshPainter p, HudValues v)
|
||||||
|
{
|
||||||
|
return p.paintAlbedo == v.paintAlbedo
|
||||||
|
&& p.paintMetallicSmoothness == v.paintMetallicSmoothness
|
||||||
|
&& p.paintOcclusion == v.paintOcclusion
|
||||||
|
&& p.paintEmission == v.paintEmission
|
||||||
|
&& p.spray == v.spray
|
||||||
|
&& p.brushColor == v.brushColor
|
||||||
|
&& p.metallic == v.metallic
|
||||||
|
&& p.smoothness == v.smoothness
|
||||||
|
&& p.metallicSmoothnessOpacity == v.metallicSmoothnessOpacity
|
||||||
|
&& p.occlusion == v.occlusion
|
||||||
|
&& p.occlusionOpacity == v.occlusionOpacity
|
||||||
|
&& p.emissionColor == v.emissionColor
|
||||||
|
&& p.emissionIntensity == v.emissionIntensity
|
||||||
|
&& p.brushSize == v.brushSize
|
||||||
|
&& p.activeBrushIndex == v.activeBrushIndex
|
||||||
|
&& p.sprayDots == v.sprayDots
|
||||||
|
&& p.sprayDotSizeMin == v.sprayDotSizeMin
|
||||||
|
&& p.sprayDotSizeMax == v.sprayDotSizeMax
|
||||||
|
&& p.sprayDotAlphaMin == v.sprayDotAlphaMin
|
||||||
|
&& p.sprayDotAlphaMax == v.sprayDotAlphaMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Snapshots the bound painter's brush values into the in-memory mirror.</summary>
|
/// <summary>Snapshots the bound painter's brush values into the in-memory mirror.</summary>
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ public partial class PaintHud : MonoBehaviour
|
|||||||
if (_hasCarry) ApplyCarry();
|
if (_hasCarry) ApplyCarry();
|
||||||
else RestoreHudState();
|
else RestoreHudState();
|
||||||
CaptureCarry(); // seed/refresh the mirror from the now-current painter
|
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
|
UpdateButtonStates(); // enable Save now that a painter is bound
|
||||||
}
|
}
|
||||||
@@ -169,7 +170,6 @@ public partial class PaintHud : MonoBehaviour
|
|||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
if (painter) painter.Picked -= SyncFromPainter;
|
if (painter) painter.Picked -= SyncFromPainter;
|
||||||
SaveHudOnTeardown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModeChanged(GameMode mode)
|
private void OnModeChanged(GameMode mode)
|
||||||
@@ -409,13 +409,14 @@ public partial class PaintHud : MonoBehaviour
|
|||||||
/// from any of its non-interactive surface (header, padding, labels, background); the component
|
/// from any of its non-interactive surface (header, padding, labels, background); the component
|
||||||
/// ignores presses that start on a widget (toggle, slider, colour picker).
|
/// ignores presses that start on a widget (toggle, slider, colour picker).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void MakeDraggable(Transform root, string panelName)
|
private void MakeDraggable(Transform root, string panelName)
|
||||||
{
|
{
|
||||||
var panel = FindDeep(root, panelName) as RectTransform;
|
var panel = FindDeep(root, panelName) as RectTransform;
|
||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
var drag = panel.GetComponent<DraggablePanel>();
|
var drag = panel.GetComponent<DraggablePanel>();
|
||||||
if (!drag) drag = panel.gameObject.AddComponent<DraggablePanel>();
|
if (!drag) drag = panel.gameObject.AddComponent<DraggablePanel>();
|
||||||
drag.target = panel;
|
drag.target = panel;
|
||||||
|
drag.Moved = OnPanelMoved; // persist the layout the moment a panel is dropped
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureFonts(Transform panel)
|
private void EnsureFonts(Transform panel)
|
||||||
|
|||||||
Reference in new issue
Block a user