Wire three material-backed look sliders (occlusion AO shift, relief normal intensity, parallax intensity) through the paint HUD, its layout/state persistence, the .paint slot format (bumped to v4), owner-authoritative network sync and the runtime reset, mirroring the existing camo-knob path so they save, restore, replicate and reset alongside the brush values. Change the occlusion AO mapping from a division threshold to a linear shift: AO fades from 1 down to (1 - shift) at the deepest painted hole, so a 0 shift no longer leaves black spots. Enable _NORMALMAP alongside _PARALLAXMAP so URP transforms the painted relief normal into world space (otherwise it discards normalTS and the relief is invisible), and pin the added keyword in the build keep-alive material. Stop the pipette from picking the occlusion/height value and turn its toggle off after a pick. Default the knobs to 0.2 shift / 1.4 normal / 1.0 parallax across the shader, painter, network and HUD fallbacks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
566 lines
27 KiB
C#
566 lines
27 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 when state actually changes - a panel drop writes immediately (OnPanelMoved), brush-value
|
|
// edits are coalesced and flushed shortly after they settle (LateUpdate debounce) - and restored on
|
|
// the next start, so the HUD opens where you left it with the brush settings you last used. There is
|
|
// 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.
|
|
//
|
|
// 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 when you move a panel or change a brush value, " +
|
|
"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;
|
|
|
|
// 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 readonly HudValues _lastSeenValues = new HudValues(); // painter values observed last frame (settle detection)
|
|
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
|
|
// 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;
|
|
|
|
// Camo knobs (HalfLambert / brightness / specular dampen) live on the player's PlayerCamoLit material
|
|
// instance, which only exists after the painter's Start. They are saved/restored with the brush values
|
|
// above, but can't be WRITTEN until the material is ready - so a restored (disk) or carried (respawn)
|
|
// value waits here and is applied in the LateUpdate ready-gate. _camoEverReady gates the carry/save so
|
|
// the not-yet-ready 0 the getter returns never overwrites the material's authored value.
|
|
private Vector3 _pendingCamo;
|
|
private bool _hasPendingCamo;
|
|
private bool _camoEverReady;
|
|
|
|
// Painted-height knobs (occlusion threshold, normal intensity, parallax intensity) ride the exact same
|
|
// material-ready deferral as the camo knobs above: they become writable only after the painter's Start,
|
|
// so a restored/carried value waits here and is applied in the LateUpdate ready-gate (shared timing).
|
|
private Vector3 _pendingHeight;
|
|
private bool _hasPendingHeight;
|
|
|
|
// Authored defaults for the runtime reset (= "as if the saved layout file had been deleted"): the
|
|
// prefab's panel positions, captured before any saved layout is restored over them, and the painter's
|
|
// prefab/material default brush values, captured before carry/restore mutate the live painter. Camo is
|
|
// material-backed (readable only once IsReady), so it is filled in the LateUpdate ready-gate instead.
|
|
private readonly Dictionary<string, Vector2> _authoredPanelPos = new Dictionary<string, Vector2>();
|
|
private bool _hasAuthoredLayout;
|
|
private readonly HudValues _defaults = new HudValues();
|
|
private bool _hasDefaults; // non-camo defaults captured (first bind, before restore/carry)
|
|
private bool _hasDefaultCamo; // camo defaults captured (first ready-gate, before any pending camo apply)
|
|
|
|
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 bool hasCamoValues; // false = pre-camo save file -> keep the material's authored camo (no clobber)
|
|
public bool hasHeightValues; // false = pre-height save file -> keep the material's authored height knobs (no clobber)
|
|
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;
|
|
public float camoHalfLambert, camoHalfLambertBrightness, camoSpecularDampen; // PlayerCamoLit material knobs
|
|
public float occlusionThreshold, heightNormalIntensity, parallaxIntensity; // painted-height knobs (material-backed)
|
|
}
|
|
|
|
/// <summary>A draggable panel was dropped -> persist the layout immediately.</summary>
|
|
private void OnPanelMoved() => 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;
|
|
state.hasCamoValues = painter.IsReady; // only claim camo when it was actually readable
|
|
state.hasHeightValues = painter.IsReady; // height knobs share the camo readiness
|
|
}
|
|
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;
|
|
state.hasCamoValues = _camoEverReady; // _carry holds valid camo only after a ready painter
|
|
state.hasHeightValues = _camoEverReady;
|
|
}
|
|
|
|
Directory.CreateDirectory(HudSaveDir);
|
|
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)
|
|
{
|
|
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();
|
|
}
|
|
|
|
// Camo is material-backed and can't be written until the painter is ready -> stash the saved values
|
|
// for the LateUpdate ready-gate. hasCamoValues guards pre-camo save files: without it we would apply
|
|
// a 0 and clobber the material's authored camo.
|
|
if (state.hasCamoValues && state.values != null)
|
|
{
|
|
_pendingCamo = new Vector3(state.values.camoHalfLambert,
|
|
state.values.camoHalfLambertBrightness,
|
|
state.values.camoSpecularDampen);
|
|
_hasPendingCamo = true;
|
|
}
|
|
|
|
// Same deferral for the painted-height knobs; hasHeightValues guards pre-height save files so they
|
|
// keep the material's authored defaults instead of being slammed to a 0 threshold/flat relief.
|
|
if (state.hasHeightValues && state.values != null)
|
|
{
|
|
_pendingHeight = new Vector3(state.values.occlusionThreshold,
|
|
state.values.heightNormalIntensity,
|
|
state.values.parallaxIntensity);
|
|
_hasPendingHeight = true;
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
if (painter)
|
|
{
|
|
// Camo knobs are material-backed and only readable/writable after the painter's Start built the
|
|
// instance (binding ran earlier, in the avatar's OnNetworkSpawn). Apply the restored (disk) or
|
|
// carried (respawn) values to the material once it is ready, then seed the sliders from it. Done
|
|
// BEFORE the carry mirror + change detection below so the just-applied value is already the
|
|
// baseline and never re-triggers a redundant auto-save. With nothing pending (first session, no
|
|
// save file) the material keeps its authored camo and the sliders just reflect it.
|
|
if (!_camoSynced && painter.IsReady)
|
|
{
|
|
// Capture the material's authored camo as the reset default BEFORE any pending (restored or
|
|
// carried) camo is applied over it. Once per HUD: a later session/respawn keeps the very
|
|
// first authored value, so a reset always returns to the genuine material default.
|
|
if (!_hasDefaultCamo)
|
|
{
|
|
_defaults.camoHalfLambert = painter.CamoHalfLambert;
|
|
_defaults.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
|
_defaults.camoSpecularDampen = painter.CamoSpecularDampen;
|
|
_defaults.occlusionThreshold = painter.OcclusionThreshold;
|
|
_defaults.heightNormalIntensity = painter.HeightNormalIntensity;
|
|
_defaults.parallaxIntensity = painter.ParallaxIntensity;
|
|
_hasDefaultCamo = true;
|
|
}
|
|
|
|
if (_hasPendingCamo)
|
|
{
|
|
painter.CamoHalfLambert = _pendingCamo.x;
|
|
painter.CamoHalfLambertBrightness = _pendingCamo.y;
|
|
painter.CamoSpecularDampen = _pendingCamo.z;
|
|
_hasPendingCamo = false;
|
|
}
|
|
if (_hasPendingHeight)
|
|
{
|
|
painter.OcclusionThreshold = _pendingHeight.x;
|
|
painter.HeightNormalIntensity = _pendingHeight.y;
|
|
painter.ParallaxIntensity = _pendingHeight.z;
|
|
_hasPendingHeight = false;
|
|
}
|
|
SyncCamoSliders();
|
|
|
|
_savedValues.camoHalfLambert = _lastSeenValues.camoHalfLambert = painter.CamoHalfLambert;
|
|
_savedValues.camoHalfLambertBrightness = _lastSeenValues.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
|
_savedValues.camoSpecularDampen = _lastSeenValues.camoSpecularDampen = painter.CamoSpecularDampen;
|
|
_savedValues.occlusionThreshold = _lastSeenValues.occlusionThreshold = painter.OcclusionThreshold;
|
|
_savedValues.heightNormalIntensity = _lastSeenValues.heightNormalIntensity = painter.HeightNormalIntensity;
|
|
_savedValues.parallaxIntensity = _lastSeenValues.parallaxIntensity = painter.ParallaxIntensity;
|
|
|
|
_camoEverReady = true;
|
|
_camoSynced = true;
|
|
}
|
|
|
|
// 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.
|
|
CaptureCarry();
|
|
|
|
// Debounced auto-save: (re)arm the settle timer only on frames where a value actually CHANGED
|
|
// since the last frame - not merely differs from disk. Sliders fire every frame while dragged,
|
|
// so this keeps the deadline ahead of the drag and writes once the values stop moving. Comparing
|
|
// against the on-disk baseline instead would push the deadline forward forever (the edit always
|
|
// differs from disk until it is written), so the save would never fire on its own - it only
|
|
// landed when something else, e.g. a panel drop, wrote the state directly.
|
|
if (persistHud && _hasSavedBaseline && !ValuesEqual(painter, _lastSeenValues))
|
|
{
|
|
CopyValues(painter, _lastSeenValues);
|
|
if (!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);
|
|
CopyValues(painter, _lastSeenValues);
|
|
}
|
|
_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
|
|
&& p.CamoHalfLambert == v.camoHalfLambert
|
|
&& p.CamoHalfLambertBrightness == v.camoHalfLambertBrightness
|
|
&& p.CamoSpecularDampen == v.camoSpecularDampen
|
|
&& p.OcclusionThreshold == v.occlusionThreshold
|
|
&& p.HeightNormalIntensity == v.heightNormalIntensity
|
|
&& p.ParallaxIntensity == v.parallaxIntensity;
|
|
}
|
|
|
|
/// <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();
|
|
|
|
// Camo is material-backed -> defer to the ready-gate using the carried (last live) values. Guarded
|
|
// by _camoEverReady so a carry taken before any material was ready can't apply a stale 0.
|
|
if (_camoEverReady)
|
|
{
|
|
_pendingCamo = new Vector3(_carry.camoHalfLambert,
|
|
_carry.camoHalfLambertBrightness,
|
|
_carry.camoSpecularDampen);
|
|
_hasPendingCamo = true;
|
|
_pendingHeight = new Vector3(_carry.occlusionThreshold,
|
|
_carry.heightNormalIntensity,
|
|
_carry.parallaxIntensity);
|
|
_hasPendingHeight = true;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// Camo is material-backed: the getters read 0 until the painter's material instance exists, so only
|
|
// mirror them once ready - otherwise preserve the desired values already in the target (the value a
|
|
// restore/carry put there, which the ready-gate still has to apply).
|
|
if (from.IsReady)
|
|
{
|
|
v.camoHalfLambert = from.CamoHalfLambert;
|
|
v.camoHalfLambertBrightness = from.CamoHalfLambertBrightness;
|
|
v.camoSpecularDampen = from.CamoSpecularDampen;
|
|
v.occlusionThreshold = from.OcclusionThreshold;
|
|
v.heightNormalIntensity = from.HeightNormalIntensity;
|
|
v.parallaxIntensity = from.ParallaxIntensity;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// Camo + height setters are a no-op until the material exists; the guaranteed deferred apply runs
|
|
// from the LateUpdate ready-gate via _pendingCamo/_pendingHeight. Writing here too keeps Copy/Apply/Equal symmetric.
|
|
to.CamoHalfLambert = v.camoHalfLambert;
|
|
to.CamoHalfLambertBrightness = v.camoHalfLambertBrightness;
|
|
to.CamoSpecularDampen = v.camoSpecularDampen;
|
|
to.OcclusionThreshold = v.occlusionThreshold;
|
|
to.HeightNormalIntensity = v.heightNormalIntensity;
|
|
to.ParallaxIntensity = v.parallaxIntensity;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// --- Reset to authored defaults (drives the HUD's reset button) ---
|
|
|
|
/// <summary>
|
|
/// Snapshots the authored panel positions from the freshly instantiated layout, before any saved
|
|
/// layout is restored over them. The runtime reset restores exactly these. Called once from Start
|
|
/// (right after the layout prefab is instantiated); idempotent.
|
|
/// </summary>
|
|
public void CaptureAuthoredLayout()
|
|
{
|
|
if (_hasAuthoredLayout || !_panel) return;
|
|
var root = _panel.transform;
|
|
foreach (var name in DraggablePanels)
|
|
if (FindDeep(root, name) is RectTransform rt)
|
|
_authoredPanelPos[name] = rt.anchoredPosition;
|
|
_hasAuthoredLayout = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snapshots the painter's prefab-default brush values on the first bind, before carry/restore mutate
|
|
/// the live painter. Non-camo only here (a freshly spawned painter holds its prefab defaults); the
|
|
/// material-backed camo is not readable yet and is captured later in the LateUpdate ready-gate.
|
|
/// </summary>
|
|
private void CaptureDefaults()
|
|
{
|
|
if (_hasDefaults || !painter) return;
|
|
CopyValues(painter, _defaults); // CopyValues skips camo until IsReady -> camo stays for the ready-gate
|
|
_hasDefaults = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the HUD to its authored defaults, exactly as if the saved layout file had been deleted: the
|
|
/// panels jump back to their prefab positions and every brush value returns to the painter's prefab /
|
|
/// material defaults. The save file is deleted too, so the reset also survives the next start.
|
|
/// </summary>
|
|
[ContextMenu("Reset HUD Layout And Values")]
|
|
public void ResetHudToDefaults()
|
|
{
|
|
// Delete the saved layout so a restart opens at the authored defaults as well (the literal intent).
|
|
ClearSavedHudState();
|
|
|
|
// Panels -> authored prefab positions.
|
|
if (_panel && _hasAuthoredLayout)
|
|
{
|
|
var root = _panel.transform;
|
|
foreach (var kv in _authoredPanelPos)
|
|
if (FindDeep(root, kv.Key) is RectTransform rt)
|
|
rt.anchoredPosition = kv.Value;
|
|
}
|
|
|
|
// Brush values -> painter prefab/material defaults, then push them to every widget.
|
|
if (painter && _hasDefaults)
|
|
{
|
|
// Reset clicked before the ready-gate captured the camo defaults? keep the live camo rather than
|
|
// slamming it to the not-yet-filled 0 (which would flatten the material's lighting).
|
|
if (!_hasDefaultCamo && painter.IsReady)
|
|
{
|
|
_defaults.camoHalfLambert = painter.CamoHalfLambert;
|
|
_defaults.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
|
_defaults.camoSpecularDampen = painter.CamoSpecularDampen;
|
|
_defaults.occlusionThreshold = painter.OcclusionThreshold;
|
|
_defaults.heightNormalIntensity = painter.HeightNormalIntensity;
|
|
_defaults.parallaxIntensity = painter.ParallaxIntensity;
|
|
}
|
|
|
|
ApplyValues(painter, _defaults);
|
|
SyncFromPainter();
|
|
SyncCamoSliders();
|
|
|
|
// ApplyValues wrote the camo + height knobs straight to the material (painter is ready at a
|
|
// runtime reset), so drop any still-pending restored values the ready-gate would re-apply over the default.
|
|
_hasPendingCamo = false;
|
|
_hasPendingHeight = false;
|
|
|
|
CaptureCarry(); // a later respawn now carries the defaults, not the pre-reset values
|
|
SnapshotSavedBaseline(); // values match the (now deleted) save -> the reset itself triggers no auto-save
|
|
}
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|