Files
chameleon/Assets/Scripts/HsvColorPicker.cs
T
adminandClaude Fable 5 2243cc823e Refactor: extract shared helpers across scripts, trim remaining doc redundancy
Deduplication pass (44 extractions):
- New SmoothedLook: shared smoothed yaw/pitch look tick + view seeding
  (FirstPersonController, FreeCamController)
- New CharacterTeleport: disable-CC-around-teleport idiom (rescue,
  owner position apply, free-cam sync)
- New GeometryUtil: RescaleToUnitScale + TransformBounds (concealment
  meter/volume, skinned collider baker)
- Paint family: canonical map read/write shared between Net and
  Persistence, snapshot decode, backend stroke dispatch, RebuildBackend,
  ResolveSeed, chunked RPC send/gate/copy, CamoKnobs/HeightKnobs
  aggregates, SanitizeFileKey, NewMaskTexture, AdoptAlbedo, FindSlider
- Player/modes: GameMode SetPlayerControl/SetPainting/SetOrbitInputEnabled,
  InputUtil.GameplayKeyPressed, NearestClimbHit, ComputeXRay delegates to
  ComputeResolutionFreeze, FingerIK TryInPlaneAngle
- Match/misc: UnhookSessionCallbacks, SendEmptyNamedMessage, SwapScenes,
  RunSessionOpAsync, CovarianceEigen, CollectClusterPoints, MatchHud
  GUIStyle cache, HsvColorPicker rehook, VoxelCenter

Documentation pass: remove 6 triplicated doc blocks and 2 restating
inline comments left after the previous reduction.

Behavior-neutral; hot-path rasterizer loops intentionally untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:33:48 +02:00

186 lines
7.1 KiB
C#

using System;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// Compact runtime HSV colour picker: a saturation/value square, a hue bar and a live swatch. The
/// square is a pure-hue base with two static gradient overlays on top (white-out -> saturation,
/// black-out -> value), so only the base tint changes per hue - no per-frame texture rebuilds. Hue is
/// never lost while dragging the square (the square only edits S/V, the bar only edits H). Alpha is
/// edited separately by a standalone slider in the HUD, not by this picker; <see cref="SetColor"/>
/// still stores the incoming alpha so <see cref="Value"/> round-trips it.
///
/// <see cref="Build"/> creates the child structure (stable names, no sprites or hooks); it can
/// be baked into a prefab. <see cref="Rebind"/> then (re)assigns the runtime gradient sprites
/// (which a prefab cannot store) and (re)hooks the drag relays - it runs for both the code-built
/// picker and one instantiated from a prefab, so the two are behaviourally identical. A leftover
/// alpha bar from a pre-split prefab is hidden by <see cref="Rebind"/>, so no re-bake is required.
/// </summary>
public class HsvColorPicker : MonoBehaviour
{
/// <summary>Raised with the new colour (RGB; alpha is preserved, not edited here) when the user moves
/// the square or hue bar.</summary>
public event Action<Color> Changed;
private float _h, _s, _v, _a = 1f;
// Gap between the stacked square / hue rows (matches the layout in Build).
private const float RowGap = 6f;
private Image _hueBase; // pure-hue background of the SV square
private RectTransform _svHandle;
private RectTransform _hueHandle;
private Image _swatch;
/// <summary>Current colour (HSV -> RGB, with the preserved alpha).</summary>
public Color Value
{
get
{
Color c = Color.HSVToRGB(_h, _s, _v);
c.a = _a;
return c;
}
}
/// <summary>Shows <paramref name="c"/> without raising <see cref="Changed"/> (external set).</summary>
public void SetColor(Color c)
{
Color.RGBToHSV(c, out _h, out _s, out _v);
_a = c.a;
Refresh();
}
/// <summary>
/// Builds the picker structure (square + hue bar + swatch) under this object,
/// top-left aligned. Layout only - no gradient sprites and no event hooks, so the result is
/// safe to bake into a prefab. Call <see cref="Rebind"/> afterwards (at runtime) to make it
/// functional.
/// </summary>
public void Build(float squareSize, float hueHeight)
{
// Saturation / value square: base hue + two named gradient overlays, top-left of this rect.
var sv = PaintHud.NewRect("SV", transform);
TopLeft(sv, new Vector2(0f, 0f), new Vector2(squareSize, squareSize));
PaintHud.AddImage(sv.gameObject, Color.red); // raycast target for the drag relay
PaintHud.AddOverlay(sv, "SatOverlay");
PaintHud.AddOverlay(sv, "ValOverlay");
sv.gameObject.AddComponent<UiPointerSurface>();
var svHandle = PaintHud.NewRect("Handle", sv);
svHandle.sizeDelta = new Vector2(12f, 12f);
PaintHud.AddImage(svHandle.gameObject, Color.white).raycastTarget = false;
// Hue bar below the square.
var hue = PaintHud.NewRect("Hue", transform);
TopLeft(hue, new Vector2(0f, -(squareSize + RowGap)), new Vector2(squareSize, hueHeight));
var hueImg = hue.gameObject.AddComponent<Image>();
hueImg.type = Image.Type.Simple;
hue.gameObject.AddComponent<UiPointerSurface>();
var hueHandle = PaintHud.NewRect("Handle", hue);
hueHandle.sizeDelta = new Vector2(4f, hueHeight + 4f);
PaintHud.AddImage(hueHandle.gameObject, Color.white).raycastTarget = false;
// Swatch, to the right of the square.
var sw = PaintHud.NewRect("Swatch", transform);
TopLeft(sw, new Vector2(squareSize + 10f, 0f), new Vector2(34f, 34f));
PaintHud.AddImage(sw.gameObject, Color.white).raycastTarget = false;
}
/// <summary>
/// Re-finds the child widgets, (re)assigns the runtime gradient sprites and (re)hooks the drag
/// relays. Idempotent (unsubscribe-then-subscribe), so it is safe whether the structure was just
/// built in code or instantiated from a prefab. Call before <see cref="SetColor"/>.
/// </summary>
public void Rebind()
{
var sv = transform.Find("SV");
if (sv)
{
_hueBase = sv.GetComponent<Image>();
var sat = sv.Find("SatOverlay");
if (sat && sat.TryGetComponent(out Image satImg)) satImg.sprite = PaintHud.SaturationSprite();
var val = sv.Find("ValOverlay");
if (val && val.TryGetComponent(out Image valImg)) valImg.sprite = PaintHud.ValueSprite();
_svHandle = sv.Find("Handle") as RectTransform;
Rehook(sv, OnSvPicked);
}
var hue = transform.Find("Hue") as RectTransform;
if (hue)
{
if (hue.TryGetComponent(out Image hueImg)) hueImg.sprite = PaintHud.HueSprite();
_hueHandle = hue.Find("Handle") as RectTransform;
Rehook(hue, OnHuePicked);
}
// Alpha now lives on a standalone slider (PaintHud), not in the picker. Hide any alpha bar a
// pre-split prefab still carries so it does not show as a dead, non-functional strip.
var leftoverAlpha = transform.Find("Alpha");
if (leftoverAlpha) leftoverAlpha.gameObject.SetActive(false);
var swatch = transform.Find("Swatch");
if (swatch) _swatch = swatch.GetComponent<Image>();
Refresh();
}
// Idempotently hooks the handler to the surface's drag relay (unsubscribe-then-subscribe).
private static void Rehook(Component surface, Action<Vector2> handler)
{
if (surface.TryGetComponent(out UiPointerSurface pointer))
{
pointer.Picked -= handler;
pointer.Picked += handler;
}
}
private void OnSvPicked(Vector2 p)
{
_s = p.x;
_v = p.y;
Refresh();
Changed?.Invoke(Value);
}
private void OnHuePicked(Vector2 p)
{
_h = p.x;
Refresh();
Changed?.Invoke(Value);
}
private void Refresh()
{
if (_hueBase) _hueBase.color = Color.HSVToRGB(_h, 1f, 1f);
if (_svHandle)
{
_svHandle.anchorMin = _svHandle.anchorMax = new Vector2(_s, _v);
_svHandle.anchoredPosition = Vector2.zero;
}
if (_hueHandle)
{
_hueHandle.anchorMin = _hueHandle.anchorMax = new Vector2(_h, 0.5f);
_hueHandle.anchoredPosition = Vector2.zero;
}
if (_swatch) _swatch.color = Color.HSVToRGB(_h, _s, _v);
}
/// <summary>Anchors a child to this rect's top-left corner with the given offset and size.</summary>
private static void TopLeft(RectTransform rt, Vector2 offset, Vector2 size)
{
rt.anchorMin = rt.anchorMax = new Vector2(0f, 1f);
rt.pivot = new Vector2(0f, 1f);
rt.anchoredPosition = offset;
rt.sizeDelta = size;
}
}