Add HUD brush mask selector and fix dilation seam artifacts
- 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>
This commit is contained in:
1 parent
d62eddc518
commit
c14525a2c8
15 files changed
+1005
-14
No files matched your search
@@ -63,6 +63,8 @@ public partial class PaintHud : MonoBehaviour
|
||||
private const string SliderSprayDotSizeMax = "Slider_SprayDotSizeMax";
|
||||
private const string SliderSprayDotAlphaMin = "Slider_SprayDotAlphaMin";
|
||||
private const string SliderSprayDotAlphaMax = "Slider_SprayDotAlphaMax";
|
||||
private const string MaskSelector = "MaskSelector";
|
||||
private const string MaskSwatchPrefix = "MaskSwatch_";
|
||||
|
||||
// Panels made draggable at runtime - grabbable from any non-widget surface (both prefab and code-gen paths).
|
||||
private static readonly string[] DraggablePanels =
|
||||
@@ -206,6 +208,8 @@ public partial class PaintHud : MonoBehaviour
|
||||
|
||||
WireSlider(panel, SliderBrushSize, painter ? painter.brushSize : 16f, v => { if (painter) painter.brushSize = Mathf.RoundToInt(v); });
|
||||
|
||||
WireMaskSelector(panel);
|
||||
|
||||
WireSlider(panel, SliderSprayDots, painter ? painter.sprayDots : 24f, v => { if (painter) painter.sprayDots = Mathf.RoundToInt(v); });
|
||||
WireSlider(panel, SliderSprayDotSizeMin, painter ? painter.sprayDotSizeMin : .05f, v => { if (painter) painter.sprayDotSizeMin = v; });
|
||||
WireSlider(panel, SliderSprayDotSizeMax, painter ? painter.sprayDotSizeMax : .18f, v => { if (painter) painter.sprayDotSizeMax = v; });
|
||||
@@ -250,6 +254,8 @@ public partial class PaintHud : MonoBehaviour
|
||||
SetSlider(panel, SliderSprayDotSizeMax, painter.sprayDotSizeMax);
|
||||
SetSlider(panel, SliderSprayDotAlphaMin, painter.sprayDotAlphaMin);
|
||||
SetSlider(panel, SliderSprayDotAlphaMax, painter.sprayDotAlphaMax);
|
||||
|
||||
RefreshMaskHighlights();
|
||||
}
|
||||
|
||||
private static void SetToggle(Transform panel, string name, bool value)
|
||||
@@ -271,6 +277,82 @@ public partial class PaintHud : MonoBehaviour
|
||||
if (picker) picker.SetColor(color);
|
||||
}
|
||||
|
||||
// Active = accent frame, inactive = subtle frame. Selection lives on the painter (activeBrushIndex);
|
||||
// the swatch click only sets it - the painter resolves the new mask on its next Update.
|
||||
private static readonly Color MaskFrameSelected = new Color(0.30f, 0.62f, 1f, 1f);
|
||||
private static readonly Color MaskFrameUnselected = new Color(0.18f, 0.18f, 0.21f, 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Binds the baked mask swatches (one per <c>MaskSwatch_i</c>) to the painter's brush masks:
|
||||
/// each swatch previews <c>painter.brushMasks[i]</c> and selects it on click. A swatch with no
|
||||
/// matching mask is hidden, so the prefab can carry more swatches than the painter has masks.
|
||||
/// </summary>
|
||||
private void WireMaskSelector(Transform panel)
|
||||
{
|
||||
var selector = FindDeep(panel, MaskSelector);
|
||||
if (!selector) return;
|
||||
var masks = painter ? painter.brushMasks : null;
|
||||
|
||||
for (int i = 0; ; i++)
|
||||
{
|
||||
var swatch = FindDeep(selector, MaskSwatchPrefix + i);
|
||||
if (!swatch) break;
|
||||
|
||||
bool has = masks != null && i < masks.Count && masks[i];
|
||||
swatch.gameObject.SetActive(has);
|
||||
if (!has) continue;
|
||||
|
||||
var preview = swatch.Find("Preview");
|
||||
var previewImg = preview ? preview.GetComponent<Image>() : null;
|
||||
if (previewImg) previewImg.sprite = MaskSprite(masks[i]);
|
||||
|
||||
var btn = swatch.GetComponent<Button>();
|
||||
if (btn)
|
||||
{
|
||||
int idx = i; // capture per swatch
|
||||
btn.onClick.RemoveAllListeners();
|
||||
btn.onClick.AddListener(() => SelectMask(idx));
|
||||
}
|
||||
}
|
||||
|
||||
RefreshMaskHighlights();
|
||||
}
|
||||
|
||||
private void SelectMask(int index)
|
||||
{
|
||||
if (painter) painter.activeBrushIndex = index;
|
||||
RefreshMaskHighlights();
|
||||
}
|
||||
|
||||
private void RefreshMaskHighlights()
|
||||
{
|
||||
if (!_panel || !painter) return;
|
||||
var selector = FindDeep(_panel.transform, MaskSelector);
|
||||
if (!selector) return;
|
||||
|
||||
for (int i = 0; ; i++)
|
||||
{
|
||||
var swatch = FindDeep(selector, MaskSwatchPrefix + i);
|
||||
if (!swatch) break;
|
||||
var frame = swatch.GetComponent<Image>();
|
||||
if (frame) frame.color = i == painter.activeBrushIndex ? MaskFrameSelected : MaskFrameUnselected;
|
||||
}
|
||||
}
|
||||
|
||||
// One sprite per mask texture, reused across rewires (display only - no CPU readback needed).
|
||||
private static readonly System.Collections.Generic.Dictionary<Texture2D, Sprite> _maskSprites
|
||||
= new System.Collections.Generic.Dictionary<Texture2D, Sprite>();
|
||||
|
||||
private static Sprite MaskSprite(Texture2D tex)
|
||||
{
|
||||
if (!tex) return null;
|
||||
if (_maskSprites.TryGetValue(tex, out var cached) && cached) return cached;
|
||||
var s = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f);
|
||||
s.hideFlags = HideFlags.HideAndDontSave;
|
||||
_maskSprites[tex] = s;
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches (idempotently) a <see cref="DraggablePanel"/> to the whole panel so it can be dragged
|
||||
/// from any of its non-interactive surface (header, padding, labels, background); the component
|
||||
|
||||
Reference in new issue
Block a user