- Add mask selector with previews to the brush card (data-driven from painter.brushMasks); persist activeBrushIndex with HUD layout. - Fix alpha/coverage artifacts when overpainting after load or backend switch: derive the dilation source marker from a deterministic UV island mask instead of resetting it to "unpainted" on every seed. - Import the two final brush mask textures (Brushes/). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
218 lines
8.6 KiB
C#
218 lines
8.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
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.
|
|
|
|
[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 active 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
|
|
|
|
private static string HudSaveDir => Path.Combine(Application.persistentDataPath, "MeshPaint");
|
|
|
|
private string HudSavePath()
|
|
{
|
|
string key = string.IsNullOrEmpty(hudSaveId)
|
|
? SanitizeHudKey(SceneManager.GetActiveScene().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)
|
|
{
|
|
state.hasValues = true;
|
|
var v = state.values;
|
|
v.paintAlbedo = painter.paintAlbedo;
|
|
v.paintMetallicSmoothness = painter.paintMetallicSmoothness;
|
|
v.paintOcclusion = painter.paintOcclusion;
|
|
v.paintEmission = painter.paintEmission;
|
|
v.spray = painter.spray;
|
|
v.brushColor = painter.brushColor;
|
|
v.metallic = painter.metallic;
|
|
v.smoothness = painter.smoothness;
|
|
v.metallicSmoothnessOpacity = painter.metallicSmoothnessOpacity;
|
|
v.occlusion = painter.occlusion;
|
|
v.occlusionOpacity = painter.occlusionOpacity;
|
|
v.emissionColor = painter.emissionColor;
|
|
v.emissionIntensity = painter.emissionIntensity;
|
|
v.brushSize = painter.brushSize;
|
|
v.activeBrushIndex = painter.activeBrushIndex;
|
|
v.sprayDots = painter.sprayDots;
|
|
v.sprayDotSizeMin = painter.sprayDotSizeMin;
|
|
v.sprayDotSizeMax = painter.sprayDotSizeMax;
|
|
v.sprayDotAlphaMin = painter.sprayDotAlphaMin;
|
|
v.sprayDotAlphaMax = painter.sprayDotAlphaMax;
|
|
}
|
|
|
|
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)
|
|
{
|
|
var v = state.values;
|
|
painter.paintAlbedo = v.paintAlbedo;
|
|
painter.paintMetallicSmoothness = v.paintMetallicSmoothness;
|
|
painter.paintOcclusion = v.paintOcclusion;
|
|
painter.paintEmission = v.paintEmission;
|
|
painter.spray = v.spray;
|
|
painter.brushColor = v.brushColor;
|
|
painter.metallic = v.metallic;
|
|
painter.smoothness = v.smoothness;
|
|
painter.metallicSmoothnessOpacity = v.metallicSmoothnessOpacity;
|
|
painter.occlusion = v.occlusion;
|
|
painter.occlusionOpacity = v.occlusionOpacity;
|
|
painter.emissionColor = v.emissionColor;
|
|
painter.emissionIntensity = v.emissionIntensity;
|
|
painter.brushSize = v.brushSize;
|
|
painter.activeBrushIndex = v.activeBrushIndex;
|
|
painter.sprayDots = v.sprayDots;
|
|
painter.sprayDotSizeMin = v.sprayDotSizeMin;
|
|
painter.sprayDotSizeMax = v.sprayDotSizeMax;
|
|
painter.sprayDotAlphaMin = v.sprayDotAlphaMin;
|
|
painter.sprayDotAlphaMax = v.sprayDotAlphaMax;
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|